mirror of https://github.com/0xERR0R/blocky.git
initial commit
This commit is contained in:
commit
01a8a402dc
|
@ -0,0 +1,4 @@
|
|||
bin/
|
||||
.idea
|
||||
.github
|
||||
testdata/
|
|
@ -0,0 +1,40 @@
|
|||
name: CI Build
|
||||
on: [push]
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
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
|
||||
|
||||
- name: Install golangci-lint
|
||||
run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.21.0
|
||||
|
||||
- name: Run golangci-lint
|
||||
run: make lint
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: Docker images
|
||||
run: make docker-build
|
|
@ -0,0 +1,54 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
- name: Build multiarch binaries
|
||||
run: make buildMultiArchRelease
|
||||
|
||||
- name: Upload amd64 binary to release
|
||||
uses: svenstaro/upload-release-action@v1-release
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: bin/blocky_amd64
|
||||
asset_name: blocky_amd64
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Upload arm32v6 binary to release
|
||||
uses: svenstaro/upload-release-action@v1-release
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: bin/blocky_arm32v6
|
||||
asset_name: blocky_arm32v6
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Build the Docker image and push
|
||||
run: |
|
||||
mkdir -p ~/.docker && echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json
|
||||
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||
make docker-build
|
||||
make dockerManifestAndPush
|
|
@ -0,0 +1,5 @@
|
|||
.idea/
|
||||
*.iml
|
||||
bin/
|
||||
config.yml
|
||||
todo.txt
|
|
@ -0,0 +1,53 @@
|
|||
linters:
|
||||
enable:
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- gosimple
|
||||
- structcheck
|
||||
- varcheck
|
||||
- ineffassign
|
||||
- deadcode
|
||||
- typecheck
|
||||
- bodyclose
|
||||
- golint
|
||||
- stylecheck
|
||||
- gosec
|
||||
- interfacer
|
||||
- unconvert
|
||||
- dupl
|
||||
- goconst
|
||||
- gocyclo
|
||||
- gocognit
|
||||
- gofmt
|
||||
- goimports
|
||||
- maligned
|
||||
- depguard
|
||||
- misspell
|
||||
- lll
|
||||
- unparam
|
||||
- dogsled
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- godox
|
||||
- nakedret
|
||||
- prealloc
|
||||
- whitespace
|
||||
- wsl
|
||||
|
||||
disable-all: false
|
||||
presets:
|
||||
- bugs
|
||||
- unused
|
||||
fast: false
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- dupl
|
|
@ -0,0 +1,38 @@
|
|||
# build stage
|
||||
FROM golang:alpine AS build-env
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
make \
|
||||
gcc \
|
||||
libc-dev \
|
||||
tzdata \
|
||||
zip \
|
||||
ca-certificates
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
# add source
|
||||
ADD . .
|
||||
|
||||
ARG opts
|
||||
RUN env ${opts} make build
|
||||
|
||||
# final stage
|
||||
FROM scratch
|
||||
COPY --from=build-env /src/bin/blocky /app/blocky
|
||||
|
||||
# the timezone data:
|
||||
COPY --from=build-env /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
# the tls certificates:
|
||||
COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["/app/blocky"]
|
|
@ -0,0 +1,47 @@
|
|||
.PHONY: all clean build test lint run buildMultiArchRelease docker-build dockerManifestAndPush 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"
|
||||
BINARY_NAME=blocky
|
||||
BIN_OUT_DIR=bin
|
||||
|
||||
all: test lint build ## Build binary (with tests)
|
||||
|
||||
clean: ## cleans output directory
|
||||
$(shell rm -rf $(BIN_OUT_DIR)/*)
|
||||
|
||||
build: ## Build binary
|
||||
go build -v -ldflags="-w -s -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" -o $(BIN_OUT_DIR)/$(BINARY_NAME)$(BINARY_SUFFIX)
|
||||
|
||||
test: ## run tests
|
||||
go test -v -cover ./...
|
||||
|
||||
lint: ## run golangcli-lint checks
|
||||
$(shell go env GOPATH)/bin/golangci-lint run
|
||||
|
||||
run: build ## Build and run binary
|
||||
./$(BIN_OUT_DIR)/$(BINARY_NAME)
|
||||
|
||||
buildMultiArchRelease: ## builds binary for multiple archs
|
||||
$(MAKE) build GOARCH=arm GOARM=6 BINARY_SUFFIX=_arm32v6
|
||||
$(MAKE) build GOARCH=amd64 BINARY_SUFFIX=_amd64
|
||||
|
||||
docker-build: ## Build multi arch docker images
|
||||
docker build --build-arg opts="GOARCH=arm GOARM=6" --pull --tag ${DOCKER_IMAGE_NAME}:${VERSION}-arm32v6 .
|
||||
docker build --build-arg opts="GOARCH=amd64" --pull --tag ${DOCKER_IMAGE_NAME}:${VERSION}-amd64 .
|
||||
|
||||
dockerManifestAndPush: ## create manifest for multi arch images and push to docker hub
|
||||
docker push ${DOCKER_IMAGE_NAME}:${VERSION}-arm32v6
|
||||
docker push ${DOCKER_IMAGE_NAME}:${VERSION}-amd64
|
||||
|
||||
docker manifest create ${DOCKER_IMAGE_NAME}:${VERSION} ${DOCKER_IMAGE_NAME}:${VERSION}-amd64 ${DOCKER_IMAGE_NAME}:${VERSION}-arm32v6
|
||||
docker manifest annotate ${DOCKER_IMAGE_NAME}:${VERSION} ${DOCKER_IMAGE_NAME}:${VERSION}-arm32v6 --os linux --arch arm
|
||||
docker manifest push ${DOCKER_IMAGE_NAME}:${VERSION} --purge
|
||||
docker manifest create ${DOCKER_IMAGE_NAME}:latest ${DOCKER_IMAGE_NAME}:${VERSION}-amd64 ${DOCKER_IMAGE_NAME}:${VERSION}-arm32v6
|
||||
docker manifest annotate ${DOCKER_IMAGE_NAME}:latest ${DOCKER_IMAGE_NAME}:${VERSION}-arm32v6 --os linux --arch arm
|
||||
docker manifest push ${DOCKER_IMAGE_NAME}:latest --purge
|
||||
|
||||
help: ## Shows help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
|
@ -0,0 +1,47 @@
|
|||
upstream:
|
||||
externalResolvers:
|
||||
# - udp:8.8.8.8
|
||||
# - udp:8.8.4.4
|
||||
- tcp-tls:1.1.1.1:853
|
||||
- tcp-tls:1.0.0.1:853
|
||||
customDNS:
|
||||
mapping:
|
||||
spx.duckdns.org: 192.168.178.3
|
||||
conditional:
|
||||
mapping:
|
||||
fritz.box: udp:192.168.178.1
|
||||
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
|
||||
blockType: zeroIp
|
||||
|
||||
clientLookup:
|
||||
upstream: udp:192.168.178.1
|
||||
singleNameOrder:
|
||||
- 2
|
||||
- 1
|
||||
|
||||
#queryLog:
|
||||
# dir: /tmp
|
||||
# perClient: true
|
||||
# logRetentionDays: 7
|
||||
|
||||
port: 55555
|
||||
logLevel: info
|
|
@ -0,0 +1,148 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// nolint:gochecknoglobals
|
||||
var netDefaultPort = map[string]uint16{
|
||||
"udp": 53,
|
||||
"tcp": 53,
|
||||
"tcp-tls": 853,
|
||||
}
|
||||
|
||||
// Upstream is the definition of external DNS server
|
||||
type Upstream struct {
|
||||
Net string
|
||||
Host string
|
||||
Port uint16
|
||||
}
|
||||
|
||||
func (u *Upstream) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var s string
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upstream, err := parseUpstream(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*u = upstream
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseUpstream creates new Upstream from passed string in format net:host:port
|
||||
func parseUpstream(upstream string) (result Upstream, err error) {
|
||||
if strings.Trim(upstream, " ") == "" {
|
||||
return Upstream{}, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(upstream, ":")
|
||||
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
err = fmt.Errorf("wrong configuration, couldn't parse input '%s', please enter net:host[:port]", upstream)
|
||||
return
|
||||
}
|
||||
|
||||
net := strings.TrimSpace(parts[0])
|
||||
|
||||
if _, ok := netDefaultPort[net]; !ok {
|
||||
err = fmt.Errorf("wrong configuration, couldn't parse net '%s', please user one of %s",
|
||||
net, reflect.ValueOf(netDefaultPort).MapKeys())
|
||||
return
|
||||
}
|
||||
|
||||
var port uint16
|
||||
|
||||
host := strings.TrimSpace(parts[1])
|
||||
|
||||
if len(parts) == 3 {
|
||||
var p int
|
||||
p, err = strconv.Atoi(strings.TrimSpace(parts[2]))
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("can't convert port to number %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if p < 1 || p > 65535 {
|
||||
err = fmt.Errorf("invalid port %d", p)
|
||||
return
|
||||
}
|
||||
|
||||
port = uint16(p)
|
||||
} else {
|
||||
port = netDefaultPort[net]
|
||||
}
|
||||
|
||||
return Upstream{Net: net, Host: host, Port: port}, nil
|
||||
}
|
||||
|
||||
// main configuration
|
||||
type Config struct {
|
||||
Upstream UpstreamConfig `yaml:"upstream"`
|
||||
CustomDNS CustomDNSConfig `yaml:"customDNS"`
|
||||
Conditional ConditionalUpstreamConfig `yaml:"conditional"`
|
||||
Blocking BlockingConfig `yaml:"blocking"`
|
||||
ClientLookup ClientLookupConfig `yaml:"clientLookup"`
|
||||
QueryLog QueryLogConfig `yaml:"queryLog"`
|
||||
Port uint16
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
}
|
||||
|
||||
type UpstreamConfig struct {
|
||||
ExternalResolvers []Upstream `yaml:"externalResolvers"`
|
||||
}
|
||||
|
||||
type CustomDNSConfig struct {
|
||||
Mapping map[string]net.IP `yaml:"mapping"`
|
||||
}
|
||||
|
||||
type ConditionalUpstreamConfig struct {
|
||||
Mapping map[string]Upstream `yaml:"mapping"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type ClientLookupConfig struct {
|
||||
Upstream Upstream `yaml:"upstream"`
|
||||
SingleNameOrder []uint `yaml:"singleNameOrder"`
|
||||
}
|
||||
|
||||
type QueryLogConfig struct {
|
||||
Dir string `yaml:"dir"`
|
||||
PerClient bool `yaml:"perClient"`
|
||||
LogRetentionDays uint64 `yaml:"logRetentionDays"`
|
||||
}
|
||||
|
||||
func NewConfig() Config {
|
||||
cfg := Config{}
|
||||
data, err := ioutil.ReadFile("config.yml")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't read config file: ", err)
|
||||
}
|
||||
|
||||
err = yaml.UnmarshalStrict(data, &cfg)
|
||||
if err != nil {
|
||||
log.Fatal("wrong file structure: ", err)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_NewConfig(t *testing.T) {
|
||||
err := os.Chdir("../testdata")
|
||||
assert.NoError(t, err)
|
||||
|
||||
cfg := NewConfig()
|
||||
|
||||
assert.Equal(t, uint16(55555), cfg.Port)
|
||||
assert.Len(t, cfg.Upstream.ExternalResolvers, 3)
|
||||
assert.Equal(t, "8.8.8.8", cfg.Upstream.ExternalResolvers[0].Host)
|
||||
assert.Equal(t, "8.8.4.4", cfg.Upstream.ExternalResolvers[1].Host)
|
||||
assert.Equal(t, "1.1.1.1", cfg.Upstream.ExternalResolvers[2].Host)
|
||||
assert.Len(t, cfg.CustomDNS.Mapping, 1)
|
||||
assert.Equal(t, net.ParseIP("192.168.178.3"), cfg.CustomDNS.Mapping["my.duckdns.org"])
|
||||
assert.Len(t, cfg.Conditional.Mapping, 1)
|
||||
assert.Equal(t, "192.168.178.1", cfg.ClientLookup.Upstream.Host)
|
||||
assert.Equal(t, []uint{2, 1}, cfg.ClientLookup.SingleNameOrder)
|
||||
assert.Len(t, cfg.Blocking.BlackLists, 2)
|
||||
assert.Len(t, cfg.Blocking.WhiteLists, 1)
|
||||
assert.Len(t, cfg.Blocking.ClientGroupsBlock, 2)
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
args string
|
||||
wantResult Upstream
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "udpWithPort",
|
||||
args: "udp:4.4.4.4:531",
|
||||
wantResult: Upstream{Net: "udp", Host: "4.4.4.4", Port: 531},
|
||||
},
|
||||
{
|
||||
name: "udpDefault",
|
||||
args: "udp:4.4.4.4",
|
||||
wantResult: Upstream{Net: "udp", Host: "4.4.4.4", Port: 53},
|
||||
},
|
||||
{
|
||||
name: "tcpWithPort",
|
||||
args: "tcp:4.4.4.4:4711",
|
||||
wantResult: Upstream{Net: "tcp", Host: "4.4.4.4", Port: 4711},
|
||||
},
|
||||
{
|
||||
name: "tcpDefault",
|
||||
args: "tcp:4.4.4.4",
|
||||
wantResult: Upstream{Net: "tcp", Host: "4.4.4.4", Port: 53},
|
||||
},
|
||||
{
|
||||
name: "tcpTlsDefault",
|
||||
args: "tcp-tls:4.4.4.4",
|
||||
wantResult: Upstream{Net: "tcp-tls", Host: "4.4.4.4", Port: 853},
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
args: "",
|
||||
wantResult: Upstream{},
|
||||
},
|
||||
{
|
||||
name: "negativePort",
|
||||
args: "tcp:4.4.4.4:-1",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalidPort",
|
||||
args: "tcp:4.4.4.4:65536",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "notNumericPort",
|
||||
args: "tcp:4.4.4.4:A53",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrongProtocol",
|
||||
args: "bla:4.4.4.4:53",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrongFormat",
|
||||
args: "tcp-4.4.4.4",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
func Test_parseUpstream(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
rr := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotResult, err := parseUpstream(rr.args)
|
||||
if (err != nil) != rr.wantErr {
|
||||
t.Errorf("parseUpstream() error = %v, wantErr %v", err, rr.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(gotResult, rr.wantResult) {
|
||||
t.Errorf("parseUpstream() = %v, want %v", gotResult, rr.wantResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
![](https://github.com/0xERR0R/blocky/workflows/CI%20Build/badge.svg) ![](https://github.com/0xERR0R/blocky/workflows/Docker%20Image%20Release/badge.svg)
|
||||
|
||||
<p align="center">
|
||||
<img height="200" src="blocky.svg">
|
||||
</p>
|
||||
|
||||
# Blocky
|
||||
Blocky is a DNS proxy for local network written in Go with following features:
|
||||
- Blocking of DNS queries with external lists (Ad-block) with whitelisting
|
||||
- Definition of black and white lists per client group (Kids, Smart home devices etc) -> for example: you can block some domains for you Kids and allow your network camera only domains from a whitelist
|
||||
- periodical reload of external black and white lists
|
||||
- Caching of DNS answers for queries -> improves DNS resolution speed and reduces amount of external DNS queries
|
||||
- Custom DNS resolution for certain domain names
|
||||
- Supports UDP, TCP and TCP over TLS DNS resolvers
|
||||
- Delegates DNS query to 2 external resolver from a list of configured resolvers, uses the answer from the fastest one -> improves you privacy and resolution time
|
||||
- Logging of all DNS queries per day / per client in a text file
|
||||
- Simple configuration in a single file
|
||||
- Only one binary in docker container, low memory footprint
|
||||
- Runs fine on raspbery pi
|
||||
|
||||
## Installation and configuration
|
||||
Create `config.yml` file with your configuration:
|
||||
```yml
|
||||
upstream:
|
||||
# these external DNS resolvers will be used. Blocky picks 2 random resolvers from the list for each query
|
||||
# format for resolver: net:host:port. net could be tcp, udp or tcp-tls. If port is empty, default port will be used (53 for udp and tcp, 853 for tcp-tls)
|
||||
externalResolvers:
|
||||
- udp:8.8.8.8
|
||||
- udp:8.8.4.4
|
||||
- udp:1.1.1.1
|
||||
- tcp-tls:1.0.0.1:853
|
||||
|
||||
# optional: custom IP address for domain name (with all sub-domains)
|
||||
# example: query "printer.lan" or "my.printer.lan" will return 192.168.178.3
|
||||
customDNS:
|
||||
mapping:
|
||||
printer.lan: 192.168.178.3
|
||||
|
||||
# optional: definition, which DNS resolver should be used for queries to the domain (with all sub-domains).
|
||||
# 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:
|
||||
mapping:
|
||||
fritz.box: udp:192.168.178.1
|
||||
|
||||
# optional: use black and white lists to block queries (for example ads, trackers, adult pages etc.)
|
||||
blocking:
|
||||
# definition of blacklist groups. Can be external link (http/https) or local file
|
||||
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
|
||||
# definition of whitelist groups. Attention: if the same group has black and whitelists, whitelists will be used to disable particular blacklist entries. If a group has only whitelist entries -> this means only domains from this list are allowed, all other domains will be blocked
|
||||
whiteLists:
|
||||
ads:
|
||||
- whitelist.txt
|
||||
# definition: which groups should be appied for which client
|
||||
clientGroupsBlock:
|
||||
# default will be used, if no special definition for a client name exists
|
||||
default:
|
||||
- ads
|
||||
- special
|
||||
# use client name or ip address
|
||||
laptop.fritz.box:
|
||||
- ads
|
||||
# which response will be sent, if query is blocked:
|
||||
# zeroIp: 0.0.0.0 will be returned (default)
|
||||
# nxDomain: return NXDOMAIN as return code
|
||||
blockType: zeroIp
|
||||
|
||||
#optional: configuration of client name resolution
|
||||
clientLookup:
|
||||
# this DNS resolver will be used to perform reverse DNS lookup (typically local router)
|
||||
upstream: udp:192.168.178.1
|
||||
# optional: some routers return multiple names for client (host name and user defined name). Define which single name should be used.
|
||||
# Example: take second name if present, if not take first name
|
||||
singleNameOrder:
|
||||
- 2
|
||||
- 1
|
||||
|
||||
# optional: write query information (question, answer, client, duration etc) to daily csv file
|
||||
queryLog:
|
||||
# directory (should be mounted as volume in docker)
|
||||
dir: /logs
|
||||
# if true, write one file per client. Writes all queries to single file otherwise
|
||||
perClient: true
|
||||
# if > 0, deletes log files which are older than ... days
|
||||
logRetentionDays: 7
|
||||
|
||||
# Port, should be 53 (UDP and TCP)
|
||||
port: 53
|
||||
# Log level (one from debug, info, warn, error)
|
||||
logLevel: info
|
||||
```
|
||||
|
||||
### Run with docker
|
||||
Start docker container with following `docker-compose.yml` file:
|
||||
```yml
|
||||
version: "2.1"
|
||||
services:
|
||||
blocky:
|
||||
image: spx01/blocky
|
||||
container_name: blocky
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
# config file
|
||||
- ./config.yml:/app/config.yml
|
||||
# write query logs in this directory. You can also use a volume
|
||||
- ./logs:/logs
|
||||
```
|
||||
|
||||
### Run standalone
|
||||
Download binary file for your architecture, put it in one directory with config file. Please be aware, you must run the binary with root privileges if you want to use port 53 or 953.
|
||||
|
||||
### Additional information
|
||||
To print runtime configuration and statistics, you can send SIGUSR1 signal to running process:
|
||||
`kill -s USR1 <PID>` or `docker kill -s SIGUSR1 blocky` for docker setup
|
|
@ -0,0 +1,768 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--?xml version="1.0" encoding="UTF-8" standalone="no"?-->
|
||||
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="127.60016mm"
|
||||
height="140.63579mm"
|
||||
viewBox="0 0 452.12655 498.31578"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="knight.svg"
|
||||
style="enable-background:new"
|
||||
inkscape:export-filename="f:\z-knight.png"
|
||||
inkscape:export-xdpi="221.17"
|
||||
inkscape:export-ydpi="221.17">
|
||||
<defs
|
||||
id="defs4">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3542">
|
||||
<rect
|
||||
style="fill:#0000ff;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="rect3544"
|
||||
width="1093.424"
|
||||
height="1518.2074"
|
||||
x="-1174.7097"
|
||||
y="-295.40738" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="271.22743"
|
||||
inkscape:cy="312.25755"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer12"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1028"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-global="false"
|
||||
showguides="false"
|
||||
fit-margin-top="5"
|
||||
fit-margin-right="5"
|
||||
fit-margin-bottom="5"
|
||||
fit-margin-left="5">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4305"
|
||||
originx="-187.25692"
|
||||
originy="-329.63927" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:rdf>
|
||||
<cc:work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:work>
|
||||
</rdf:rdf>
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer11"
|
||||
inkscape:label="background"
|
||||
style="display:inline"
|
||||
transform="translate(-187.25691,-224.40713)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="gopher-body"
|
||||
style="display:inline;opacity:1"
|
||||
transform="translate(-187.25691,-224.40713)">
|
||||
<g
|
||||
transform="matrix(-0.20408679,0.36109427,0.8060854,0.48598006,306.56137,186.46802)"
|
||||
id="g4640"
|
||||
style="display:inline;opacity:1">
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 767.29926,387.32674 c 11.1235,7.96555 31.77795,11.29978 44.73159,15.54502 12.95363,4.24526 18.14889,9.35948 22.12936,13.37285 3.98046,4.01338 5.94428,7.14463 4.71807,9.52723 -1.2262,2.38259 -5.54351,3.99405 -14.00119,4.81166 -8.45765,0.81761 -15.90978,0.12055 -23.02358,-1.72572 -7.11381,-1.84628 -13.80694,-4.86649 -21.70559,-8.603 -7.89866,-3.73649 -17.3272,-8.0507 -25.81115,-14.18439 -8.48395,-6.13369 -17.62324,-13.90003 -23.14238,-24.13356 -5.51915,-10.23352 -5.78201,-21.34406 -5.37146,-30.88264 0.41055,-9.53859 1.51092,-17.55377 2.71572,-23.74931 1.20482,-6.19553 2.71509,-10.67437 4.77102,-13.66952 2.05591,-2.99513 4.65165,-4.52673 7.71923,-4.52673 3.06759,0 5.70357,1.83092 7.62535,5.49926 1.9218,3.66832 3.04778,9.24444 3.28639,16.76004 0.23861,7.51561 -0.67126,17.08072 0.34029,27.19831 1.01155,10.1176 3.89485,20.79494 15.01833,28.7605 z"
|
||||
id="path4642"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#e1d6b9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 760.81735,387.61463 c 8.35351,7.22933 23.40419,11.34465 36.92829,14.85447 13.52408,3.50986 21.76315,7.50998 26.41399,11.29491 4.65086,3.78492 7.04347,6.96136 6.89289,9.28045 -0.15059,2.31908 -3.07202,3.85186 -9.99413,4.53735 -6.92209,0.68549 -13.12478,-0.17957 -19.18856,-2.15841 -6.06375,-1.97886 -12.01277,-5.06603 -19.62326,-8.64782 -7.61047,-3.5818 -16.94465,-7.61787 -24.98938,-13.21535 -8.04472,-5.59749 -15.82286,-12.65396 -20.9022,-21.24583 -5.07935,-8.59186 -6.01346,-17.801 -5.99188,-25.91871 0.0216,-8.1177 0.93462,-15.14861 1.86635,-20.66954 0.93173,-5.52092 2.01706,-9.59713 3.38259,-12.30465 1.36554,-2.70753 3.03466,-4.06947 5.01979,-4.01398 1.98511,0.0555 3.57672,1.84704 4.61437,5.2751 1.03765,3.42807 1.44745,8.54444 1.4737,15.15288 0.0262,6.60845 -0.43638,14.76057 0.91317,23.27473 1.34954,8.51418 4.83074,17.27506 13.18427,24.5044 z"
|
||||
id="path4644"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
id="g4533-2"
|
||||
transform="matrix(-0.60102903,0.32221978,0.53870829,0.77401445,526.12645,47.501077)" />
|
||||
<g
|
||||
style="opacity:1"
|
||||
transform="matrix(-0.32879267,0.17361606,0.20143296,0.28338802,180.57308,285.47661)"
|
||||
id="g4404">
|
||||
<path
|
||||
sodipodi:nodetypes="sssssssssssssssss"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4406"
|
||||
d="m -626.54672,402.3529 c 2.22767,10.86299 0.34493,21.82632 -3.86747,31.42527 -4.21252,9.59894 -10.55173,17.86115 -17.72096,24.29983 -7.1694,6.43883 -15.25476,11.10591 -24.5716,13.61353 -9.31698,2.50761 -20.94966,4.46936 -31.63903,1.98398 -10.68939,-2.48537 -18.0688,-9.22838 -24.09401,-15.89285 -6.02508,-6.66442 -12.35923,-14.47524 -22.96531,-22.06805 -10.60584,-7.59266 -20.8648,-15.59839 -25.16123,-23.3775 -4.29632,-7.77931 -7.008,-15.66934 -7.81517,-23.39095 -0.80717,-7.7215 0.35908,-14.55922 3.12288,-20.54462 2.76393,-5.98548 7.12557,-11.1208 12.7854,-15.40902 5.65998,-4.28811 12.61751,-7.73606 20.64204,-10.24271 8.02465,-2.50651 17.11262,-4.07552 27.13941,-4.41504 10.0268,-0.3395 20.06604,0.59388 29.76158,2.87504 9.69543,2.2813 19.05511,5.92037 27.47739,11.02309 8.42215,5.10286 15.89307,11.69212 21.60465,19.6287 5.71147,7.93674 13.0738,19.62846 15.30143,30.4913 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#96d6ff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
sodipodi:nodetypes="csssccscsccscscccsccscsssscscscc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4408"
|
||||
d="m -784.21409,457.33922 c -0.56136,0.0656 -1.08141,0.1809 -1.55606,0.33615 -0.63289,0.20699 -1.18396,0.48516 -1.6349,0.82686 -0.45093,0.3417 -0.80184,0.74659 -1.02778,1.21891 -0.22595,0.47234 -0.32669,1.01119 -0.27449,1.62035 0.0522,0.60917 0.25282,1.23371 0.57968,1.84938 0.32687,0.61567 0.98957,1.25218 1.83531,1.84156 0.84574,0.58937 1.35671,1.20529 1.82543,1.72857 0.46713,0.52147 1.13451,0.85371 2.02424,0.92674 0.10253,0.008 0.12328,-0.30471 0.0344,-0.32876 -0.78083,-0.20262 -1.25826,-0.72023 -1.71877,-1.11076 -0.4254,-0.46645 -0.87231,-1.01406 -1.62104,-1.54604 -0.74871,-0.53197 -1.47289,-1.09304 -1.77689,-1.63886 -0.30398,-0.54584 -0.49685,-1.10009 -0.55469,-1.64239 -0.0579,-0.54231 0.0245,-1.0222 0.21918,-1.44322 0.19469,-0.42103 0.50198,-0.78371 0.90168,-1.08623 0.39973,-0.30252 0.89062,-0.54587 1.4577,-0.7237 0.28355,-0.0889 0.5872,-0.16119 0.90722,-0.21465 0.32002,-0.0535 0.6576,-0.0885 1.01178,-0.10163 0.70839,-0.0255 1.4163,0.0392 2.10043,0.1987 0.68412,0.15947 1.34499,0.41522 1.93838,0.77329 0.59338,0.35806 1.11885,0.81986 1.52108,1.37653 0.40222,0.55667 0.92117,1.37523 1.07925,2.13677 0.12981,0.62539 0.0734,1.25844 -0.13288,1.83379 -0.0385,0.10712 0.4977,0.29416 0.62787,-0.0111 0.24265,-0.5698 0.23445,-1.24057 0.1026,-1.8741 -0.17834,-0.85666 -0.69031,-1.76937 -1.13671,-2.40019 -0.4464,-0.6308 -1.03123,-1.15292 -1.68895,-1.55276 -0.65772,-0.39984 -1.38674,-0.68003 -2.14271,-0.85021 -0.75599,-0.17016 -1.54036,-0.23166 -2.32498,-0.19142 -0.19617,0.0101 -0.38815,0.0268 -0.57528,0.0484 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
transform="matrix(13.851095,0,0,13.851095,10133.213,-6001.611)" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -753.77185,413.0219 c -0.13663,-2.61847 2.18018,-4.94804 7.2193,-6.20054 7.65443,-1.90257 20.03831,1.84566 27.93811,5.67152 4.33357,2.09883 8.88981,3.89076 12.66635,7.19411 1.28185,1.12133 2.51799,2.28349 3.36855,4.40869 -1.65849,0.577 -4.10492,-0.92134 -5.87278,-2.13046 -6.96771,-4.76531 -14.69502,-8.08983 -22.67695,-9.12646 -6.71591,-0.87187 -8.86923,-3.11022 -14.75541,-2.56175 -3.72583,0.34716 -4.90626,2.13878 -7.88716,2.74489 z"
|
||||
id="path4365-1-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cssscsssc" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -720.16989,411.68353 c 0.28532,-2.32502 0.86962,3.90377 -0.31886,5.45995 -4.46007,5.84 -8.20289,12.32072 -12.42083,18.36519 -1.37385,1.96787 -3.29463,0.0414 -2.42738,-2.09874 0.88118,-2.1739 2.06053,-3.99898 3.34915,-5.8153 1.20809,-1.70147 2.81353,-3.0576 3.88834,-4.85958 2.06619,-3.46267 2.39577,-6.62873 4.25443,-10.2393 0.63712,-1.23818 3.5225,0.42546 3.67386,-0.80905 z"
|
||||
id="path4367-9-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssss" />
|
||||
</g>
|
||||
<g
|
||||
style="display:inline;opacity:1"
|
||||
id="g4198"
|
||||
transform="matrix(0.69027452,0,0,0.73815345,662.65805,219.87628)">
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -140.71724,398.66408 c -9.31409,71.69689 -25.7611,141.32 -83.87724,188.8641 -73.31672,59.97949 -186.48319,61.17047 -281.81894,4.26066 -27.57065,-16.45805 -49.52457,-62.17665 -53.04177,-91.74122 -7.35191,-61.79791 -1.78113,-96.91393 -8.12884,-153.94253 -5.05249,-45.39216 -29.63784,-82.95495 -27.30836,-137.00138 1.56315,-36.26681 11.06536,-78.46439 40.50727,-100.88356 38.57103,-29.370718 83.60539,-46.188952 134.68095,-45.031125 72.73731,1.648875 151.17838,6.326503 212.18714,49.939365 43.544,31.12796 68.50323,82.53699 72.90385,135.3004 4.52019,54.19698 -0.16075,104.48555 -6.10406,150.23529 z"
|
||||
id="path4188"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8dc9f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -158.93683,464.92976 c -15.56115,65.9367 -58.42288,127.39267 -134.42207,151.72082 -70.61462,22.6045 -141.88424,10.56397 -210.57664,-32.28314 -26.14623,-16.30879 -46.09162,-61.46233 -48.95901,-89.47579 -6.03547,-58.9646 -2.56071,-95.43877 -8.30519,-149.8595 -4.7951,-45.42661 -28.02123,-78.34585 -27.29597,-132.22289 0.47399,-35.21112 8.99044,-76.95773 37.82112,-98.79995 36.52466,-27.671205 78.3526,-45.238515 126.45621,-45.012482 76.22124,0.358155 162.16208,5.533182 222.84373,56.658952 55.47879,46.74224 63.38318,129.04796 60.81019,193.3049 -2.12217,52.99813 -7.67242,100.63054 -18.37237,145.96908 z"
|
||||
id="ellipse4190"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssss" />
|
||||
</g>
|
||||
<g
|
||||
id="g4376"
|
||||
transform="matrix(0.40138799,-0.13710458,0.13710458,0.40138799,491.2872,42.949045)"
|
||||
style="opacity:1">
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#96d6ff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -626.57295,401.69566 c 2.24713,11.35067 0.36741,22.38948 -3.843,32.03835 -4.21053,9.64886 -10.54997,17.90531 -17.7192,24.34399 -7.1694,6.43883 -15.25457,11.1106 -24.57171,13.61082 -9.31727,2.5002 -20.94956,4.47176 -31.64526,1.82793 -10.69571,-2.64383 -18.09209,-9.81214 -24.14818,-17.25062 -6.05597,-7.43843 -12.44269,-16.56671 -23.09665,-25.35944 -10.65372,-8.79255 -20.95218,-17.78817 -25.30072,-26.87318 -4.34843,-9.08528 -7.1154,-18.36084 -7.98,-27.52156 -0.86459,-9.1606 0.24716,-17.36404 2.9617,-24.58398 2.71467,-7.22004 7.03243,-13.45488 12.66059,-18.5369 5.6283,-5.08191 12.56665,-9.01064 20.59229,-11.48936 8.02576,-2.47858 17.13537,-3.50537 27.20916,-2.66707 10.0738,0.83832 20.1809,3.47234 29.95223,7.6529 9.77122,4.18068 19.21426,9.9086 27.71179,16.89733 8.49741,6.98886 16.03465,15.24007 21.79567,24.41557 5.7609,9.17565 13.1742,22.14471 15.42129,33.49522 z"
|
||||
id="path4398"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -784.27135,455.90422 c -0.56339,0.0147 -1.08437,0.10666 -1.55902,0.26191 -0.63289,0.20699 -1.18231,0.52669 -1.63059,0.93484 -0.44828,0.40815 -0.79558,0.90361 -1.01756,1.4752 -0.22199,0.5716 -0.31844,1.21792 -0.26185,1.93717 0.0566,0.71926 0.26134,1.4471 0.59196,2.157 0.33063,0.7099 0.99621,1.41858 1.84494,2.08284 0.84872,0.66425 1.36325,1.36931 1.83382,1.93901 0.46898,0.56774 1.13678,0.9105 2.02675,0.98962 0.10256,0.009 0.12294,-0.31321 0.034,-0.33899 -0.78143,-0.21746 -1.26048,-0.77583 -1.72293,-1.21489 -0.42768,-0.5236 -0.87838,-1.16625 -1.63058,-1.78505 -0.75217,-0.61879 -1.47924,-1.25213 -1.78697,-1.89162 -0.30772,-0.63951 -0.50455,-1.29287 -0.56648,-1.9378 -0.062,-0.64492 0.0165,-1.22191 0.20772,-1.73042 0.1912,-0.50852 0.49539,-0.94884 0.89287,-1.30706 0.3975,-0.35822 0.88707,-0.63484 1.45426,-0.80994 0.2836,-0.0875 0.58767,-0.1494 0.90851,-0.1822 0.32084,-0.0328 0.65966,-0.0369 1.01552,-0.008 0.71174,0.0585 1.42446,0.24383 2.11396,0.53794 0.6895,0.29412 1.35628,0.69807 1.95502,1.19025 0.59873,0.49218 1.12894,1.07271 1.53474,1.71893 0.4058,0.64623 0.9285,1.5589 1.08808,2.35795 0.13104,0.65619 0.075,1.29927 -0.13103,1.88026 -0.0384,0.10817 0.49808,0.30362 0.62824,-0.002 0.24262,-0.57052 0.23429,-1.24452 0.10166,-1.89748 -0.17938,-0.88293 -0.69436,-1.871 -1.14416,-2.58711 -0.44981,-0.71609 -1.03943,-1.35821 -1.70275,-1.89855 -0.66333,-0.54034 -1.3987,-0.97968 -2.16052,-1.29649 -0.76184,-0.31679 -1.55154,-0.51173 -2.33984,-0.56369 -0.19709,-0.013 -0.38986,-0.0163 -0.57767,-0.0116 z"
|
||||
id="path4369"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="csssccscsccscscccsccscsssscscscc"
|
||||
transform="matrix(13.851095,0,0,13.851095,10133.213,-6001.611)" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -730.27274,382.91266 c 1.8068,-2.76405 6.31309,-3.63001 13.24575,-1.6171 10.53068,3.05761 22.43414,14.97755 28.94834,24.04709 3.57338,4.97534 7.6424,9.78266 9.64772,15.62449 0.68055,1.98294 1.27611,3.97774 0.68898,6.70435 -2.4056,-0.49416 -4.1871,-3.62313 -5.37952,-6.01329 -4.69962,-9.4202 -11.38574,-17.86492 -20.09536,-24.13889 -7.3284,-5.27852 -8.20487,-8.9719 -15.61502,-12.25742 -4.69053,-2.07967 -7.44128,-1.02076 -11.44089,-2.34923 z"
|
||||
id="path4365-1"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cssscsssc" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -689.31909,403.49962 c 2.08771,-2.1886 -1.9021,4.5559 -4.48533,5.36905 -9.69439,3.05157 -19.01784,7.22624 -28.57811,10.64488 -3.11327,1.11257 -3.94795,-2.11026 -1.30738,-3.72982 2.68251,-1.64492 5.45711,-2.73872 8.35507,-3.75217 2.71578,-0.94874 5.64428,-1.2851 8.27731,-2.4236 5.06052,-2.18718 7.83343,-5.20599 12.75841,-7.67984 1.68866,-0.84854 3.86766,2.73608 4.97603,1.5739 z"
|
||||
id="path4367-9"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssss" />
|
||||
</g>
|
||||
<g
|
||||
id="g4634"
|
||||
transform="matrix(0.13058783,-0.42795023,-0.60869797,-0.11092817,657.59614,910.80957)"
|
||||
style="display:inline;opacity:1">
|
||||
<path
|
||||
sodipodi:nodetypes="sssssssssssssssss"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4636"
|
||||
d="m 423.50332,581.83521 c -0.004,4.40048 -1.19837,7.58856 -3.37524,9.82844 -2.17687,2.23987 -5.33154,3.55156 -9.14619,4.44292 -3.81465,0.89135 -8.28246,1.39523 -13.05675,1.83828 -4.77428,0.44304 -9.85163,0.79076 -14.95001,1.09928 -5.09838,0.30851 -9.94541,0.34741 -14.40217,0.0862 -4.45676,-0.26122 -8.52354,-0.79908 -11.99271,-1.71189 -3.46915,-0.91282 -6.33736,-2.21356 -8.3562,-4.09288 -2.01885,-1.87935 -3.18709,-4.34475 -3.25466,-7.51083 -0.0676,-3.16607 0.9983,-5.4859 2.92534,-7.0838 1.92703,-1.5979 4.71248,-2.46394 8.09977,-2.84688 3.38729,-0.38293 7.37282,-0.28336 11.77044,-0.16051 4.39762,0.12284 9.21051,0.23456 14.33166,-0.12202 5.12115,-0.35659 10.27171,-1.47349 15.16022,-2.54099 4.88852,-1.06749 9.50395,-2.05149 13.43823,-2.27114 3.9343,-0.21967 7.17754,0.32322 9.39823,2.04598 2.22069,1.72276 3.41425,4.59936 3.41004,8.99986 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#e1d6b9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
sodipodi:nodetypes="csscsscssssssssssssssssssssccsssc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4638"
|
||||
d="m 411.91406,568.54883 c -3.75011,-0.0271 -8.08701,0.53975 -12.76172,1.28711 -5.34251,0.85413 -11.10706,1.92059 -17.00976,2.32617 -5.9027,0.40562 -11.41103,0.38326 -16.44727,0.41406 -5.03624,0.0309 -9.6045,0.1607 -13.50781,0.85938 -3.9033,0.69867 -7.13503,1.96743 -9.4082,3.96875 -2.27316,2.00131 -3.58535,4.71676 -3.65235,8.17578 -0.067,3.45901 1.21821,6.3073 3.54297,8.58008 2.32476,2.27278 5.68789,3.9795 9.76172,5.25 4.07385,1.27051 8.85237,2.11894 14.05664,2.59765 5.20427,0.47871 10.83381,0.56134 16.70313,0.22266 5.86931,-0.33868 11.47146,-0.78653 16.60547,-1.34961 5.13399,-0.56309 9.79334,-1.22365 13.70703,-2.34375 1.48913,-0.4262 2.86677,-0.9287 4.12695,-1.51953 2.54507,-1.19325 2.05015,-6.17249 -0.0996,-4.54102 -1.99172,1.51153 -4.14364,1.68162 -7.15735,2.35061 -3.67269,0.81527 -8.18136,0.99111 -12.55008,1.3428 -4.3687,0.35167 -8.7789,1.78431 -13.31332,2.07736 -4.53444,0.29304 -8.86787,0.32801 -12.93181,0.0702 -4.06396,-0.25785 -7.85651,-0.78075 -11.12475,-1.64296 -3.26823,-0.86221 -5.99695,-2.08037 -7.8846,-3.81399 -1.88765,-1.73365 -2.92537,-3.9871 -2.97865,-6.80086 -0.0533,-2.81374 0.90176,-4.8192 2.66881,-6.10562 1.76704,-1.28641 5.61732,-0.58475 8.69196,-0.71399 3.07463,-0.12925 6.90624,-0.54484 10.78772,-0.41733 3.88147,0.12754 6.54592,-0.48119 11.04844,-1.2139 4.50252,-0.73264 9.15212,-2.3434 13.88736,-3.72101 4.73523,-1.37761 9.22461,-2.34259 13.00861,-2.55385 0.473,-0.0264 0.93707,-0.0422 1.38868,-0.0449 1.16046,-0.007 2.25007,0.0442 3.25,0.23633 1.15313,0.22156 2.31543,-2.86146 -0.83789,-2.92773 -0.51177,-0.0108 -1.03459,-0.045 -1.57032,-0.0488 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer9"
|
||||
inkscape:label="helmet-inside"
|
||||
transform="translate(-50.232073,-102.51234)"
|
||||
style="display:none">
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 424.59552,206.08005 52.3701,17.23572 -0.33146,47.39826 -28.8367,11.93242 z"
|
||||
id="path4783"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="gopher-face"
|
||||
style="display:inline"
|
||||
transform="translate(-187.25691,-224.40713)">
|
||||
<g
|
||||
id="g4818"
|
||||
transform="matrix(-0.65610141,0,0,0.65610141,666.99278,177.20199)">
|
||||
<path
|
||||
sodipodi:nodetypes="sssssssssssssssss"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4812"
|
||||
d="m 547.42756,318.16456 c -0.44046,14.77191 -4.12869,29.02667 -10.38967,42.25266 -6.26099,13.22599 -15.09198,25.42687 -25.80466,35.99686 -10.71268,10.57 -23.30432,19.50822 -37.11826,26.08983 -13.81394,6.58161 -28.85103,10.80263 -44.50193,11.8618 -15.65091,1.05917 -30.4406,-1.15844 -43.81781,-6.16756 -13.37721,-5.00911 -25.3405,-12.8075 -35.30087,-22.80416 -9.96037,-9.99666 -17.91599,-22.19037 -23.26581,-35.90798 -5.34983,-13.71761 -8.0915,-28.95913 -7.64195,-44.98105 0.44955,-16.02192 4.04447,-31.2937 10.1422,-45.07896 6.09773,-13.78526 14.69591,-26.08175 25.16951,-36.25747 10.4736,-10.17571 22.82245,-18.23043 36.46168,-23.66123 13.63924,-5.4308 28.57214,-8.24285 44.22923,-8.02541 15.6571,0.21745 30.56095,3.42714 44.11009,8.94154 13.54914,5.5144 25.7404,13.33722 35.92568,22.91495 10.18529,9.57774 18.36233,20.91345 23.87736,33.53282 5.51504,12.61936 8.36566,26.52144 7.92521,41.29336 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
sodipodi:nodetypes="sssssssssssssssss"
|
||||
inkscape:connector-curvature="0"
|
||||
id="ellipse4814"
|
||||
d="m 539.72249,314.79002 c 10e-4,13.89984 -3.01572,27.53808 -8.51346,40.35257 -5.49774,12.81449 -13.48047,24.80543 -23.37659,35.2527 -9.89612,10.44726 -21.70519,19.34133 -34.78531,25.87862 -13.08011,6.53727 -27.4256,10.71236 -42.3773,11.7667 -14.9517,1.05435 -29.09103,-1.11258 -41.85904,-5.93108 -12.76803,-4.81852 -24.16883,-12.28715 -33.66552,-21.79076 -9.49671,-9.50362 -17.08979,-21.04298 -22.23241,-33.95465 -5.14261,-12.91166 -7.83328,-27.19561 -7.52333,-42.13595 0.30995,-14.94034 3.58995,-29.10832 9.22975,-41.85842 5.63981,-12.7501 13.63743,-24.08168 23.39638,-33.47108 9.75897,-9.38941 21.27795,-16.83842 34.00359,-21.94183 12.72563,-5.10342 26.66067,-7.86812 41.28534,-7.94317 14.62467,-0.0751 28.55938,2.53224 41.26083,7.24431 12.70145,4.71207 24.16709,11.5339 33.81555,20.03646 9.64847,8.50257 17.47884,18.68937 22.90117,30.21241 5.42232,11.52304 8.43889,24.38332 8.44035,38.28317 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<circle
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394455;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="path4828"
|
||||
cx="383.30621"
|
||||
cy="309.06165"
|
||||
r="30.809652" />
|
||||
<circle
|
||||
r="15.152287"
|
||||
cy="294.91949"
|
||||
cx="369.66916"
|
||||
id="circle4830"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(-0.49821858,-0.255998,-0.255998,0.49821858,861.52844,319.81615)"
|
||||
id="g4822">
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 544.2609,323.96628 c -5.95391,12.33766 -15.20034,24.2228 -25.89846,35.91934 -10.69814,11.69654 -22.74349,23.28172 -34.52447,34.21851 -11.78099,10.93679 -23.27607,21.15489 -34.23709,29.30247 -10.96102,8.14759 -21.47285,14.18083 -32.04267,16.95199 -10.56982,2.77117 -20.29711,2.02561 -29.30402,-1.67713 -9.00692,-3.70274 -20.58076,-7.76561 -27.66538,-16.71749 -7.08461,-8.95188 -12.84054,-20.18257 -16.5035,-33.03389 -3.66297,-12.85133 -5.229,-27.32914 -3.92417,-42.72858 1.30484,-15.39944 5.36688,-30.24976 11.81788,-43.75488 6.45101,-13.5051 15.29008,-25.65823 26.00811,-35.78271 10.71803,-10.12447 28.44246,-20.29305 42.24879,-25.86698 13.80633,-5.57394 28.83304,-8.62768 44.20973,-8.80364 15.3767,-0.17594 29.62737,2.52591 41.94358,7.37479 12.31622,4.84887 22.69735,11.85058 30.35956,20.34718 7.66222,8.49661 12.60139,18.48263 14.06496,29.34879 1.4636,10.86615 -0.59894,22.56457 -6.55285,34.90223 z"
|
||||
id="path4824"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 538.18032,322.65868 c -5.17728,11.63182 -13.27733,23.10077 -22.96883,34.40428 -9.69151,11.30351 -20.93897,22.46482 -32.34413,32.7753 -11.40514,10.31051 -22.90789,19.71873 -33.85893,27.13351 -10.95103,7.41476 -21.39599,12.82014 -31.59528,15.28718 -10.19931,2.46703 -19.30202,1.76338 -27.56839,-1.62958 -8.26637,-3.39295 -19.13397,-6.9512 -25.3913,-15.16185 -6.25732,-8.21068 -11.24381,-18.53447 -14.30417,-30.37519 -3.06035,-11.84072 -4.18965,-25.20221 -2.68634,-39.42576 1.5033,-14.22354 5.50837,-27.94818 11.67956,-40.43838 6.17119,-12.4902 14.50792,-23.74111 24.54768,-33.13895 10.03978,-9.39782 26.99021,-19.0621 39.83566,-24.2929 12.84546,-5.2308 26.78412,-8.15811 41.0009,-8.45853 14.21678,-0.30038 27.34319,2.03758 38.64284,6.33106 11.29965,4.29349 20.7704,10.54463 27.74089,18.16875 6.97048,7.62413 11.43794,16.6127 12.81335,26.51165 1.37541,9.89894 -0.36624,20.67759 -5.54351,32.30941 z"
|
||||
id="path4826"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
<circle
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394455;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="path4828-0"
|
||||
cx="441.87363"
|
||||
cy="238.13869"
|
||||
r="27.721321"
|
||||
transform="rotate(9.4590451)" />
|
||||
<circle
|
||||
r="13.633434"
|
||||
cy="224.78665"
|
||||
cx="434.41431"
|
||||
id="circle4830-3"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
transform="rotate(9.4590451)" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer14"
|
||||
inkscape:label="arm"
|
||||
transform="translate(-50.232073,-102.51234)"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:5.73911715;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4964"
|
||||
width="54.988182"
|
||||
height="16.638491"
|
||||
x="-98.824585"
|
||||
y="482.47922"
|
||||
transform="rotate(-29.393453)" />
|
||||
<path
|
||||
style="opacity:1;fill:#aeece6;fill-opacity:1;stroke:none;stroke-width:4.50708008;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 200.04708,436.85004 12.61982,22.5354 151.43789,-73.91611 30.64815,-29.29602 -44.07934,10.20407 z"
|
||||
id="path4949"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#d3f5f1;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 421.07031,379.91797 -47.01758,10.88476 -160.66992,75.16993 1.25391,2.24023 159.59961,-75.68359 41.54297,-9.54492 -28.10742,26.86914 -161.98829,78.08593 0.94532,1.68946 0.32031,0.33007 161.42969,-78.79101 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4974"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#d3f5f1;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 204.60937,441.01706 148.35938,-69.375 27.42187,-7.96875 -21.79687,15.70312 -149.29688,71.25 z"
|
||||
id="path4972"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.50708008;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 187.24472,433.75337 1.2748,-7.64876 8.92357,1.59349 22.3089,38.24384 -2.86829,7.33007 -8.60486,-4.46178 z"
|
||||
id="path4953"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 196.22213,428.9844 -6.29767,-0.82864 22.70476,38.61467 6.64089,-0.66292 -4.60016,-2.28343 -20.57873,-33.47364 z"
|
||||
id="path4955"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 148.82388,459.97557 -2.15446,11.60097 9.28077,14.91554 10.93806,0.66291 -0.82864,-5.96622 -10.60661,-18.56155 z"
|
||||
id="path4966"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 149.65252,462.4615 c 0.16573,1.49155 9.11504,15.9099 9.11504,15.9099 l -1.823,5.80048 -8.78359,-13.42398 z"
|
||||
id="path4968"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 193.59375,430.23581 20.50781,33.98437 1.17188,0.70313 0.35156,-1.05469 -20.15625,-33.28125 z"
|
||||
id="path4970"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer7"
|
||||
inkscape:label="gopher-mouth"
|
||||
style="display:inline"
|
||||
transform="translate(-187.25691,-224.40713)">
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 498.0625,437.94867 -6.36763,0.0828 -3.71113,-0.0821 c -1.18372,-0.0262 -2.23819,0.53559 -3.00662,1.36379 -0.76845,0.82822 -1.14658,1.97521 -1.32551,3.22687 l -1.01303,7.08562 -1.40711,7.111 c -0.25342,1.28069 0.0841,2.40965 0.70518,3.23132 0.6211,0.82165 1.57363,1.28978 2.69674,1.31649 l 3.7446,0.0891 7.40657,-0.17258 c 1.42055,-0.0331 2.74014,-0.58514 3.70785,-1.43299 0.96771,-0.84787 1.54004,-2.00084 1.65553,-3.2592 l 0.6476,-7.05621 0.52522,-7.04505 c 0.0935,-1.25398 -0.46676,-2.37726 -1.25366,-3.18163 -0.78689,-0.80437 -1.85738,-1.2842 -3.00457,-1.27716 z"
|
||||
id="rect4659"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="scssscssscssscsss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 496.89993,440.09359 -5.19684,0.0698 -2.47497,-0.10149 c -0.94018,-0.0386 -1.80825,0.43586 -2.46124,1.11384 -0.65298,0.67797 -1.03424,1.61771 -1.21175,2.64338 l -1.0026,5.79325 -1.25494,5.80832 c -0.22406,1.03701 0.002,1.97056 0.48938,2.64162 0.48783,0.67105 1.26653,1.03411 2.19892,1.07115 l 2.54193,0.101 5.88547,-0.12754 c 1.11447,-0.0242 2.17518,-0.47212 2.97321,-1.1643 0.79803,-0.69218 1.30904,-1.6349 1.43939,-2.66511 l 0.73009,-5.77006 0.63032,-5.76301 c 0.11259,-1.02637 -0.28558,-1.94744 -0.89178,-2.6062 -0.60618,-0.65877 -1.45658,-1.05733 -2.39458,-1.04471 z"
|
||||
id="rect4661"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="scssscssscssscsss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 467.92106,431.94061 c 0.17729,2.27145 1.57656,4.32647 3.56538,6.17684 1.98881,1.85037 4.73553,3.49055 7.9169,4.83408 3.18137,1.34353 6.76993,2.37673 10.40491,2.92876 3.63499,0.55204 7.31771,0.61337 10.93742,0.17695 3.61969,-0.43645 6.8614,-1.30517 9.67542,-2.37849 2.81402,-1.07332 5.17844,-2.3467 7.04073,-3.75925 1.86231,-1.41254 3.23922,-2.97722 4.10853,-4.72358 0.86932,-1.74636 1.22997,-3.67959 0.91461,-5.76285 -0.31535,-2.08326 -1.29186,-4.11481 -2.79935,-5.98131 -1.5075,-1.86649 -3.53491,-3.56576 -5.91642,-4.97983 -2.3815,-1.41407 -5.11304,-2.54212 -8.12844,-3.28158 -3.0154,-0.73946 -6.31783,-1.09096 -9.93094,-0.97174 -3.6131,0.11924 -7.2186,0.69446 -10.6419,1.64517 -3.4233,0.95069 -6.6496,2.2832 -9.33875,3.91065 -2.68913,1.62746 -4.89892,3.50256 -6.18894,5.61926 -1.32139,2.16817 -1.77021,4.61153 -1.61916,6.54692 z"
|
||||
id="ellipse4650"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#e1d0cb;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 475.57039,431.43056 c 0.31019,1.80429 1.36577,3.48937 2.98663,4.99917 1.62086,1.5098 3.80505,2.84719 6.28703,3.91437 2.48197,1.06719 5.24944,1.8562 8.07117,2.27071 2.82174,0.4145 5.70079,0.45265 8.53169,0.10713 2.83089,-0.34553 5.35911,-1.02976 7.553,-1.90451 2.19389,-0.87475 4.04484,-1.93848 5.497,-3.12538 1.45217,-1.1869 2.50911,-2.50179 3.13219,-3.93394 0.62308,-1.43214 0.81446,-2.98543 0.48985,-4.63056 -0.32461,-1.64514 -1.13916,-3.22548 -2.3414,-4.6674 -1.20224,-1.44192 -2.78948,-2.74346 -4.65903,-3.82078 -1.86955,-1.07733 -4.01937,-1.92982 -6.38974,-2.4811 -2.37037,-0.55129 -4.96168,-0.80162 -7.76722,-0.68542 -2.80553,0.11621 -5.57317,0.58631 -8.1874,1.34158 -2.61424,0.75528 -5.07126,1.79757 -7.14628,3.06167 -2.07504,1.26412 -3.75959,2.75051 -4.8326,4.37276 -1.07302,1.62225 -1.53509,3.37741 -1.22489,5.1817 z"
|
||||
id="ellipse4652"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 485.60866,420.41917 c 0.45232,1.29294 1.43586,2.44115 2.79664,3.4102 1.36078,0.96906 3.0934,1.76079 4.97332,2.36791 1.87992,0.60712 3.89927,1.0315 5.87533,1.25741 1.97606,0.2259 3.90879,0.25223 5.71982,0.052 1.81102,-0.20028 3.33955,-0.60742 4.63321,-1.17435 1.29367,-0.56695 2.35232,-1.29343 3.18646,-2.14861 0.83413,-0.85519 1.44471,-1.8405 1.79916,-2.93195 0.35445,-1.09146 0.45213,-2.29028 0.21175,-3.55738 -0.24038,-1.2671 -0.80099,-2.48156 -1.64917,-3.57911 -0.84818,-1.09755 -1.9831,-2.07741 -3.35494,-2.8723 -1.37184,-0.7949 -2.98056,-1.40441 -4.76729,-1.7664 -1.78672,-0.36199 -3.75169,-0.47615 -5.82322,-0.29097 -2.07153,0.18518 -4.05358,0.65136 -5.84566,1.3298 -1.79207,0.67844 -3.39432,1.56902 -4.69144,2.60198 -1.29713,1.03296 -2.28898,2.20893 -2.84443,3.45293 -0.55546,1.24399 -0.67186,2.55593 -0.21954,3.84888 z"
|
||||
id="path4648"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssssss" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer12"
|
||||
inkscape:label="gopher-hands"
|
||||
style="display:inline"
|
||||
transform="translate(-187.25691,-224.40713)">
|
||||
<g
|
||||
id="g4533"
|
||||
transform="matrix(-0.18127985,-0.40906258,-0.53287823,0.31441862,696.3032,549.41392)">
|
||||
<path
|
||||
sodipodi:nodetypes="sssssssssssssssss"
|
||||
inkscape:connector-curvature="0"
|
||||
id="ellipse4523"
|
||||
d="m 423.50332,581.83521 c -0.004,4.40048 -1.19837,7.58856 -3.37524,9.82844 -2.17687,2.23987 -5.33154,3.55156 -9.14619,4.44292 -3.81465,0.89135 -8.28246,1.39523 -13.05675,1.83828 -4.77428,0.44304 -9.85163,0.79076 -14.95001,1.09928 -5.09838,0.30851 -9.94541,0.34741 -14.40217,0.0862 -4.45676,-0.26122 -8.52354,-0.79908 -11.99271,-1.71189 -3.46915,-0.91282 -6.33736,-2.21356 -8.3562,-4.09288 -2.01885,-1.87935 -3.18709,-4.34475 -3.25466,-7.51083 -0.0676,-3.16607 0.9983,-5.4859 2.92534,-7.0838 1.92703,-1.5979 4.71248,-2.46394 8.09977,-2.84688 3.38729,-0.38293 7.37282,-0.28336 11.77044,-0.16051 4.39762,0.12284 9.21051,0.23456 14.33166,-0.12202 5.12115,-0.35659 10.27171,-1.47349 15.16022,-2.54099 4.88852,-1.06749 9.50395,-2.05149 13.43823,-2.27114 3.9343,-0.21967 7.17754,0.32322 9.39823,2.04598 2.22069,1.72276 3.41425,4.59936 3.41004,8.99986 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#e1d6b9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
sodipodi:nodetypes="ssscsscssssssssssssssssssssccsss"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4521"
|
||||
d="m 411.91406,568.54883 c -3.75011,-0.0271 -8.08701,0.53975 -12.76172,1.28711 -5.34251,0.85413 -11.10706,1.92059 -17.00976,2.32617 -5.9027,0.40562 -11.41103,0.38326 -16.44727,0.41406 -5.03624,0.0309 -9.6045,0.1607 -13.50781,0.85938 -3.9033,0.69867 -7.13503,1.96743 -9.4082,3.96875 -2.27316,2.00131 -3.58535,4.71676 -3.65235,8.17578 -0.067,3.45901 1.21821,6.3073 3.54297,8.58008 2.32476,2.27278 5.68789,3.9795 9.76172,5.25 4.07385,1.27051 8.85237,2.11894 14.05664,2.59765 5.20427,0.47871 10.83381,0.56134 16.70313,0.22266 5.86931,-0.33868 11.47146,-0.78653 16.60547,-1.34961 5.13399,-0.56309 9.79334,-1.22365 13.70703,-2.34375 1.48913,-0.4262 2.86677,-0.9287 4.12695,-1.51953 2.54507,-1.19325 2.05015,-6.17249 -0.0996,-4.54102 -1.99172,1.51153 -4.55969,2.50355 -7.57031,3.20703 -3.66893,0.85731 -7.96668,1.34146 -12.5586,1.76758 -4.59191,0.42612 -9.47527,0.75991 -14.3789,1.05664 -4.90363,0.29673 -9.56506,0.33523 -13.85156,0.084 -4.28652,-0.25124 -8.19851,-0.76855 -11.53516,-1.64649 -3.33664,-0.87795 -6.09539,-2.12996 -8.03711,-3.9375 -1.94173,-1.80756 -3.06587,-4.17751 -3.13086,-7.22265 -0.065,-3.04513 0.96102,-5.2776 2.81445,-6.81446 1.85342,-1.53686 4.53117,-2.36997 7.78907,-2.73828 3.2579,-0.36831 7.09262,-0.27244 11.32226,-0.1543 4.22963,0.11816 8.85767,0.22578 13.7832,-0.11718 4.92553,-0.34297 9.88026,-1.41664 14.58204,-2.44336 4.70178,-1.02671 9.13982,-1.97234 12.92382,-2.1836 0.473,-0.0264 0.93707,-0.0422 1.38868,-0.0449 1.16046,-0.007 2.25007,0.0442 3.25,0.23633 1.15313,0.22156 2.31543,-2.86146 -0.83789,-2.92773 -0.51177,-0.0108 -1.03459,-0.045 -1.57032,-0.0488 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer8"
|
||||
inkscape:label="suite"
|
||||
style="display:none"
|
||||
transform="translate(-50.232073,-102.51234)">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4895"
|
||||
d="m 324.09413,544.86506 4.14425,29.21457 7.57767,4.77275 12.8596,1.47382 14.30183,-6.0413 -2.4518,-23.40769 -16.10633,-10.21508 z"
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
id="path4897"
|
||||
d="m 344.42024,540.66221 -20.32581,4.20197 4.14421,29.21509 7.57708,4.77407 12.85439,1.47227 -3.89893,-39.44148 z"
|
||||
style="opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path4899"
|
||||
d="m 332.09763,543.20922 -8.0032,1.65496 4.14421,29.21509 7.53319,4.74609 z"
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 133.24543,359.71004 -10.949,61.23368 16.91521,74.00049 17.65613,48.74633 67.19668,26.82571 118.43572,-0.56394 55.24739,-58.43528 29.16816,-70.93165 8.61786,-100.0998 c 0,0 -72.92039,21.21319 -76.23495,21.21319 -3.31457,0 -140.53747,-21.21319 -140.53747,-21.21319 l -65.08989,-2.0062 z"
|
||||
id="path4859"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccsccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 170.86719,361.25977 -19.25586,12.57617 -9.48242,9.85547 -11.67969,65.31445 18.04297,78.93555 18.83398,51.99609 71.67578,28.61328 125.48438,-0.59766 L 262.5,584.41797 l -41,-27.5 -36.5,-114.5 3.4668,-80.61524 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4861"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
transform="translate(-137.02484,-121.89479)"
|
||||
style="display:inline;opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 393.43109,506.1931 101.71875,7.96875 67.03125,-21.5625 6.25233,1.41475 -7.65858,24.83525 -63.75,47.34375 -101.71875,-30.46875 -63.97243,-44.63617 z"
|
||||
id="path4866"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 189.26154,547.3143 0.66292,29.49961 6.96058,5.63476 12.59534,2.98311 14.91553,-4.30893 0.33146,-23.53341 -14.78637,-12.04679 z"
|
||||
id="path4883"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 209.94187,545.54284 -20.67994,1.77063 0.66284,29.50013 6.95984,5.63598 12.59034,2.98096 0.78918,-39.62585 z"
|
||||
id="path4890"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 197.40463,546.61584 -8.1427,0.69763 0.66284,29.50013 6.91956,5.60302 z"
|
||||
id="path4885"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 357.1875,433.51706 -4.6875,70.3125 -12.65625,59.53125 22.5,-60.9375 -0.46875,-71.25 z"
|
||||
id="path4873"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 421.40625,440.07956 -49.6875,3.28125 31.875,0.9375 z"
|
||||
id="path4875"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 246.5625,431.17331 71.25,8.4375 -42.65625,0.46875 z"
|
||||
id="path4877"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 237.1875,483.67331 110.15625,17.8125 -67.03125,-4.21875 z"
|
||||
id="path4879"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 372.1875,499.14206 29.0625,-3.75 -14.0625,5.15625 z"
|
||||
id="path4881"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 275.23207,493.95315 9.375,52.03125 -2.8125,-52.5 z"
|
||||
id="path4979"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 271.71644,437.23439 7.03125,41.25001 -2.10937,-41.71876 z"
|
||||
id="path4981"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer13"
|
||||
inkscape:label="shield"
|
||||
style="display:inline"
|
||||
transform="translate(-50.232073,-102.51234)">
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4926"
|
||||
d="m 368.8138,406.45401 c 26.75894,-10.96313 50.24152,-24.28175 65.61035,-43.43363 18.1912,19.63416 31.82885,23.22438 49.75819,26.19107 2.75091,30.51252 -5.39746,121.91404 -64.45432,149.96295 -64.70891,-40.98189 -47.40645,-88.18419 -50.91422,-132.72039 z"
|
||||
style="display:inline;opacity:1;fill:#1f8179;fill-opacity:1;stroke:none;stroke-width:4.70907879;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#30cabd;fill-opacity:1;stroke:none;stroke-width:4.36651993;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 374.9586,407.67174 c 24.81238,-10.16563 46.58673,-22.51541 60.83756,-40.27408 16.86789,18.20587 29.51348,21.53492 46.13856,24.2858 2.5508,28.2929 -5.00482,113.04548 -59.76562,139.05397 -60.0017,-38.00067 -43.9579,-81.76926 -47.2105,-123.06569 z"
|
||||
id="path4912"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d3f5f1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:18.2656765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 401.41757,414.65658 -5.64924,4.23257 -3.34776,6.33337 3.90311,8.75369 -2.36694,20.23813 38.52429,28.08241 38.52542,-43.69159 -2.36692,-19.27912 3.90309,-10.33512 -3.34776,-4.97695 -5.64924,-1.94365 -9.19454,9.07274 -21.87005,0.0704 -21.86889,8.79047 z"
|
||||
id="path4378"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4938"
|
||||
d="m 403.12876,417.42326 -5.21749,3.90909 -3.09191,5.84934 3.60481,8.08468 -2.18604,18.69142 35.58003,25.93618 35.58108,-40.35242 -2.18602,-17.80569 3.60479,-9.54526 -3.09191,-4.59658 -5.21749,-1.7951 -8.49184,8.37935 -20.19861,0.065 -20.19753,8.11865 z"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#aeece6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:16.8697052;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
style="opacity:1;fill:#d3f5f1;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 464.84961,391.89062 c -0.60619,0.75541 -1.22992,1.49906 -1.86133,2.23633 -0.11899,0.1386 -0.2395,0.2761 -0.35937,0.41407 -0.61784,0.71283 -1.24577,1.41706 -1.88672,2.11328 -0.002,0.002 -0.002,0.004 -0.004,0.006 l 3.38672,7.25781 c 15.09239,6.97647 29.7818,15.46409 46.125,17.75 2.81589,4.8446 3.0277,16.3255 2.36133,35.51562 2.05837,-15.93817 2.24593,-29.86353 1.49609,-38.81054 -0.0713,-0.20193 -0.13752,-0.40999 -0.21289,-0.60743 -1.10276,-0.18301 -2.18955,-0.36868 -3.26172,-0.56054 -1.06774,-0.19107 -2.12248,-0.38856 -3.16406,-0.5957 -0.071,-0.0141 -0.14001,-0.0307 -0.21094,-0.0449 -0.97781,-0.19605 -1.94773,-0.39829 -2.90625,-0.61523 -1.01852,-0.23052 -2.02852,-0.47517 -3.02929,-0.73633 -0.12001,-0.0313 -0.23765,-0.0697 -0.35743,-0.10156 -0.88127,-0.23408 -1.758,-0.47437 -2.6289,-0.73828 -0.20951,-0.0635 -0.41599,-0.1397 -0.625,-0.20508 -0.78661,-0.24611 -1.57262,-0.49604 -2.35352,-0.77149 -0.0972,-0.0343 -0.19387,-0.0746 -0.29101,-0.10937 -0.87846,-0.31448 -1.75449,-0.64278 -2.62891,-1 -0.25919,-0.10587 -0.51834,-0.22992 -0.77734,-0.33984 -0.72558,-0.30799 -1.45242,-0.61517 -2.17774,-0.95704 -0.0626,-0.0295 -0.12489,-0.0659 -0.1875,-0.0957 -0.91114,-0.43326 -1.82227,-0.8864 -2.73633,-1.37891 -0.26804,-0.1444 -0.53815,-0.31305 -0.80664,-0.46289 -0.71043,-0.39651 -1.42013,-0.79206 -2.13476,-1.22851 -0.24026,-0.14672 -0.48376,-0.31731 -0.72461,-0.46875 -0.75591,-0.47533 -1.51238,-0.95253 -2.27539,-1.47656 -0.99849,-0.68575 -2.00319,-1.41381 -3.01758,-2.18946 -0.0311,-0.0238 -0.0626,-0.0523 -0.0937,-0.0762 -0.98515,-0.75566 -1.97825,-1.55104 -2.98242,-2.39844 -1.03491,-0.87333 -2.08135,-1.79735 -3.14063,-2.77539 -1.05619,-0.97516 -2.12633,-2.00327 -3.21093,-3.08984 -10e-4,-10e-4 -0.003,-0.003 -0.004,-0.004 -1.09002,-1.0921 -2.1954,-2.24304 -3.31836,-3.45508 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4940"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#27a49a;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 464.84961,391.89062 c -15.20089,18.94259 -38.42799,32.11565 -64.89453,42.95899 3.46944,44.04953 -13.64244,90.73548 50.35937,131.26953 0.008,-0.004 0.0155,-0.008 0.0234,-0.0117 -0.1884,-0.20405 -0.37246,-0.40845 -0.57031,-0.61133 -43.17831,-31.88529 -48.49457,-87.76825 -44.0293,-127.30859 13.29063,-4.62307 59.10077,-33.73629 59.10077,-33.73629 0,0 0.42898,-6.93602 0.14533,-10.36918 -0.46271,-0.48457 0.33389,-1.68557 -0.13477,-2.19141 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4928"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer6"
|
||||
inkscape:label="helmet"
|
||||
style="display:none;opacity:1"
|
||||
transform="translate(-50.232073,-102.51234)">
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 379.24023,128.24414 -12.60546,3.7168 -22.02539,30.16015 -125.19532,-10.54687 -44.22265,16.23437 -32.35547,-33.37304 -39.95703,11.15429 -30.400394,54.25391 50.697264,26.69727 -15.76172,42.0332 16,137 h 18.5625 l 25.4375,-32 105,21 110,8 64,-23 20.1211,-0.16797 4.60937,-4.95703 c 0,0 5.89679,-12.21131 8.66016,-30.80078 l 33.55469,-58.2461 v -48.08398 l -79.92383,-53.45117 -25.32813,-4.95703 9.08594,-40.23243 z m 17.49415,24.18945 a 7.864841,11.931376 33.165464 0 1 5.23828,10.875 7.864841,11.931376 33.165464 0 1 -12.01954,10.875 7.864841,11.931376 33.165464 0 1 -5.23828,-10.875 7.864841,11.931376 33.165464 0 1 12.01954,-10.875 z M 126.5625,180.73438 a 19.24034,14.622658 30.833296 0 1 11.89258,3.41015 19.24034,14.622658 30.833296 0 1 9.02734,22.41797 19.24034,14.622658 30.833296 0 1 -24.01562,2.69336 19.24034,14.622658 30.833296 0 1 -9.02735,-22.41797 19.24034,14.622658 30.833296 0 1 12.12305,-6.10351 z m 378.3125,58 1.41406,48.78906 -284.25781,26.16406 -7.07031,-65.76172 z m -100.00781,70.27148 9.35742,0.22461 -9.04883,64.9668 -7.33008,-1.23047 z m -17.9375,0.58008 -1.85352,65.56836 -7.41992,-0.41602 -0.0527,-64.3457 z m -27.36524,0.91992 4.94922,65.40625 -7.42383,0.35351 -6.71875,-63.99218 z"
|
||||
id="path4701"
|
||||
transform="scale(0.93749999)"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#9b9b9b;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 206.49727,144.26345 -21.54466,24.52776 218.33607,5.38027 -80.17565,-21.81579 z"
|
||||
id="path4753"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 197.5,179.41797 -7.09305,53.26007 23.42689,11.34889 266.07031,-8.43555 26.10682,-1.287 -75.7574,-49.02298 -0.83203,-0.16211 -78.41609,-9.93414 -61.98554,-1.53581 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4748"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#9b9b9b;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 379.24023,128.24414 -9.85937,2.90625 c 4.05918,3.86793 8.60142,8.14649 9.09766,8.14649 0.60056,0 21.82631,0.64358 38.3164,1.14062 l 0.39844,-1.75977 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4755"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 171.82603,216.81351 c 0,0 -37.05247,9.00497 -49.83384,9.10446 l 1.18359,0.62305 -15.76172,42.0332 16,137 h 18.5625 l 25.4375,-32 0.94532,0.18945 -14.09384,-49.77443 21.09774,-75.00291 z"
|
||||
id="path4760"
|
||||
transform="scale(0.93749999)"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 135.05664,315.10156 v 90.47266 h 6.91992 l 25.4375,-32 105,21 110,8 64,-23 20.1211,-0.16797 4.60937,-4.95703 c 0,0 1.87983,-3.9794 4.00781,-10.68555 l -25.43329,-0.22503 -63.992,24.39495 -113.49072,-14.84961 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4765"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 134.79883,173.33594 -61.158205,24.43554 -1.162109,2.07227 50.697264,26.69727 -4.74219,12.64453 54.71935,-22.20174 z m -8.23633,7.39844 c 3.9725,-0.0177 8.16388,1.1842 11.89258,3.41015 9.12492,5.44612 13.16668,15.4832 9.02734,22.41797 -4.13971,6.93389 -14.89171,8.13974 -24.01562,2.69336 -9.12492,-5.44612 -13.16669,-15.4832 -9.02735,-22.41797 2.28739,-3.8317 6.7518,-6.07936 12.12305,-6.10351 z"
|
||||
transform="scale(0.93749999)"
|
||||
id="path4770"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#9b9b9b;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 130.59378,129.01646 -6.79485,30.16252 6.79485,1.49155 4.80612,-30.99116 z"
|
||||
id="path4776"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 337.03125,291.01706 1.28906,1.17187 4.33594,60.11719 -1.05469,0.11719 z"
|
||||
id="path4839"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 362.57812,290.07956 1.75781,1.28906 -2.10937,59.88281 -1.28906,0.35157 z"
|
||||
id="path4841"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#616161;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 388.47656,289.96237 1.05469,1.64063 -8.55469,58.94531 -1.28906,0.35156 z"
|
||||
id="path4843"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
transform="translate(-137.02484,-121.89479)"
|
||||
style="display:inline;opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
|
||||
d="m 256.01765,494.86308 24.85922,-35.46582 112.69514,25.19067 102.08855,8.61787 -105.40311,-7.6235 -107.72329,-22.53903 z"
|
||||
id="path4845"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 102.91718,251.32383 36.95738,-17.89864 22.20757,-0.66291 -23.53339,5.80048 z"
|
||||
id="path4853"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 112.26562,312.34518 14.53125,-7.26562 15.70313,-1.17188 -19.6875,5.39063 z"
|
||||
id="path4855"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#7d7d7d;fill-opacity:1;stroke:none;stroke-width:4.6875;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 360.62446,371.14529 59.4964,-21.87612 21.37894,0.49718 -22.04185,1.82301 z"
|
||||
id="path4857"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer10"
|
||||
inkscape:label="stud"
|
||||
style="display:none"
|
||||
transform="translate(-50.232073,-102.51234)" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="palette"
|
||||
style="display:none"
|
||||
transform="translate(-187.25691,-224.40713)">
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394655;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4162"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="779.60529"
|
||||
y="21.967466" />
|
||||
<rect
|
||||
y="21.967466"
|
||||
x="824.60529"
|
||||
height="40.789474"
|
||||
width="40.789474"
|
||||
id="rect4170"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052742;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bce8ff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4208"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="779.60529"
|
||||
y="86.967468" />
|
||||
<rect
|
||||
y="-127.75694"
|
||||
x="824.60529"
|
||||
height="40.789474"
|
||||
width="40.789474"
|
||||
id="rect4223"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#abccd9;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c3b0cb;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4227"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="779.60529"
|
||||
y="131.96747" />
|
||||
<rect
|
||||
y="131.96747"
|
||||
x="824.60529"
|
||||
height="40.789474"
|
||||
width="40.789474"
|
||||
id="rect4231"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#e1d0cb;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f5c3d2;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4233"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="869.60529"
|
||||
y="131.96747" />
|
||||
<rect
|
||||
y="176.96747"
|
||||
x="779.60529"
|
||||
height="40.789474"
|
||||
width="40.789474"
|
||||
id="rect4248"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#cec4ad;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<rect
|
||||
transform="scale(1,-1)"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#96d6ff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4263"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="869.60529"
|
||||
y="-127.75694" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2ce;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4267"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="824.60529"
|
||||
y="176.96747" />
|
||||
<rect
|
||||
y="-327.75693"
|
||||
x="779.60529"
|
||||
height="40.789474"
|
||||
width="40.789474"
|
||||
id="rect4280"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#24b8eb;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
transform="scale(1,-1)" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8aa9ff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4284"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="824.60529"
|
||||
y="286.96747" />
|
||||
<rect
|
||||
y="331.96747"
|
||||
x="779.60529"
|
||||
height="40.789474"
|
||||
width="40.789474"
|
||||
id="rect4297"
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d4edf1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#394d54;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4301"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="779.60529"
|
||||
y="241.96747" />
|
||||
<rect
|
||||
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d6e2ff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:9.21052647;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
id="rect4303"
|
||||
width="40.789474"
|
||||
height="40.789474"
|
||||
x="824.60529"
|
||||
y="331.96747" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 78 KiB |
|
@ -0,0 +1,20 @@
|
|||
module blocky
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/dns v1.1.22
|
||||
github.com/onsi/ginkgo v1.11.0 // indirect
|
||||
github.com/onsi/gomega v1.8.1 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.4
|
||||
)
|
|
@ -0,0 +1,82 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
|
||||
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
|
||||
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18 h1:xFbv3LvlvQAmbNJFCBKRv1Ccvnh9FVsW0FX2kTWWowE=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,253 @@
|
|||
package lists
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = 30 * time.Second
|
||||
listUpdatePeriod = 4 * time.Hour
|
||||
)
|
||||
|
||||
type Matcher interface {
|
||||
// matches passed domain name against cached list entries
|
||||
Match(domain string, groupsToCheck []string) (found bool, group string)
|
||||
|
||||
// returns current configuration and stats
|
||||
Configuration() []string
|
||||
}
|
||||
|
||||
type ListCache struct {
|
||||
groupCaches map[string][]string
|
||||
lock sync.RWMutex
|
||||
|
||||
groupToLinks map[string][]string
|
||||
}
|
||||
|
||||
func (b *ListCache) Configuration() (result []string) {
|
||||
result = append(result, "group links:")
|
||||
for group, links := range b.groupToLinks {
|
||||
result = append(result, fmt.Sprintf(" %s:", group))
|
||||
for _, link := range links {
|
||||
result = append(result, fmt.Sprintf(" - %s", link))
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, "group caches:")
|
||||
|
||||
var total int
|
||||
|
||||
for group, cache := range b.groupCaches {
|
||||
result = append(result, fmt.Sprintf(" %s: %d entries", group, len(cache)))
|
||||
total += len(cache)
|
||||
}
|
||||
|
||||
result = append(result, fmt.Sprintf(" TOTAL: %d entries", total))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// removes duplicates
|
||||
func unique(in []string) []string {
|
||||
keys := make(map[string]bool)
|
||||
|
||||
var list []string
|
||||
|
||||
for _, entry := range in {
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
|
||||
list = append(list, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func NewListCache(groupToLinks map[string][]string) *ListCache {
|
||||
groupCaches := make(map[string][]string)
|
||||
|
||||
b := &ListCache{
|
||||
groupToLinks: groupToLinks,
|
||||
groupCaches: groupCaches,
|
||||
}
|
||||
b.refresh()
|
||||
|
||||
go periodicUpdate(b)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// triggers periodical refresh (and download) of list entries
|
||||
func periodicUpdate(cache *ListCache) {
|
||||
ticker := time.NewTicker(listUpdatePeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
cache.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func logger() *logrus.Entry {
|
||||
return logrus.WithField("prefix", "list_cache")
|
||||
}
|
||||
|
||||
// downloads and reads files with domain names and creates cache for them
|
||||
func createCacheForGroup(links []string) []string {
|
||||
cache := make([]string, 0)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
c := make(chan []string, len(links))
|
||||
|
||||
for _, link := range links {
|
||||
wg.Add(1)
|
||||
|
||||
go processFile(link, c, &wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case res := <-c:
|
||||
cache = append(cache, res...)
|
||||
default:
|
||||
close(c)
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
cache = unique(cache)
|
||||
sort.Strings(cache)
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func (b *ListCache) Match(domain string, groupsToCheck []string) (found bool, group string) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
for _, g := range groupsToCheck {
|
||||
if contains(domain, b.groupCaches[g]) {
|
||||
return true, g
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func contains(domain string, cache []string) bool {
|
||||
idx := sort.SearchStrings(cache, domain)
|
||||
if idx < len(cache) {
|
||||
return cache[idx] == strings.ToLower(domain)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *ListCache) refresh() {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
for group, links := range b.groupToLinks {
|
||||
b.groupCaches[group] = createCacheForGroup(links)
|
||||
|
||||
logger().WithFields(logrus.Fields{
|
||||
"group": group,
|
||||
"total_count": len(b.groupCaches[group]),
|
||||
}).Info("group import finished")
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(link string) (io.ReadCloser, error) {
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
logger().WithField("link", link).Info("starting download")
|
||||
|
||||
//nolint:bodyclose
|
||||
resp, err := client.Get(link)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func readFile(file string) (io.ReadCloser, error) {
|
||||
logger().WithField("file", file).Info("starting processing of file")
|
||||
file = strings.TrimPrefix(file, "file://")
|
||||
|
||||
return os.Open(file)
|
||||
}
|
||||
|
||||
// downloads file (or reads local file) and writes file content as string array in the channel
|
||||
func processFile(link string, ch chan<- []string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
result := make([]string, 0)
|
||||
|
||||
var r io.ReadCloser
|
||||
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(link, "http") {
|
||||
r, err = downloadFile(link)
|
||||
} else {
|
||||
r, err = readFile(link)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger().Warn("error during file processing: ", err)
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var count int
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// skip comments
|
||||
if !strings.HasPrefix(line, "#") {
|
||||
result = append(result, processLine(line))
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
logger().Warn("can't parse file: ", err)
|
||||
} else {
|
||||
logger().WithFields(logrus.Fields{
|
||||
"source": link,
|
||||
"count": count,
|
||||
}).Info("file imported")
|
||||
}
|
||||
ch <- result
|
||||
}
|
||||
|
||||
// return only first column (see hosts format)
|
||||
func processLine(line string) string {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package lists
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_NoMatch_With_Empty_List(t *testing.T) {
|
||||
file1 := tempFile("#empty file")
|
||||
defer os.Remove(file1.Name())
|
||||
|
||||
lists := map[string][]string{
|
||||
"gr1": {file1.Name()},
|
||||
}
|
||||
|
||||
sut := NewListCache(lists)
|
||||
|
||||
found, group := sut.Match("google.com", []string{"gr1"})
|
||||
assert.Equal(t, false, found)
|
||||
assert.Equal(t, "", group)
|
||||
}
|
||||
|
||||
func Test_Match_Download_Multiple_Groups(t *testing.T) {
|
||||
server1 := testServer("blocked1.com\nblocked1a.com")
|
||||
defer server1.Close()
|
||||
|
||||
server2 := testServer("blocked2.com")
|
||||
defer server2.Close()
|
||||
|
||||
server3 := testServer("blocked3.com\nblocked1a.com")
|
||||
defer server3.Close()
|
||||
|
||||
lists := map[string][]string{
|
||||
"gr1": {server1.URL, server2.URL},
|
||||
"gr2": {server3.URL},
|
||||
}
|
||||
|
||||
sut := NewListCache(lists)
|
||||
|
||||
found, group := sut.Match("blocked1.com", []string{"gr1", "gr2"})
|
||||
assert.Equal(t, true, found)
|
||||
assert.Equal(t, "gr1", group)
|
||||
|
||||
found, group = sut.Match("blocked1a.com", []string{"gr1", "gr2"})
|
||||
assert.Equal(t, true, found)
|
||||
assert.Equal(t, "gr1", group)
|
||||
|
||||
found, group = sut.Match("blocked1a.com", []string{"gr2"})
|
||||
assert.Equal(t, true, found)
|
||||
assert.Equal(t, "gr2", group)
|
||||
}
|
||||
|
||||
func Test_Match_Download_No_Group(t *testing.T) {
|
||||
server1 := testServer("blocked1.com\nblocked1a.com")
|
||||
defer server1.Close()
|
||||
|
||||
server2 := testServer("blocked2.com")
|
||||
defer server2.Close()
|
||||
|
||||
server3 := testServer("blocked3.com\nblocked1a.com")
|
||||
defer server3.Close()
|
||||
|
||||
lists := map[string][]string{
|
||||
"gr1": {server1.URL, server2.URL},
|
||||
"gr2": {server3.URL},
|
||||
}
|
||||
|
||||
sut := NewListCache(lists)
|
||||
|
||||
found, group := sut.Match("blocked1.com", []string{})
|
||||
assert.Equal(t, false, found)
|
||||
assert.Equal(t, "", group)
|
||||
}
|
||||
|
||||
func Test_Match_Files_Multiple_Groups(t *testing.T) {
|
||||
file1 := tempFile("blocked1.com\nblocked1a.com")
|
||||
defer os.Remove(file1.Name())
|
||||
|
||||
file2 := tempFile("blocked2.com")
|
||||
defer os.Remove(file2.Name())
|
||||
|
||||
file3 := tempFile("blocked3.com\nblocked1a.com")
|
||||
defer os.Remove(file3.Name())
|
||||
|
||||
lists := map[string][]string{
|
||||
"gr1": {file1.Name(), file2.Name()},
|
||||
"gr2": {"file://" + file3.Name()},
|
||||
}
|
||||
|
||||
sut := NewListCache(lists)
|
||||
|
||||
found, group := sut.Match("blocked1.com", []string{"gr1", "gr2"})
|
||||
assert.Equal(t, true, found)
|
||||
assert.Equal(t, "gr1", group)
|
||||
|
||||
found, group = sut.Match("blocked1a.com", []string{"gr1", "gr2"})
|
||||
assert.Equal(t, true, found)
|
||||
assert.Equal(t, "gr1", group)
|
||||
|
||||
found, group = sut.Match("blocked1a.com", []string{"gr2"})
|
||||
assert.Equal(t, true, found)
|
||||
assert.Equal(t, "gr2", group)
|
||||
}
|
||||
|
||||
func tempFile(data string) *os.File {
|
||||
f, err := ioutil.TempFile("", "prefix")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func testServer(data string) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
_, err := rw.Write([]byte(data))
|
||||
if err != nil {
|
||||
log.Fatal("can't write to buffer:", err)
|
||||
}
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/server"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var version = "undefined"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var buildTime = "undefined"
|
||||
|
||||
func main() {
|
||||
cfg := config.NewConfig()
|
||||
configureLog(&cfg)
|
||||
|
||||
printBanner()
|
||||
|
||||
signals := make(chan os.Signal)
|
||||
done := make(chan bool)
|
||||
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
server, err := server.NewServer(&cfg)
|
||||
if err != nil {
|
||||
log.Fatal("cant start server ", err)
|
||||
}
|
||||
|
||||
server.Start()
|
||||
|
||||
go func() {
|
||||
<-signals
|
||||
log.Infof("Terminating...")
|
||||
server.Stop()
|
||||
done <- true
|
||||
}()
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
func configureLog(cfg *config.Config) {
|
||||
if level, err := log.ParseLevel(cfg.LogLevel); err != nil {
|
||||
log.Fatalf("invalid log level %s %v", cfg.LogLevel, err)
|
||||
} else {
|
||||
log.SetLevel(level)
|
||||
}
|
||||
|
||||
logFormatter := &prefixed.TextFormatter{
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
FullTimestamp: true,
|
||||
ForceFormatting: true,
|
||||
ForceColors: true,
|
||||
QuoteEmptyFields: true}
|
||||
|
||||
logFormatter.SetColorScheme(&prefixed.ColorScheme{
|
||||
PrefixStyle: "blue+b",
|
||||
TimestampStyle: "white+h",
|
||||
})
|
||||
|
||||
logrus.SetFormatter(logFormatter)
|
||||
}
|
||||
|
||||
func printBanner() {
|
||||
log.Info("_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/")
|
||||
log.Info("_/ _/")
|
||||
log.Info("_/ _/")
|
||||
log.Info("_/ _/ _/ _/ _/")
|
||||
log.Info("_/ _/_/_/ _/ _/_/ _/_/_/ _/ _/ _/ _/ _/")
|
||||
log.Info("_/ _/ _/ _/ _/ _/ _/ _/_/ _/ _/ _/")
|
||||
log.Info("_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/")
|
||||
log.Info("_/ _/_/_/ _/ _/_/ _/_/_/ _/ _/ _/_/_/ _/")
|
||||
log.Info("_/ _/ _/")
|
||||
log.Info("_/ _/_/ _/")
|
||||
log.Info("_/ _/")
|
||||
log.Info("_/ _/")
|
||||
log.Infof("_/ Version: %-18s Build time: %-18s _/", version, buildTime)
|
||||
log.Info("_/ _/")
|
||||
log.Info("_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/")
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/lists"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
BlockTTL = 6 * 60 * 60
|
||||
)
|
||||
|
||||
type BlockType uint8
|
||||
|
||||
const (
|
||||
ZeroIP BlockType = iota
|
||||
NxDomain
|
||||
)
|
||||
|
||||
func (b BlockType) String() string {
|
||||
return [...]string{"ZeroIP", "NxDomain"}[b]
|
||||
}
|
||||
|
||||
// nolint:gochecknoglobals
|
||||
var typeToZeroIP = map[uint16]net.IP{
|
||||
dns.TypeA: net.IPv4zero,
|
||||
dns.TypeAAAA: net.IPv6zero,
|
||||
}
|
||||
|
||||
func resolveBlockType(cfg config.BlockingConfig) BlockType {
|
||||
cfgBlockType := strings.TrimSpace(strings.ToUpper(cfg.BlockType))
|
||||
if cfgBlockType == "" || cfgBlockType == "ZEROIP" {
|
||||
return ZeroIP
|
||||
}
|
||||
|
||||
if cfgBlockType == "NXDOMAIN" {
|
||||
return NxDomain
|
||||
}
|
||||
|
||||
logrus.Fatalf("unknown blockType, please use one of: ZeroIP, NxDomain")
|
||||
|
||||
return ZeroIP
|
||||
}
|
||||
|
||||
// checks request's question (domain name) against black and white lists
|
||||
type BlockingResolver struct {
|
||||
NextResolver
|
||||
blacklistMatcher lists.Matcher
|
||||
whitelistMatcher lists.Matcher
|
||||
clientGroupsBlock map[string][]string
|
||||
blockType BlockType
|
||||
whitelistOnlyGroups []string
|
||||
}
|
||||
|
||||
func NewBlockingResolver(cfg config.BlockingConfig) ChainedResolver {
|
||||
bt := resolveBlockType(cfg)
|
||||
blacklistMatcher := lists.NewListCache(cfg.BlackLists)
|
||||
whitelistMatcher := lists.NewListCache(cfg.WhiteLists)
|
||||
whitelistOnlyGroups := determineWhitelistOnlyGroups(&cfg)
|
||||
|
||||
return &BlockingResolver{
|
||||
blockType: bt,
|
||||
clientGroupsBlock: cfg.ClientGroupsBlock,
|
||||
blacklistMatcher: blacklistMatcher,
|
||||
whitelistMatcher: whitelistMatcher,
|
||||
whitelistOnlyGroups: whitelistOnlyGroups,
|
||||
}
|
||||
}
|
||||
|
||||
// returns groups, which have only whitelist entries
|
||||
func determineWhitelistOnlyGroups(cfg *config.BlockingConfig) (result []string) {
|
||||
for g, links := range cfg.WhiteLists {
|
||||
if len(links) > 0 {
|
||||
if _, found := cfg.BlackLists[g]; !found {
|
||||
result = append(result, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(result)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// sets answer and/or return code for DNS response, if request should be blocked
|
||||
func (r *BlockingResolver) handleBlocked(question dns.Question, response *dns.Msg) (*dns.Msg, error) {
|
||||
switch r.blockType {
|
||||
case ZeroIP:
|
||||
rr, err := util.CreateAnswerFromQuestion(question, typeToZeroIP[question.Qtype], BlockTTL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response.Answer = append(response.Answer, rr)
|
||||
|
||||
case NxDomain:
|
||||
response.Rcode = dns.RcodeNameError
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (r *BlockingResolver) Configuration() (result []string) {
|
||||
if len(r.clientGroupsBlock) > 0 {
|
||||
result = append(result, "clientGroupsBlock")
|
||||
for key, val := range r.clientGroupsBlock {
|
||||
result = append(result, fmt.Sprintf(" %s = \"%s\"", key, strings.Join(val, ";")))
|
||||
}
|
||||
|
||||
result = append(result, fmt.Sprintf("blockType = \"%s\"", r.blockType))
|
||||
|
||||
result = append(result, "blacklist:")
|
||||
for _, c := range r.blacklistMatcher.Configuration() {
|
||||
result = append(result, fmt.Sprintf(" %s", c))
|
||||
}
|
||||
|
||||
result = append(result, "whitelist:")
|
||||
for _, c := range r.whitelistMatcher.Configuration() {
|
||||
result = append(result, fmt.Sprintf(" %s", c))
|
||||
}
|
||||
} else {
|
||||
result = []string{"deactivated"}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *BlockingResolver) Resolve(request *Request) (*Response, error) {
|
||||
logger := withPrefix(request.Log, "blacklist_resolver")
|
||||
groupsToCheck := r.groupsToCheckForClient(request)
|
||||
|
||||
if len(groupsToCheck) > 0 {
|
||||
logger.WithField("groupsToCheck", strings.Join(groupsToCheck, "; ")).Debug("checking groups for request")
|
||||
|
||||
for _, question := range request.Req.Question {
|
||||
domain := util.ExtractDomain(question)
|
||||
logger := logger.WithField("domain", domain)
|
||||
whitelistOnlyAlowed := reflect.DeepEqual(groupsToCheck, r.whitelistOnlyGroups)
|
||||
|
||||
if whitelisted, group := r.matches(groupsToCheck, r.whitelistMatcher, domain); whitelisted {
|
||||
logger.WithField("group", group).Debugf("domain is whitelisted")
|
||||
} else {
|
||||
if whitelistOnlyAlowed {
|
||||
logger.WithField("client_groups", groupsToCheck).Debug("white list only for client group(s), blocking...")
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(request.Req)
|
||||
resp, err := r.handleBlocked(question, response)
|
||||
|
||||
return &Response{Res: resp, Reason: fmt.Sprintf("BLOCKED (WHITELIST ONLY)")}, err
|
||||
}
|
||||
if blocked, group := r.matches(groupsToCheck, r.blacklistMatcher, domain); blocked {
|
||||
logger.WithField("group", group).Debug("domain is blocked")
|
||||
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(request.Req)
|
||||
resp, err := r.handleBlocked(question, response)
|
||||
|
||||
return &Response{Res: resp, Reason: fmt.Sprintf("BLOCKED (%s)", group)}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.WithField("next_resolver", r.next).Trace("go to next resolver")
|
||||
|
||||
return r.next.Resolve(request)
|
||||
}
|
||||
|
||||
// returns groups which should be checked for client's request
|
||||
func (r *BlockingResolver) groupsToCheckForClient(request *Request) (groups []string) {
|
||||
// try client names
|
||||
for _, cName := range request.ClientNames {
|
||||
groupsByName, found := r.clientGroupsBlock[cName]
|
||||
if found {
|
||||
groups = append(groups, groupsByName...)
|
||||
}
|
||||
}
|
||||
|
||||
// try IP
|
||||
groupsByIP, found := r.clientGroupsBlock[request.ClientIP.String()]
|
||||
|
||||
if found {
|
||||
groups = append(groups, groupsByIP...)
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
if !found {
|
||||
// return default
|
||||
groups = r.clientGroupsBlock["default"]
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(groups)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *BlockingResolver) matches(groupsToCheck []string, m lists.Matcher,
|
||||
domain string) (blocked bool, group string) {
|
||||
if len(groupsToCheck) > 0 {
|
||||
found, group := m.Match(domain, groupsToCheck)
|
||||
if found {
|
||||
return true, group
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (r BlockingResolver) String() string {
|
||||
return fmt.Sprintf("blacklist resolver")
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var clientBlock = map[string][]string{
|
||||
"default": {"gr0"},
|
||||
"client1": {"gr1", "gr2"},
|
||||
"altName": {"gr4"},
|
||||
"192.168.178.55": {"gr3"},
|
||||
}
|
||||
|
||||
type MatcherMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (b *MatcherMock) Configuration() (result []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *MatcherMock) Match(domain string, groupsToCheck []string) (found bool, group string) {
|
||||
args := b.Called(domain, groupsToCheck)
|
||||
return args.Bool(0), args.String(1)
|
||||
}
|
||||
|
||||
func Test_Resolve_ClientName_IpZero(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "blocked1.com", []string{"gr1", "gr2", "gr3"}).Return(true, "gr1")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", mock.Anything, mock.Anything).Return(false, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
|
||||
req := util.NewMsgWithQuestion("blocked1.com.", dns.TypeA)
|
||||
|
||||
// A
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"client1"},
|
||||
ClientIP: net.ParseIP("192.168.178.55"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "blocked1.com. 21600 IN A 0.0.0.0", resp.Res.Answer[0].String())
|
||||
b.AssertExpectations(t)
|
||||
|
||||
// AAAA
|
||||
req = util.NewMsgWithQuestion("blocked1.com.", dns.TypeAAAA)
|
||||
resp, err = sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"client1"},
|
||||
ClientIP: net.ParseIP("192.168.178.55"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "blocked1.com. 21600 IN AAAA ::", resp.Res.Answer[0].String())
|
||||
b.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_ClientIp_A_IpZero(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "blocked1.com", []string{"gr3"}).Return(true, "gr1")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", mock.Anything, mock.Anything).Return(false, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
|
||||
req := util.NewMsgWithQuestion("blocked1.com.", dns.TypeA)
|
||||
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.55"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "blocked1.com. 21600 IN A 0.0.0.0", resp.Res.Answer[0].String())
|
||||
b.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_ClientWith2Names_A_IpZero(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "blocked1.com", []string{"gr1", "gr2", "gr3", "gr4"}).Return(true, "gr1")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", mock.Anything, mock.Anything).Return(false, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
|
||||
req := util.NewMsgWithQuestion("blocked1.com.", dns.TypeA)
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"client1", "altName"},
|
||||
ClientIP: net.ParseIP("192.168.178.55"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "blocked1.com. 21600 IN A 0.0.0.0", resp.Res.Answer[0].String())
|
||||
|
||||
b.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_Default_A_IpZero(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "blocked1.com", []string{"gr0"}).Return(true, "gr1")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", mock.Anything, mock.Anything).Return(false, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
|
||||
req := util.NewMsgWithQuestion("blocked1.com.", dns.TypeA)
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.1"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "blocked1.com. 21600 IN A 0.0.0.0", resp.Res.Answer[0].String())
|
||||
b.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_Default_Block_With_Whitelist(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "blocked1.com", []string{"gr0"}).Return(true, "gr")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", "blocked1.com", []string{"gr0"}).Return(true, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(new(Response), nil)
|
||||
sut.Next(m)
|
||||
|
||||
req := util.NewMsgWithQuestion("blocked1.com.", dns.TypeA)
|
||||
_, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.1"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
w.AssertExpectations(t)
|
||||
assert.Equal(t, 0, len(b.Calls))
|
||||
}
|
||||
|
||||
func Test_Resolve_Whitelist_Only(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", "whitelisted.com", []string{"gr0"}).Return(true, "gr0")
|
||||
w.On("Match", mock.Anything, []string{"gr0"}).Return(false, "gr0")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
whitelistMatcher: w,
|
||||
whitelistOnlyGroups: []string{"gr0"},
|
||||
}
|
||||
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(new(Response), nil)
|
||||
sut.Next(m)
|
||||
|
||||
req := util.NewMsgWithQuestion("whitelisted.com.", dns.TypeA)
|
||||
_, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.1"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
w.AssertExpectations(t)
|
||||
assert.Equal(t, 0, len(b.Calls))
|
||||
|
||||
req = new(dns.Msg)
|
||||
req.SetQuestion("google.com.", dns.TypeA)
|
||||
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.1"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "google.com. 21600 IN A 0.0.0.0", resp.Res.Answer[0].String())
|
||||
w.AssertExpectations(t)
|
||||
b.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_determineWhitelistOnlyGroups(t *testing.T) {
|
||||
assert.Equal(t, []string{"w1"}, determineWhitelistOnlyGroups(&config.BlockingConfig{
|
||||
BlackLists: map[string][]string{},
|
||||
WhiteLists: map[string][]string{"w1": {"l1"}},
|
||||
}))
|
||||
|
||||
assert.Equal(t, []string{"b1", "default"}, determineWhitelistOnlyGroups(&config.BlockingConfig{
|
||||
BlackLists: map[string][]string{
|
||||
"w1": {"y"},
|
||||
},
|
||||
WhiteLists: map[string][]string{
|
||||
"w1": {"l1"},
|
||||
"default": {"s1"},
|
||||
"b1": {"x"}},
|
||||
}))
|
||||
}
|
||||
|
||||
func Test_Resolve_Default_A_NxRecord(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "blocked1.com", []string{"gr0"}).Return(true, "gr1")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", mock.Anything, mock.Anything).Return(false, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
blockType: NxDomain,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
|
||||
req := util.NewMsgWithQuestion("blocked1.com.", dns.TypeA)
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.1"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeNameError, resp.Res.Rcode)
|
||||
b.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_NoBlock(t *testing.T) {
|
||||
b := &MatcherMock{}
|
||||
b.On("Match", "example.com", []string{"gr0"}).Return(false, "")
|
||||
|
||||
w := &MatcherMock{}
|
||||
w.On("Match", mock.Anything, mock.Anything).Return(false, "gr1")
|
||||
|
||||
sut := BlockingResolver{
|
||||
clientGroupsBlock: clientBlock,
|
||||
blacklistMatcher: b,
|
||||
blockType: NxDomain,
|
||||
whitelistMatcher: w,
|
||||
}
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(new(Response), nil)
|
||||
sut.Next(m)
|
||||
|
||||
req := util.NewMsgWithQuestion("example.com.", dns.TypeA)
|
||||
_, err := sut.Resolve(&Request{
|
||||
Req: req,
|
||||
ClientNames: []string{"unknown"},
|
||||
ClientIP: net.ParseIP("192.168.178.1"),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
b.AssertExpectations(t)
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// caches answers from dns queries with their TTL time, to avoid external resolver calls for recurrent queries
|
||||
type CachingResolver struct {
|
||||
NextResolver
|
||||
cacheA *cache.Cache
|
||||
cacheAAAA *cache.Cache
|
||||
}
|
||||
|
||||
const minTTL = 250
|
||||
|
||||
type Type uint8
|
||||
|
||||
const (
|
||||
A Type = iota
|
||||
AAAA
|
||||
)
|
||||
|
||||
func NewCachingResolver() ChainedResolver {
|
||||
return &CachingResolver{
|
||||
cacheA: cache.New(15*time.Minute, 5*time.Minute),
|
||||
cacheAAAA: cache.New(15*time.Minute, 5*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CachingResolver) getCache(queryType uint16) *cache.Cache {
|
||||
switch queryType {
|
||||
case dns.TypeA:
|
||||
return r.cacheA
|
||||
case dns.TypeAAAA:
|
||||
return r.cacheAAAA
|
||||
default:
|
||||
log.Error("unknown type: ", queryType)
|
||||
}
|
||||
|
||||
return r.cacheA
|
||||
}
|
||||
|
||||
func (r *CachingResolver) Configuration() (result []string) {
|
||||
result = append(result, fmt.Sprintf("A cache items count = %d", r.cacheA.ItemCount()))
|
||||
result = append(result, fmt.Sprintf("AAAA cache items count = %d", r.cacheAAAA.ItemCount()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *CachingResolver) Resolve(request *Request) (response *Response, err error) {
|
||||
logger := withPrefix(request.Log, "caching_resolver")
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(request.Req)
|
||||
|
||||
for _, question := range request.Req.Question {
|
||||
domain := util.ExtractDomain(question)
|
||||
logger := logger.WithField("domain", domain)
|
||||
|
||||
// we caching only A and AAAA queries
|
||||
if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||
val, expiresAt, found := r.getCache(question.Qtype).GetWithExpiration(domain)
|
||||
|
||||
if found {
|
||||
logger.Debug("domain is cached")
|
||||
|
||||
// calculate remaining TTL
|
||||
remainingTTL := uint32(time.Until(expiresAt).Seconds())
|
||||
|
||||
resp.Answer = val.([]dns.RR)
|
||||
for _, rr := range resp.Answer {
|
||||
rr.Header().Ttl = remainingTTL
|
||||
}
|
||||
|
||||
return &Response{Res: resp, Reason: fmt.Sprintf("CACHED (ttl %d)", remainingTTL)}, nil
|
||||
}
|
||||
|
||||
logger.WithField("next_resolver", r.next).Debug("not in cache: go to next resolver")
|
||||
response, err = r.next.Resolve(request)
|
||||
|
||||
if err == nil {
|
||||
var maxTTL uint32
|
||||
|
||||
for _, a := range response.Res.Answer {
|
||||
// if TTL < mitTTL -> adjust the value, set minTTL
|
||||
if a.Header().Ttl < minTTL {
|
||||
logger.WithFields(log.Fields{
|
||||
"TTL": a.Header().Ttl,
|
||||
"min_TTL": minTTL,
|
||||
}).Debugf("ttl is < than min TTL, using min value")
|
||||
|
||||
a.Header().Ttl = minTTL
|
||||
}
|
||||
|
||||
if maxTTL < a.Header().Ttl {
|
||||
maxTTL = a.Header().Ttl
|
||||
}
|
||||
}
|
||||
|
||||
// put value into cache
|
||||
r.getCache(question.Qtype).Set(domain, response.Res.Answer, time.Duration(maxTTL)*time.Second)
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("not A/AAAA: go to next %s", r.next)
|
||||
return r.next.Resolve(request)
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (r CachingResolver) String() string {
|
||||
return fmt.Sprintf("caching resolver")
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/util"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func Test_Resolve_A_WithCachingAndMinTtl(t *testing.T) {
|
||||
sut := NewCachingResolver()
|
||||
m := &resolverMock{}
|
||||
mockResp, err := util.NewMsgWithAnswer("example.com. 300 IN A 123.122.121.120")
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: mockResp}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
// first request
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "example.com. 300 IN A 123.122.121.120", resp.Res.Answer[0].String())
|
||||
assert.Equal(t, 1, len(m.Calls))
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// second request
|
||||
resp, err = sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
|
||||
// ttl is smaler
|
||||
assert.Equal(t, "example.com. 299 IN A 123.122.121.120", resp.Res.Answer[0].String())
|
||||
|
||||
// still one call to resolver
|
||||
assert.Equal(t, 1, len(m.Calls))
|
||||
|
||||
m.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_AAAA_WithCachingAndMinTtl(t *testing.T) {
|
||||
sut := NewCachingResolver()
|
||||
m := &resolverMock{}
|
||||
|
||||
mockResp, err := util.NewMsgWithAnswer("example.com. 123 IN AAAA 2001:0db8:85a3:08d3:1319:8a2e:0370:7344")
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: mockResp}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeAAAA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
// first request
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "example.com. 250 IN AAAA 2001:db8:85a3:8d3:1319:8a2e:370:7344", resp.Res.Answer[0].String())
|
||||
assert.Equal(t, 1, len(m.Calls))
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// second request
|
||||
resp, err = sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
|
||||
// ttl is smaler
|
||||
assert.Equal(t, "example.com. 249 IN AAAA 2001:db8:85a3:8d3:1319:8a2e:370:7344", resp.Res.Answer[0].String())
|
||||
|
||||
// still one call to resolver
|
||||
assert.Equal(t, 1, len(m.Calls))
|
||||
|
||||
m.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_Resolve_MX(t *testing.T) {
|
||||
sut := NewCachingResolver()
|
||||
m := &resolverMock{}
|
||||
mockResp, err := util.NewMsgWithAnswer("google.de.\t180\tIN\tMX\t20\talt1.aspmx.l.google.com.")
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: mockResp}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.TypeMX),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "google.de.\t180\tIN\tMX\t20 alt1.aspmx.l.google.com.", resp.Res.Answer[0].String())
|
||||
assert.Equal(t, 1, len(m.Calls))
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ClientNamesResolver tries to determine client name by asking responsible DNS server vie rDNS (reverse lookup)
|
||||
type ClientNamesResolver struct {
|
||||
cache *cache.Cache
|
||||
externalResolver Resolver
|
||||
singleNameOrder []uint
|
||||
NextResolver
|
||||
}
|
||||
|
||||
func NewClientNamesResolver(cfg config.ClientLookupConfig) ChainedResolver {
|
||||
var r Resolver
|
||||
if (config.Upstream{}) != cfg.Upstream {
|
||||
r = NewUpstreamResolver(cfg.Upstream)
|
||||
}
|
||||
|
||||
return &ClientNamesResolver{
|
||||
cache: cache.New(1*time.Hour, 1*time.Hour),
|
||||
externalResolver: r,
|
||||
singleNameOrder: cfg.SingleNameOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ClientNamesResolver) Configuration() (result []string) {
|
||||
if r.externalResolver != nil {
|
||||
result = append(result, fmt.Sprintf("singleNameOrder = \"%v\"", r.singleNameOrder))
|
||||
result = append(result, fmt.Sprintf("externalResolver = \"%s\"", r.externalResolver))
|
||||
result = append(result, fmt.Sprintf("cache item count = %d", r.cache.ItemCount()))
|
||||
} else {
|
||||
result = []string{"deactivated, use only IP address"}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ClientNamesResolver) Resolve(request *Request) (*Response, error) {
|
||||
clientNames := r.getClientNames(request)
|
||||
|
||||
request.ClientNames = clientNames
|
||||
request.Log = request.Log.WithField("client_names", strings.Join(clientNames, "; "))
|
||||
|
||||
return r.next.Resolve(request)
|
||||
}
|
||||
|
||||
// returns names of client
|
||||
func (r *ClientNamesResolver) getClientNames(request *Request) []string {
|
||||
ip := request.ClientIP
|
||||
c, found := r.cache.Get(ip.String())
|
||||
|
||||
if found {
|
||||
if t, ok := c.([]string); ok {
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
names := r.resolveClientNames(ip, withPrefix(request.Log, "client_names_resolver"))
|
||||
r.cache.Set(ip.String(), names, cache.DefaultExpiration)
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// performs reverse DNS lookup
|
||||
func (r *ClientNamesResolver) resolveClientNames(ip net.IP, logger *logrus.Entry) (result []string) {
|
||||
if r.externalResolver != nil {
|
||||
reverse, err := dns.ReverseAddr(ip.String())
|
||||
|
||||
if err != nil {
|
||||
logger.Warnf("can't create reverse address for %s", ip.String())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := r.externalResolver.Resolve(&Request{
|
||||
Req: util.NewMsgWithQuestion(reverse, dns.TypePTR),
|
||||
Log: logger,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error("can't resolve client name", err)
|
||||
return
|
||||
}
|
||||
|
||||
var clientNames []string
|
||||
|
||||
for _, answer := range resp.Res.Answer {
|
||||
if t, ok := answer.(*dns.PTR); ok {
|
||||
hostName := strings.TrimSuffix(t.Ptr, ".")
|
||||
clientNames = append(clientNames, hostName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(clientNames) == 0 {
|
||||
clientNames = []string{ip.String()}
|
||||
}
|
||||
|
||||
// optional: if singleNameOrder is set, use only one name in the defined order
|
||||
if len(r.singleNameOrder) > 0 {
|
||||
for _, i := range r.singleNameOrder {
|
||||
if i > 0 && int(i) <= len(clientNames) {
|
||||
result = []string{clientNames[i-1]}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = clientNames
|
||||
}
|
||||
|
||||
logger.WithField("client_names", strings.Join(result, "; ")).Debug("resolved client name(s)")
|
||||
} else {
|
||||
result = []string{ip.String()}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (r ClientNamesResolver) String() string {
|
||||
return fmt.Sprintf("client names resolver")
|
||||
}
|
||||
|
||||
// reset client name cache
|
||||
func (r *ClientNamesResolver) FlushCache() {
|
||||
r.cache.Flush()
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestClientNamesFromUpstream(t *testing.T) {
|
||||
callCount := 0
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
callCount++
|
||||
r, err := dns.ReverseAddr("192.168.178.25")
|
||||
assert.NoError(t, err)
|
||||
|
||||
response, err := util.NewMsgWithAnswer(fmt.Sprintf("%s 300 IN PTR myhost", r))
|
||||
|
||||
assert.NoError(t, err)
|
||||
return response
|
||||
})
|
||||
|
||||
sut := NewClientNamesResolver(config.ClientLookupConfig{Upstream: upstream})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
// first request
|
||||
request := &Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err := sut.Resolve(request)
|
||||
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "myhost", request.ClientNames[0])
|
||||
|
||||
// second request
|
||||
request = &Request{ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err = sut.Resolve(request)
|
||||
|
||||
// use cache -> call count 1
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, request.ClientNames, 1)
|
||||
assert.Equal(t, "myhost", request.ClientNames[0])
|
||||
}
|
||||
|
||||
func TestClientInfoFromUpstreamSingleNameWithOrder(t *testing.T) {
|
||||
callCount := 0
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
callCount++
|
||||
r, err := dns.ReverseAddr("192.168.178.25")
|
||||
assert.NoError(t, err)
|
||||
|
||||
response, err := util.NewMsgWithAnswer(fmt.Sprintf("%s 300 IN PTR myhost", r))
|
||||
|
||||
assert.NoError(t, err)
|
||||
return response
|
||||
})
|
||||
|
||||
sut := NewClientNamesResolver(config.ClientLookupConfig{
|
||||
Upstream: upstream,
|
||||
SingleNameOrder: []uint{2, 1}})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
// first request
|
||||
request := &Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err := sut.Resolve(request)
|
||||
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "myhost", request.ClientNames[0])
|
||||
|
||||
// second request
|
||||
request = &Request{ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err = sut.Resolve(request)
|
||||
|
||||
// use cache -> call count 1
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, request.ClientNames, 1)
|
||||
assert.Equal(t, "myhost", request.ClientNames[0])
|
||||
}
|
||||
|
||||
func TestClientInfoFromUpstreamMultipleNames(t *testing.T) {
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
r, err := dns.ReverseAddr("192.168.178.25")
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr1, err := dns.NewRR(fmt.Sprintf("%s 300 IN PTR myhost1", r))
|
||||
assert.NoError(t, err)
|
||||
rr2, err := dns.NewRR(fmt.Sprintf("%s 300 IN PTR myhost2", r))
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.Answer = []dns.RR{rr1, rr2}
|
||||
|
||||
return msg
|
||||
})
|
||||
|
||||
sut := NewClientNamesResolver(config.ClientLookupConfig{Upstream: upstream})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err := sut.Resolve(request)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, request.ClientNames, 2)
|
||||
assert.Equal(t, "myhost1", request.ClientNames[0])
|
||||
assert.Equal(t, "myhost2", request.ClientNames[1])
|
||||
}
|
||||
|
||||
func TestClientInfoFromUpstreamMultipleNamesSingleNameOrder(t *testing.T) {
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
r, err := dns.ReverseAddr("192.168.178.25")
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr1, err := dns.NewRR(fmt.Sprintf("%s 300 IN PTR myhost1", r))
|
||||
assert.NoError(t, err)
|
||||
rr2, err := dns.NewRR(fmt.Sprintf("%s 300 IN PTR myhost2", r))
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.Answer = []dns.RR{rr1, rr2}
|
||||
|
||||
return msg
|
||||
})
|
||||
|
||||
sut := NewClientNamesResolver(config.ClientLookupConfig{
|
||||
Upstream: upstream,
|
||||
SingleNameOrder: []uint{2, 1}})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err := sut.Resolve(request)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, request.ClientNames, 1)
|
||||
assert.Equal(t, "myhost2", request.ClientNames[0])
|
||||
}
|
||||
|
||||
func TestClientInfoFromUpstreamNotFound(t *testing.T) {
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetRcode(request, dns.RcodeNameError)
|
||||
|
||||
return msg
|
||||
})
|
||||
|
||||
sut := NewClientNamesResolver(config.ClientLookupConfig{Upstream: upstream})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err := sut.Resolve(request)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, request.ClientNames, 1)
|
||||
assert.Equal(t, "192.168.178.25", request.ClientNames[0])
|
||||
}
|
||||
|
||||
func TestClientInfoWithoutUpstream(t *testing.T) {
|
||||
sut := NewClientNamesResolver(config.ClientLookupConfig{})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{ClientIP: net.ParseIP("192.168.178.25"),
|
||||
Log: logrus.NewEntry(logrus.New())}
|
||||
_, err := sut.Resolve(request)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, request.ClientNames, 1)
|
||||
assert.Equal(t, "192.168.178.25", request.ClientNames[0])
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ConditionalUpstreamResolver delegates DNS question to other DNS resolver dependent on domain name in question
|
||||
type ConditionalUpstreamResolver struct {
|
||||
NextResolver
|
||||
mapping map[string]Resolver
|
||||
}
|
||||
|
||||
func NewConditionalUpstreamResolver(cfg config.ConditionalUpstreamConfig) ChainedResolver {
|
||||
m := make(map[string]Resolver)
|
||||
for domain, upstream := range cfg.Mapping {
|
||||
m[strings.ToLower(domain)] = NewUpstreamResolver(upstream)
|
||||
}
|
||||
|
||||
return &ConditionalUpstreamResolver{mapping: m}
|
||||
}
|
||||
|
||||
func (r *ConditionalUpstreamResolver) Configuration() (result []string) {
|
||||
if len(r.mapping) > 0 {
|
||||
for key, val := range r.mapping {
|
||||
result = append(result, fmt.Sprintf("%s = \"%s\"", key, val))
|
||||
}
|
||||
} else {
|
||||
result = []string{"deactivated"}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ConditionalUpstreamResolver) Resolve(request *Request) (*Response, error) {
|
||||
logger := withPrefix(request.Log, "conditional_resolver")
|
||||
|
||||
if len(r.mapping) > 0 {
|
||||
for _, question := range request.Req.Question {
|
||||
domain := util.ExtractDomain(question)
|
||||
|
||||
// try with domain with and without sub-domains
|
||||
for len(domain) > 0 {
|
||||
r, found := r.mapping[domain]
|
||||
if found {
|
||||
response, err := r.Resolve(request)
|
||||
if err == nil {
|
||||
response.Reason = "CONDITIONAL"
|
||||
}
|
||||
|
||||
logger.WithFields(logrus.Fields{
|
||||
"answer": util.AnswerToString(response.Res.Answer),
|
||||
"domain": domain,
|
||||
"upstream": r,
|
||||
}).Debugf("received response from conditional upstream")
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
if i := strings.Index(domain, "."); i >= 0 {
|
||||
domain = domain[i+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.WithField("next_resolver", r.next).Trace("go to next resolver")
|
||||
|
||||
return r.next.Resolve(request)
|
||||
}
|
||||
|
||||
func (r ConditionalUpstreamResolver) String() string {
|
||||
return fmt.Sprintf("conditional resolver")
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/util"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func setup() (sut *ConditionalUpstreamResolver, cond *resolverMock, next *resolverMock) {
|
||||
cond = &resolverMock{}
|
||||
next = &resolverMock{}
|
||||
sut = &ConditionalUpstreamResolver{
|
||||
mapping: map[string]Resolver{
|
||||
"fritz.box": cond,
|
||||
"other.box": cond,
|
||||
},
|
||||
}
|
||||
|
||||
cond.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
next.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(next)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Test_Resolve_Conditional_Exact(t *testing.T) {
|
||||
sut, conditionalResolver, nextResolver := setup()
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("fritz.box.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "CONDITIONAL", resp.Reason)
|
||||
conditionalResolver.AssertExpectations(t)
|
||||
nextResolver.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Conditional_ExactLast(t *testing.T) {
|
||||
sut, conditionalResolver, nextResolver := setup()
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("other.box.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "CONDITIONAL", resp.Reason)
|
||||
conditionalResolver.AssertExpectations(t)
|
||||
nextResolver.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Conditional_Subdomain(t *testing.T) {
|
||||
sut, conditionalResolver, nextResolver := setup()
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("test.fritz.box.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
_, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
conditionalResolver.AssertExpectations(t)
|
||||
nextResolver.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Conditional_Not_Match(t *testing.T) {
|
||||
sut, conditionalResolver, nextResolver := setup()
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
_, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
nextResolver.AssertExpectations(t)
|
||||
conditionalResolver.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const customDNSTTL = 60 * 60
|
||||
|
||||
// CustomDNSResolver resolves passed domain name to ip address defined in domain-IP map
|
||||
type CustomDNSResolver struct {
|
||||
NextResolver
|
||||
mapping map[string]net.IP
|
||||
}
|
||||
|
||||
func NewCustomDNSResolver(cfg config.CustomDNSConfig) ChainedResolver {
|
||||
m := make(map[string]net.IP)
|
||||
for url, ip := range cfg.Mapping {
|
||||
m[strings.ToLower(url)] = ip
|
||||
}
|
||||
|
||||
return &CustomDNSResolver{mapping: m}
|
||||
}
|
||||
|
||||
func (r *CustomDNSResolver) Configuration() (result []string) {
|
||||
if len(r.mapping) > 0 {
|
||||
for key, val := range r.mapping {
|
||||
result = append(result, fmt.Sprintf("%s = \"%s\"", key, val))
|
||||
}
|
||||
} else {
|
||||
result = []string{"deactivated"}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *CustomDNSResolver) Resolve(request *Request) (*Response, error) {
|
||||
logger := withPrefix(request.Log, "custom_dns_resolver")
|
||||
|
||||
if len(r.mapping) > 0 {
|
||||
for _, question := range request.Req.Question {
|
||||
domain := util.ExtractDomain(question)
|
||||
for len(domain) > 0 {
|
||||
ip, found := r.mapping[domain]
|
||||
if found {
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(request.Req)
|
||||
|
||||
if (ip.To4() != nil && question.Qtype == dns.TypeA) ||
|
||||
(strings.Contains(ip.String(), ":") && question.Qtype == dns.TypeAAAA) {
|
||||
rr, err := util.CreateAnswerFromQuestion(question, ip, customDNSTTL)
|
||||
|
||||
if err == nil {
|
||||
response.Answer = append(response.Answer, rr)
|
||||
|
||||
logger.WithFields(logrus.Fields{
|
||||
"answer": util.AnswerToString(response.Answer),
|
||||
"domain": domain,
|
||||
}).Debugf("returning custom dns entry")
|
||||
|
||||
return &Response{Res: response, Reason: "CUSTOM DNS"}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response.Rcode = dns.RcodeNameError
|
||||
|
||||
return &Response{Res: response, Reason: "CUSTOM DNS"}, nil
|
||||
}
|
||||
|
||||
if i := strings.Index(domain, "."); i >= 0 {
|
||||
domain = domain[i+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.WithField("resolver", r.next).Trace("go to next resolver")
|
||||
|
||||
return r.next.Resolve(request)
|
||||
}
|
||||
|
||||
func (r CustomDNSResolver) String() string {
|
||||
return fmt.Sprintf("custom resolver")
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func Test_Resolve_Custom_Name_Ip4_A(t *testing.T) {
|
||||
sut := NewCustomDNSResolver(config.CustomDNSConfig{
|
||||
Mapping: map[string]net.IP{"custom.domain": net.ParseIP("192.168.143.123")}})
|
||||
m := &resolverMock{}
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("custom.domain.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "custom.domain. 3600 IN A 192.168.143.123", resp.Res.Answer[0].String())
|
||||
m.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Custom_Name_Ip4_AAAA(t *testing.T) {
|
||||
sut := NewCustomDNSResolver(config.CustomDNSConfig{
|
||||
Mapping: map[string]net.IP{"custom.domain": net.ParseIP("192.168.143.123")}})
|
||||
m := &resolverMock{}
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("custom.domain.", dns.TypeAAAA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeNameError, resp.Res.Rcode)
|
||||
m.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Custom_Name_Ip6_AAAA(t *testing.T) {
|
||||
sut := NewCustomDNSResolver(config.CustomDNSConfig{
|
||||
Mapping: map[string]net.IP{"custom.domain": net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}})
|
||||
m := &resolverMock{}
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("custom.domain.", dns.TypeAAAA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "custom.domain. 3600 IN AAAA 2001:db8:85a3::8a2e:370:7334", resp.Res.Answer[0].String())
|
||||
m.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Custom_Name_Subdomain(t *testing.T) {
|
||||
sut := NewCustomDNSResolver(config.CustomDNSConfig{
|
||||
Mapping: map[string]net.IP{"custom.domain": net.ParseIP("192.168.143.123")}})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("ABC.CUSTOM.DOMAIN.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "ABC.CUSTOM.DOMAIN. 3600 IN A 192.168.143.123", resp.Res.Answer[0].String())
|
||||
m.AssertNotCalled(t, "Resolve", mock.Anything)
|
||||
}
|
||||
|
||||
func Test_Resolve_Delegate_Next(t *testing.T) {
|
||||
sut := NewCustomDNSResolver(config.CustomDNSConfig{
|
||||
Mapping: map[string]net.IP{"custom.domain": net.ParseIP("192.168.143.123")}})
|
||||
m := &resolverMock{}
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
_, _ = sut.Resolve(request)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type resolverMock struct {
|
||||
mock.Mock
|
||||
NextResolver
|
||||
}
|
||||
|
||||
func (r *resolverMock) Configuration() (result []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r *resolverMock) Resolve(req *Request) (*Response, error) {
|
||||
args := r.Called(req)
|
||||
return args.Get(0).(*Response), args.Error(1)
|
||||
}
|
||||
|
||||
func TestUDPUpstreamWithResponse(response *dns.Msg) config.Upstream {
|
||||
return TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
func TestUDPUpstream(fn func(request *dns.Msg) (response *dns.Msg)) config.Upstream {
|
||||
a, err := net.ResolveUDPAddr("udp4", ":0")
|
||||
if err != nil {
|
||||
log.Fatal("can't resolve address: ", err)
|
||||
}
|
||||
|
||||
ln, err := net.ListenUDP("udp4", a)
|
||||
if err != nil {
|
||||
log.Fatal("can't create connection: ", err)
|
||||
}
|
||||
|
||||
ladr := ln.LocalAddr().String()
|
||||
host := strings.Split(ladr, ":")[0]
|
||||
p, err := strconv.Atoi(strings.Split(ladr, ":")[1])
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("can't convert port: ", err)
|
||||
}
|
||||
|
||||
port := uint16(p)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
buffer := make([]byte, 1024)
|
||||
n, addr, err := ln.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
log.Fatal("error on reading from udp: ", err)
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
err = msg.Unpack(buffer[0 : n-1])
|
||||
if err != nil {
|
||||
log.Fatal("can't deserialize message: ", err)
|
||||
}
|
||||
|
||||
response := fn(msg)
|
||||
response.SetReply(msg)
|
||||
|
||||
b, err := response.Pack()
|
||||
if err != nil {
|
||||
log.Fatal("can't serialize message: ", err)
|
||||
}
|
||||
|
||||
_, err = ln.WriteToUDP(b, addr)
|
||||
if err != nil {
|
||||
log.Fatal("can't write to UDP: ", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return config.Upstream{Net: "udp", Host: host, Port: port}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ParallelBestResolver delegates the DNS message to 2 upstream resolvers and returns the fastest answer
|
||||
type ParallelBestResolver struct {
|
||||
resolvers []Resolver
|
||||
}
|
||||
|
||||
func NewParallelBestResolver(resolvers []Resolver) Resolver {
|
||||
return &ParallelBestResolver{resolvers: resolvers}
|
||||
}
|
||||
|
||||
func (r *ParallelBestResolver) Configuration() (result []string) {
|
||||
result = append(result, "upstream resolvers:")
|
||||
for _, res := range r.resolvers {
|
||||
result = append(result, fmt.Sprintf("- %s", res))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ParallelBestResolver) Resolve(request *Request) (*Response, error) {
|
||||
logger := request.Log.WithField("prefix", "parallel_best_resolver")
|
||||
|
||||
r1, r2 := r.pickRandom()
|
||||
logger.Debugf("using %s and %s as resolver", r1, r2)
|
||||
|
||||
ch1 := make(chan struct {
|
||||
*Response
|
||||
error
|
||||
})
|
||||
ch2 := make(chan struct {
|
||||
*Response
|
||||
error
|
||||
})
|
||||
|
||||
var err1, err2 error
|
||||
|
||||
logger.WithField("resolver", r1).Debug("delegating to resolver")
|
||||
|
||||
go resolve(request, r1, ch1)
|
||||
|
||||
logger.WithField("resolver", r2).Debug("delegating to resolver")
|
||||
|
||||
go resolve(request, r2, ch2)
|
||||
|
||||
for err1 == nil || err2 == nil {
|
||||
select {
|
||||
case msg1 := <-ch1:
|
||||
if msg1.error != nil {
|
||||
err1 = msg1.error
|
||||
ch1 = nil
|
||||
|
||||
logger.WithField("resolver", r1).Debug("resolution failed from resolver, cause: ", msg1.error)
|
||||
} else {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"resolver": r1,
|
||||
"answer": util.AnswerToString(msg1.Response.Res.Answer),
|
||||
}).Debug("using response from resolver")
|
||||
return msg1.Response, nil
|
||||
}
|
||||
case msg2 := <-ch2:
|
||||
if msg2.error != nil {
|
||||
err2 = msg2.error
|
||||
ch2 = nil
|
||||
|
||||
logger.WithField("resolver", r2).Debug("resolution failed from resolver, cause: ", msg2.error)
|
||||
} else {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"resolver": r2,
|
||||
"answer": util.AnswerToString(msg2.Response.Res.Answer),
|
||||
}).Debug("using response from resolver")
|
||||
return msg2.Response, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("resolution was not successful, errors: '%v', '%v'", err1, err2)
|
||||
}
|
||||
|
||||
// pick 2 different random resolvers from the resolver pool
|
||||
func (r *ParallelBestResolver) pickRandom() (resolver1, resolver2 Resolver) {
|
||||
resolver1 = r.resolvers[rand.Intn(len(r.resolvers))]
|
||||
for resolver2 == resolver1 || resolver2 == nil {
|
||||
resolver2 = r.resolvers[rand.Intn(len(r.resolvers))]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func resolve(req *Request, resolver Resolver, ch chan struct {
|
||||
*Response
|
||||
error
|
||||
}) {
|
||||
defer close(ch)
|
||||
|
||||
resp, err := resolver.Resolve(req)
|
||||
ch <- struct {
|
||||
*Response
|
||||
error
|
||||
}{resp, err}
|
||||
}
|
||||
|
||||
func (r ParallelBestResolver) String() string {
|
||||
return fmt.Sprintf("parallel best resolver '%s'", r.resolvers)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/util"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func Test_Resolve_Best_Result(t *testing.T) {
|
||||
fast := &resolverMock{}
|
||||
|
||||
mockResp, err := util.NewMsgWithAnswer("example.com. 123 IN A 192.168.178.44")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fast.On("Resolve", mock.Anything).Return(&Response{Res: mockResp}, nil)
|
||||
|
||||
slow := &resolverMock{}
|
||||
slow.On("Resolve", mock.Anything).WaitUntil(time.After(50*time.Millisecond)).Return(&Response{Res: new(dns.Msg)}, nil)
|
||||
|
||||
sut := NewParallelBestResolver([]Resolver{slow, fast})
|
||||
|
||||
resp, err := sut.Resolve(&Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "example.com. 123 IN A 192.168.178.44", resp.Res.Answer[0].String())
|
||||
fast.AssertExpectations(t)
|
||||
slow.AssertExpectations(t)
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
cleanUpRunPeriod = 12 * time.Hour
|
||||
queryLoggingResolverPrefix = "query_logging_resolver"
|
||||
logChanCap = 1000
|
||||
)
|
||||
|
||||
// QueryLoggingResolver writes query information (question, answer, duration, ...) into
|
||||
// log file or as log entry (if log directory is not configured)
|
||||
type QueryLoggingResolver struct {
|
||||
NextResolver
|
||||
logDir string
|
||||
perClient bool
|
||||
logRetentionDays uint64
|
||||
logChan chan *queryLogEntry
|
||||
}
|
||||
|
||||
type queryLogEntry struct {
|
||||
request *Request
|
||||
response *Response
|
||||
start time.Time
|
||||
durationMs int64
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func NewQueryLoggingResolver(cfg config.QueryLogConfig) ChainedResolver {
|
||||
if cfg.Dir != "" && unix.Access(cfg.Dir, unix.W_OK) != nil {
|
||||
logger(queryLoggingResolverPrefix).Fatalf("query log directory '%s' does not exist or is not writable", cfg.Dir)
|
||||
}
|
||||
|
||||
logChan := make(chan *queryLogEntry, logChanCap)
|
||||
|
||||
resolver := QueryLoggingResolver{
|
||||
logDir: cfg.Dir,
|
||||
perClient: cfg.PerClient,
|
||||
logRetentionDays: cfg.LogRetentionDays,
|
||||
logChan: logChan,
|
||||
}
|
||||
|
||||
go resolver.writeLog()
|
||||
|
||||
if cfg.LogRetentionDays > 0 {
|
||||
go resolver.periodicCleanUp()
|
||||
}
|
||||
|
||||
return &resolver
|
||||
}
|
||||
|
||||
// triggers periodically cleanup of old log files
|
||||
func (r *QueryLoggingResolver) periodicCleanUp() {
|
||||
ticker := time.NewTicker(cleanUpRunPeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
r.doCleanUp()
|
||||
}
|
||||
}
|
||||
|
||||
// deletes old log files
|
||||
func (r *QueryLoggingResolver) doCleanUp() {
|
||||
logger := logger(queryLoggingResolverPrefix)
|
||||
|
||||
logger.Trace("starting clean up")
|
||||
|
||||
files, err := ioutil.ReadDir(r.logDir)
|
||||
if err != nil {
|
||||
logger.WithField("log_dir", r.logDir).Error("can't list log directory: ", err)
|
||||
}
|
||||
|
||||
// search for log files, which names starts with date
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f.Name(), ".log") && len(f.Name()) > 10 {
|
||||
t, err := time.Parse("2006-01-02", f.Name()[:10])
|
||||
if err == nil {
|
||||
differenceDays := uint64(time.Since(t).Hours() / 24)
|
||||
if r.logRetentionDays > 0 && differenceDays > r.logRetentionDays {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"file": f.Name(),
|
||||
"ageInDays": differenceDays,
|
||||
"logRetentionDays": r.logRetentionDays,
|
||||
}).Info("existing log file is older than retention time and will be deleted")
|
||||
|
||||
err := os.Remove(filepath.Join(r.logDir, f.Name()))
|
||||
if err != nil {
|
||||
logger.WithField("file", f.Name()).Error("can't remove file: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *QueryLoggingResolver) Resolve(request *Request) (*Response, error) {
|
||||
logger := withPrefix(request.Log, queryLoggingResolverPrefix)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
resp, err := r.next.Resolve(request)
|
||||
|
||||
duration := time.Since(start).Milliseconds()
|
||||
|
||||
if err == nil {
|
||||
select {
|
||||
case r.logChan <- &queryLogEntry{
|
||||
request: request,
|
||||
response: resp,
|
||||
start: start,
|
||||
durationMs: duration,
|
||||
logger: logger}:
|
||||
default:
|
||||
logger.Error("query log writer is too slow, log entry will be dropped")
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// write entry: if log directory is configured, write to log file
|
||||
func (r *QueryLoggingResolver) writeLog() {
|
||||
for logEntry := range r.logChan {
|
||||
if r.logDir != "" {
|
||||
var clientPrefix string
|
||||
|
||||
start := time.Now()
|
||||
|
||||
dateString := logEntry.start.Format("2006-01-02")
|
||||
|
||||
if r.perClient {
|
||||
clientPrefix = strings.Join(logEntry.request.ClientNames, "-")
|
||||
} else {
|
||||
clientPrefix = "ALL"
|
||||
}
|
||||
|
||||
writePath := filepath.Join(r.logDir, fmt.Sprintf("%s_%s.log", dateString, clientPrefix))
|
||||
|
||||
file, err := os.OpenFile(writePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
|
||||
|
||||
if err != nil {
|
||||
logEntry.logger.WithField("file_name", writePath).Error("can't create/open file", err)
|
||||
} else {
|
||||
writer := createCsvWriter(file)
|
||||
|
||||
err := writer.Write(createQueryLogRow(logEntry))
|
||||
if err != nil {
|
||||
logEntry.logger.WithField("file_name", writePath).Error("can't write to file", err)
|
||||
}
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
halfCap := cap(r.logChan) / 2
|
||||
|
||||
// if log channel is > 50% full, this could be a problem with slow writer (external storage over network etc.)
|
||||
if len(r.logChan) > halfCap {
|
||||
logEntry.logger.WithField("channel_len",
|
||||
len(r.logChan)).Warnf("query log writer is too slow, write duration: %d ms", time.Since(start).Milliseconds())
|
||||
}
|
||||
} else {
|
||||
logEntry.logger.WithFields(
|
||||
logrus.Fields{
|
||||
"response_reason": logEntry.response.Reason,
|
||||
"answer": util.AnswerToString(logEntry.response.Res.Answer),
|
||||
"duration_ms": logEntry.durationMs,
|
||||
},
|
||||
).Infof("query resolved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createCsvWriter(file io.Writer) *csv.Writer {
|
||||
writer := csv.NewWriter(file)
|
||||
writer.Comma = '\t'
|
||||
|
||||
return writer
|
||||
}
|
||||
|
||||
func createQueryLogRow(logEntry *queryLogEntry) []string {
|
||||
request := logEntry.request
|
||||
response := logEntry.response
|
||||
|
||||
return []string{
|
||||
logEntry.start.Format("2006-01-02 15:04:05"),
|
||||
request.ClientIP.String(),
|
||||
strings.Join(request.ClientNames, "; "),
|
||||
fmt.Sprintf("%d", logEntry.durationMs),
|
||||
response.Reason,
|
||||
util.QuestionToString(request.Req.Question),
|
||||
util.AnswerToString(response.Res.Answer),
|
||||
dns.RcodeToString[response.Res.Rcode],
|
||||
}
|
||||
}
|
||||
|
||||
func (r *QueryLoggingResolver) Configuration() (result []string) {
|
||||
if r.logDir != "" {
|
||||
result = append(result, fmt.Sprintf("logDir= \"%s\"", r.logDir))
|
||||
result = append(result, fmt.Sprintf("perClient = %t", r.perClient))
|
||||
result = append(result, fmt.Sprintf("logRetentionDays= %d", r.logRetentionDays))
|
||||
|
||||
if r.logRetentionDays == 0 {
|
||||
result = append(result, "log cleanup deactivated")
|
||||
}
|
||||
} else {
|
||||
result = []string{"deactivated"}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r QueryLoggingResolver) String() string {
|
||||
return fmt.Sprintf("query logging resolver")
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func Test_doCleanUp(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "queryLoggingResolver")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 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"))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
f2, err := os.Create(filepath.Join(tmpDir, fmt.Sprintf("%s-test.log", dateBefore8Days.Format("2006-01-02"))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
sut := NewQueryLoggingResolver(config.QueryLogConfig{
|
||||
Dir: tmpDir,
|
||||
LogRetentionDays: 7,
|
||||
})
|
||||
|
||||
sut.(*QueryLoggingResolver).doCleanUp()
|
||||
|
||||
// file 1 exist
|
||||
_, err = os.Stat(f1.Name())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// file 2 was deleted
|
||||
_, err = os.Stat(f2.Name())
|
||||
assert.Error(t, err)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func Test_Resolve_WithEmptyConfig(t *testing.T) {
|
||||
sut := NewQueryLoggingResolver(config.QueryLogConfig{})
|
||||
m := &resolverMock{}
|
||||
resp, err := util.NewMsgWithAnswer("example.com. 300 IN A 123.122.121.120")
|
||||
assert.NoError(t, err)
|
||||
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: resp, Reason: "reason"}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
_, err = sut.Resolve(&Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New())})
|
||||
assert.NoError(t, err)
|
||||
m.AssertExpectations(t)
|
||||
}
|
||||
func Test_Resolve_WithLoggingPerClient(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "queryLoggingResolver")
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
sut := NewQueryLoggingResolver(config.QueryLogConfig{
|
||||
Dir: tmpDir,
|
||||
PerClient: true,
|
||||
})
|
||||
|
||||
m := &resolverMock{}
|
||||
resp, err := util.NewMsgWithAnswer("example.com. 300 IN A 123.122.121.120")
|
||||
assert.NoError(t, err)
|
||||
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: resp, Reason: "reason"}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
// request client1
|
||||
_, err = sut.Resolve(&Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New())})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// request client2
|
||||
_, err = sut.Resolve(&Request{
|
||||
ClientIP: net.ParseIP("192.168.178.26"),
|
||||
ClientNames: []string{"client2"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New())})
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
|
||||
// client1
|
||||
csvLines := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client1.log", time.Now().Format("2006-01-02"))))
|
||||
|
||||
assert.Len(t, csvLines, 1)
|
||||
assert.Equal(t, "192.168.178.25", csvLines[0][1])
|
||||
assert.Equal(t, "client1", csvLines[0][2])
|
||||
assert.Equal(t, "reason", csvLines[0][4])
|
||||
assert.Equal(t, "A (google.de.)", csvLines[0][5])
|
||||
assert.Equal(t, "A (123.122.121.120)", csvLines[0][6])
|
||||
|
||||
// client2
|
||||
csvLines = readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_client2.log", time.Now().Format("2006-01-02"))))
|
||||
|
||||
assert.Len(t, csvLines, 1)
|
||||
assert.Equal(t, "192.168.178.26", csvLines[0][1])
|
||||
assert.Equal(t, "client2", csvLines[0][2])
|
||||
assert.Equal(t, "reason", csvLines[0][4])
|
||||
assert.Equal(t, "A (google.de.)", csvLines[0][5])
|
||||
assert.Equal(t, "A (123.122.121.120)", csvLines[0][6])
|
||||
}
|
||||
|
||||
func Test_Resolve_WithLoggingAll(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "queryLoggingResolver")
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
sut := NewQueryLoggingResolver(config.QueryLogConfig{
|
||||
Dir: tmpDir,
|
||||
PerClient: false,
|
||||
})
|
||||
|
||||
m := &resolverMock{}
|
||||
resp, err := util.NewMsgWithAnswer("example.com. 300 IN A 123.122.121.120")
|
||||
assert.NoError(t, err)
|
||||
|
||||
m.On("Resolve", mock.Anything).Return(&Response{Res: resp, Reason: "reason"}, nil)
|
||||
sut.Next(m)
|
||||
|
||||
// request client1
|
||||
_, err = sut.Resolve(&Request{
|
||||
ClientIP: net.ParseIP("192.168.178.25"),
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New())})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// request client2
|
||||
_, err = sut.Resolve(&Request{
|
||||
ClientIP: net.ParseIP("192.168.178.26"),
|
||||
ClientNames: []string{"client2"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New())})
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
m.AssertExpectations(t)
|
||||
|
||||
csvLines := readCsv(filepath.Join(tmpDir, fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02"))))
|
||||
assert.Len(t, csvLines, 2)
|
||||
|
||||
// client1 -> first line
|
||||
assert.Equal(t, "192.168.178.25", csvLines[0][1])
|
||||
assert.Equal(t, "client1", csvLines[0][2])
|
||||
assert.Equal(t, "reason", csvLines[0][4])
|
||||
assert.Equal(t, "A (google.de.)", csvLines[0][5])
|
||||
assert.Equal(t, "A (123.122.121.120)", csvLines[0][6])
|
||||
|
||||
// client2 -> second line
|
||||
assert.Equal(t, "192.168.178.26", csvLines[1][1])
|
||||
assert.Equal(t, "client2", csvLines[1][2])
|
||||
assert.Equal(t, "reason", csvLines[1][4])
|
||||
assert.Equal(t, "A (google.de.)", csvLines[1][5])
|
||||
assert.Equal(t, "A (123.122.121.120)", csvLines[1][6])
|
||||
}
|
||||
|
||||
func readCsv(file string) [][]string {
|
||||
var result [][]string
|
||||
|
||||
csvFile, err := os.Open(file)
|
||||
if err != nil {
|
||||
log.Fatal("can't open file", err)
|
||||
}
|
||||
|
||||
reader := csv.NewReader(bufio.NewReader(csvFile))
|
||||
reader.Comma = '\t'
|
||||
|
||||
for {
|
||||
line, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Fatal("can't read line", err)
|
||||
}
|
||||
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ClientIP net.IP
|
||||
ClientNames []string
|
||||
Req *dns.Msg
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
type ResponseType uint8
|
||||
|
||||
const (
|
||||
Resolved ResponseType = iota
|
||||
Blocked
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Res *dns.Msg
|
||||
Reason string
|
||||
}
|
||||
type Resolver interface {
|
||||
Resolve(req *Request) (*Response, error)
|
||||
Configuration() []string
|
||||
}
|
||||
|
||||
type ChainedResolver interface {
|
||||
Resolver
|
||||
Next(n Resolver)
|
||||
GetNext() Resolver
|
||||
}
|
||||
|
||||
type NextResolver struct {
|
||||
next Resolver
|
||||
}
|
||||
|
||||
func (r *NextResolver) Next(n Resolver) {
|
||||
r.next = n
|
||||
}
|
||||
|
||||
func (r *NextResolver) GetNext() Resolver {
|
||||
return r.next
|
||||
}
|
||||
|
||||
func logger(prefix string) *logrus.Entry {
|
||||
return logrus.WithField("prefix", prefix)
|
||||
}
|
||||
|
||||
func withPrefix(logger *logrus.Entry, prefix string) *logrus.Entry {
|
||||
return logger.WithField("prefix", prefix)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// UpstreamResolver sends request to external DNS server
|
||||
type UpstreamResolver struct {
|
||||
NextResolver
|
||||
client *dns.Client
|
||||
upstream string
|
||||
}
|
||||
|
||||
func NewUpstreamResolver(upstream config.Upstream) Resolver {
|
||||
client := new(dns.Client)
|
||||
client.Net = upstream.Net
|
||||
|
||||
return &UpstreamResolver{
|
||||
client: client,
|
||||
upstream: net.JoinHostPort(upstream.Host, strconv.Itoa(int(upstream.Port)))}
|
||||
}
|
||||
|
||||
func (r *UpstreamResolver) Configuration() (result []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r *UpstreamResolver) Resolve(request *Request) (response *Response, err error) {
|
||||
logger := withPrefix(request.Log, "upstream_resolver")
|
||||
|
||||
attempt := 1
|
||||
|
||||
var rtt time.Duration
|
||||
|
||||
var resp *dns.Msg
|
||||
|
||||
for attempt <= 3 {
|
||||
if resp, rtt, err = r.client.Exchange(request.Req, r.upstream); err == nil {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"answer": util.AnswerToString(resp.Answer),
|
||||
"return_code": dns.RcodeToString[resp.Rcode],
|
||||
"upstream": r.upstream,
|
||||
"response_time_ms": rtt.Milliseconds(),
|
||||
}).Debugf("received response from upstream")
|
||||
|
||||
return &Response{Res: resp, Reason: fmt.Sprintf("RESOLVED (%s) in %d ms", r.upstream, rtt.Milliseconds())}, err
|
||||
}
|
||||
|
||||
if errNet, ok := err.(net.Error); ok && (errNet.Timeout() || errNet.Temporary()) {
|
||||
logger.WithField("attempt", attempt).Debugf("Temporary network error / Timeout occurred, retrying...")
|
||||
attempt++
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r UpstreamResolver) String() string {
|
||||
return fmt.Sprintf("upstream '%s'", r.upstream)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Resolve_Upstream(t *testing.T) {
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
response, err := util.NewMsgWithAnswer("example.com 123 IN A 123.124.122.122")
|
||||
|
||||
assert.NoError(t, err)
|
||||
return response
|
||||
})
|
||||
|
||||
sut := NewUpstreamResolver(upstream)
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
resp, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Res.Rcode)
|
||||
assert.Equal(t, "example.com. 123 IN A 123.124.122.122", resp.Res.Answer[0].String())
|
||||
}
|
||||
|
||||
func TestUpstreamTimeout(t *testing.T) {
|
||||
counter := 0
|
||||
attemptsWithTimeout := 2
|
||||
|
||||
upstream := TestUDPUpstream(func(request *dns.Msg) (response *dns.Msg) {
|
||||
counter++
|
||||
// timeout on first x attempts
|
||||
if counter <= attemptsWithTimeout {
|
||||
fmt.Print("timeout")
|
||||
time.Sleep(110 * time.Millisecond)
|
||||
}
|
||||
response, err := util.NewMsgWithAnswer("example.com 123 IN A 123.124.122.122")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return response
|
||||
})
|
||||
|
||||
sut := NewUpstreamResolver(upstream).(*UpstreamResolver)
|
||||
sut.client.ReadTimeout = 100 * time.Millisecond
|
||||
|
||||
request := &Request{
|
||||
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
|
||||
Log: logrus.NewEntry(logrus.New()),
|
||||
}
|
||||
|
||||
// first request -> after 2 timeouts success
|
||||
response, err := sut.Resolve(request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if response != nil {
|
||||
assert.Equal(t, dns.RcodeSuccess, response.Res.Rcode)
|
||||
assert.Equal(t, "example.com.\t123\tIN\tA\t123.124.122.122", response.Res.Answer[0].String())
|
||||
}
|
||||
|
||||
attemptsWithTimeout = 3
|
||||
counter = 0
|
||||
|
||||
// second request
|
||||
// all 3 attempts with timeout
|
||||
response, err = sut.Resolve(request)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.HasSuffix(err.Error(), "i/o timeout"))
|
||||
assert.Nil(t, response)
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/resolver"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
udpServer *dns.Server
|
||||
tcpServer *dns.Server
|
||||
queryResolver resolver.Resolver
|
||||
}
|
||||
|
||||
func logger() *logrus.Entry {
|
||||
return logrus.WithField("prefix", "server")
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config) (*Server, error) {
|
||||
udpHandler := dns.NewServeMux()
|
||||
tcpHandler := dns.NewServeMux()
|
||||
udpServer := &dns.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
Net: "udp",
|
||||
Handler: udpHandler,
|
||||
NotifyStartedFunc: func() {
|
||||
logger().Infof("udp server is up and running")
|
||||
},
|
||||
UDPSize: 65535}
|
||||
tcpServer := &dns.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
Net: "tcp",
|
||||
Handler: tcpHandler,
|
||||
NotifyStartedFunc: func() {
|
||||
logger().Infof("tcp server is up and running")
|
||||
},
|
||||
}
|
||||
|
||||
var queryResolver resolver.Resolver
|
||||
|
||||
clientNamesResolver := resolver.NewClientNamesResolver(cfg.ClientLookup)
|
||||
queryLoggingResolver := resolver.NewQueryLoggingResolver(cfg.QueryLog)
|
||||
conditionalUpstreamResolver := resolver.NewConditionalUpstreamResolver(cfg.Conditional)
|
||||
customDNSResolver := resolver.NewCustomDNSResolver(cfg.CustomDNS)
|
||||
blacklistResolver := resolver.NewBlockingResolver(cfg.Blocking)
|
||||
|
||||
cachingResolver := resolver.NewCachingResolver()
|
||||
parallelUpstreamResolver := createParallelUpstreamResolver(cfg.Upstream.ExternalResolvers)
|
||||
|
||||
clientNamesResolver.Next(queryLoggingResolver)
|
||||
queryLoggingResolver.Next(conditionalUpstreamResolver)
|
||||
conditionalUpstreamResolver.Next(customDNSResolver)
|
||||
customDNSResolver.Next(blacklistResolver)
|
||||
blacklistResolver.Next(cachingResolver)
|
||||
cachingResolver.Next(parallelUpstreamResolver)
|
||||
|
||||
queryResolver = clientNamesResolver
|
||||
|
||||
server := Server{
|
||||
udpServer: udpServer,
|
||||
tcpServer: tcpServer,
|
||||
queryResolver: queryResolver,
|
||||
}
|
||||
|
||||
server.printConfiguration()
|
||||
|
||||
udpHandler.HandleFunc(".", server.OnRequest)
|
||||
tcpHandler.HandleFunc(".", server.OnRequest)
|
||||
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
func (s *Server) printConfiguration() {
|
||||
logger().Info("current configuration:")
|
||||
|
||||
res := s.queryResolver
|
||||
for res != nil {
|
||||
logger().Infof("-> resolver: '%s'", res)
|
||||
|
||||
for _, c := range res.Configuration() {
|
||||
logger().Infof(" %s", c)
|
||||
}
|
||||
|
||||
if c, ok := res.(resolver.ChainedResolver); ok {
|
||||
res = c.GetNext()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createParallelUpstreamResolver(upstream []config.Upstream) resolver.Resolver {
|
||||
if len(upstream) == 1 {
|
||||
return resolver.NewUpstreamResolver(upstream[0])
|
||||
}
|
||||
|
||||
resolvers := make([]resolver.Resolver, len(upstream))
|
||||
|
||||
for i, u := range upstream {
|
||||
resolvers[i] = resolver.NewUpstreamResolver(u)
|
||||
}
|
||||
|
||||
return resolver.NewParallelBestResolver(resolvers)
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
logger().Info("Starting server")
|
||||
|
||||
go func() {
|
||||
if err := s.udpServer.ListenAndServe(); err != nil {
|
||||
logger().Fatalf("start %s listener failed: %v", s.udpServer.Net, err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := s.tcpServer.ListenAndServe(); err != nil {
|
||||
logger().Fatalf("start %s listener failed: %v", s.tcpServer.Net, err)
|
||||
}
|
||||
}()
|
||||
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, syscall.SIGUSR1)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-signals
|
||||
s.printConfiguration()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
logger().Info("Stopping server")
|
||||
|
||||
if err := s.udpServer.Shutdown(); err != nil {
|
||||
logger().Fatalf("stop %s listener failed: %v", s.udpServer.Net, err)
|
||||
}
|
||||
|
||||
if err := s.tcpServer.Shutdown(); err != nil {
|
||||
logger().Fatalf("stop %s listener failed: %v", s.tcpServer.Net, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) OnRequest(w dns.ResponseWriter, request *dns.Msg) {
|
||||
logger().Debug("new request")
|
||||
|
||||
clientIP := resolveClientIP(w.RemoteAddr())
|
||||
r := &resolver.Request{
|
||||
ClientIP: clientIP,
|
||||
Req: request,
|
||||
Log: logrus.WithFields(logrus.Fields{
|
||||
"question": util.QuestionToString(request.Question),
|
||||
"client_ip": clientIP,
|
||||
}),
|
||||
}
|
||||
|
||||
response, err := s.queryResolver.Resolve(r)
|
||||
|
||||
if err != nil {
|
||||
logger().Errorf("error on processing request: %v", err)
|
||||
dns.HandleFailed(w, request)
|
||||
} else {
|
||||
response.Res.MsgHdr.RecursionAvailable = request.MsgHdr.RecursionDesired
|
||||
|
||||
if err := w.WriteMsg(response.Res); err != nil {
|
||||
logger().Error("can't write message: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveClientIP(addr net.Addr) net.IP {
|
||||
var clientIP net.IP
|
||||
if t, ok := addr.(*net.UDPAddr); ok {
|
||||
clientIP = t.IP
|
||||
} else if t, ok := addr.(*net.TCPAddr); ok {
|
||||
clientIP = t.IP
|
||||
}
|
||||
|
||||
return clientIP
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"blocky/config"
|
||||
"blocky/resolver"
|
||||
"blocky/util"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var mockClientName string
|
||||
|
||||
// test case definition
|
||||
var tests = []struct {
|
||||
name string
|
||||
request *dns.Msg
|
||||
mockClientName string
|
||||
respValidator func(*testing.T, *dns.Msg)
|
||||
}{
|
||||
{
|
||||
// resolve query via external dns
|
||||
name: "resolveWithUpstream",
|
||||
request: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "google.de.\t250\tIN\tA\t123.124.122.122", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// custom dnd entry with exact match
|
||||
name: "customDns",
|
||||
request: util.NewMsgWithQuestion("custom.lan.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "custom.lan.\t3600\tIN\tA\t192.168.178.55", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// sub domain custom dns
|
||||
name: "customDnsWithSubdomain",
|
||||
request: util.NewMsgWithQuestion("host.lan.home.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "host.lan.home.\t3600\tIN\tA\t192.168.178.56", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// delegate to special dns upstream
|
||||
name: "conditional",
|
||||
request: util.NewMsgWithQuestion("host.fritz.box.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "host.fritz.box.\t3600\tIN\tA\t192.168.178.2", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// blocking default group
|
||||
name: "blockDefault",
|
||||
request: util.NewMsgWithQuestion("doubleclick.net.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "doubleclick.net.\t21600\tIN\tA\t0.0.0.0", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// blocking default group with sub domain
|
||||
name: "blockDefaultWithSubdomain",
|
||||
request: util.NewMsgWithQuestion("www.bild.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "www.bild.de.\t21600\tIN\tA\t0.0.0.0", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// no blocking default group with sub domain
|
||||
name: "noBlockDefaultWithSubdomain",
|
||||
request: util.NewMsgWithQuestion("bild.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "bild.de.\t250\tIN\tA\t123.124.122.122", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// white and block default group
|
||||
name: "whiteBlackDefault",
|
||||
request: util.NewMsgWithQuestion("heise.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "heise.de.\t250\tIN\tA\t123.124.122.122", resp.Answer[0].String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// no block client whitelist only
|
||||
name: "noBlockWhitelistOnly",
|
||||
mockClientName: "clWhitelistOnly",
|
||||
request: util.NewMsgWithQuestion("heise.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "123.124.122.122", resp.Answer[0].(*dns.A).A.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// block client whitelist only
|
||||
name: "blockWhitelistOnly",
|
||||
mockClientName: "clWhitelistOnly",
|
||||
request: util.NewMsgWithQuestion("google.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "0.0.0.0", resp.Answer[0].(*dns.A).A.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// block client with 2 groups
|
||||
name: "block2groups1",
|
||||
mockClientName: "clAdsAndYoutube",
|
||||
request: util.NewMsgWithQuestion("www.bild.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "0.0.0.0", resp.Answer[0].(*dns.A).A.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// block client with 2 groups
|
||||
name: "block2groups2",
|
||||
mockClientName: "clAdsAndYoutube",
|
||||
request: util.NewMsgWithQuestion("youtube.com.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "0.0.0.0", resp.Answer[0].(*dns.A).A.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// lient with 1 group: no block if domain in other group
|
||||
name: "noBlockBlacklistOtherGroup",
|
||||
mockClientName: "clYoutubeOnly",
|
||||
request: util.NewMsgWithQuestion("www.bild.de.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "123.124.122.122", resp.Answer[0].(*dns.A).A.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
// block client with 1 group
|
||||
name: "blockBlacklist",
|
||||
mockClientName: "clYoutubeOnly",
|
||||
request: util.NewMsgWithQuestion("youtube.com.", dns.TypeA),
|
||||
respValidator: func(t *testing.T, resp *dns.Msg) {
|
||||
assert.Equal(t, dns.RcodeSuccess, resp.Rcode)
|
||||
assert.Equal(t, "0.0.0.0", resp.Answer[0].(*dns.A).A.String())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func TestDnsRequest(t *testing.T) {
|
||||
upstreamGoogle := resolver.TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
response, err := util.NewMsgWithAnswer(fmt.Sprintf("%s %d %s %s %s",
|
||||
util.ExtractDomain(request.Question[0]), 123, "IN", "A", "123.124.122.122"))
|
||||
|
||||
assert.NoError(t, err)
|
||||
return response
|
||||
})
|
||||
upstreamFritzbox := resolver.TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
response, err := util.NewMsgWithAnswer(fmt.Sprintf("%s %d %s %s %s",
|
||||
util.ExtractDomain(request.Question[0]), 3600, "IN", "A", "192.168.178.2"))
|
||||
|
||||
assert.NoError(t, err)
|
||||
return response
|
||||
})
|
||||
|
||||
upstreamClient := resolver.TestUDPUpstream(func(request *dns.Msg) *dns.Msg {
|
||||
response, err := util.NewMsgWithAnswer(fmt.Sprintf("%s %d %s %s %s",
|
||||
util.ExtractDomain(request.Question[0]), 3600, "IN", "PTR", mockClientName))
|
||||
|
||||
assert.NoError(t, err)
|
||||
return response
|
||||
})
|
||||
|
||||
// create server
|
||||
server, err := NewServer(&config.Config{
|
||||
CustomDNS: config.CustomDNSConfig{
|
||||
Mapping: map[string]net.IP{
|
||||
"custom.lan": net.ParseIP("192.168.178.55"),
|
||||
"lan.home": net.ParseIP("192.168.178.56"),
|
||||
},
|
||||
},
|
||||
Conditional: config.ConditionalUpstreamConfig{
|
||||
Mapping: map[string]config.Upstream{"fritz.box": upstreamFritzbox},
|
||||
},
|
||||
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"}},
|
||||
WhiteLists: map[string][]string{
|
||||
"ads": {"../testdata/heise.de.txt"},
|
||||
"whitelist": {"../testdata/heise.de.txt"},
|
||||
},
|
||||
ClientGroupsBlock: map[string][]string{
|
||||
"default": {"ads"},
|
||||
"clWhitelistOnly": {"whitelist"},
|
||||
"clAdsAndYoutube": {"ads", "youtube"},
|
||||
"clYoutubeOnly": {"youtube"},
|
||||
},
|
||||
},
|
||||
Upstream: config.UpstreamConfig{
|
||||
ExternalResolvers: []config.Upstream{upstreamGoogle},
|
||||
},
|
||||
ClientLookup: config.ClientLookupConfig{
|
||||
Upstream: upstreamClient,
|
||||
},
|
||||
|
||||
Port: 55555,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// start server
|
||||
go func() {
|
||||
server.Start()
|
||||
}()
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
for _, tt := range tests {
|
||||
tst := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res := server.queryResolver
|
||||
for res != nil {
|
||||
if t, ok := res.(*resolver.ClientNamesResolver); ok {
|
||||
t.FlushCache()
|
||||
break
|
||||
}
|
||||
if c, ok := res.(resolver.ChainedResolver); ok {
|
||||
res = c.GetNext()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mockClientName = tst.mockClientName
|
||||
response := requestServer(tst.request)
|
||||
|
||||
tst.respValidator(t, response)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 26106 43273 ns/op 12518 B/op 137 allocs/op
|
||||
func BenchmarkServerExternalResolver(b *testing.B) {
|
||||
msg, _ := util.NewMsgWithAnswer(fmt.Sprintf("example.com IN A 123.124.122.122"))
|
||||
upstreamExternal := resolver.TestUDPUpstreamWithResponse(msg)
|
||||
|
||||
// create server
|
||||
server, err := NewServer(&config.Config{
|
||||
Upstream: config.UpstreamConfig{
|
||||
ExternalResolvers: []config.Upstream{upstreamExternal},
|
||||
},
|
||||
Port: 55555,
|
||||
})
|
||||
|
||||
assert.NoError(b, err)
|
||||
|
||||
// start server
|
||||
go func() {
|
||||
server.Start()
|
||||
}()
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = requestServer(util.NewMsgWithQuestion("google.de.", dns.TypeA))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func requestServer(request *dns.Msg) *dns.Msg {
|
||||
conn, err := net.Dial("udp", ":55555")
|
||||
if err != nil {
|
||||
log.Fatal("could not connect to server: ", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
msg, err := request.Pack()
|
||||
if err != nil {
|
||||
log.Fatal("can't pack request: ", err)
|
||||
}
|
||||
|
||||
_, err = conn.Write(msg)
|
||||
if err != nil {
|
||||
log.Fatal("can't send request to server: ", err)
|
||||
}
|
||||
|
||||
out := make([]byte, 1024)
|
||||
|
||||
if _, err := conn.Read(out); err == nil {
|
||||
response := new(dns.Msg)
|
||||
err := response.Unpack(out)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("can't unpack response: ", err)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
log.Fatal("could not read from connection", err)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
upstream:
|
||||
externalResolvers:
|
||||
- udp:8.8.8.8
|
||||
- udp:8.8.4.4
|
||||
- udp:1.1.1.1
|
||||
customDNS:
|
||||
mapping:
|
||||
my.duckdns.org: 192.168.178.3
|
||||
conditional:
|
||||
mapping:
|
||||
fritz.box: udp:192.168.178.1
|
||||
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
|
||||
#blockMode: zeroIP
|
||||
clientLookup:
|
||||
upstream: udp:192.168.178.1
|
||||
singleNameOrder:
|
||||
- 2
|
||||
- 1
|
||||
|
||||
queryLog:
|
||||
dir: /opt/log
|
||||
perClient: true
|
||||
|
||||
port: 55555
|
||||
logLevel: debug
|
|
@ -0,0 +1 @@
|
|||
doubleclick.net
|
|
@ -0,0 +1 @@
|
|||
heise.de
|
|
@ -0,0 +1 @@
|
|||
www.bild.de
|
|
@ -0,0 +1 @@
|
|||
youtube.com
|
|
@ -0,0 +1,80 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func qTypeToString() func(uint16) string {
|
||||
innerMap := map[uint16]string{
|
||||
dns.TypeA: "A",
|
||||
dns.TypeAAAA: "AAAA",
|
||||
dns.TypeCNAME: "CNAME",
|
||||
dns.TypePTR: "PTR",
|
||||
dns.TypeMX: "MX",
|
||||
}
|
||||
|
||||
return func(key uint16) string {
|
||||
return innerMap[key]
|
||||
}
|
||||
}
|
||||
|
||||
func AnswerToString(answer []dns.RR) string {
|
||||
answers := make([]string, len(answer))
|
||||
|
||||
for i, record := range answer {
|
||||
switch v := record.(type) {
|
||||
case *dns.A:
|
||||
answers[i] = fmt.Sprintf("A (%s)", v.A)
|
||||
case *dns.AAAA:
|
||||
answers[i] = fmt.Sprintf("AAAA (%s)", v.AAAA)
|
||||
case *dns.CNAME:
|
||||
answers[i] = fmt.Sprintf("CNAME (%s)", v.Target)
|
||||
case *dns.PTR:
|
||||
answers[i] = fmt.Sprintf("PTR (%s)", v.Ptr)
|
||||
default:
|
||||
answers[i] = fmt.Sprint(record)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(answers, ", ")
|
||||
}
|
||||
|
||||
func QuestionToString(questions []dns.Question) string {
|
||||
result := make([]string, len(questions))
|
||||
for i, question := range questions {
|
||||
result[i] = fmt.Sprintf("%s (%s)", qTypeToString()(question.Qtype), question.Name)
|
||||
}
|
||||
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func CreateAnswerFromQuestion(question dns.Question, ip net.IP, remainingTTL uint32) (dns.RR, error) {
|
||||
return dns.NewRR(fmt.Sprintf("%s %d %s %s %s", question.Name, remainingTTL, "IN", qTypeToString()(question.Qtype), ip))
|
||||
}
|
||||
|
||||
func ExtractDomain(question dns.Question) string {
|
||||
return strings.TrimSuffix(strings.ToLower(question.Name), ".")
|
||||
}
|
||||
|
||||
func NewMsgWithQuestion(question string, mType uint16) *dns.Msg {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(question, mType)
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func NewMsgWithAnswer(answer string) (*dns.Msg, error) {
|
||||
rr, err := dns.NewRR(answer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.Answer = []dns.RR{rr}
|
||||
|
||||
return msg, nil
|
||||
}
|
Loading…
Reference in New Issue