├── etc
├── certs
│ └── .gitignore
├── .gitignore
├── conf.d
│ ├── recursor.toml
│ ├── authority.tomli
│ └── nsnitch.toml
├── monitor.toml
└── config.toml
├── .gitignore
├── logo.png
├── .travis.yml
├── Dockerfile
├── responder
├── recursive_dns_test.go
├── http-handlers
│ ├── http_handler_wellknown.go
│ ├── http_handler_default.go
│ ├── http_handler_speedtest.go
│ ├── http_handler_stats.go
│ ├── http_handler_status.go
│ ├── http_handler_randomizer.go
│ ├── http_handler_report.go
│ ├── helpers.go
│ ├── http_handler_geolookup.go
│ ├── security.go
│ └── http_handler_bllookup.go
├── net_helpers.go
├── dns_common.go
├── recursive_dns_helpers.go
├── randomizer
│ └── randomizer.go
├── blacklist
│ └── blacklist.go
├── authoritative_dns.go
└── snitch_http.go
├── zones
├── dyna.go
├── zone_set.go
├── group.go
└── loader.go
├── install-deps.sh
├── install-deps.bat
├── common
├── netblock.go
├── supported_linux.go
├── supported_other.go
├── supported_windows.go
├── supported_bsd.go
├── geo.go
├── interface.go
├── common_test.go
├── data.go
└── common.go
├── director
├── notify_windows.go
├── notify_nonwindows.go
└── player.go
├── server-install.sh
├── .goreleaser.yml
├── anycast
├── peer.go
├── link_other.go
├── routestatus.go
├── bgpconfig.go
├── link_linux.go
└── anycast.go
├── Gopkg.toml
├── runtime
├── tor.go
├── ippool.go
├── feedback.go
├── limiter.go
├── limiter_test.go
├── garbageman.go
├── torupdater.go
├── runtime.go
├── geoupdater.go
├── geo.go
└── stats.go
├── NOTICES.md
├── DOH.md
├── go.mod
├── log
└── log.go
├── tenta-dns.go
├── netinterface
└── interface.go
├── README.md
├── stresser
└── stresser.go
└── monitor
└── monitor.go
/etc/certs/.gitignore:
--------------------------------------------------------------------------------
1 | /*
2 |
--------------------------------------------------------------------------------
/etc/.gitignore:
--------------------------------------------------------------------------------
1 | localcfg.toml
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /responses.db/
2 | /geo.db/
3 | .idea/
4 | dist/
5 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tenta-browser/tenta-dns/HEAD/logo.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | os:
3 | - linux
4 | - osx
5 | go:
6 | - 1.x
7 | - master
8 |
--------------------------------------------------------------------------------
/etc/conf.d/recursor.toml:
--------------------------------------------------------------------------------
1 | servertype="recursor"
2 |
3 | # opennic=true
4 |
5 | [domaains]
6 | [domains.nameinspector]
7 | hostname="dns.tenta.io"
8 | ipv4="127.0.0.1"
--------------------------------------------------------------------------------
/etc/conf.d/authority.tomli:
--------------------------------------------------------------------------------
1 | servertype="authority"
2 |
3 | # Full path to standard zone files
4 | zonespath='/etc/nsnitch/zones.d'
5 |
6 | [domaains]
7 | [domains.ns1]
8 | hostname="ns1.tenta.io"
9 | ipv4="127.0.0.1"
10 | [domains.ns2]
11 | hostname="ns2.tenta.io"
12 | ipv4="127.0.0.2"
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine
2 |
3 | RUN mkdir -p /etc/nsnitch/conf.d && \
4 | mkdir -p /etc/nsnitch/certs && \
5 | mkdir -p /etc/nsnitch/geo.db
6 |
7 |
8 | ADD $GOPATH/github.com/tenta-browser/tenta-dns/etc/words.txt /etc/nsnitch/words.txt
9 | ADD $GOPATH/github.com/tenta-browser/tenta-dns/etc/config.toml /etc/nsnitch/config.toml
10 |
11 | COPY tenta-dns /app/
12 |
13 | CMD ["/app/tenta-dns", "-config", "/etc/nsnitch/config.toml"]
--------------------------------------------------------------------------------
/responder/recursive_dns_test.go:
--------------------------------------------------------------------------------
1 | package responder
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestTokenize(t *testing.T) {
8 | t1 := tokenizeDomain(".foo.bar.example.com.")
9 | ct1 := []string{"com.", "example.com.", "bar.example.com.", "foo.bar.example.com."}
10 |
11 | if len(t1) != len(ct1) {
12 | t.Fatalf("Length of output and control does not correspond.\n")
13 | t.FailNow()
14 | }
15 |
16 | for i, token := range t1 {
17 | if token != ct1[i] {
18 | t.Fatalf("Token %d mismatch [%s] vs [%s].\n", i, token, ct1[i])
19 | t.FailNow()
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/etc/monitor.toml:
--------------------------------------------------------------------------------
1 | # path to the main Tenta DNS config
2 | dnsconf="/etc/nsnitch/monitor.toml"
3 | # exclude these IPs from testing (if set up, anycast IPs go here)
4 | exclude=[]
5 | # HTTP checkup API configuration
6 | ip="127.0.0.1"
7 | domain="monitor.example.com"
8 | #cert="/etc/nsnitch/certs/example.crt"
9 | #key="/etc/nsnitch/certs/example.key"
10 | # domains to test in each period
11 | target=["tenta.com", "github.com", "example.com"]
12 | # expressed in ms, timeout is the maximum time a DNS test can run before it gets invalidated
13 | timeout=100
14 | # whether the service was started via systemd
15 | systemd=false
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_wellknown.go:
--------------------------------------------------------------------------------
1 | package http_handlers
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "net/http"
6 | "github.com/tenta-browser/tenta-dns/runtime"
7 | "fmt"
8 | )
9 |
10 | func HandleHTTPWellKnown(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry, path string, body []byte, mime string) httpHandler {
11 | bodylen := fmt.Sprintf("%d", len(body))
12 | return wrapExtendedHttpHandler(rt, lgr, "well-known", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
13 | lgr.Debugf("Serving well-known %s", path)
14 | w.Header().Set("Content-Type", mime)
15 | w.Header().Set("Content-Length", bodylen)
16 | extraHeaders(cfg, w, r)
17 | w.WriteHeader(200)
18 | w.Write(body)
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/zones/dyna.go:
--------------------------------------------------------------------------------
1 | package zones
2 |
3 | import (
4 | "errors"
5 | "github.com/miekg/dns"
6 | "strings"
7 | )
8 |
9 | const TypeDYNA uint16 = 0xFF01
10 |
11 | type DYNA struct {
12 | def string // rdata representing the definition
13 | }
14 |
15 | func NewDYNA() dns.PrivateRdata { return &DYNA{""} }
16 |
17 | func (rd *DYNA) Len() int { return len([]byte(rd.def)) }
18 | func (rd *DYNA) String() string { return rd.def }
19 | func (rd *DYNA) Parse(txt []string) error {
20 | rd.def = strings.TrimSpace(strings.Join(txt, " "))
21 | return nil
22 | }
23 | func (rd *DYNA) Pack(buf []byte) (int, error) {
24 | return 0, errors.New("Not implemented")
25 | }
26 | func (rd *DYNA) Unpack(buf []byte) (int, error) {
27 | return 0, errors.New("Not implemented")
28 | }
29 | func (rd *DYNA) Copy(dest dns.PrivateRdata) error {
30 | return errors.New("Not implemented")
31 | }
32 |
--------------------------------------------------------------------------------
/install-deps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # NSnitch DNS Server
4 | #
5 | # Copyright 2017 Tenta, LLC
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | #
19 | # For any questions, please contact developer@tenta.io
20 | #
21 | # install-deps.sh: Install libs on *nix / Mac
22 |
23 | echo "Installing dependencies"
24 |
25 | go get -u -v github.com/golang/dep/cmd/dep
26 | $GOPATH/bin/dep ensure
27 |
--------------------------------------------------------------------------------
/install-deps.bat:
--------------------------------------------------------------------------------
1 | ::# NSnitch DNS Server
2 | ::#
3 | ::# Copyright 2017 Tenta, LLC
4 | ::#
5 | ::# Licensed under the Apache License, Version 2.0 (the "License");
6 | ::# you may not use this file except in compliance with the License.
7 | ::# You may obtain a copy of the License at
8 | ::#
9 | ::# http://www.apache.org/licenses/LICENSE-2.0
10 | ::#
11 | ::# Unless required by applicable law or agreed to in writing, software
12 | ::# distributed under the License is distributed on an "AS IS" BASIS,
13 | ::# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | ::# See the License for the specific language governing permissions and
15 | ::# limitations under the License.
16 | ::#
17 | ::# For any questions, please contact developer@tenta.io
18 | ::#
19 | ::# install-deps.bat: Windows dependency installer
20 |
21 | @ECHO OFF
22 |
23 | echo Installing dependencies
24 |
25 | go get -u -v github.com/golang/dep/cmd/dep
26 | %GOPATH%\bin\dep ensure
27 |
--------------------------------------------------------------------------------
/common/netblock.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * netblock.go: Netblock object declaration
21 | */
22 |
23 | package common
24 |
25 | type Netblock struct {
26 | IP string
27 | Netmask uint8
28 | Force bool
29 | Communities []string
30 | }
31 |
--------------------------------------------------------------------------------
/common/supported_linux.go:
--------------------------------------------------------------------------------
1 | // +build linux
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * supported_linux.go: Whether current platform is supported or not for anycast
23 | */
24 |
25 | package common
26 |
27 | func AnycastSupported() bool {
28 | return true
29 | }
30 |
--------------------------------------------------------------------------------
/director/notify_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * notify_windows.go: Signal watcher for windows targets
23 | */
24 |
25 | package director
26 |
27 | import "os"
28 |
29 | func getWatcher() chan os.Signal {
30 | return make(chan os.Signal)
31 | }
32 |
--------------------------------------------------------------------------------
/director/notify_nonwindows.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * notify_nonwindows.go: Signal watcher for non-windows targets
23 | */
24 |
25 | package director
26 |
27 | import "os"
28 |
29 | func getWatcher() chan os.Signal {
30 | return make(chan os.Signal)
31 | }
32 |
--------------------------------------------------------------------------------
/common/supported_other.go:
--------------------------------------------------------------------------------
1 | // +build !linux,!windows,!dragonfly,!freebsd,!netbsd,!openbsd
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * supported_other.go: Whether current platform is supported or not for anycast
23 | */
24 |
25 | package common
26 |
27 | func AnycastSupported() bool {
28 | return true
29 | }
30 |
--------------------------------------------------------------------------------
/common/supported_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * supported_windows.go: Whether current platform is supported or not for anycast
23 | */
24 |
25 | package common
26 |
27 | // TODO: Figure out how to add a remove interfaces dynamically on windows
28 | func AnycastSupported() bool {
29 | return false
30 | }
31 |
--------------------------------------------------------------------------------
/common/supported_bsd.go:
--------------------------------------------------------------------------------
1 | // +build dragonfly freebsd netbsd openbsd
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * supported_bsd.go: Whether current platform is supported or not for anycast
23 | */
24 |
25 | package common
26 |
27 | // TODO: Add support for using the bsd ifconfig command to dynamically add and remove interfaces
28 | func AnycastSupported() bool {
29 | return false
30 | }
31 |
--------------------------------------------------------------------------------
/zones/zone_set.go:
--------------------------------------------------------------------------------
1 | package zones
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | "github.com/sirupsen/logrus"
6 | "sort"
7 | )
8 |
9 | type ZoneSet map[string]map[uint16][]ZoneEntry // Map from strings to maps from RR_types to ZoneEntry lists
10 |
11 | type ZoneEntryType uint8
12 |
13 | const (
14 | ZoneEntryTypeRR ZoneEntryType = iota
15 | ZoneEntryTypeWeighted
16 | ZoneEntryTypeNearest
17 | ZoneEntryTypeGeo
18 | )
19 |
20 | type ZoneEntry struct {
21 | Kind ZoneEntryType
22 | RR *dns.RR
23 | }
24 |
25 | func NewZoneSet() ZoneSet {
26 | ret := make(map[string]map[uint16][]ZoneEntry)
27 | return ret
28 | }
29 |
30 | func NewZoneSetItem() map[uint16][]ZoneEntry {
31 | return make(map[uint16][]ZoneEntry)
32 | }
33 |
34 | func (z ZoneSet) PrintToLog(lg *logrus.Entry) {
35 | urls := make([]string, 0)
36 | for url, _ := range z {
37 | urls = append(urls, url)
38 | }
39 | sort.Strings(urls)
40 | for _, url := range urls {
41 | lg.Debugf("URL '%s'", url)
42 | item := z[url]
43 | for kind, elist := range item {
44 | lg.Debugf(" Kind %s has %d entries", dns.TypeToString[kind], len(elist))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/server-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set +x
4 |
5 | if [ -z "$GOPATH" ]; then
6 | GOPATH="/usr/local/src/gopath"
7 | echo "GOPATH is empty, setting it to $GOPATH"
8 | fi
9 |
10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
11 |
12 | echo "Installing src to $GOPATH/src"
13 |
14 | rm -rf $GOPATH/src/tenta-dns
15 | mkdir -p $GOPATH/src/tenta-dns
16 | cp -R $DIR/src/tenta-dns/* $GOPATH/src/tenta-dns/
17 |
18 | echo "Installing dependencies to $GOPATH/pkg"
19 |
20 | oldifs=$IFS
21 | IFS='
22 | '
23 | for line in `cat ./deps.list`; do
24 | echo "Installing $line"
25 | go get -u -v $line
26 | done
27 | IFS=$oldifs
28 |
29 | echo "Installing gobgp"
30 |
31 | go get github.com/osrg/gobgp/gobgp
32 |
33 | echo "Compiling to $GOPATH/bin"
34 |
35 | go install -v tenta-dns
36 |
37 | echo "Setting up configs"
38 |
39 | mkdir -p /etc/nsnitch/conf.d
40 | mkdir -p /etc/nsnitch/certs
41 | mkdir -p /etc/nsnitch/geo.db
42 |
43 | cp $DIR/etc/words.txt /etc/nsnitch/words.txt
44 | if [ -f /etc/nsnitch/config.toml ]; then
45 | cp $DIR/etc/config.toml /etc/nsnitch/config.toml.new
46 | else
47 | cp $DIR/etc/config.toml /etc/nsnitch/config.toml
48 | fi
49 |
--------------------------------------------------------------------------------
/responder/net_helpers.go:
--------------------------------------------------------------------------------
1 | package responder
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tenta-browser/tenta-dns/runtime"
7 | )
8 |
9 | func hostInfo(v4 bool, net string, d *runtime.ServerDomain) (ip string, port int) {
10 | if v4 {
11 | ip = d.IPv4
12 | } else {
13 | ip = fmt.Sprintf("[%s]", d.IPv6)
14 | }
15 | if net == "tcp" {
16 | if d.DnsTcpPort <= runtime.PORT_UNSET {
17 | panic("Unable to start a TCP recursive DNS server without a valid TCP port")
18 | }
19 | port = d.DnsTcpPort
20 | } else if net == "udp" {
21 | if d.DnsUdpPort <= runtime.PORT_UNSET {
22 | panic("Unable to start a UDP recursive DNS server without a valid UDP port")
23 | }
24 | port = d.DnsUdpPort
25 | } else if net == "tls" {
26 | if d.DnsTlsPort <= runtime.PORT_UNSET {
27 | panic("Unable to start a TLS recursive DNS server without a valid TLS port")
28 | }
29 | port = d.DnsTlsPort
30 | } else if net == "https" {
31 | if d.HttpsPort <= runtime.PORT_UNSET {
32 | panic("Unable to start a HTTPS recursive DNS server without a valid HTTPS port")
33 | }
34 | port = d.HttpsPort
35 | } else {
36 | panic(fmt.Sprintf("Unknown network type %s", net))
37 | }
38 | return
39 | }
40 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: tenta-dns
2 | release:
3 | github:
4 | owner: tenta-browser
5 | name: tenta-dns
6 | name_template: '{{.Tag}}'
7 | brew:
8 | commit_author:
9 | name: tenta-browser
10 | email: info@tenta.com
11 | install: bin.install "tenta-dns"
12 | builds:
13 | - goos:
14 | - linux
15 | - darwin
16 | - windows
17 | goarch:
18 | - amd64
19 | - "386"
20 | main: tenta-dns.go
21 | ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
22 | binary: tenta-dns
23 | archive:
24 | format: tar.gz
25 | name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{
26 | .Arm }}{{ end }}'
27 | files:
28 | - LICENSE*
29 | - README*
30 | - changelog*
31 | - CHANGELOG*
32 | fpm:
33 | bindir: /usr/local/bin
34 | snapshot:
35 | name_template: SNAPSHOT-{{ .Commit }}
36 | checksum:
37 | name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
38 | dist: dist
39 | sign:
40 | cmd: gpg
41 | args:
42 | - --output
43 | - $signature
44 | - --detach-sig
45 | - $artifact
46 | signature: ${artifact}.sig
47 | artifacts: none
48 | dockers:
49 | - image: tenta/tenta-dns
50 | latest: true
51 | tag_template: '{{ .Tag }}'
--------------------------------------------------------------------------------
/anycast/peer.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * peer.go: Anycast Peer object declaration and primitives
21 | */
22 |
23 | package anycast
24 |
25 | type Peer struct {
26 | IP string
27 | AS uint32
28 | Password string
29 | Description string
30 | }
31 |
32 | func (p Peer) Equals(o Peer) bool {
33 | if p.IP != o.IP {
34 | return false
35 | }
36 | if p.AS != o.AS {
37 | return false
38 | }
39 | if p.Password != o.Password {
40 | return false
41 | }
42 | if p.Description != o.Description {
43 | return false
44 | }
45 | return true
46 | }
47 |
--------------------------------------------------------------------------------
/anycast/link_other.go:
--------------------------------------------------------------------------------
1 | // +build !linux
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * link_other.go: Placeholder for non-linux link management
23 | */
24 |
25 | package anycast
26 |
27 | import (
28 | "github.com/tenta-browser/tenta-dns/common"
29 |
30 | "github.com/sirupsen/logrus"
31 | )
32 |
33 | func addLink(dummies *[]string, b common.Netblock, cnt *uint, lg *logrus.Entry) {
34 | panic("Link adding not supported on this platform")
35 | }
36 |
37 | func removeLinks(dummies *[]string, lg *logrus.Entry) {
38 | lg.Errorf("Cannot remove links, not supported on this system type")
39 | }
40 |
--------------------------------------------------------------------------------
/zones/group.go:
--------------------------------------------------------------------------------
1 | package zones
2 |
3 | import (
4 | "errors"
5 | "github.com/miekg/dns"
6 | "github.com/sirupsen/logrus"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | const TypeGROUP uint16 = 0xFF00
12 |
13 | type GROUP struct {
14 | def string // rdata representing the definition
15 | lg *logrus.Entry
16 | }
17 |
18 | func NewGROUP(log *logrus.Entry) func() dns.PrivateRdata {
19 | return func() dns.PrivateRdata { return &GROUP{"", log} }
20 | }
21 |
22 | func (rd *GROUP) Len() int { return len([]byte(rd.def)) }
23 | func (rd *GROUP) String() string { return rd.def }
24 | func (rd *GROUP) Parse(txt []string) error {
25 | rd.def = strings.TrimSpace(strings.Join(txt, " "))
26 | for _, line := range txt {
27 | line = strings.TrimSpace(line)
28 | match, err := regexp.MatchString("^[a-z]+=[a-z0-9\\.]+$", line)
29 | if err != nil || !match {
30 | return err
31 | }
32 | pos := strings.Index(line, "=")
33 | arg := line[:pos]
34 | val := line[pos+1:]
35 | rd.lg.Debugf("Got argument %s with value %s", arg, val)
36 | }
37 | return nil
38 | }
39 | func (rd *GROUP) Pack(buf []byte) (int, error) {
40 | return 0, errors.New("Not implemented")
41 | }
42 | func (rd *GROUP) Unpack(buf []byte) (int, error) {
43 | return 0, errors.New("Not implemented")
44 | }
45 | func (rd *GROUP) Copy(dest dns.PrivateRdata) error {
46 | return errors.New("Not implemented")
47 | }
48 |
--------------------------------------------------------------------------------
/etc/config.toml:
--------------------------------------------------------------------------------
1 | # Full path to use for LevelDb Database
2 | databasepath="/etc/nsnitch/responses.db"
3 | # Number of seconds to cache records for
4 | databasettl=300
5 |
6 | # Pull path to module configuration files
7 | includepath="/etc/nsnitch/conf.d"
8 |
9 | # MaxMind Key for GeoIP download
10 | # Only necessary if you're using an NSnitch module
11 | #maxmindkey="LICENSE_KEY"
12 | #geodbpath="/etc/nsnitch/geo.db"
13 |
14 | # Url for Tor Updates
15 | torurl="https://check.torproject.org/exit-addresses"
16 |
17 | # Port Settings
18 | defaulthttpport=80
19 | defaulthttpsport=443
20 | defaultdnsudpport=53
21 | defaultdnstcpport=53
22 | defaultdnstlsport=853
23 |
24 | # bgp settings
25 | # Only necessary if you're using BGP
26 | #[peers]
27 | # [peer.example]
28 | # localas=396093
29 | # localip="10.0.0.1"
30 | # peeras=6500
31 | # peerip="1.2.3.4"
32 | # peerpassword="secret"
33 |
34 | # BGP Advertisement blocks
35 | # Only necessary if you're using BGP
36 | #[netblocks]
37 | # [netblock]
38 | # ip="10.1.0.0"
39 | # netmask="24"
40 | # set to true in order to force announcing this range, even if no
41 | # configured mofule requires it
42 | #force=false
43 | # [netblocks]
44 | # ip="192.168.1.0"
45 | # netmask="24"
46 |
47 | # Lookup Credentials allow for Executing Lookups against the API
48 | threadedresolver=false
49 | [lookupcreds]
50 | "ABC"="123"
51 | "XYZ"="456"
52 |
53 | # Admin Credentials allow for Controlling Tenta DNS
54 | [admincreds]
55 | "ABC"="789"
56 | "XYZ"="012"
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | [[constraint]]
2 | name = "github.com/BurntSushi/toml"
3 | version = "0.3.0"
4 |
5 | [[constraint]]
6 | name = "github.com/coreos/go-systemd"
7 | version = "16.0.0"
8 |
9 | [[constraint]]
10 | name = "github.com/gorilla/mux"
11 | version = "1.6.1"
12 |
13 | [[constraint]]
14 | name = "github.com/leonelquinteros/gorand"
15 | version = "1.0.0"
16 |
17 | [[constraint]]
18 | name = "github.com/mattn/go-colorable"
19 | version = "0.0.9"
20 |
21 | [[constraint]]
22 | name = "github.com/miekg/dns"
23 | branch = "master"
24 |
25 | [[constraint]]
26 | name = "github.com/milosgajdos83/tenus"
27 | branch = "master"
28 |
29 | [[constraint]]
30 | name = "github.com/muesli/cache2go"
31 | branch = "master"
32 |
33 | [[constraint]]
34 | name = "github.com/oschwald/maxminddb-golang"
35 | version = "1.2.1"
36 |
37 | [[constraint]]
38 | name = "github.com/osrg/gobgp"
39 | version = "1.28.0"
40 |
41 | [[constraint]]
42 | branch = "master"
43 | name = "github.com/sasha-s/go-hll"
44 |
45 | [[constraint]]
46 | name = "github.com/sirupsen/logrus"
47 | version = "1.0.4"
48 |
49 | [[constraint]]
50 | branch = "master"
51 | name = "github.com/syndtr/goleveldb"
52 |
53 | [[constraint]]
54 | branch = "master"
55 | name = "github.com/tenta-browser/go-highway"
56 |
57 | [[constraint]]
58 | branch = "master"
59 | name = "github.com/tevino/abool"
60 |
61 | [[constraint]]
62 | name = "github.com/x-cray/logrus-prefixed-formatter"
63 | version = "0.5.2"
64 |
65 | [prune]
66 | go-tests = true
67 | unused-packages = true
68 |
--------------------------------------------------------------------------------
/anycast/routestatus.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * routestatus.go: BGP route announcement routines
21 | */
22 |
23 | package anycast
24 |
25 | import "fmt"
26 |
27 | type RouteStatus int
28 |
29 | const (
30 | RouteStatusUnknown = iota
31 | RouteStatusPending
32 | RouteStatusAnnounced
33 | RouteStatusCriticalFailure
34 | )
35 |
36 | type RouteUpdate struct {
37 | IP string
38 | Status RouteStatus
39 | }
40 |
41 | func (s RouteStatus) String() string {
42 | switch s {
43 | case RouteStatusPending:
44 | return "Pending"
45 | case RouteStatusAnnounced:
46 | return "Announced"
47 | case RouteStatusCriticalFailure:
48 | return "CritFail"
49 | case RouteStatusUnknown:
50 | fallthrough
51 | default:
52 | return "Unknown"
53 | }
54 | }
55 |
56 | func (r RouteUpdate) String() string {
57 | return fmt.Sprintf("%s is in state %s", r.IP, r.Status.String())
58 | }
59 |
--------------------------------------------------------------------------------
/anycast/bgpconfig.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * bgpconfig.go: Routine collection over bgp configuration objects
21 | */
22 |
23 | package anycast
24 |
25 | type BGPConfig struct {
26 | AS uint32
27 | IPv4 string
28 | IPv6 string
29 | EnableCommunities bool
30 | EnableRPC bool
31 | RPCPort uint32
32 | }
33 |
34 | func (b BGPConfig) Equals(o BGPConfig) bool {
35 | if b.AS != o.AS {
36 | return false
37 | }
38 | if b.IPv4 != o.IPv4 {
39 | return false
40 | }
41 | if b.IPv6 != o.IPv6 {
42 | return false
43 | }
44 | if b.EnableCommunities != o.EnableCommunities {
45 | return false
46 | }
47 | if b.EnableRPC != o.EnableRPC {
48 | return false
49 | }
50 | if b.RPCPort != o.RPCPort {
51 | return false
52 | }
53 | return true
54 | }
55 |
56 | func (b BGPConfig) Enabled() bool {
57 | return !b.Equals(BGPConfig{})
58 | }
59 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_default.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_default.go: Default (404) route
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "net/http"
27 |
28 | "github.com/tenta-browser/tenta-dns/common"
29 | "github.com/tenta-browser/tenta-dns/runtime"
30 |
31 | "github.com/sirupsen/logrus"
32 | )
33 |
34 | func HandleHTTPDefault(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry) httpHandler {
35 | return wrapExtendedHttpHandler(rt, lgr, "error", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
36 | data := &common.DefaultJSONObject{
37 | Status: "ERROR",
38 | Type: "TENTA_NSNITCH",
39 | Data: nil,
40 | Message: "Not Found",
41 | Code: 404,
42 | }
43 | lg.Debug("404 Not Found")
44 | extraHeaders(cfg, w, r)
45 | w.WriteHeader(http.StatusNotFound)
46 | mustMarshall(w, data, lg)
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/runtime/tor.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * tor.go: Tor node checker
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "fmt"
27 | "time"
28 | )
29 |
30 | type TorNode struct {
31 | NodeId string
32 | Published *time.Time
33 | Updated *time.Time
34 | Addresses []ExitAddress
35 | }
36 |
37 | type ExitAddress struct {
38 | IP string
39 | Date *time.Time
40 | }
41 |
42 | func NewTorNode() *TorNode {
43 | return &TorNode{Addresses: make([]ExitAddress, 0)}
44 | }
45 |
46 | func (t *TorNode) toString() string {
47 | return fmt.Sprintf("Node %s with %d IPs", t.NodeId, len(t.Addresses))
48 | }
49 |
50 | type TorHash struct {
51 | hash map[string]string
52 | cnt int
53 | }
54 |
55 | func NewTorHash() *TorHash {
56 | return &TorHash{hash: make(map[string]string), cnt: 0}
57 | }
58 |
59 | func (t *TorHash) Add(node *TorNode) {
60 | for _, addr := range node.Addresses {
61 | t.hash[addr.IP] = node.NodeId
62 | t.cnt += 1
63 | }
64 | }
65 |
66 | func (t *TorHash) Exists(ip string) (string, bool) {
67 | if nodeid, ok := t.hash[ip]; ok {
68 | return nodeid, true
69 | }
70 | return "", false
71 | }
72 |
73 | func (t *TorHash) Len() int {
74 | return t.cnt
75 | }
76 |
--------------------------------------------------------------------------------
/common/geo.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * geo.go: Geo data structure definitions
21 | */
22 |
23 | package common
24 |
25 | type ISP struct {
26 | Organization string `maxminddb:"organization" json:"organization"`
27 | ASNumber uint `maxminddb:"autonomous_system_number" json:"as_number"`
28 | ASOrganization string `maxminddb:"autonomous_system_organization" json:"as_owner"`
29 | ISP string `maxminddb:"isp" json:"isp"`
30 | }
31 |
32 | type Position struct {
33 | Latitude float32 `maxminddb:"latitude" json:"latitude"`
34 | Longitude float32 `maxminddb:"longitude" json:"longitude"`
35 | Radius uint `maxminddb:"accuracy_radius" json:"uncertainty_km"`
36 | TimeZone string `maxminddb:"time_zone" json:"time_zone"`
37 | }
38 |
39 | type GeoLocation struct {
40 | Position *Position `json:"position"`
41 | ISP *ISP `json:"network"`
42 | City string `json:"city"`
43 | Country string `json:"country"`
44 | CountryISO string `json:"iso_country"`
45 | Location string `json:"location"`
46 | LocationI18n map[string]string `json:"localized_location"`
47 | TorNode *string `json:"tor_node"`
48 | }
49 |
--------------------------------------------------------------------------------
/runtime/ippool.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * ippool.go: Manages IP pool rotation
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "math/rand"
27 | "net"
28 | "time"
29 |
30 | "github.com/tenta-browser/tenta-dns/log"
31 | )
32 |
33 | type Pool struct {
34 | p []net.IP
35 | r *rand.Rand
36 | }
37 |
38 | func StartIPPool(cfgIPs []string) *Pool {
39 | l := log.GetLogger("runtime")
40 | var ips []net.IP
41 | if cfgIPs == nil || len(cfgIPs) == 0 {
42 | l.Infof("Starting without any outbound IP specified. Default IP will be used.")
43 | ips = nil
44 | } else {
45 | ips = make([]net.IP, 0)
46 | for _, strIP := range cfgIPs {
47 | ip := net.ParseIP(strIP)
48 | ips = append(ips, ip)
49 | }
50 | l.Infof("Initialized IP Pool with [%v]", ips)
51 | }
52 | return &Pool{ips, rand.New(rand.NewSource(time.Now().UnixNano()))}
53 | }
54 |
55 | func (p *Pool) RandomizeUDPDialer() *net.Dialer {
56 | ret := &net.Dialer{}
57 | if len(p.p) > 0 {
58 | ret.LocalAddr = &net.UDPAddr{IP: p.p[rand.Intn(len(p.p))]}
59 | }
60 | return ret
61 | }
62 |
63 | func (p *Pool) RandomizeTCPDialer() *net.Dialer {
64 | ret := &net.Dialer{}
65 | if len(p.p) > 0 {
66 | ret.LocalAddr = &net.TCPAddr{IP: p.p[rand.Intn(len(p.p))]}
67 | }
68 | return ret
69 | }
70 |
--------------------------------------------------------------------------------
/etc/conf.d/nsnitch.toml:
--------------------------------------------------------------------------------
1 | # What type of server are we running
2 | servertype="nsnitch"
3 |
4 | # Information about this node
5 | [node]
6 | city="Somecity"
7 | state="Somestate"
8 | country="United States"
9 | countryiso="US"
10 | latitude=0.0
11 | longitude=0.0
12 | org="Datacenter Name"
13 | as=0
14 | isp="Local Network"
15 | timezone="UTC"
16 |
17 | # The system mode to use for redirection
18 | # UUID Mode genenrates UUIDs as the subdomains
19 | #redirectmode="uuid"
20 | # wordlist mode generates plausible subdomains from wordlist
21 | redirectmode="wordlist"
22 | wordlistpath="/etc/nsnitch/words.txt"
23 | # Provide values for "/.well-known" responses
24 |
25 | [wellknowns]
26 | [wellknowns.ABC123]
27 | # The path under, "/.well-known/", this expands to "/.well-known/pki-validation/ABC123.txt"
28 | path="pki-validation/ABC123.txt"
29 | # The literal body to serve, optionally Base64 encoded
30 | body="ABC123 certificate.authority.net xzy987"
31 | # Whether the body is base64 encoded
32 | base64=false
33 | # The mime type to use when serving this response
34 | mimetype="text/plain"
35 |
36 | # The list of domains to bind to
37 | [domains]
38 | [domains.domain1]
39 | hostname="example.org"
40 | ipv4="127.0.0.2"
41 | nameservers=["1.2.3.4", "5.6.7.8", "9.10.11.12", "13.14.15.16"]
42 | [domains.domain2]
43 | hostname="example.com"
44 | ipv4="127.0.0.1"
45 | certfile="/etc/nsnitch/certs/star_nstoro_com.chained.crt"
46 | keyfile="/etc/nsnitch/certs/star_nstoro_com.key"
47 | nameservers=["1.2.3.4", "5.6.7.8"]
48 | caaiodef=["reports@example.net"]
49 | caaiwild=["letsencrypt.com"]
50 | # caaissue is also available, but not very useful in this scenario
51 |
52 | # Domains to enable CORS for
53 | corsdomains=["tenta.io", "tenta.com"]
54 |
55 | # List of DNS Blacklists to test in the blacklist api
56 | blacklists=["bogons.cymru.com","zen.spamhaus.org","socks.dnsbl.sorbs.net","dnsbl-1.uceprotect.net"]
57 | blacklistttl=86400
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_speedtest.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_speedtest.go: Speedtest API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "fmt"
27 | "net/http"
28 | "strconv"
29 |
30 | "github.com/gorilla/mux"
31 | "github.com/tenta-browser/tenta-dns/runtime"
32 |
33 | "github.com/sirupsen/logrus"
34 | )
35 |
36 | func HandleHTTPSpeedtest(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry) httpHandler {
37 | lg := lgr.WithField("api", "speedtest")
38 | return func(w http.ResponseWriter, r *http.Request) {
39 | vars := mux.Vars(r)
40 | fileIndStr := vars["size_exp"]
41 | fileInd, e := strconv.Atoi(fileIndStr)
42 | if e != nil {
43 | lg.Errorf("Cannot convert expected numeric input (got [%s])", fileIndStr)
44 | w.WriteHeader(400)
45 | return
46 | }
47 | if fileInd < 0 || fileInd > runtime.SPEEDTEST_MAX_FILESIZE_EXPONENT {
48 | lg.Errorf("Invalid file size requested [%d]", fileInd)
49 | w.WriteHeader(400)
50 | return
51 | }
52 |
53 | w.Header().Set("Content-Disposition", "attachment; filename=speedtest.txt")
54 | w.Header().Set("Content-Type", "text/plain")
55 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(rt.SpeedTestFiles[fileInd])))
56 | w.Write([]byte(rt.SpeedTestFiles[fileInd]))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/director/player.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * player.go: A wrapper around the services to handle panic and recover accordingly
21 | */
22 |
23 | package director
24 |
25 | import (
26 | "github.com/tenta-browser/tenta-dns/runtime"
27 | "sync/atomic"
28 | "time"
29 | )
30 |
31 | type starter func()
32 | type cleaner func()
33 |
34 | type player struct {
35 | id string
36 | st starter
37 | cl runtime.FailureNotifier
38 | fails uint32
39 | started time.Time
40 | running bool
41 | dead bool
42 | dnotify chan failure
43 | }
44 |
45 | type failure struct {
46 | p *player
47 | r interface{}
48 | }
49 |
50 | func newPlayer(id string, dnotify chan failure, start starter, clean runtime.FailureNotifier) *player {
51 | ret := &player{}
52 | ret.id = id
53 | ret.dnotify = dnotify
54 | ret.st = start
55 | ret.cl = clean
56 | ret.dead = false
57 | ret.running = false
58 | return ret
59 | }
60 |
61 | func (p *player) start() {
62 | p.running = true
63 | p.started = time.Now()
64 | go func() {
65 | defer func() {
66 | p.running = false
67 | if rcv := recover(); rcv != nil {
68 | if p.cl != nil {
69 | p.cl()
70 | }
71 | atomic.AddUint32(&p.fails, 1)
72 | p.dnotify <- failure{p, rcv}
73 | }
74 | }()
75 | p.st()
76 | }()
77 | }
78 |
79 | func (p *player) didStart() bool {
80 | return !p.started.Equal(time.Time{})
81 | }
82 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_stats.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_stats.go: Server stats API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "github.com/tenta-browser/tenta-dns/common"
27 | "github.com/tenta-browser/tenta-dns/runtime"
28 | "net/http"
29 |
30 | "github.com/sirupsen/logrus"
31 | )
32 |
33 | type StatsLookupError struct {
34 | s string
35 | Code int
36 | }
37 |
38 | func (e *StatsLookupError) Error() string {
39 | return e.s
40 | }
41 |
42 | func HandleHTTPStatsLookup(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry) httpHandler {
43 | return wrapExtendedHttpHandler(rt, lgr, "stats", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
44 | var err *StatsLookupError
45 |
46 | keys, lookuperr := rt.Stats.ListKeys(rt)
47 | if lookuperr != nil {
48 | err = &StatsLookupError{s: "Unable to fetch keys", Code: 500}
49 | }
50 |
51 | data := &common.DefaultJSONObject{
52 | Status: "OK",
53 | Type: "TENTA_NSNITCH",
54 | Data: &common.StatsData{},
55 | Message: "",
56 | Code: 200,
57 | }
58 |
59 | if err == nil {
60 | data.Data.(*common.StatsData).Keys = keys
61 | } else {
62 | data.Status = "ERROR"
63 | data.Code = err.Code
64 | lg.WithField("status_code", err.Code).Debugf("Request failed: %s", err.Error())
65 |
66 | }
67 | extraHeaders(cfg, w, r)
68 | mustMarshall(w, data, lg)
69 | })
70 | }
71 |
--------------------------------------------------------------------------------
/common/interface.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * interface.go: Declaration and routines of (network) Interface objects
21 | */
22 |
23 | package common
24 |
25 | import (
26 | "fmt"
27 | "net"
28 | )
29 |
30 | type InterfaceType int
31 | type InterfaceState int
32 |
33 | const (
34 | TypeUnknown InterfaceType = iota
35 | TypeIPv4
36 | TypeIPv6
37 | )
38 |
39 | const (
40 | StateUnknown InterfaceState = iota
41 | StateUp
42 | StateDown
43 | StateMissing
44 | StateCriticalFailure
45 | )
46 |
47 | type Interface struct {
48 | ID string
49 | IP net.IP
50 | Type InterfaceType
51 | Name string
52 | }
53 |
54 | func (i Interface) String() string {
55 | return fmt.Sprintf("For %s, %s %s", i.ID, i.IP.String(), i.Type.String())
56 | }
57 |
58 | type Status struct {
59 | State InterfaceState
60 | ID string
61 | }
62 |
63 | func (s Status) String() string {
64 | return fmt.Sprintf("%s is %s", s.ID, s.State.String())
65 | }
66 |
67 | func (t InterfaceType) String() string {
68 | switch t {
69 | case TypeIPv4:
70 | return "IPv4"
71 | case TypeIPv6:
72 | return "IPv6"
73 | case TypeUnknown:
74 | fallthrough
75 | default:
76 | return "Unknown"
77 | }
78 | }
79 |
80 | func (s InterfaceState) String() string {
81 | switch s {
82 | case StateUp:
83 | return "Up"
84 | case StateDown:
85 | return "Down"
86 | case StateMissing:
87 | return "Missing"
88 | case StateCriticalFailure:
89 | return "CritFail"
90 | case StateUnknown:
91 | fallthrough
92 | default:
93 | return "Unknown"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/NOTICES.md:
--------------------------------------------------------------------------------
1 | Open Source Licenses
2 | --------------------
3 |
4 | We depend on the many great open source licenses, listed below:
5 |
6 | * [Golang DNS LIbrary](https://github.com/miekg/dns) by Miek Gieben. [View License](https://github.com/miekg/dns/blob/master/LICENSE)
7 | * [Golang LevelDB](github.com/syndtr/goleveldb/leveldb) by Suryandaru Triandana. [View License](https://github.com/syndtr/goleveldb/blob/master/LICENSE)
8 | * [Golang Random Library](github.com/leonelquinteros/gorand) by Leonel Quinteros. [View License](https://github.com/leonelquinteros/gorand/blob/master/LICENSE)
9 | * [Golang URL Multiplexer Library](github.com/gorilla/mux) by The Gorilla Toolkit Team. [View License](https://github.com/gorilla/mux/blob/master/LICENSE)
10 | * [Golang TOML Parser Library](github.com/BurntSushi/toml) by Andrew Gallant. [View License](https://github.com/BurntSushi/toml/blob/master/COPYING)
11 | * [Golang HyperLogLog Cardinality Algorithm](github.com/sasha-s/go-hll) by Github User _sasha-s_. [View License](https://github.com/sasha-s/go-hll/blob/master/LICENSE)
12 | * [Golang MaxMind DB Library](github.com/oschwald/maxminddb-golang) by Gregory Oschwald. [View License](https://github.com/oschwald/maxminddb-golang/blob/master/LICENSE)
13 | * [Golang Implementation of Google's Highway Hash](github.com/dgryski/go-highway) by Damian Gryski.
14 | * [Golang BGP Library](github.com/osrg/gobgp/api) by OSRG. [View License](https://github.com/osrg/gobgp/blob/master/LICENSE)
15 | * [Golang Structured Logging Library](github.com/sirupsen/logrus) by Simon Eskildsen. [View License](https://github.com/sirupsen/logrus/blob/master/LICENSE)
16 | * [Golang Command Prompt Text Coloring Library](github.com/mattn/go-colorable) by Guthub User _mattn_. [View License](https://github.com/mattn/go-colorable/blob/master/LICENSE)
17 | * [Golang Prefixed Log Formatter for Logrus](github.com/x-cray/logrus-prefixed-formatter) by Denis Parchenko. [View License](https://github.com/x-cray/logrus-prefixed-formatter/blob/master/LICENSE)
18 | * [Golang Atomic Boolean Library](github.com/tevino/abool) by Github User _tevino_. [View License](https://github.com/tevino/abool/blob/master/LICENSE)
19 | * [Golang Caching Library](github.com/muesli/cache2go) by Christian Muehlhaeuser. [View License](https://github.com/muesli/cache2go/blob/master/LICENSE.txt)
20 | * [Golang Linux Networking Library](github.com/milosgajdos83/tenus) by Milos Gajdos. [View License](https://github.com/milosgajdos83/tenus/blob/master/LICENSE)
--------------------------------------------------------------------------------
/DOH.md:
--------------------------------------------------------------------------------
1 | # Tenta DNS and DNS over HTTPS (DoH)
2 | After a lengthy iteration, we are finally launching our v2 DNS resolver, incorporating hundreds of fixes and improvements.
3 | Tenta DNS at its core, is about three things: speed, precision and privacy. This is why besides the standard TCP and UDP protocols, we support DNS-over-TLS and DNS-over-HTTPS. What we have learned from the first iteration, we perfected in this one. Tenta DNS has always-on DNSSEC validation, a more robust handling of the occasional implementation inconsistencies,
4 | and a caching subsystem tailored specifically for a DNS resolver. Finally, our DNS-over-TLS service is active (albeit, probably unused in the vast majority of cases) during upstream queries too.
5 |
6 | To use DNS-over-HTTPS, we have set up two domains, https://opennic.tenta.io/dns-query and https://iana.tenta.io/dns-query, which use OpenNIC and ICANN root servers, respectively.
7 |
8 | The querying works in a REST API fashion. It takes two arguments, `name` and `type` (eg: https://opennic.tenta.io/dns-query?name=example.com&type=A), and provides an answer in JSON format.
9 |
10 | We opted for a simplified JSON API approach to DoH because it removes the difficulty of including DNS queries into lightweight applications.
11 |
12 | Our response format for DoH queries is as follows:
13 | **Status**: integer; analogous to classic DNS message's RCODE
14 | **TC**, **RD**, **RA**, **AD**, **CD**: boolean; relevant flags describing the nature of the DNS response, same as in a classic DNS message
15 | **Question**: structure (string, integer), describing the queried name, and the queried type
16 | **Answer**, **Authority**, **Additional**: array of structures (string, integer, integer, string); describing every DNS records name, type, and TTL value and their record-specific data.
17 |
18 | An _example_ response (to the _example_ request) is
19 | ```javascript
20 | {
21 | "Status":0,
22 | "TC":false,
23 | "RD":true,
24 | "RA":true,
25 | "AD":false,
26 | "CD":false,
27 | "Question":[
28 | {"Name":"example.com","Type":1}],
29 | "Answer":[
30 | {"Name":"example.com.","Type":1,"TTL":86400,"Data":"93.184.216.34"},
31 | {"Name":"example.com.","Type":46,"TTL":86400,"Data":"A 8 2 86400 20180627015845 20180606075626 4354 example.com. gpgx3XIhF4XZg5Nw0eo7TmCD1zfKX9YtMq9PuSh3eAc4fJrvyS/VWy2bz/KYhgiXQe6PvtOLZbgTT2O9knkHIlAsmnznEowSrgWYaCkkkNnoC8Ii1Ikg87PCZ7FffTposk/4HRG6yXZlo9+++YZAfAC0cc9FFYpQXqxVLf9/aDQ="}]
32 | }
33 | ```
34 |
--------------------------------------------------------------------------------
/anycast/link_linux.go:
--------------------------------------------------------------------------------
1 | // +build linux
2 |
3 | /**
4 | * Tenta DNS Server
5 | *
6 | * Copyright 2017 Tenta, LLC
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | *
20 | * For any questions, please contact developer@tenta.io
21 | *
22 | * link_linux.go: Link management on Linux platforms
23 | */
24 |
25 | package anycast
26 |
27 | import (
28 | "fmt"
29 | "github.com/tenta-browser/tenta-dns/common"
30 | "net"
31 |
32 | "github.com/milosgajdos83/tenus"
33 | "github.com/sirupsen/logrus"
34 | )
35 |
36 | func addLink(d *[]string, b common.Netblock, cnt *uint, lg *logrus.Entry) {
37 | name := fmt.Sprintf("bgp%d", *cnt)
38 | *cnt += 1
39 | ip, ipnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", b.IP, b.Netmask))
40 | if err != nil {
41 | lg.Errorf("Error parsing CIDR to create link link %s for %s: %s", name, b.IP, err.Error())
42 | return
43 | }
44 | lg.Debugf("Adding link %s for %s", name, b.IP)
45 | link, err := tenus.NewLink(name)
46 | if err != nil {
47 | lg.Errorf("Error creating link %s for %s: %s", name, b.IP, err.Error())
48 | return
49 | }
50 | *d = append(*d, name)
51 | err = link.SetLinkIp(ip, ipnet)
52 | if err != nil {
53 | lg.Errorf("Error setting link IP address %s for %s: %s", b.IP, name, err.Error())
54 | return
55 | }
56 | err = link.SetLinkUp()
57 | if err != nil {
58 | lg.Errorf("Error setting link %s for %s Up: %s", name, b.IP, err.Error())
59 | return
60 | }
61 | }
62 |
63 | func removeLinks(d *[]string, lg *logrus.Entry) {
64 | for _, name := range *d {
65 | lg.Debugf("Removing link %s", name)
66 | link, err := tenus.NewLinkFrom(name)
67 | if err != nil {
68 | lg.Errorf("Unable to retrieve link %s", name)
69 | continue
70 | }
71 | err = link.SetLinkDown()
72 | if err != nil {
73 | lg.Warnf("Unable to set link %s Down", name)
74 | }
75 | err = link.DeleteLink()
76 | if err != nil {
77 | lg.Errorf("Unable to remove link %s, you may need to do so manually", name)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_status.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_status.go: Server status API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "encoding/binary"
27 | "fmt"
28 | "net/http"
29 | "time"
30 |
31 | "github.com/tenta-browser/tenta-dns/common"
32 | "github.com/tenta-browser/tenta-dns/runtime"
33 |
34 | "github.com/sirupsen/logrus"
35 | )
36 |
37 | func HandleHTTPStatus(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry) httpHandler {
38 | return wrapExtendedHttpHandler(rt, lgr, "status", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
39 | startTimeBytes, err := rt.DBGet([]byte(runtime.KEY_START_TIME))
40 | runfor := uint32(0)
41 | data := &common.DefaultJSONObject{
42 | Status: "OK",
43 | Type: "TENTA_NSNITCH",
44 | Data: map[string]string{
45 | "uptime": "unset",
46 | "database": "available",
47 | "geoupdate": "unset",
48 | },
49 | Message: "System Okay",
50 | Code: http.StatusOK,
51 | }
52 | if err != nil {
53 | data.Status = "Error"
54 | data.Data.(map[string]string)["database"] = "failed"
55 | data.Message = "System Error"
56 | data.Code = http.StatusInternalServerError
57 | } else {
58 | runfor = uint32(time.Now().Unix()) - uint32(binary.LittleEndian.Uint64(startTimeBytes))
59 | data.Data.(map[string]string)["uptime"] = fmt.Sprintf("%d", runfor)
60 | if geoUpdateTimeBytes, err := rt.DBGet([]byte(runtime.KEY_GEODB_UPDATED)); err == nil {
61 | updated := uint32(time.Now().Unix()) - uint32(binary.LittleEndian.Uint64(geoUpdateTimeBytes))
62 | data.Data.(map[string]string)["geoupdate"] = fmt.Sprintf("%d", updated)
63 | }
64 | }
65 | extraHeaders(cfg, w, r)
66 | w.WriteHeader(data.Code)
67 | mustMarshall(w, data, lg)
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module tenta-dns
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/BurntSushi/toml v0.3.1
7 | github.com/armon/go-radix v1.0.0 // indirect
8 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
9 | github.com/dgryski/go-bits v0.0.0-20180113010104-bd8a69a71dc2 // indirect
10 | github.com/dgryski/go-farm v0.0.0-20171119141306-ac7624ea8da3 // indirect
11 | github.com/eapache/channels v1.1.0 // indirect
12 | github.com/eapache/queue v1.0.2 // indirect
13 | github.com/google/uuid v1.1.1 // indirect
14 | github.com/gorilla/mux v1.7.4
15 | github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e // indirect
16 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
17 | github.com/influxdata/influxdb v1.8.0 // indirect
18 | github.com/intel-go/cpuid v0.0.0-20181003105527-1a4a6f06a1c6 // indirect
19 | github.com/jessevdk/go-flags v1.3.0 // indirect
20 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
21 | github.com/k-sone/critbitgo v1.3.1-0.20191024122315-48c9e1530131 // indirect
22 | github.com/leonelquinteros/gorand v1.0.2
23 | github.com/magiconair/properties v1.7.3 // indirect
24 | github.com/mattn/go-colorable v0.1.6
25 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
26 | github.com/miekg/dns v1.1.29
27 | github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 // indirect
28 | github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f
29 | github.com/oschwald/maxminddb-golang v1.7.0
30 | github.com/osrg/gobgp v0.0.0-20180202030203-5dfe0ba86f40
31 | github.com/pelletier/go-buffruneio v0.2.0 // indirect
32 | github.com/pelletier/go-toml v1.0.0 // indirect
33 | github.com/sasha-s/go-hll v0.0.0-20180522065212-c6eb27aee351
34 | github.com/sirupsen/logrus v1.6.0
35 | github.com/spf13/afero v0.0.0-20170217164146-9be650865eab // indirect
36 | github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 // indirect
37 | github.com/spf13/viper v1.0.0 // indirect
38 | github.com/syndtr/goleveldb v1.0.0
39 | github.com/tenta-browser/go-highway v0.0.0-20171109222445-574cd915533d
40 | github.com/tenta-browser/tenta-dns v0.0.0-20190201184509-fb72fe2da5d6
41 | github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5
42 | github.com/vishvananda/netlink v0.0.0-20170802012344-a95659537721 // indirect
43 | github.com/vishvananda/netns v0.0.0-20170707011535-86bef332bfc3 // indirect
44 | github.com/x-cray/logrus-prefixed-formatter v0.5.2
45 | google.golang.org/grpc v1.29.1 // indirect
46 | gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
47 | )
48 |
--------------------------------------------------------------------------------
/responder/dns_common.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * dns_common.go: Constants and functions used all across responder package
21 | */
22 |
23 | package responder
24 |
25 | import (
26 | "fmt"
27 | "runtime/debug"
28 |
29 | "github.com/miekg/dns"
30 | )
31 |
32 | const (
33 | StatsQueryTotal = "resolver:queries:total"
34 | StatsQueryUDP = "resolver:queries:udp"
35 | StatsQueryTCP = "resolver:queries:tcp"
36 | StatsQueryTLS = "resolver:queries:tls"
37 | /// will fine tune the definition of failure
38 | StatsQueryFailure = "resolver:queries:failed"
39 | StatsQueryLimitedIps = "resolver:queries:limited_ips"
40 | StatsQueryUniqueIps = "resolver:queries:remote_ips"
41 | )
42 |
43 | var EDNSOptions = map[uint16]bool{
44 | dns.EDNS0LLQ: true,
45 | dns.EDNS0UL: true,
46 | dns.EDNS0NSID: true,
47 | dns.EDNS0DAU: true,
48 | dns.EDNS0DHU: true,
49 | dns.EDNS0N3U: true,
50 | dns.EDNS0SUBNET: true,
51 | dns.EDNS0EXPIRE: true,
52 | dns.EDNS0COOKIE: true,
53 | dns.EDNS0TCPKEEPALIVE: true,
54 | dns.EDNS0PADDING: true,
55 | dns.EDNS0LOCALSTART: true,
56 | dns.EDNS0LOCALEND: true,
57 | }
58 |
59 | type StackAddedPanic struct {
60 | trc []byte
61 | rcv interface{}
62 | }
63 |
64 | func (s *StackAddedPanic) String() string {
65 | return fmt.Sprintf("[%v]\n%s", s.rcv, s.trc)
66 | }
67 |
68 | type dnsHandler func(w dns.ResponseWriter, r *dns.Msg)
69 |
70 | func dnsRecoverWrap(hndl dnsHandler, notify chan interface{}) dnsHandler {
71 | return func(w dns.ResponseWriter, r *dns.Msg) {
72 | defer func() {
73 | if rcv := recover(); rcv != nil {
74 | snd := &StackAddedPanic{debug.Stack(), rcv}
75 | fmt.Printf("This is how we write this [%s]\n", snd)
76 | notify <- snd
77 | }
78 | }()
79 | hndl(w, r)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_randomizer.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_randomizer.go: Domain Name Randomizer API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "fmt"
27 | "github.com/tenta-browser/tenta-dns/common"
28 | "github.com/tenta-browser/tenta-dns/responder/randomizer"
29 | "github.com/tenta-browser/tenta-dns/runtime"
30 | "net/http"
31 |
32 | "github.com/sirupsen/logrus"
33 | )
34 |
35 | func HandleHTTPRedirector(cfg runtime.NSnitchConfig, rt *runtime.Runtime, d *runtime.ServerDomain, lgr *logrus.Entry) httpHandler {
36 | var rnd randomizer.Randomizer
37 | if cfg.RedirectMode == "wordlist" {
38 | rnd = randomizer.NewWordListRandomizer(cfg)
39 | } else {
40 | rnd = randomizer.NewUUIDRandomizer()
41 | }
42 | return wrapExtendedHttpHandler(rt, lgr, "randomizer", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
43 | data := &common.DefaultJSONObject{
44 | Status: "OK",
45 | Type: "TENTA_NSNITCH",
46 | Data: nil,
47 | Message: "",
48 | Code: 200,
49 | }
50 | api_response := false
51 | query := r.URL.Query()
52 | if _, ok := query["api_response"]; ok {
53 | api_response = true
54 | }
55 | subdomain, err := rnd.Rand()
56 | extraHeaders(cfg, w, r)
57 | if err != nil {
58 | data.Code = 500
59 | data.Message = "Internal Server Error"
60 | data.Status = "ERROR"
61 | mustMarshall(w, data, lg)
62 | } else {
63 | scheme := "http"
64 | if r.TLS != nil {
65 | scheme = "https"
66 | }
67 | redirecturl := fmt.Sprintf("%s://%s.%s/api/v1/report", scheme, subdomain, d.HostName)
68 | if api_response {
69 | data.Data = &map[string]string{"url": redirecturl}
70 | mustMarshall(w, data, lg)
71 | } else {
72 | http.Redirect(w, r, redirecturl, http.StatusFound)
73 | }
74 | }
75 | })
76 | }
77 |
--------------------------------------------------------------------------------
/zones/loader.go:
--------------------------------------------------------------------------------
1 | package zones
2 |
3 | import (
4 | "github.com/tenta-browser/tenta-dns/log"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "regexp"
9 |
10 | "github.com/miekg/dns"
11 | )
12 |
13 | func LoadZones(path string) (ZoneSet, error) {
14 | lg := log.GetLogger("zone-parser")
15 |
16 | ret := NewZoneSet()
17 | files, err := ioutil.ReadDir(path)
18 | if err != nil {
19 | lg.Errorf("Unable to open zones path %s: %s", path, err.Error())
20 | return ret, err
21 | }
22 |
23 | for _, file := range files {
24 | lg.Debugf("Found file %s", file.Name())
25 | match, err := regexp.MatchString("^.*\\.tdns$", file.Name())
26 | if err == nil && match {
27 | origin := file.Name()[0 : len(file.Name())-4]
28 | lg.Infof("Loading zone %s", origin)
29 | path := filepath.Join(path, file.Name())
30 | reader, err := os.Open(path)
31 | if err != nil {
32 | lg.Errorf("Failed to open file %s: %s", path, err.Error())
33 | return ret, err
34 | }
35 | // defer reader.Close()
36 | dns.PrivateHandle("GROUP", TypeGROUP, NewGROUP(lg))
37 | dns.PrivateHandle("DYNA", TypeDYNA, NewDYNA)
38 | for tkn := range dns.ParseZone(reader, origin, path) {
39 | if tkn.Error != nil {
40 | lg.Errorf("Zone %s: %s", origin, tkn.Error.Error())
41 | return ret, err
42 | } else {
43 | switch tkn.RR.Header().Rrtype {
44 | case TypeGROUP:
45 | fallthrough
46 | case TypeDYNA:
47 | lg.Debugf("Skipping custom type")
48 | lg.Debugf("Zone %s: %s [%s]", origin, tkn.RR.String(), tkn.Comment)
49 | break
50 | default:
51 | if len(tkn.Header().Name) < 1 {
52 | lg.Warnf("Got back a token with an empty name: %s", tkn.RR.String())
53 | break
54 | }
55 | if _, ok := ret[tkn.Header().Name]; !ok {
56 | ret[tkn.Header().Name] = NewZoneSetItem()
57 | }
58 | if _, ok := ret[tkn.Header().Name][tkn.Header().Rrtype]; !ok {
59 | ret[tkn.Header().Name][tkn.Header().Rrtype] = make([]ZoneEntry, 0)
60 | }
61 | ret[tkn.Header().Name][tkn.Header().Rrtype] = append(ret[tkn.Header().Name][tkn.Header().Rrtype], ZoneEntry{ZoneEntryTypeRR, &tkn.RR})
62 | break
63 | }
64 | }
65 | }
66 | // It's impossible to exit this loop without going through this finalizer. In the event this changes,
67 | // care will need to be taken to ensure that the reader is closed and the types are removed.
68 | dns.PrivateHandleRemove(TypeDYNA)
69 | dns.PrivateHandleRemove(TypeGROUP)
70 | reader.Close()
71 | }
72 | }
73 |
74 | return ret, nil
75 | }
76 |
--------------------------------------------------------------------------------
/common/common_test.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * common_test.go: Tests for common functions
21 | */
22 |
23 | package common
24 |
25 | import (
26 | "bytes"
27 | "net"
28 | "testing"
29 | )
30 |
31 | func TestAddSuffix(t *testing.T) {
32 |
33 | tt := []struct {
34 | start []byte
35 | suffix string
36 | expected []byte
37 | }{
38 | {[]byte(""), "", []byte("/")},
39 | {[]byte("a"), "", []byte("a/")},
40 | {[]byte(""), "a", []byte("/a")},
41 | {[]byte("ü"), "ü", []byte("ü/ü")},
42 | {[]byte(""), "ü", []byte("/ü")},
43 | {[]byte("ü"), "", []byte("ü/")},
44 | {[]byte("test"), "path", []byte("test/path")},
45 | {[]byte("test/again"), "path", []byte("test/again/path")},
46 | {[]byte("test/again\\"), "path", []byte("test/again\\/path")},
47 | {[]byte{0xe2, 0x8c, 0x98}, "\u2318", []byte{0xe2, 0x8c, 0x98, 0x2f, 0xe2, 0x8c, 0x98}},
48 | }
49 |
50 | for _, test := range tt {
51 | actual := AddSuffix(test.start, test.suffix)
52 | if !bytes.Equal(test.expected, actual) {
53 | t.Errorf("AddSuffix(%#v, %#v) returned %#v, wanted %#v", test.start, test.suffix, actual, test.expected)
54 | }
55 | }
56 | }
57 |
58 | func TestIsPrivateIp(t *testing.T) {
59 |
60 | tt := []struct {
61 | a net.IP
62 | expected bool
63 | }{
64 | {net.IPv4(10, 0, 0, 0), true},
65 | {net.IPv4(10, 255, 0, 0), true},
66 | {net.IPv4(10, 255, 255, 0), true},
67 | {net.IPv4(10, 255, 255, 255), true},
68 | {net.IPv4(11, 0, 0, 0), false},
69 | {net.IPv4(172, 16, 0, 0), true},
70 | {net.IPv4(172, 16, 255, 0), true},
71 | {net.IPv4(172, 16, 255, 0), true},
72 | {net.IPv4(172, 16, 255, 255), true},
73 | {net.IPv4(172, 31, 0, 0), true},
74 | {net.IPv4(172, 31, 255, 0), true},
75 | {net.IPv4(172, 31, 255, 255), true},
76 | {net.IPv4(172, 32, 0, 0), false},
77 | {net.IPv4(192, 168, 0, 0), true},
78 | {net.IPv4(192, 168, 255, 0), true},
79 | {net.IPv4(192, 168, 255, 255), true},
80 | {net.IPv4(192, 169, 0, 0), false},
81 | }
82 |
83 | for _, test := range tt {
84 | actual := IsPrivateIp(test.a)
85 | if actual != test.expected {
86 | t.Errorf("IsPrivateIp(%v) returned %v, wanted %v", test.a, actual, test.expected)
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/common/data.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * data.go: Data structure definitions
21 | */
22 |
23 | package common
24 |
25 | type DefaultJSONObject struct {
26 | Status string `json:"status"`
27 | Type string `json:"type"`
28 | Data interface{} `json:"data"`
29 | Message string `json:"message"`
30 | Code int `json:"code"`
31 | }
32 |
33 | type DNSTelemetry struct {
34 | IP string `json:"ip"`
35 | IPFamily string `json:"ip_family"`
36 | NetType string `json:"net_type"`
37 | TLSEnabled bool `json:"tls_enabled"`
38 | DNSSECOk bool `json:"dnssec_enabled"`
39 | NSIDEnabled bool `json:"nsid_requested"`
40 | RequestTime uint64 `json:"request_time,string"`
41 | KeepAlive bool `json:"tcp_keep_alive_enabled"`
42 | KeepAliveTTL uint `json:"tcp_keep_alive_ms"`
43 | Cookie bool `json:"dns_cookie"`
44 | ExtraEDNS0 int `json:"additional_edns0_records"`
45 | FlagRD bool `json:"recursion_desired"`
46 | FlagCD bool `json:"checking_disabled"`
47 | RequestLoc *GeoLocation `json:"request_location"`
48 | Subnet *ClientSubnet `json:"client_subnet"`
49 | NodeLoc *GeoLocation `json:"node_location"`
50 | RFC4343Fail bool `json:"rfc4343_intolerance"`
51 | }
52 |
53 | type ClientSubnet struct {
54 | IP string `json:"ip"`
55 | Netmask int `json:"netmask"`
56 | IPFamily string `json:"ip_family"`
57 | DraftMode bool `json:"draft_mode"`
58 | SubnetLoc *GeoLocation `json:"subnet_location"`
59 | }
60 |
61 | type GeoLookupData struct {
62 | NodeLoc *GeoLocation `json:"node_location"`
63 | RequestLoc *GeoLocation `json:"request_location"`
64 | IP string `json:"ip_address"`
65 | }
66 |
67 | type StatsData struct {
68 | Keys *[]string `json:"keys"`
69 | Counters *map[string]uint64 `json:"counters"`
70 | PFCounters *map[string]uint64 `json:"cardinalities"`
71 | }
72 |
73 | type BlacklistData struct {
74 | OnBlacklist bool `json:"blacklisted"`
75 | NumberOfLists int `json:"number_of_lists"`
76 | NumberFound int `json:"number_blacklisted"`
77 | Results map[string]bool `json:"results"`
78 | }
79 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * log.go: Helper functions for logging
21 | */
22 |
23 | package log
24 |
25 | import (
26 | "fmt"
27 | "io/ioutil"
28 | "os"
29 | "strings"
30 | "time"
31 |
32 | "github.com/mattn/go-colorable"
33 | "github.com/sirupsen/logrus"
34 | prefixed "github.com/x-cray/logrus-prefixed-formatter"
35 | )
36 |
37 | // EventualLogger -- a structure to buffer log entries whose usefulnes cannot be determined at creation time (but only eventually)
38 | type EventualLogger []string
39 |
40 | // Queuef -- buffers the interpreted message to be written later
41 | func (l *EventualLogger) Queuef(format string, args ...interface{}) {
42 | interpreted := fmt.Sprintf(format, args...)
43 | lines := strings.Split(interpreted, "\n")
44 | if *l == nil {
45 | *l = make(EventualLogger, 0)
46 | }
47 | for _, line := range lines {
48 | *l = append(*l, fmt.Sprintf("[%s]%s\n", time.Now().Format("15:04:05.000"), line))
49 | }
50 | }
51 |
52 | // Flush -- writes out everything from buffer
53 | func (l *EventualLogger) Flush(target *logrus.Entry) {
54 | for _, e := range *l {
55 | //target.Infof(e)
56 | fmt.Printf("%s", e)
57 | }
58 | }
59 |
60 | // FlushPrefixed -- just like Flush(), applies the specified prefix to all lines
61 | func (l *EventualLogger) FlushExt(target *logrus.Entry, prefix string) {
62 | for _, e := range *l {
63 | fmt.Printf("%s%s", prefix, e)
64 | }
65 | }
66 |
67 | func (l *EventualLogger) FlushToString() (s string) {
68 | for _, line := range *l {
69 | s += line
70 | }
71 | return
72 | }
73 |
74 | func (l *EventualLogger) FlushToFile(nameHint string) {
75 | if _, e := os.Stat(nameHint); e == nil {
76 | return
77 | }
78 | ioutil.WriteFile(nameHint, []byte(l.FlushToString()), 0666)
79 | }
80 |
81 | var log *logrus.Logger = logrus.New()
82 |
83 | func init() {
84 | log.Level = logrus.PanicLevel
85 | log.Out = colorable.NewColorableStdout()
86 | formatter := &prefixed.TextFormatter{ForceColors: true, ForceFormatting: true}
87 | formatter.SetColorScheme(&prefixed.ColorScheme{DebugLevelStyle: "green+b", InfoLevelStyle: "green+h"})
88 | log.Formatter = formatter
89 | // TODO: Deal with how to log to files or something
90 | }
91 |
92 | func SetLogLevel(lvl logrus.Level) {
93 | log.Level = lvl
94 | }
95 |
96 | func GetLogger(pkg string) *logrus.Entry {
97 | return log.WithField("prefix", pkg)
98 | }
99 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_report.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_report.go: NSNitch Report API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "encoding/json"
27 | "fmt"
28 | "net/http"
29 | "strings"
30 | "time"
31 |
32 | "github.com/tenta-browser/tenta-dns/common"
33 | "github.com/tenta-browser/tenta-dns/runtime"
34 |
35 | "github.com/miekg/dns"
36 | "github.com/sirupsen/logrus"
37 | "github.com/syndtr/goleveldb/leveldb"
38 | )
39 |
40 | func HandleHTTPReport(cfg runtime.NSnitchConfig, rt *runtime.Runtime, d *runtime.ServerDomain, lgr *logrus.Entry) httpHandler {
41 | return wrapExtendedHttpHandler(rt, lgr, "report", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
42 | key := []byte(fmt.Sprintf("query/%s", r.Host))
43 | if r.Host != strings.ToLower(r.Host) {
44 | lg.Debugf("Hostname contains uppercase %s", r.Host)
45 | key = []byte(fmt.Sprintf("query/%s", strings.ToLower(r.Host)))
46 | }
47 |
48 | if !dns.IsSubDomain(d.HostName, r.Host) {
49 | lg.Warnf("Handling request for invalid domain. Serving: %s, Requested: %s", d.HostName, r.Host)
50 | HandleHTTPDefault(cfg, rt, lg)(w, r)
51 | return
52 | }
53 |
54 | data := &common.DefaultJSONObject{
55 | Status: "OK",
56 | Type: "TENTA_NSNITCH",
57 | Data: nil,
58 | Message: "",
59 | Code: 200,
60 | }
61 |
62 | _, err := rt.DBGet(common.AddSuffix(key, runtime.KEY_NAME))
63 | if err != nil {
64 | // Fix for race where this request completes before the DNS goroutine has finished writing the DB record
65 | time.Sleep(500 * time.Millisecond)
66 | _, err := rt.DBGet(common.AddSuffix(key, runtime.KEY_NAME))
67 | if err != nil {
68 | lg.Warnf("DB Error: %s", err.Error())
69 | data.Status = "ERROR"
70 | if err == leveldb.ErrNotFound {
71 | data.Message = "Not Found"
72 | data.Code = http.StatusNotFound
73 | } else {
74 | data.Message = "Internal Error"
75 | data.Code = http.StatusInternalServerError
76 | }
77 | extraHeaders(cfg, w, r)
78 | w.WriteHeader(data.Code)
79 | mustMarshall(w, data, lg)
80 | return
81 | }
82 | }
83 | recarr, _ := rt.DBGet(common.AddSuffix(key, runtime.KEY_DATA))
84 | rec := &common.DNSTelemetry{}
85 | json.Unmarshal(recarr, rec)
86 |
87 | data.Data = rec
88 |
89 | extraHeaders(cfg, w, r)
90 | mustMarshall(w, data, lg)
91 | })
92 | }
93 |
--------------------------------------------------------------------------------
/responder/http-handlers/helpers.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * helpers.go: HTTP Handler Helpers
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "encoding/json"
27 | "fmt"
28 | "net"
29 | "net/http"
30 | "time"
31 |
32 | "github.com/tenta-browser/tenta-dns/common"
33 | "github.com/tenta-browser/tenta-dns/runtime"
34 |
35 | "github.com/leonelquinteros/gorand"
36 | "github.com/sirupsen/logrus"
37 | )
38 |
39 | type httpHandler func(w http.ResponseWriter, r *http.Request)
40 | type extendedHttpHandler func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry)
41 |
42 | func wrapExtendedHttpHandler(rt *runtime.Runtime, lg *logrus.Entry, name string, ehandler extendedHttpHandler) httpHandler {
43 | return func(w http.ResponseWriter, r *http.Request) {
44 | start := time.Now()
45 | request_id, err := gorand.UUIDv4()
46 | if err != nil {
47 | lg.Errorf("Unable to generate request id: %s", err.Error())
48 | panic(err)
49 | }
50 | // TODO: Should we put a canonical "request timestamp" here?
51 | lg = lg.WithField("request_uri", r.RequestURI).WithField("request_id", request_id).WithField("http_handler", name)
52 | lg.Debug("Handling request")
53 | ehandler(w, r, lg)
54 | end := time.Now()
55 | latency := uint64(end.Unix() - start.Unix())
56 | rt.Stats.Tick("http", "requests:all")
57 | rt.Stats.Tick("http", fmt.Sprintf("requests:%s", name))
58 | rt.Stats.Latency("http:requests:all", latency)
59 | rt.Stats.Latency(fmt.Sprintf("http:requests:%s", name), latency)
60 | ip, _, err := net.SplitHostPort(r.RemoteAddr)
61 | if err == nil {
62 | rt.Stats.Card("http:remote_ips", ip)
63 | }
64 | }
65 | }
66 |
67 | func extraHeaders(cfg runtime.NSnitchConfig, w http.ResponseWriter, r *http.Request) {
68 | origin := r.Header.Get("Origin")
69 | corsenabled := false
70 | for _, org := range cfg.CorsDomains {
71 | if fmt.Sprintf("http://%s", org) == origin {
72 | corsenabled = true
73 | break
74 | }
75 | if fmt.Sprintf("https://%s", org) == origin {
76 | corsenabled = true
77 | break
78 | }
79 | }
80 | if corsenabled {
81 | w.Header().Set("Access-Control-Allow-Origin", origin)
82 | }
83 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
84 | }
85 |
86 | func mustMarshall(w http.ResponseWriter, djo *common.DefaultJSONObject, lg *logrus.Entry) {
87 | out, err := json.Marshal(djo)
88 | if err != nil {
89 | lg.Warnf("Failed marshalling JSON: %s", err.Error())
90 | w.Write([]byte("{\"status\":\"ERROR\",\"type\":\"TENTA_NSNITCH\",\"data\":null,\"message\":\"Internal Server Error\",\"code\":500}"))
91 | } else {
92 | w.Write(out)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/responder/recursive_dns_helpers.go:
--------------------------------------------------------------------------------
1 | package responder
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/miekg/dns"
7 | "github.com/tenta-browser/tenta-dns/runtime"
8 | )
9 |
10 | type JSONQuestion struct {
11 | Name string
12 | RRtype uint16 `json:"Type"`
13 | }
14 |
15 | type JSONRR struct {
16 | *JSONQuestion
17 | TTL uint32
18 | Data string
19 | }
20 |
21 | type JSONResponse struct {
22 | Status int
23 | TC, RD, RA, AD, CD bool
24 | Question []*JSONQuestion
25 | Answer, Authority, Additional []*JSONRR `json:",omitempty"`
26 | }
27 |
28 | func JSONFromMsg(in *dns.Msg) *JSONResponse {
29 | resp := &JSONResponse{Status: in.Rcode, TC: in.Truncated, RD: in.RecursionDesired, RA: in.RecursionAvailable, AD: in.AuthenticatedData, CD: in.CheckingDisabled,
30 | Question: []*JSONQuestion{&JSONQuestion{Name: in.Question[0].Name, RRtype: in.Question[0].Qtype}}}
31 |
32 | if in.Answer != nil {
33 | resp.Answer = []*JSONRR{}
34 | for _, ans := range in.Answer {
35 | resp.Answer = append(resp.Answer, &JSONRR{&JSONQuestion{ans.Header().Name, ans.Header().Rrtype}, ans.Header().Ttl, strings.TrimLeft(ans.String(), ans.Header().String())})
36 | }
37 | }
38 |
39 | if in.Ns != nil {
40 | resp.Authority = []*JSONRR{}
41 | for _, ans := range in.Ns {
42 | resp.Authority = append(resp.Authority, &JSONRR{&JSONQuestion{ans.Header().Name, ans.Header().Rrtype}, ans.Header().Ttl, strings.TrimLeft(ans.String(), ans.Header().String())})
43 | }
44 | }
45 | cleanExtra := cleanAdditionalSection(in.Extra)
46 | if len(cleanExtra) != 0 {
47 | resp.Additional = []*JSONRR{}
48 | for _, ans := range cleanExtra {
49 | resp.Additional = append(resp.Additional, &JSONRR{&JSONQuestion{ans.Header().Name, ans.Header().Rrtype}, ans.Header().Ttl, strings.TrimLeft(ans.String(), ans.Header().String())})
50 | }
51 | }
52 |
53 | return resp
54 | }
55 |
56 | // Politely decline to answer ANY queries
57 | func refuseAny(w dns.ResponseWriter, r *dns.Msg, rt *runtime.Runtime) {
58 | rt.Stats.Tick("resolver", "refuse-any")
59 | hinfo := &dns.HINFO{
60 | Cpu: "ANY obsolete",
61 | Os: "See draft-ietf-dnsop-refuse-any",
62 | }
63 | msg := new(dns.Msg)
64 | msg.SetReply(r)
65 | msg.Compress = true
66 | msg.Answer = append(msg.Answer, hinfo)
67 | msg.SetRcode(r, dns.RcodeSuccess)
68 | w.WriteMsg(msg)
69 | }
70 |
71 | /// Set up a couple of typed cache returns, for specific usages
72 | /// General policy is to skip type conversion errors, and return valid data
73 |
74 | func ToA(cr interface{}) (ret []*dns.A) {
75 | if rr, ok := cr.([]dns.RR); ok {
76 | for _, r := range rr {
77 | if a, ok := r.(*dns.A); ok {
78 | ret = append(ret, a)
79 | }
80 | }
81 | } else if rs, ok := cr.(*dns.Msg); ok {
82 | for _, r := range rs.Answer {
83 | if a, ok := r.(*dns.A); ok {
84 | ret = append(ret, a)
85 | }
86 | }
87 | }
88 | return
89 | }
90 |
91 | func ToNS(cr interface{}) (ret []*dns.NS) {
92 | if rr, ok := cr.([]dns.RR); ok {
93 | for _, r := range rr {
94 | if ns, ok := r.(*dns.NS); ok {
95 | ret = append(ret, ns)
96 | }
97 | }
98 | } else if rs, ok := cr.(*dns.Msg); ok {
99 | for _, r := range rs.Answer {
100 | if ns, ok := r.(*dns.NS); ok {
101 | ret = append(ret, ns)
102 | }
103 | }
104 | }
105 | return
106 | }
107 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_geolookup.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_geolookup.go: Geo Data API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "fmt"
27 | "github.com/tenta-browser/tenta-dns/common"
28 | "github.com/tenta-browser/tenta-dns/runtime"
29 | "net"
30 | "net/http"
31 |
32 | "github.com/gorilla/mux"
33 | "github.com/sirupsen/logrus"
34 | )
35 |
36 | type GeoLookupError struct {
37 | s string
38 | Code int
39 | }
40 |
41 | func (e *GeoLookupError) Error() string {
42 | return e.s
43 | }
44 |
45 | func HandleHTTPGeoLookup(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry) httpHandler {
46 | nodeloc := cfg.Node.MakeNodeLoc()
47 | return wrapExtendedHttpHandler(rt, lgr, "geolookup", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
48 | var ip string
49 | var err *GeoLookupError
50 | vars := mux.Vars(r)
51 |
52 | if vars != nil && len(vars) > 0 {
53 | query := r.URL.Query()
54 | if query != nil && len(query) > 0 {
55 | if _, ok := vars["foreign_ip"]; ok {
56 | ip = vars["foreign_ip"]
57 | sigError := computeSignature(cfg.Base, r, query)
58 | if sigError != nil {
59 | lg.Warnf("Signature Error: %s", sigError)
60 | err = &GeoLookupError{s: "Invalid signature", Code: 403}
61 | }
62 | } else {
63 | err = &GeoLookupError{s: "Missing required path param 'foreign_ip'", Code: 404}
64 | }
65 | } else {
66 | err = &GeoLookupError{s: "Missing query string", Code: 400}
67 | }
68 | } else {
69 | var spliterr error
70 | ip, _, spliterr = net.SplitHostPort(r.RemoteAddr)
71 | if spliterr != nil {
72 | err = &GeoLookupError{s: fmt.Sprintf("Unble to split remote address: %s", err.Error()), Code: 500}
73 | }
74 | }
75 |
76 | data := &common.DefaultJSONObject{
77 | Status: "OK",
78 | Type: "TENTA_NSNITCH",
79 | Data: &common.GeoLookupData{
80 | RequestLoc: nil,
81 | NodeLoc: nodeloc,
82 | IP: "",
83 | },
84 | Message: "",
85 | Code: 200,
86 | }
87 | if err == nil {
88 | geoquery := rt.Geo.Query(ip)
89 | georesponse, err := geoquery.Response()
90 | if err == nil && (georesponse.ISP.ASNumber != 0 || georesponse.Location != "") {
91 | data.Data.(*common.GeoLookupData).RequestLoc = georesponse
92 | }
93 | data.Data.(*common.GeoLookupData).IP = ip
94 | } else {
95 | data.Status = "ERROR"
96 | data.Code = err.Code
97 | lg.WithField("status_code", err.Code).Debugf("Request failed: %s", err.Error())
98 | }
99 | extraHeaders(cfg, w, r)
100 | mustMarshall(w, data, lg)
101 | })
102 | }
103 |
--------------------------------------------------------------------------------
/responder/http-handlers/security.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * security.go: API security functions
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "crypto/hmac"
27 | "crypto/sha256"
28 | "errors"
29 | "fmt"
30 | "github.com/tenta-browser/tenta-dns/runtime"
31 | "net"
32 | "net/http"
33 | "net/url"
34 | "strconv"
35 | "time"
36 | )
37 |
38 | func computeSignature(cfg runtime.Config, r *http.Request, q url.Values) error {
39 | var timestamp, key, token, request_url, token_now, secret string
40 | var placeholder []string
41 | var timestamp_int, timestamp_now int64
42 | var error_counter uint
43 | var ok bool
44 | var err error
45 | if placeholder, ok = q["timestamp"]; !ok {
46 | return errors.New("Missing 'timestamp' query parameter")
47 | }
48 | if len(placeholder) < 1 {
49 | return errors.New("Empty 'timetamp' query parameter")
50 | }
51 | timestamp = placeholder[0]
52 | if placeholder, ok = q["key"]; !ok {
53 | return errors.New("Missing 'key' parameter")
54 | }
55 | if len(placeholder) < 1 {
56 | return errors.New("Empty 'key' parameter")
57 | }
58 | key = placeholder[0]
59 | if secret, ok = cfg.LookupCreds[key]; !ok {
60 | return errors.New("Cannot find specified key")
61 | }
62 | if placeholder, ok = q["token"]; !ok {
63 | return errors.New("Missing 'token' parameter")
64 | }
65 | if len(placeholder) < 1 {
66 | return errors.New("Empty 'token' parameter")
67 | }
68 | token = placeholder[0]
69 | if timestamp_int, err = strconv.ParseInt(timestamp, 10, 64); err != nil {
70 | return errors.New("Could not convert timestamp")
71 | }
72 |
73 | scheme := "http"
74 | if r.TLS != nil {
75 | scheme = "https"
76 | }
77 | host, _, err := net.SplitHostPort(r.Host)
78 | if err != nil {
79 | host = r.Host
80 | }
81 | request_url = fmt.Sprintf("%s://%s%s", scheme, host, r.URL.Path)
82 |
83 | mac := hmac.New(sha256.New, []byte(secret))
84 | mac.Write([]byte(request_url))
85 | mac.Write([]byte(timestamp))
86 | token_now = fmt.Sprintf("%x", mac.Sum(nil))
87 | timestamp_now = time.Now().Unix()
88 |
89 | var diff int64
90 | var must_set uint = 10
91 |
92 | // Constant time code
93 | if timestamp_now > timestamp_int {
94 | diff = timestamp_now - timestamp_int
95 | } else {
96 | diff = timestamp_int - timestamp_now
97 | }
98 | if diff < 60 {
99 | must_set = 0 // No error
100 | } else {
101 | must_set = 1 // Error
102 | }
103 | error_counter += must_set
104 | must_set = 10
105 |
106 | if hmac.Equal([]byte(token_now), []byte(token)) {
107 | must_set = 0 // No Error
108 | } else {
109 | must_set = 1 // Error
110 | }
111 | error_counter += must_set
112 |
113 | if error_counter == 0 {
114 | return nil
115 | } else {
116 | return errors.New("Signature mismatch")
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/responder/http-handlers/http_handler_bllookup.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http_handler_bllookup.go: DNS RBL Lookup API
21 | */
22 |
23 | package http_handlers
24 |
25 | import (
26 | "fmt"
27 | "net"
28 | "net/http"
29 |
30 | "github.com/tenta-browser/tenta-dns/common"
31 | "github.com/tenta-browser/tenta-dns/responder/blacklist"
32 | "github.com/tenta-browser/tenta-dns/runtime"
33 |
34 | "github.com/gorilla/mux"
35 | "github.com/sirupsen/logrus"
36 | )
37 |
38 | type BLLookupError struct {
39 | s string
40 | Code int
41 | }
42 |
43 | func (e *BLLookupError) Error() string {
44 | return e.s
45 | }
46 |
47 | func HandleHTTPBLLookup(cfg runtime.NSnitchConfig, rt *runtime.Runtime, lgr *logrus.Entry) httpHandler {
48 | return wrapExtendedHttpHandler(rt, lgr, "blacklist", func(w http.ResponseWriter, r *http.Request, lg *logrus.Entry) {
49 | var ip string
50 | var err *BLLookupError
51 | var ret *common.BlacklistData
52 | vars := mux.Vars(r)
53 |
54 | if vars != nil && len(vars) > 0 {
55 | query := r.URL.Query()
56 | if query != nil && len(query) > 0 {
57 | if _, ok := vars["foreign_ip"]; ok {
58 | ip = vars["foreign_ip"]
59 | sigError := computeSignature(cfg.Base, r, query)
60 | if sigError != nil {
61 | lg.Warnf("Signature Error: %s", sigError)
62 | err = &BLLookupError{s: "Invalid signature", Code: 403}
63 | }
64 | } else {
65 | err = &BLLookupError{s: "Missing required path param 'foreign_ip'", Code: 404}
66 | }
67 | } else {
68 | err = &BLLookupError{s: "Missing query string", Code: 400}
69 | }
70 | } else {
71 | var spliterr error
72 | ip, _, spliterr = net.SplitHostPort(r.RemoteAddr)
73 | if spliterr != nil {
74 | err = &BLLookupError{s: fmt.Sprintf("Unble to split remote address: %s", spliterr.Error()), Code: 500}
75 | }
76 | }
77 |
78 | bl := blacklist.New(rt, cfg)
79 | rawip := net.ParseIP(ip)
80 | if rawip != nil {
81 | ipv4 := rawip.To4()
82 | if ipv4 != nil {
83 | ret = bl.Check(ipv4)
84 | } else {
85 | err = &BLLookupError{s: fmt.Sprintf("Unable to perform lookup on IPv6 at present"), Code: 422}
86 | }
87 | } else {
88 | err = &BLLookupError{s: fmt.Sprintf("Unable to parse IP address: %s", ip), Code: 400}
89 | }
90 |
91 | data := &common.DefaultJSONObject{
92 | Status: "OK",
93 | Type: "TENTA_NSNITCH",
94 | Data: ret,
95 | Message: "",
96 | Code: 200,
97 | }
98 |
99 | if err == nil {
100 | data.Message = fmt.Sprintf("Did lookup for %s", ip)
101 | } else {
102 | data.Status = "ERROR"
103 | data.Code = err.Code
104 | lg.WithField("status_code", err.Code).Debugf("Request failed: %s", err.Error())
105 | }
106 | extraHeaders(cfg, w, r)
107 | mustMarshall(w, data, lg)
108 | })
109 | }
110 |
--------------------------------------------------------------------------------
/tenta-dns.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * nsnitch.go: Main entry point
21 | */
22 |
23 | package main
24 |
25 | import (
26 | "flag"
27 | "fmt"
28 | "os"
29 | "os/signal"
30 | "syscall"
31 |
32 | "github.com/tenta-browser/tenta-dns/director"
33 | "github.com/tenta-browser/tenta-dns/log"
34 | "github.com/tenta-browser/tenta-dns/runtime"
35 |
36 | "github.com/coreos/go-systemd/daemon"
37 | "github.com/sirupsen/logrus"
38 | )
39 |
40 | var (
41 | cfgfile = flag.String("config", "", "Path to the configuration file")
42 | checkonly = flag.Bool("check", false, "Perform a config check and immediately exit")
43 | ignoreplatform = flag.Bool("ignoreplatform", false, "Ignore platform when performing config checking (e.g. check configs on windows to run on linux)")
44 | quiet = flag.Bool("quiet", false, "Don't produce any output to the terminal")
45 | verbose = flag.Bool("verbose", false, "Produce lots of output to the terminal (overrides the -quiet flag)")
46 | systemd = flag.Bool("systemd", false, "Assume running under systemd and send control notifications. NOTE: Behavior is undefined if using this flag without systemd")
47 | printver = flag.Bool("version", false, "Print the version and exit")
48 | )
49 |
50 | var version string
51 |
52 | func usage() {
53 | fmt.Printf("Tenta DNS %s", version)
54 | fmt.Println("")
55 | fmt.Println("Full featured DNS server with DNSSEC and DNS-over-TLS")
56 | fmt.Println("Options:")
57 | flag.PrintDefaults()
58 | }
59 |
60 | func main() {
61 | log.SetLogLevel(logrus.InfoLevel)
62 | flag.Usage = usage
63 | flag.Parse()
64 |
65 | if *printver {
66 | fmt.Println(version)
67 | os.Exit(0)
68 | }
69 |
70 | if *quiet {
71 | log.SetLogLevel(logrus.FatalLevel)
72 | }
73 | if *verbose {
74 | log.SetLogLevel(logrus.DebugLevel)
75 | }
76 |
77 | if *systemd {
78 | daemon.SdNotify(false, "RELOADING=1")
79 | }
80 |
81 | lg := log.GetLogger("main")
82 | lg.Info("Starting up")
83 |
84 | if *cfgfile == "" {
85 | lg.Error("Error: Missing Config path path")
86 | usage()
87 | os.Exit(1)
88 | }
89 |
90 | if *checkonly && *ignoreplatform {
91 | lg.Debug("Ignoring platform restrictions in config file check")
92 | }
93 | hld := runtime.ParseConfig(*cfgfile, *checkonly, *checkonly && *ignoreplatform)
94 | if *checkonly {
95 | lg.Info("Config files are valid, exiting from config check mode")
96 | os.Exit(0)
97 | }
98 |
99 | d := director.NewDirector(hld)
100 | d.Orchestrate(*systemd)
101 |
102 | sig := make(chan os.Signal)
103 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
104 | s := <-sig
105 | lg.Info(fmt.Sprintf("Signal (%s) received, stopping", s))
106 | if *systemd {
107 | daemon.SdNotify(false, "STOPPING=1")
108 | }
109 |
110 | d.Stop()
111 |
112 | os.Exit(0)
113 | }
114 |
--------------------------------------------------------------------------------
/netinterface/interface.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * netinterface.go: Network interface management routines
21 | */
22 |
23 | package netinterface
24 |
25 | import (
26 | "fmt"
27 | "github.com/tenta-browser/tenta-dns/common"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "net"
30 | "sync"
31 | "time"
32 |
33 | "github.com/sirupsen/logrus"
34 | )
35 |
36 | func WatchInterfaces(ifaces []common.Interface) (chan common.Status, chan bool, *sync.WaitGroup) {
37 | notify := make(chan common.Status, 1024)
38 | stop := make(chan bool, 1)
39 | lg := log.GetLogger("netinterface")
40 | wg := &sync.WaitGroup{}
41 | go func() {
42 | defer func() {
43 | if rcv := recover(); rcv != nil {
44 | lg.Errorf("Got a panic from the interface watcher. Sending a fatal failure: %s", rcv)
45 | notify <- common.Status{State: common.StateCriticalFailure, ID: fmt.Sprintf("%s", rcv)}
46 | }
47 | wg.Done()
48 | }()
49 | wg.Add(1)
50 | manageInterfaces(ifaces, notify, stop, lg)
51 | }()
52 | return notify, stop, wg
53 | }
54 |
55 | func manageInterfaces(ifaces []common.Interface, notify chan common.Status, stop chan bool, lg *logrus.Entry) {
56 | lg.Debug("Starting up")
57 | t := time.NewTicker(time.Millisecond * 250)
58 | run := true
59 | prev := make(map[string]common.InterfaceState)
60 | for _, i := range ifaces {
61 | prev[i.ID] = common.StateMissing
62 | }
63 | for run {
64 | select {
65 | case <-t.C:
66 | up := make(map[string]bool, 0)
67 | hfaces, err := net.Interfaces()
68 | if err != nil {
69 | lg.Errorf("Unable to list interfaces: %s", err.Error())
70 | panic(err.Error())
71 | }
72 | for _, h := range hfaces {
73 | haddrs, err := h.Addrs()
74 | if err != nil {
75 | lg.Errorf("Unable to list addresses on %s: %s", h.Name, err.Error())
76 | panic(err.Error())
77 | }
78 | for _, a := range haddrs {
79 | var ip net.IP
80 | switch v := a.(type) {
81 | case *net.IPNet:
82 | ip = v.IP
83 | case *net.IPAddr:
84 | ip = v.IP
85 | default:
86 | panic(fmt.Sprintf("Got an uknown address type from a hardware interface: %s", a.String()))
87 | }
88 | for _, i := range ifaces {
89 | if i.IP.Equal(ip) {
90 | up[i.ID] = true
91 | }
92 | }
93 | }
94 | }
95 | for id := range prev {
96 | if _, ok := up[id]; ok {
97 | if prev[id] != common.StateUp {
98 | prev[id] = common.StateUp
99 | notify <- common.Status{State: common.StateUp, ID: id}
100 | }
101 | } else {
102 | if prev[id] != common.StateDown {
103 | prev[id] = common.StateDown
104 | notify <- common.Status{State: common.StateDown, ID: id}
105 | }
106 | }
107 | }
108 | case <-stop:
109 | run = false
110 | }
111 | }
112 | lg.Debug("Shutting down")
113 | }
114 |
--------------------------------------------------------------------------------
/responder/randomizer/randomizer.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * randomizer.go: Generate random subdomain names
21 | */
22 |
23 | package randomizer
24 |
25 | import (
26 | "bufio"
27 | "bytes"
28 | "errors"
29 | "fmt"
30 | "io"
31 | "os"
32 | "strings"
33 |
34 | "github.com/tenta-browser/tenta-dns/common"
35 | "github.com/tenta-browser/tenta-dns/log"
36 | "github.com/tenta-browser/tenta-dns/runtime"
37 |
38 | "github.com/leonelquinteros/gorand"
39 | )
40 |
41 | type Randomizer interface {
42 | Rand() (string, error)
43 | }
44 |
45 | type UUIDRandomizer struct{}
46 |
47 | func NewUUIDRandomizer() Randomizer {
48 | return Randomizer(UUIDRandomizer{})
49 | }
50 |
51 | func (rnd UUIDRandomizer) Rand() (string, error) {
52 | uuid, err := gorand.UUIDv4()
53 | return string(uuid[:]), err
54 | }
55 |
56 | type WordRandomizer struct {
57 | wordlist []string
58 | wordlistlen uint
59 | }
60 |
61 | func NewWordListRandomizer(cfg runtime.NSnitchConfig) Randomizer {
62 | lg := log.GetLogger("wordlist")
63 | lg.Debug("Setting up wordlist randomizer")
64 | rnd := WordRandomizer{}
65 | var (
66 | err error
67 | part []byte
68 | prefix bool
69 | )
70 | file, err := os.Open(cfg.WordListPath)
71 | if err != nil {
72 | lg.Errorf("Unable to open wordlist file %s", cfg.WordListPath)
73 | panic(errors.New("Unable to open wordlist file"))
74 | }
75 | defer file.Close()
76 |
77 | reader := bufio.NewReader(file)
78 | buffer := bytes.NewBuffer(make([]byte, 0))
79 |
80 | for {
81 | if part, prefix, err = reader.ReadLine(); err != nil {
82 | break
83 | }
84 | buffer.Write(part)
85 | if !prefix {
86 | rnd.wordlist = append(rnd.wordlist, strings.ToLower(buffer.String()))
87 | buffer.Reset()
88 | }
89 | }
90 |
91 | if err != io.EOF {
92 | lg.Warnf("Failed reading %s: %s", cfg.WordListPath, err.Error())
93 | }
94 |
95 | rnd.wordlistlen = uint(len(rnd.wordlist))
96 |
97 | lg.Debugf("Read %d words while setting up the wordlist randomizer", rnd.wordlistlen)
98 |
99 | return Randomizer(rnd)
100 | }
101 |
102 | func (rnd WordRandomizer) Rand() (string, error) {
103 | buffer := bytes.NewBuffer(make([]byte, 0))
104 | for i := 0; i < 4; i += 1 {
105 | if i != 0 {
106 | switch common.RandInt(7) {
107 | case 0, 1:
108 | buffer.WriteString(fmt.Sprintf("%d", common.RandInt(100)))
109 | case 2, 3, 4:
110 | buffer.WriteString("-")
111 | break
112 | case 5:
113 | buffer.WriteString("4")
114 | break
115 | default:
116 | // Do nothing, no separtor
117 | break
118 | }
119 | }
120 | word := rnd.wordlist[common.RandInt(rnd.wordlistlen)]
121 | // switch common.RandInt(4) {
122 | // case 0, 1, 2:
123 | // for i, v := range word {
124 | // word = string(unicode.ToUpper(v)) + word[i+1:]
125 | // break
126 | // }
127 | // break
128 | // default:
129 | // // Do nothing
130 | // break
131 | // }
132 | buffer.WriteString(word)
133 | }
134 | return buffer.String(), nil
135 | }
136 |
--------------------------------------------------------------------------------
/common/common.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * common.go: Common functions
21 | */
22 |
23 | package common
24 |
25 | import (
26 | "crypto/rand"
27 | "github.com/tenta-browser/tenta-dns/log"
28 | "math/big"
29 | "net"
30 | "os"
31 | "crypto/tls"
32 | )
33 |
34 | func AddSuffix(start []byte, suffix string) []byte {
35 | return append(start, []byte("/"+suffix)...)
36 | }
37 |
38 | func RandInt(max uint) uint {
39 | bi, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
40 | if err != nil {
41 | lg := log.GetLogger("common")
42 | lg.Fatalf("Unable to generate a random number: %s", err.Error())
43 | os.Exit(5)
44 | }
45 | return uint(bi.Uint64())
46 | }
47 |
48 | func IsPrivateIp(a net.IP) bool {
49 | a = a.To4()
50 | return a[0] == 10 || // class A private network
51 | (a[0] == 172 && a[1] >= 16 && a[1] <= 31) || // class B private networks
52 | (a[0] == 192 && a[1] == 168) // class C private networks
53 | }
54 |
55 | func TLSConfigModernHTTPS() *tls.Config {
56 | return &tls.Config{
57 | MinVersion: tls.VersionTLS12,
58 | CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
59 | PreferServerCipherSuites: true,
60 | CipherSuites: []uint16{
61 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
62 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
63 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
64 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
65 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
66 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
67 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
68 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
69 | },
70 | }
71 | }
72 |
73 | func TLSConfigLegacyHTTPS() *tls.Config {
74 | return &tls.Config{
75 | MinVersion: tls.VersionTLS10,
76 | CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
77 | PreferServerCipherSuites: true,
78 | CipherSuites: []uint16{
79 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
80 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
81 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
82 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
83 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
84 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
85 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
86 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
87 | tls.TLS_RSA_WITH_AES_256_CBC_SHA,
88 | tls.TLS_RSA_WITH_AES_128_CBC_SHA,
89 | tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
90 | },
91 | }
92 | }
93 |
94 | func TLSConfigDNS() *tls.Config {
95 | return &tls.Config{
96 | MinVersion: tls.VersionTLS12,
97 | CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
98 | PreferServerCipherSuites: true,
99 | CipherSuites: []uint16{
100 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
101 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
102 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
103 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
104 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
105 | tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
106 | },
107 | }
108 | }
--------------------------------------------------------------------------------
/runtime/feedback.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * feedback.go: Provides primitives for Slack webhook notifications
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "bytes"
27 | "fmt"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "net/http"
30 | "net/url"
31 | "sync"
32 | "time"
33 |
34 | "github.com/sirupsen/logrus"
35 | )
36 |
37 | type Payload struct {
38 | operator, dom, err, stack string
39 | }
40 |
41 | type Feedback struct {
42 | nopmode bool
43 | msg chan []byte
44 | stop chan bool
45 | wg *sync.WaitGroup
46 | nodeId, whURL string
47 | l *logrus.Entry
48 | }
49 |
50 | func formatOperator(base, detail string) string {
51 | return fmt.Sprintf("Operator `%s/%s`", base, detail)
52 | }
53 |
54 | func (p *Payload) ShortEncode(op string) []byte {
55 |
56 | return []byte("payload=" + url.QueryEscape(fmt.Sprintf("{\"text\":\"%s failed to resolve domain `%s`\n`%s`\"}", op, p.dom, p.err)))
57 | }
58 |
59 | func (p *Payload) LongEncode(op string) []byte {
60 | return []byte("payload=" + url.QueryEscape(fmt.Sprintf("{\"text\":\"Operator `%s` failed to resolve domain `%s`\n`%s`\n%s\"}", op, p.dom, p.err, p.stack)))
61 | }
62 |
63 | func NewPayload(operator, dom, err, stack string) *Payload {
64 | return &Payload{operator: operator, dom: dom, err: err, stack: stack}
65 | }
66 |
67 | func StartFeedback(cfg Config, rt *Runtime) *Feedback {
68 | f := &Feedback{}
69 | if t, ok := cfg.SlackFeedback["url"]; !ok {
70 | f.nopmode = true
71 | } else {
72 | f.msg = make(chan []byte)
73 | f.stop = make(chan bool)
74 | f.whURL = t
75 | if nodeId, ok := cfg.SlackFeedback["node"]; ok {
76 | f.nodeId = nodeId
77 | } else {
78 | f.nodeId = "placeholder"
79 | }
80 | f.l = log.GetLogger("feedback")
81 | wg := &sync.WaitGroup{}
82 | f.wg = wg
83 | go f.startFeedbackService()
84 | f.wg.Add(1)
85 | f.l.Infof("Started Feedback service")
86 | }
87 | return f
88 | }
89 |
90 | func (f *Feedback) SendFeedback(p *Payload) {
91 | if !f.nopmode {
92 | f.msg <- p.ShortEncode(formatOperator(f.nodeId, p.operator))
93 | }
94 | }
95 |
96 | func (f *Feedback) SendMessage(s, opDetails string) {
97 | if !f.nopmode {
98 | m := "payload=" + url.QueryEscape(fmt.Sprintf("{\"text\": \"%s -- %s\"}", formatOperator(f.nodeId, opDetails), s))
99 | f.msg <- []byte(m)
100 | }
101 | }
102 |
103 | func (f *Feedback) startFeedbackService() {
104 | defer f.wg.Done()
105 | for {
106 | select {
107 | case <-f.stop:
108 | f.l.Infof("Stop signal received. Exiting.")
109 | return
110 | case b := <-f.msg:
111 | resp, err := http.Post(f.whURL, "application/x-www-form-urlencoded", bytes.NewReader(b))
112 | defer resp.Body.Close()
113 | if err != nil {
114 | f.l.Infof("Unable to send to Slack. Cause [%s]", err.Error())
115 | } else if resp.StatusCode != 200 {
116 | f.l.Infof("Unable to send to Slack. HTTP status [%d]", resp.StatusCode)
117 | }
118 | break
119 | case <-time.After(100 * time.Millisecond):
120 | break
121 | }
122 | }
123 | }
124 |
125 | func (f *Feedback) Stop() {
126 | if !f.nopmode {
127 | defer f.wg.Wait()
128 | f.stop <- true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/runtime/limiter.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * limiter.go: Rate limiter implementation
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "math"
27 | "math/rand"
28 | "net"
29 | "sync"
30 | "time"
31 |
32 | "github.com/tenta-browser/tenta-dns/log"
33 |
34 | "github.com/sirupsen/logrus"
35 | )
36 |
37 | const UPDATE_DELAY = time.Minute
38 |
39 | type Limiter struct {
40 | limits map[string]uint64
41 | limitLock *sync.RWMutex
42 | sender chan string
43 | stop chan bool
44 | wg *sync.WaitGroup
45 | tables [5]map[string]uint64
46 | offset uint64
47 | lg *logrus.Entry
48 | v4mask net.IPMask
49 | v6mask net.IPMask
50 | }
51 |
52 | func StartLimiter(offset uint64) *Limiter {
53 | ret := &Limiter{}
54 | ret.offset = offset
55 | ret.lg = log.GetLogger("limiter")
56 | ret.sender = make(chan string, 16384)
57 | ret.limitLock = &sync.RWMutex{}
58 | ret.limits = make(map[string]uint64)
59 | ret.tables = [5]map[string]uint64{}
60 | ret.v4mask = net.CIDRMask(24, 32)
61 | ret.v6mask = net.CIDRMask(40, 128)
62 | ret.stop = make(chan bool)
63 | ret.wg = &sync.WaitGroup{}
64 | ret.wg.Add(1)
65 | go countlimits(ret)
66 | return ret
67 | }
68 |
69 | func (l *Limiter) Stop() {
70 | defer l.wg.Wait()
71 | l.stop <- true
72 | }
73 |
74 | func (l *Limiter) Count(key string) {
75 | select {
76 | case l.sender <- key:
77 | break
78 | case <-time.After(time.Millisecond):
79 | l.lg.Debug("Skipping count due to chan overflow")
80 | break
81 | }
82 | }
83 |
84 | func (l *Limiter) CountAndPass(ip net.IP) bool {
85 | var key string
86 | if ip.To4() != nil {
87 | key = ip.Mask(l.v4mask).String()
88 | } else {
89 | key = ip.Mask(l.v6mask).String()
90 | }
91 | //l.lg.Debugf("Checking %s", key)
92 | l.Count(key)
93 | l.limitLock.RLock()
94 | limit := l.limits[key]
95 | l.limitLock.RUnlock()
96 | if limit < uint64(rand.Int31()) {
97 | return true
98 | }
99 | return false
100 | }
101 |
102 | func countlimits(l *Limiter) {
103 | defer l.wg.Done()
104 | cl := &sync.WaitGroup{}
105 | t := time.NewTicker(UPDATE_DELAY)
106 | defer t.Stop()
107 | cycle := 0
108 | curr := make(map[string]uint64)
109 | for {
110 | select {
111 | case <-l.stop:
112 | l.lg.Debug("Shutting down counter")
113 | cl.Wait() // Wait for any pending update goroutine to complete
114 | return
115 | case key := <-l.sender:
116 | curr[key] += 1
117 | break
118 | case <-t.C:
119 | l.tables[cycle] = curr
120 | curr = make(map[string]uint64)
121 | cycle = (cycle + 1) % 5
122 | go updatecounts(l, cl)
123 | }
124 | }
125 | }
126 |
127 | func updatecounts(l *Limiter, cl *sync.WaitGroup) {
128 | cl.Add(1)
129 | defer cl.Done()
130 | st := time.Now()
131 | limits := make(map[string]uint64)
132 | slices := uint64(0)
133 | for _, ptr := range l.tables {
134 | if ptr != nil {
135 | slices += 1
136 | for key, cnt := range ptr {
137 | limits[key] += cnt
138 | }
139 | }
140 | }
141 | for key, cnt := range limits {
142 | limits[key] = cnt / slices
143 | if limits[key] <= l.offset {
144 | delete(limits, key)
145 | continue
146 | } else if limits[key] > 2<<30-1 {
147 | limits[key] = math.MaxInt32
148 | } else if limits[key] <= l.offset*2 {
149 | step := (2<<28 - 1) / l.offset
150 | limits[key] = limits[key] * step
151 | } else {
152 | limits[key] = limits[key] + (2<<30-1-limits[key])>>1
153 | }
154 | }
155 | l.limitLock.Lock()
156 | defer l.limitLock.Unlock()
157 | l.limits = limits
158 | l.lg.Infof("Rate counter aggregation took %v", time.Now().Sub(st))
159 | }
160 |
--------------------------------------------------------------------------------
/runtime/limiter_test.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * limiter_test.go: Rate limiter tests
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "github.com/tenta-browser/tenta-dns/log"
27 | "net"
28 | "testing"
29 | "time"
30 |
31 | "github.com/sirupsen/logrus"
32 | )
33 |
34 | func TestLimiter_Basic(t *testing.T) {
35 | log.SetLogLevel(logrus.DebugLevel)
36 | tip := net.ParseIP("1.2.3.4")
37 | offset := 500
38 | l := StartLimiter(uint64(offset))
39 | pass := l.CountAndPass(tip)
40 | if !pass {
41 | t.Fatal("Expecing a pass on a new new limier, but didn't get one")
42 | }
43 | for i := 0; i < offset-1; i += 1 {
44 | l.CountAndPass(tip)
45 | }
46 | time.Sleep(UPDATE_DELAY + time.Second)
47 | pass = l.CountAndPass(tip)
48 | if !pass {
49 | t.Fatal("Expecing a pass while less than limit, but didn't get one")
50 | }
51 | limits := 0
52 | for i := 0; i < 100; i += 1 {
53 | if !l.CountAndPass(tip) {
54 | limits += 1
55 | }
56 | }
57 | l.lg.Debugf("Got limited %d times out of 100 (%f)", limits, float32(limits)/float32(100))
58 | l.Stop()
59 | }
60 |
61 | func TestLimiter_AndAHalf(t *testing.T) {
62 | log.SetLogLevel(logrus.DebugLevel)
63 | tip := net.ParseIP("1.2.3.4")
64 | offset := 500
65 | l := StartLimiter(uint64(offset))
66 | pass := l.CountAndPass(tip)
67 | if !pass {
68 | t.Fatal("Expecing a pass on a new new limier, but didn't get one")
69 | }
70 | for i := 0; i < offset-1+int(offset/2); i += 1 {
71 | l.CountAndPass(tip)
72 | }
73 | time.Sleep(UPDATE_DELAY + time.Second)
74 | limits := 0
75 | for i := 0; i < 100; i += 1 {
76 | if !l.CountAndPass(tip) {
77 | limits += 1
78 | }
79 | }
80 | l.lg.Debugf("Got limited %d times out of 100 (%f)", limits, float32(limits)/float32(100))
81 | l.Stop()
82 | }
83 |
84 | func TestLimiter_DoubleOffset(t *testing.T) {
85 | log.SetLogLevel(logrus.DebugLevel)
86 | tip := net.ParseIP("1.2.3.4")
87 | offset := 500
88 | l := StartLimiter(uint64(offset))
89 | pass := l.CountAndPass(tip)
90 | if !pass {
91 | t.Fatal("Expecing a pass on a new new limier, but didn't get one")
92 | }
93 | for i := 0; i < offset*2-1; i += 1 {
94 | l.CountAndPass(tip)
95 | }
96 | time.Sleep(UPDATE_DELAY + time.Second)
97 | limits := 0
98 | for i := 0; i < 100; i += 1 {
99 | if !l.CountAndPass(tip) {
100 | limits += 1
101 | }
102 | }
103 | l.lg.Debugf("Got limited %d times out of 100 (%f)", limits, float32(limits)/float32(100))
104 | limits = 0
105 | for i := 0; i < 100; i += 1 {
106 | if !l.CountAndPass(tip) {
107 | limits += 1
108 | }
109 | }
110 | l.lg.Debugf("Got limited %d times out of 100 (%f)", limits, float32(limits)/float32(100))
111 | l.Stop()
112 | }
113 |
114 | func TestLimiter_TripleOffset(t *testing.T) {
115 | log.SetLogLevel(logrus.DebugLevel)
116 | tip := net.ParseIP("1.2.3.4")
117 | offset := 500
118 | l := StartLimiter(uint64(offset))
119 | pass := l.CountAndPass(tip)
120 | if !pass {
121 | t.Fatal("Expecing a pass on a new new limier, but didn't get one")
122 | }
123 | for i := 0; i < offset*3-1; i += 1 {
124 | l.CountAndPass(tip)
125 | }
126 | time.Sleep(UPDATE_DELAY + time.Second)
127 | limits := 0
128 | for i := 0; i < 100; i += 1 {
129 | if !l.CountAndPass(tip) {
130 | limits += 1
131 | }
132 | }
133 | l.lg.Debugf("Got limited %d times out of 100 (%f)", limits, float32(limits)/float32(100))
134 | limits = 0
135 | for i := 0; i < 100; i += 1 {
136 | if !l.CountAndPass(tip) {
137 | limits += 1
138 | }
139 | }
140 | l.lg.Debugf("Got limited %d times out of 100 (%f)", limits, float32(limits)/float32(100))
141 | l.Stop()
142 | }
143 |
--------------------------------------------------------------------------------
/runtime/garbageman.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * garbageman.go: Database cleanup facility
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "fmt"
27 | "github.com/tenta-browser/tenta-dns/common"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "time"
30 |
31 | "github.com/syndtr/goleveldb/leveldb"
32 | "github.com/syndtr/goleveldb/leveldb/util"
33 | )
34 |
35 | const (
36 | KEY_NAME = "name"
37 | KEY_DATA = "data"
38 | KEY_HOSTNAME = "hostname"
39 | )
40 |
41 | func garbageman(cfg Config, rt *Runtime) {
42 | defer rt.wg.Done()
43 | lg := log.GetLogger("garbageman")
44 | lg.Debug("Starting up")
45 | ticker := time.NewTicker(time.Second)
46 | var collected int
47 | for {
48 | select {
49 | case <-ticker.C:
50 | //lg.Debug("Collecting garbage")
51 | now := time.Now().Unix()
52 | prefix := (now - cfg.DatabaseTTL)
53 |
54 | // Handle DNS Lookup entries
55 | iter := rt.DB.NewIterator(&util.Range{Start: []byte("queries/"), Limit: append([]byte(fmt.Sprintf("queries/%d", prefix)), 0xFF)}, nil)
56 | droplist := make([][]byte, 0)
57 | for iter.Next() {
58 | k := make([]byte, len(iter.Key()))
59 | copy(k, iter.Key())
60 | droplist = append(droplist, k)
61 | v := make([]byte, len(iter.Value()))
62 | copy(v, iter.Value())
63 | droplist = append(droplist, common.AddSuffix(v, KEY_NAME))
64 | droplist = append(droplist, common.AddSuffix(v, KEY_DATA))
65 | droplist = append(droplist, common.AddSuffix(v, KEY_HOSTNAME))
66 | }
67 | iter.Release()
68 | batch := new(leveldb.Batch)
69 | droplist_len := len(droplist)
70 | for i := 0; i < droplist_len; i += 1 {
71 | batch.Delete(droplist[i])
72 | collected += 1
73 | }
74 | if droplist_len > 0 {
75 | rt.Stats.TickN("database", "get", uint64(droplist_len))
76 | rt.Stats.TickN("database", "delete", uint64(droplist_len))
77 | }
78 | if err := rt.DB.Write(batch, nil); err != nil {
79 | lg.Warnf("Failed applying transaction: %s", err.Error())
80 | rt.Stats.TickN("database", "get_error", uint64(droplist_len))
81 | rt.Stats.TickN("database", "delete_error", uint64(droplist_len))
82 | }
83 |
84 | // Handle Blacklist Entries
85 | // TOD: Store blacklist in cache2go
86 | //blprefix := (now - cfg.BlacklistTTL)
87 | //iter = rt.DB.NewIterator(&util.Range{Start: []byte("blacklist/"), Limit: append([]byte(fmt.Sprintf("blacklist/%d", blprefix)), 0xFF)}, nil)
88 | //droplist = make([][]byte, 0)
89 | //for iter.Next() {
90 | // k := make([]byte, len(iter.Key()))
91 | // copy(k, iter.Key())
92 | // v := make([]byte, len(iter.Value()))
93 | // copy(v, iter.Value())
94 | // droplist = append(droplist, k)
95 | // droplist = append(droplist, v)
96 | //}
97 | //iter.Release()
98 | //batch = new(leveldb.Batch)
99 | //droplist_len = len(droplist)
100 | //for i := 0; i < droplist_len; i += 1 {
101 | // batch.Delete(droplist[i])
102 | // collected += 1
103 | //}
104 | if droplist_len > 0 {
105 | rt.Stats.TickN("database", "get", uint64(droplist_len))
106 | rt.Stats.TickN("database", "delete", uint64(droplist_len))
107 | }
108 | if err := rt.DB.Write(batch, nil); err != nil {
109 | lg.Debug("error applying transaction")
110 | rt.Stats.TickN("database", "get_error", uint64(droplist_len))
111 | rt.Stats.TickN("database", "delete_error", uint64(droplist_len))
112 | }
113 |
114 | if now%300 == 0 {
115 | lg.WithField("now", now).Debugf("Collected: %d", collected)
116 | collected = 0
117 | }
118 | case <-rt.stop:
119 | ticker.Stop()
120 | lg.Debug("Shutting down")
121 | return
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/runtime/torupdater.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * torupdater.go: Tor node list updater
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "bufio"
27 | "errors"
28 | "fmt"
29 | "github.com/tenta-browser/tenta-dns/log"
30 | "io"
31 | "net/http"
32 | "strings"
33 | "time"
34 | )
35 |
36 | const (
37 | STATE_NODE = iota
38 | STATE_PUBLISHED
39 | STATE_UPDATED
40 | STATE_ADDRESS
41 | )
42 |
43 | func torupdater(cfg Config, rt *Runtime) {
44 | defer rt.wg.Done()
45 |
46 | lg := log.GetLogger("torupdater")
47 |
48 | ticker := time.NewTicker(time.Hour * 6)
49 |
50 | lg.Info("Starting up")
51 |
52 | for {
53 | lg.Info("Checking for updates")
54 |
55 | resp, err := http.Get(cfg.TorUrl)
56 | if err != nil {
57 | lg.Errorf("Unable to get tor list: %s", err.Error())
58 | } else {
59 | // Happy days, we got data
60 |
61 | nodes, err := tokenizeresponse(resp.Body)
62 | if err != nil {
63 | lg.Warnf("Got an error tokenizing updates: %s", err)
64 | }
65 | resp.Body.Close()
66 |
67 | lg.Debugf("Successfully got %d tor nodes", len(nodes))
68 | hash := NewTorHash()
69 | for _, node := range nodes {
70 | hash.Add(node)
71 | }
72 |
73 | lg.Debugf("Successfully built a TorHash with %d entries", hash.Len())
74 |
75 | rt.Geo.tordb = hash
76 | }
77 |
78 | select {
79 | case <-ticker.C:
80 | // Nothing to do here, just loop to the top
81 | case <-rt.stop:
82 | ticker.Stop()
83 | lg.Info("Shutting down")
84 | return
85 | }
86 | }
87 | }
88 |
89 | /**
90 | * Parse a series of entries like this:
91 | *
92 | * ExitNode 47E25A3042414FAA1D934D546FBF9E60E80678E2
93 | * Published 2017-10-25 08:25:17
94 | * LastStatus 2017-10-25 09:03:28
95 | * ExitAddress 80.82.67.166 2017-10-25 09:08:02
96 | *
97 | * This is a straight forward state based parser with the small
98 | * wrinkle that a single node may have 1 _or more_ ExitAddresses,
99 | * so we have to scan until we see the a new ExitNode or we finish.
100 | */
101 | func tokenizeresponse(body io.ReadCloser) ([]*TorNode, error) {
102 | scanner := bufio.NewScanner(body)
103 |
104 | state := STATE_NODE
105 | node := NewTorNode()
106 | ret := make([]*TorNode, 0)
107 |
108 | for scanner.Scan() {
109 | line := scanner.Text()
110 |
111 | switch state {
112 | case STATE_PUBLISHED:
113 | if strings.HasPrefix(line, "Published") {
114 | node.Published = parsetortime(line[10:])
115 | state = STATE_UPDATED
116 | } else {
117 | return nil, errors.New("Published prefix not detected")
118 | }
119 | break
120 | case STATE_UPDATED:
121 | if strings.HasPrefix(line, "LastStatus") {
122 | node.Updated = parsetortime(line[11:])
123 | state = STATE_ADDRESS
124 | } else {
125 | return nil, errors.New("LastStatus prefix not detected")
126 | }
127 | break
128 | case STATE_ADDRESS:
129 | if strings.HasPrefix(line, "ExitAddress") {
130 | parts := strings.SplitAfter(line, " ")
131 | ip := &ExitAddress{IP: strings.Trim(parts[1], " "), Date: parsetortime(fmt.Sprintf("%s%s", parts[2], parts[3]))}
132 | node.Addresses = append(node.Addresses, *ip)
133 | break
134 | } else if strings.HasPrefix(line, "ExitNode") {
135 | ret = append(ret, node)
136 | node = NewTorNode()
137 | state = STATE_NODE
138 | // Fallthrough
139 | } else {
140 | return nil, errors.New("No transtion from address state found")
141 | }
142 | fallthrough
143 | case STATE_NODE:
144 | if strings.HasPrefix(line, "ExitNode") {
145 | node.NodeId = line[9:]
146 | state = STATE_PUBLISHED
147 | } else {
148 | return nil, errors.New("Exit node prefix not detected")
149 | }
150 | break
151 | default:
152 | return nil, errors.New("State error")
153 | }
154 | }
155 | // Handle the case where we got to the end of the file and we have a pending
156 | // node which we haven't put onto the output array yet
157 | if state == STATE_ADDRESS {
158 | ret = append(ret, node)
159 | } else {
160 | // We didn't get a complete node at the end, which is still an error
161 | return nil, errors.New("Ended in an incorrect state")
162 | }
163 | return ret, nil
164 | }
165 |
166 | func parsetortime(t string) *time.Time {
167 | // 2017-05-06 10:02:47
168 | timestamp, err := time.Parse("2006-01-02 15:04:05", t)
169 | if err != nil {
170 | return nil
171 | }
172 | return ×tamp
173 | }
174 |
--------------------------------------------------------------------------------
/responder/blacklist/blacklist.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * blacklist.go: DNS Blacklist lookups
21 | */
22 |
23 | package blacklist
24 |
25 | import (
26 | "bytes"
27 | "fmt"
28 | "net"
29 | "strconv"
30 | "strings"
31 | "sync"
32 | "time"
33 |
34 | "github.com/tenta-browser/tenta-dns/common"
35 | "github.com/tenta-browser/tenta-dns/runtime"
36 |
37 | "github.com/leonelquinteros/gorand"
38 | "github.com/miekg/dns"
39 | "github.com/syndtr/goleveldb/leveldb"
40 | )
41 |
42 | // Enforcer enforces blacklists.
43 | type Enforcer struct {
44 | rt *runtime.Runtime
45 | cfg runtime.NSnitchConfig
46 | dns *dns.Client
47 | }
48 |
49 | // New returns an initialized blacklist Enforcer.
50 | func New(rt *runtime.Runtime, cfg runtime.NSnitchConfig) *Enforcer {
51 | return &Enforcer{
52 | rt: rt,
53 | cfg: cfg,
54 | dns: &dns.Client{
55 | DialTimeout: 4 * time.Second,
56 | ReadTimeout: 4 * time.Second,
57 | WriteTimeout: time.Second,
58 | },
59 | }
60 | }
61 |
62 | type dnscheckresponse struct {
63 | list string
64 | isError bool
65 | isFound bool
66 | }
67 |
68 | // Check checks an IP address against registered blacklists.
69 | func (e *Enforcer) Check(ip net.IP) *common.BlacklistData {
70 | // TODO: Add deadline for lookups
71 | ret := &common.BlacklistData{Results: make(map[string]bool)}
72 | responses := make(chan *dnscheckresponse)
73 | wg := &sync.WaitGroup{}
74 | wg.Add(len(e.cfg.Blacklists))
75 | for _, rbl := range e.cfg.Blacklists {
76 | go e.lookup(ip, rbl, responses)
77 | }
78 | go func() {
79 | wg.Wait()
80 | close(responses)
81 | }()
82 | for resp := range responses {
83 | wg.Done()
84 | if resp.isError {
85 | continue
86 | }
87 | ret.Results[resp.list] = resp.isFound
88 | ret.NumberOfLists++
89 | if resp.isFound {
90 | ret.NumberFound++
91 | }
92 | }
93 | if ret.NumberFound > 0 {
94 | ret.OnBlacklist = true
95 | }
96 | return ret
97 | }
98 |
99 | func ipToURL(ip net.IP, bl string) string {
100 | // TODO: Add support for IPv6.
101 | ipparts := strings.Split(ip.String(), ".")
102 | return fmt.Sprintf("%s.%s.%s.%s.%s.", ipparts[3], ipparts[2], ipparts[1], ipparts[0], bl)
103 | }
104 |
105 | type cacheValue int
106 |
107 | func (c cacheValue) bytes() []byte {
108 | return []byte(strconv.Itoa(int(c)))
109 | }
110 |
111 | const (
112 | cvFalse cacheValue = iota
113 | cvTrue
114 | cvError
115 | )
116 |
117 | func (e *Enforcer) lookup(ip net.IP, rbl string, responder chan<- *dnscheckresponse) {
118 | url := ipToURL(ip, rbl)
119 | cachekey := []byte(fmt.Sprintf("rbl/%s", url))
120 |
121 | ret := &dnscheckresponse{list: rbl}
122 | val, dberr := e.rt.DB.Get(cachekey, nil)
123 | e.rt.Stats.Tick("database", "get")
124 | if dberr == nil {
125 | //fmt.Printf("Blacklist: Loaded %s from cache\n", url)
126 | if bytes.Equal(val, cvTrue.bytes()) {
127 | ret.isFound = true
128 | } else if bytes.Equal(val, cvError.bytes()) {
129 | ret.isError = true
130 | }
131 | responder <- ret
132 | return
133 | }
134 | if dberr != leveldb.ErrNotFound {
135 | e.rt.Stats.Tick("database", "get_error")
136 | }
137 |
138 | //fmt.Printf("Blacklist: Doing lookup on %s\n", url)
139 | m := new(dns.Msg)
140 | m.SetQuestion(url, dns.TypeA)
141 | in, _, dnserr := e.dns.Exchange(m, "8.8.8.8:53")
142 | if dnserr != nil {
143 | //fmt.Printf("Blacklist: Error performing lookup on %s: %v\n", url, dnserr)
144 | ret.isError = true
145 | } else {
146 | //fmt.Printf("Blacklist: Performed lookup in %d time\n", rtt)
147 | if in.Rcode == dns.RcodeNameError {
148 | //fmt.Println("Blacklist: No record found")
149 | ret.isFound = false
150 | } else {
151 | //fmt.Println("Blacklist: Record found")
152 | ret.isFound = true
153 | }
154 | }
155 | trans, dberr := e.rt.DB.OpenTransaction()
156 | if dberr != nil {
157 | responder <- ret
158 | return
159 | }
160 | e.writeCache(trans, cachekey, ret)
161 | }
162 |
163 | func (e *Enforcer) writeCache(trans *leveldb.Transaction, k []byte, resp *dnscheckresponse) {
164 | var v cacheValue
165 | switch {
166 | case resp.isError:
167 | v = cvError
168 | case resp.isFound:
169 | v = cvTrue
170 | default:
171 | v = cvFalse
172 | }
173 | trans.Put(k, v.bytes(), nil)
174 | key := append([]byte("blacklist/"), []byte(strconv.FormatInt(time.Now().Unix(), 10))...)
175 | key = append(key, []byte("/")...)
176 | uuid, _ := gorand.UUIDv4()
177 | key = append(key, []byte(uuid[:])...)
178 | trans.Put(key, k, nil)
179 | if err := trans.Commit(); err != nil {
180 | e.rt.Stats.TickN("database", "put_error", 2)
181 | trans.Discard()
182 | }
183 | e.rt.Stats.TickN("database", "put", 2)
184 | }
185 |
--------------------------------------------------------------------------------
/responder/authoritative_dns.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * authoritative_dns.go: Authoritative DNS server responder implementation
21 | */
22 |
23 | package responder
24 |
25 | import (
26 | "crypto/tls"
27 | "fmt"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "github.com/tenta-browser/tenta-dns/runtime"
30 | "github.com/tenta-browser/tenta-dns/zones"
31 |
32 | "github.com/miekg/dns"
33 | "github.com/sirupsen/logrus"
34 | "github.com/tenta-browser/tenta-dns/common"
35 | )
36 |
37 | func AuthoritativeDNSServer(cfg runtime.AuthorityConfig, rt *runtime.Runtime, v4 bool, net string, d *runtime.ServerDomain) {
38 | serveAuthoritativeDNS(cfg, rt, v4, net, d)
39 | }
40 |
41 | func serveAuthoritativeDNS(cfg runtime.AuthorityConfig, rt *runtime.Runtime, v4 bool, net string, d *runtime.ServerDomain) {
42 | ip, port := hostInfo(v4, net, d)
43 | addr := fmt.Sprintf("%s:%d", ip, port)
44 | lg := log.GetLogger("dnsauthority").WithField("host_name", d.HostName).WithField("address", ip).WithField("port", port).WithField("proto", net)
45 | notifyStarted := func() {
46 | lg.Infof("Started %s dns on %s", net, addr)
47 | }
48 | lg.Debugf("Preparing %s dns on %s", net, addr)
49 |
50 | pchan := make(chan interface{}, 1)
51 | srv := &dns.Server{Addr: addr, Net: net, NotifyStartedFunc: notifyStarted, Handler: dns.HandlerFunc(dnsRecoverWrap(handleAuthoritative(rt, *cfg.Zones, lg), pchan))}
52 |
53 | defer rt.OnFinishedOrPanic(func() {
54 | srv.Shutdown()
55 | lg.Infof("Stopped %s dns on %s", net, addr)
56 | }, pchan)
57 |
58 | if net == "tls" {
59 | go func() {
60 | cert, err := tls.LoadX509KeyPair(d.CertFile, d.KeyFile)
61 | if err != nil {
62 | lg.Warnf("Failed to setup %s dns server on %s for %s: %s", net, addr, d.HostName, err.Error())
63 | return
64 | }
65 |
66 | tlscfg := common.TLSConfigDNS()
67 | tlscfg.Certificates = []tls.Certificate{cert}
68 |
69 | srv.Net = "tcp-tls"
70 | srv.TLSConfig = tlscfg
71 |
72 | if err := srv.ListenAndServe(); err != nil {
73 | lg.Warnf("Failed to setup %s dns server on %s for %s: %s", net, addr, d.HostName, err.Error())
74 | }
75 | }()
76 | } else {
77 | go func() {
78 | if err := srv.ListenAndServe(); err != nil {
79 | lg.Warnf("Problem while serving DNS: %s", err.Error())
80 | }
81 | }()
82 | }
83 | }
84 |
85 | func handleAuthoritative(rt *runtime.Runtime, z zones.ZoneSet, lg *logrus.Entry) dnsHandler {
86 | return func(w dns.ResponseWriter, r *dns.Msg) {
87 |
88 | // Set up the response object
89 | m := new(dns.Msg)
90 | m.SetReply(r)
91 | m.MsgHdr.Authoritative = true
92 | m.MsgHdr.RecursionAvailable = false
93 |
94 | rt.Stats.Count("dns:queries:all")
95 | rt.Stats.Count("dns:queries:authoritative")
96 | rt.Stats.Tick("dns", "queries:all")
97 | rt.Stats.Tick("dns", "queries:authoritative")
98 |
99 | if len(r.Question) < 1 {
100 | lg.Warnf("No question set")
101 | rt.Stats.Count("dns:queries:error:no_question")
102 | m.SetRcode(r, dns.RcodeRefused)
103 | w.WriteMsg(m)
104 | return
105 | }
106 |
107 | q := r.Question[0]
108 |
109 | if q.Qclass != dns.ClassINET {
110 | lg.Warnf("Got a non INET class type: %s", dns.ClassToString[q.Qclass])
111 | m.SetRcode(r, dns.RcodeRefused)
112 | w.WriteMsg(m)
113 | return
114 | }
115 |
116 | switch q.Qtype {
117 | case dns.TypeA:
118 | fallthrough
119 | case dns.TypeAAAA:
120 | fallthrough
121 | case dns.TypeMX:
122 | fallthrough
123 | case dns.TypeTXT:
124 | if !checkNameAndQtype(q, z, m, r, w, lg) {
125 | return
126 | }
127 | addAnswers(q.Name, q.Qtype, z, m, lg)
128 | break
129 | case dns.TypeANY:
130 | if !checkName(q, z, m, r, w, lg) {
131 | return
132 | }
133 | for _, t := range []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeMX, dns.TypeTXT, dns.TypeNS} {
134 | if _, ok := z[q.Name][t]; ok {
135 | addAnswers(q.Name, t, z, m, lg)
136 | }
137 | }
138 | break
139 | default:
140 | lg.Debugf("Unknoown QType %s", dns.TypeToString[q.Qtype])
141 | m.SetRcode(r, dns.RcodeRefused)
142 | w.WriteMsg(m)
143 | return
144 | }
145 |
146 | m.SetRcode(r, dns.RcodeSuccess)
147 | w.WriteMsg(m)
148 | }
149 | }
150 |
151 | func addAnswers(name string, t uint16, z zones.ZoneSet, m *dns.Msg, lg *logrus.Entry) {
152 | for _, item := range z[name][t] {
153 | switch item.Kind {
154 | case zones.ZoneEntryTypeRR:
155 | m.Answer = append(m.Answer, *item.RR)
156 | default:
157 | lg.Debugf("Skipping unhandled zone entry type")
158 | }
159 | }
160 | }
161 |
162 | func checkName(q dns.Question, z zones.ZoneSet, m *dns.Msg, r *dns.Msg, w dns.ResponseWriter, lg *logrus.Entry) bool {
163 | if _, ok := z[q.Name]; !ok {
164 | lg.Debugf("Name %s not found", q.Name)
165 | m.SetRcode(r, dns.RcodeNameError)
166 | w.WriteMsg(m)
167 | return false
168 | }
169 | return true
170 | }
171 | func checkNameAndQtype(q dns.Question, z zones.ZoneSet, m *dns.Msg, r *dns.Msg, w dns.ResponseWriter, lg *logrus.Entry) bool {
172 | if !checkName(q, z, m, r, w, lg) {
173 | return false
174 | }
175 | if _, ok := z[q.Name][q.Qtype]; !ok {
176 | lg.Debugf("No entries of type %s found for %s", dns.TypeToString[q.Qtype], q.Name)
177 | m.SetRcode(r, dns.RcodeNameError)
178 | w.WriteMsg(m)
179 | return false
180 | }
181 | return true
182 | }
183 |
--------------------------------------------------------------------------------
/responder/snitch_http.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * http.go: HTTP Server
21 | */
22 |
23 | package responder
24 |
25 | import (
26 | "fmt"
27 | "net/http"
28 | "time"
29 |
30 | "github.com/tenta-browser/tenta-dns/log"
31 | handlers "github.com/tenta-browser/tenta-dns/responder/http-handlers"
32 | "github.com/tenta-browser/tenta-dns/runtime"
33 |
34 | "encoding/base64"
35 |
36 | "github.com/gorilla/mux"
37 | "github.com/sirupsen/logrus"
38 | "github.com/tenta-browser/tenta-dns/common"
39 | )
40 |
41 | func SnitchHTTPServer(cfg runtime.NSnitchConfig, rt *runtime.Runtime, v4 bool, net string, d *runtime.ServerDomain) {
42 | var ip string
43 | if v4 {
44 | ip = d.IPv4
45 | } else {
46 | ip = fmt.Sprintf("[%s]", d.IPv6)
47 | }
48 | var port int
49 | if net == "http" {
50 | if d.HttpPort <= runtime.PORT_UNSET {
51 | panic("Unable to start a HTTP nsnitch without a valid port")
52 | }
53 | port = d.HttpPort
54 | } else if net == "https" {
55 | if d.HttpsPort <= runtime.PORT_UNSET {
56 | panic("Unable to start a HTTPS nsnitch without a valid port")
57 | }
58 | port = d.HttpsPort
59 | } else {
60 | log.GetLogger("httpsnitch").Warnf("Unknown HTTP net type %s", net)
61 | return
62 | }
63 |
64 | lg := log.GetLogger("httpsnitch").WithField("address", ip).WithField("proto", "http").WithField("port", port).WithField("host_name", d.HostName)
65 |
66 | pchan := make(chan interface{}, 1)
67 | router := mux.NewRouter()
68 | router.NotFoundHandler = http.HandlerFunc(httpPanicWrap(handlers.HandleHTTPDefault(cfg, rt, lg), pchan))
69 | router.HandleFunc("/api/v1/status", httpPanicWrap(handlers.HandleHTTPStatus(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
70 | router.HandleFunc("/api/v1/report", httpPanicWrap(handlers.HandleHTTPReport(cfg, rt, d, lg), pchan)).Methods("GET").Host(fmt.Sprintf("{subdomain:[A-Za-z0-9\\-_]+}.%s", d.HostName))
71 | router.HandleFunc("/api/v1/randomizer", httpPanicWrap(handlers.HandleHTTPRedirector(cfg, rt, d, lg), pchan)).Methods("GET").Host(d.HostName)
72 | router.HandleFunc("/api/v1/geolookup", httpPanicWrap(handlers.HandleHTTPGeoLookup(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
73 | router.HandleFunc("/api/v1/geolookup/{foreign_ip}", httpPanicWrap(handlers.HandleHTTPGeoLookup(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
74 | router.HandleFunc("/api/v1/blacklist", httpPanicWrap(handlers.HandleHTTPBLLookup(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
75 | router.HandleFunc("/api/v1/blacklist/{foreign_ip}", httpPanicWrap(handlers.HandleHTTPBLLookup(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
76 | router.HandleFunc("/api/v1/stats", httpPanicWrap(handlers.HandleHTTPStatsLookup(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
77 | router.HandleFunc("/speedtest/{size_exp:[0-9]+}", httpPanicWrap(handlers.HandleHTTPSpeedtest(cfg, rt, lg), pchan)).Methods("GET").Host(d.HostName)
78 | for _, wk := range cfg.WellKnowns {
79 | var b []byte
80 | if wk.Base64 {
81 | var err error
82 | b, err = base64.StdEncoding.DecodeString(wk.Body)
83 | if err != nil {
84 | lg.Warnf("Unable to decode body from well known %s: %s", wk.Path, err)
85 | continue
86 | }
87 | } else {
88 | b = []byte(wk.Body)
89 | }
90 | lg.Infof("Installing well known %s", wk.Path)
91 | router.HandleFunc(fmt.Sprintf("/.well-known/%s", wk.Path), httpPanicWrap(handlers.HandleHTTPWellKnown(cfg, rt, lg, wk.Path, b, wk.MimeType), pchan)).Methods("GET").Host(d.HostName)
92 | }
93 |
94 | if net == "http" {
95 | serveHTTP(rt, d, ip, port, router, lg, pchan)
96 | } else {
97 | serveHTTPS(rt, d, ip, port, router, lg, pchan)
98 | }
99 | }
100 |
101 | func httpPanicWrap(hndl func(w http.ResponseWriter, r *http.Request), notify chan interface{}) func(w http.ResponseWriter, r *http.Request) {
102 | return func(w http.ResponseWriter, r *http.Request) {
103 | defer func() {
104 | if rcv := recover(); rcv != nil {
105 | notify <- rcv
106 | }
107 | }()
108 | hndl(w, r)
109 | }
110 | }
111 |
112 | func serveHTTP(rt *runtime.Runtime, d *runtime.ServerDomain, ip string, port int, handler http.Handler, lg *logrus.Entry, pchan chan interface{}) {
113 |
114 | addr := fmt.Sprintf("%s:%d", ip, port)
115 |
116 | srv := &http.Server{
117 | Addr: addr,
118 | WriteTimeout: 59 * time.Second,
119 | ReadTimeout: 20 * time.Second,
120 | Handler: handler,
121 | }
122 | defer rt.OnFinishedOrPanic(func() {
123 | srv.Shutdown(nil)
124 | lg.Infof("Shutdown HTTP server for %s", d.HostName)
125 | }, pchan)
126 | lg.Info("Started listening for HTTP")
127 |
128 | go func() {
129 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
130 | lg.Warnf("Failed to setup HTTP server: %s", err.Error())
131 | }
132 | }()
133 | }
134 |
135 | func serveHTTPS(rt *runtime.Runtime, d *runtime.ServerDomain, ip string, port int, handler http.Handler, lg *logrus.Entry, pchan chan interface{}) {
136 |
137 | addr := fmt.Sprintf("%s:%d", ip, port)
138 |
139 | srv := &http.Server{
140 | Addr: addr,
141 | WriteTimeout: 59 * time.Second,
142 | ReadTimeout: 20 * time.Second,
143 | Handler: handler,
144 | }
145 | defer rt.OnFinishedOrPanic(func() {
146 | srv.Shutdown(nil)
147 | lg.Info("Shutdown HTTPS server")
148 | }, pchan)
149 |
150 | srv.TLSConfig = common.TLSConfigLegacyHTTPS()
151 |
152 | lg.Info("Started listening for HTTPS")
153 |
154 | go func() {
155 | if err := srv.ListenAndServeTLS(d.CertFile, d.KeyFile); err != nil && err != http.ErrServerClosed {
156 | lg.Warnf("Failed to setup HTTPS server: %s", err.Error())
157 | }
158 | }()
159 | }
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Tenta DNS
2 | =========
3 |
4 | [](https://travis-ci.org/tenta-browser/tenta-dns)
5 | [](https://goreportcard.com/report/github.com/tenta-browser/tenta-dns)
6 | [](https://godoc.org/github.com/tenta-browser/tenta-dns)
7 |
8 | 
9 |
10 | A full-fledged DNS solution, including DNSSEC and DNS-over-TLS
11 |
12 | Tenta DNS provides a DNS server suite comprising an authoritative DNS server, recursive DNS server, and NSnitch,
13 | which provides a DNS server capable of recording the IP address of requests made against
14 | it and then makes that IP available via a JSON API. Tenta DNS also provides lookups for
15 | Tor Node membership, DNS blacklist status and Geo data. Finally, Tenta DNS includes built-in
16 | BGP integration, offering single engine convenience for DNS anycasting. We welcome people to
17 | use our hosted versions of recursive resolver and NSnitch. Please see `Usage`,
18 | for details on how to set Tenta DNS as your default DNS resolver, or `APIs`,
19 | for NSnitch REST API information.
20 |
21 | Contact: developer@tenta.io
22 |
23 | Usage
24 | =====
25 |
26 | Just want to use our hosted recursive resolver? We offer two options, using either [OpenNIC](https://opennic.org)
27 | root servers or the normal ICANN root servers.
28 |
29 | Our OpenNIC nameservers are at `99.192.182.100` and `99.192.182.101`
30 |
31 | ICANN nameservers are at `99.192.182.200` and `99.192.182.201`
32 |
33 | Please consult our [how-to page](https://tenta.com/dns-setup-guides), on setting up your DNS resolver.
34 |
35 | Installation
36 | ============
37 |
38 | 1. Run `install-deps.sh` (or `install-deps.bat` on windows).
39 | 1. Run `build.sh` or (or `build.bat` on windows).
40 | 1. Modify `etc/config.toml` and `etc\conf.d\*.toml` for your installation.
41 | 1. 🙈🙉🙊
42 |
43 | REST APIs
44 | =========
45 |
46 | We'd be thrilled for people to use our APIs as part of your app or system. In order to use our hosted API, please provide
47 | a link to https://tenta.com/ with the text "Powered by Tenta" or similar. If you need to perform arbitrary lookups (e.g.
48 | you want information for an IP different than the requesting IP, like from a server), message us for an API key. If
49 | you need CORS whitelisted for the public APIs, please email us with your domain name(s).
50 |
51 | All APIs under the path `/api/v1`.
52 |
53 | * `status`: Public status checking endpoint for basic liveness monitoring
54 | * `report`: Generate a report from a specific DNS lookup. Only works on subdomains, explicity looked up via DNS already.
55 | * `randomizer`: Generate (and optionally redirect to) a random subdomain. Set `?api_response=true` to get a JSON result
56 | instead of a redirect.
57 | * `geolookup`: GeoIP info about the requesting IP.
58 | * `geolookup/{IP}`: GeoIP info about the specified IP address. Requires auth.
59 | * `blacklist`: Perform DNS blacklist lookup for the requesting IP.
60 | * `blacklist/{IP}`: DNS blacklist info for the specified IP address. Requires auth.
61 | * `stats`: Work in Progress. Server performance information.
62 |
63 | Explanation of NSnitch DNS Probe
64 | ================================
65 |
66 | In addition to the REST APIs, core functionality relies upon DNS lookups. After creating glue records pointing
67 | `ns1.nstoro.com` and `ns2.nstoro.com` to the IP(s) of a Tenta DNS server.
68 |
69 | 1. From javascript, load nstoro.com/api/v1/randomizer, it will redirect to abc123.nstoro.com/api/v1/report (where abc123 is a big random)
70 | 1. Since the domain name is not cached (since it's totally random), the browser initiates a DNS lookup
71 | 1. Since the intermediate resolver cannot have it cached, it too initiates a DNS lookup
72 | 1. When nsnitch gets the lookup, it returns a valid answer for the domain name, and stores the IP that contacted it along with details
73 | 1. When the browser actually makes the request, the stored data is sent back
74 | 1. Data automatically expires after 5 minutes
75 |
76 | External Dependencies
77 | =====================
78 |
79 | We rely on lots of excellent open source libraries, including [miekg/dns](https://github.com/miekg/dns) and
80 | [osrg/gobgp](https://github.com/osrg/gobgp), as well as many others. For a complete list of our dependencies and required notification,
81 | please take a look at [NOTICES.md](NOTICES.md)
82 |
83 | The `words.txt` file used for random names in NSnitch is from [dreamsteep.com](http://diginoodles.com/The_English_Open_Word_List_%28EOWL%29).
84 |
85 | License
86 | =======
87 |
88 | Licensed under the Apache License, Version 2.0 (the "License");
89 | you may not use this file except in compliance with the License.
90 | You may obtain a copy of the License at
91 |
92 | http://www.apache.org/licenses/LICENSE-2.0
93 |
94 | Unless required by applicable law or agreed to in writing, software
95 | distributed under the License is distributed on an "AS IS" BASIS,
96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
97 | See the License for the specific language governing permissions and
98 | limitations under the License.
99 |
100 | Please see [LICENSE](LICENSE) for more. For any questions, please contact developer@tenta.io
101 |
102 | Credits
103 | =======
104 |
105 | You're welcome to use the hosted version of our JSON APIs free on your site. We kindly ask that in return you show us some link love to https://tenta.com. We’d love to know how you’re using it, so do let us know!
106 |
107 | Contributing
108 | ============
109 |
110 | We welcome contributions, feedback and plain old complaining. Feel free to open
111 | an issue or shoot us a message to developer@tenta.io. If you'd like to contribute,
112 | please open a pull request and send us an email to sign a contributor agreement.
113 |
114 | About Tenta
115 | ===========
116 |
117 | Tenta DNS is brought to you by Team Tenta. Tenta is your [private, encrypted browser](https://tenta.com) that protects your data instead of selling it. We're building a next-generation browser that combines all the privacy tools you need, including built-in OpenVPN. Everything is encrypted by default. That means your bookmarks, saved tabs, web history, web traffic, downloaded files, IP address and DNS. A truly incognito browser that's fast and easy.
118 |
--------------------------------------------------------------------------------
/runtime/runtime.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * runtime.go: Core runtime
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "encoding/binary"
27 | "os"
28 | "sync"
29 | "time"
30 |
31 | "github.com/tenta-browser/tenta-dns/log"
32 |
33 | "github.com/sirupsen/logrus"
34 | "github.com/syndtr/goleveldb/leveldb"
35 | "github.com/syndtr/goleveldb/leveldb/errors"
36 | )
37 |
38 | const KEY_START_TIME = "start-time"
39 | const KEY_GEODB_UPDATED = "geodb-updated-time"
40 |
41 | const (
42 | SPEEDTEST_MAX_FILESIZE_EXPONENT = 16 /// 2^exponent will be the resulting filesize for speedtest (basically, we have 1, 2, 4 ... 1024)
43 | SPEEDTEST_ONE_UNIT = "MTen9Qfgh66JKZTwVsPKwaHqYM1mU5SW7cANEa6OG9RSGWsa1N327O7K3lDXQ8wLcjFV5HtoOSmKQKmj0rycBExMPW4Sni9AI7gyb8pwI7OoC9KXyRsKo9iFDB3OdhmraOHmrD9vPQo2FwuR4xC8NM5twOhQfWVysh0gIvlP9TtkspyUQBxHKoCSsI5NZavwXKG1BYLopvczB0vxoQna45AaUZpAKKigoPQOo5pnghxaGYpKAYvKCNYKYCE6Sm8s2irGTmkZTu47ayNzZ1euBkWcuQJ2y6vjoIpKOC2kPpqFo1j79Hm1v8ppz2lwV9gw1OWxhkRVgQc2yEm3KCutYhywTjVUdWoXq5LfeI487WUa4EUVkcmX193VBT1ZzeuVsiQU7yZfGwIDlB5DiwVBDMba1uP9Qyf8FB0O1YsHXpawhiu03UAxgrnojHSGYWWDHBNKhiqvBGxxWfRutP77m0uczZtxzvWYML2zR6VRQ55fOb1MLqrfRPakcZknHAMbe6rflE0XuubjC3PgZvs2BAwPX5MtrVDXxEnBgot4MrTE0ka78Aj1FRTaOvYwEFKFoPsfYitxWvYc13zGAs6XTRjykwdN3SkmloLtHnJ6H996m2VjH9RXof5ta3FkmSddNLxPrXeQNhKKSgFIS5Kb0JTFr3sFapRGQpgT4qIpqbsEbeQfVbBkqVPmy3gUlfDkVlK3FQ5CuVDuY20cpORKCbPzReCBtxu27gKJokrSFJXFQ7ZLh9K3jbMgavtpGS65UTJmK4TZsWKQrzSMsyU6GHIJ9Bjw7Ak5Slb1tpOOrVOmIXnksBIGtFEw9WopK4jx4Rx5GGb3wPajHNI0RCioA2ShkdXDiKAyzNaHI6izdfyRHYHD7Jjjud72EE8Q3Em7XRICDVNVNuKyohPy4hwu32TrRwsGOoEQuiJr8Oeq6YEq5MWS3dxvR00CSIxTQhMTdsBrZdDhA4bmpRUYSb9CWmy3Djwszu8VC6iKxKzH1sX2d1vyyHggXfFyQAhcxz5j"
44 | )
45 |
46 | //noinspection GoNameStartsWithPackageName
47 | type Runtime struct {
48 | wg *sync.WaitGroup
49 | DB *leveldb.DB
50 | Geo *Geo
51 | Stats *Stats
52 | IPPool *Pool
53 | stop chan bool
54 | started uint
55 | lg *logrus.Entry
56 | RateLimiter *Limiter
57 | SlackWH *Feedback
58 | Cache *DNSCacheHolder
59 | SpeedTestFiles map[int]string
60 | }
61 |
62 | type finisher func()
63 |
64 | type FailureNotifier func()
65 |
66 | func NewRuntime(cfg Config) *Runtime {
67 | rt := new(Runtime)
68 | var wg sync.WaitGroup
69 | rt.wg = &wg
70 | rt.stop = make(chan bool, 4)
71 | rt.lg = log.GetLogger("runtime")
72 |
73 | if len(cfg.GeoDBPath) > 0 {
74 | if stat, err := os.Stat(cfg.GeoDBPath); err != nil || !stat.IsDir() {
75 | rt.lg.Errorf("Unable to open geo database path %s", cfg.GeoDBPath)
76 | panic(err)
77 | }
78 | }
79 |
80 | db, err := leveldb.OpenFile(cfg.DatabasePath, nil)
81 | if err != nil {
82 | rt.lg.Errorf("Unable to open database %s: %s", cfg.DatabasePath, err.Error())
83 | panic(err)
84 | }
85 |
86 | rt.DB = db
87 | rt.lg.Debugf("Using database %s", cfg.DatabasePath)
88 |
89 | startTimeBytes := make([]byte, 8)
90 | binary.LittleEndian.PutUint64(startTimeBytes, uint64(time.Now().Unix()))
91 | if err = rt.DB.Put([]byte(KEY_START_TIME), startTimeBytes, nil); err != nil {
92 | rt.lg.Errorf("Error: Unable to write to DB: %s", err.Error())
93 | panic(err)
94 | }
95 |
96 | rt.IPPool = StartIPPool(cfg.OutboundIPs)
97 | rt.SlackWH = StartFeedback(cfg, rt)
98 | rt.Stats = StartStats(rt)
99 | rt.RateLimiter = StartLimiter(cfg.RateThreshold)
100 | rt.Cache = StartCache(rt.lg, CACHE_OPENNIC, CACHE_IANA)
101 | rt.AddService()
102 | go garbageman(cfg, rt)
103 |
104 | if len(cfg.GeoDBPath) > 0 {
105 | rt.Geo = StartGeo(cfg, rt, false)
106 |
107 | rt.AddService()
108 | go geoupdater(cfg, rt)
109 |
110 | rt.AddService()
111 | go torupdater(cfg, rt)
112 | } else {
113 | rt.Geo = StartGeo(cfg, rt, true)
114 |
115 | rt.lg.Debug("Not starting geo service, as it's not configured to run")
116 | }
117 |
118 | /// adding speedtest dummy files
119 | rt.SpeedTestFiles = map[int]string{0: SPEEDTEST_ONE_UNIT}
120 |
121 | for i := 1; i <= SPEEDTEST_MAX_FILESIZE_EXPONENT; i++ {
122 | nextElement := SPEEDTEST_ONE_UNIT
123 | for j := i - 1; j >= 0; j-- {
124 | nextElement += rt.SpeedTestFiles[j]
125 | }
126 | rt.SpeedTestFiles[i] = nextElement
127 | }
128 |
129 | return rt
130 | }
131 |
132 | func (rt *Runtime) AddService() {
133 | rt.wg.Add(1)
134 | rt.started += 1
135 | }
136 |
137 | func (rt *Runtime) OnFinished(fn finisher) {
138 | defer rt.wg.Done()
139 | <-rt.stop
140 | if fn != nil {
141 | fn()
142 | }
143 | }
144 |
145 | func (rt *Runtime) OnFinishedOrPanic(fn finisher, pchan chan interface{}) {
146 | defer rt.wg.Done()
147 | var rcv *interface{}
148 | select {
149 | case <-rt.stop:
150 | break
151 | case r := <-pchan:
152 | rcv = &r
153 | rt.started -= 1 // We won't be shutting it down later, so let it go
154 | break
155 | }
156 | if fn != nil {
157 | fn()
158 | }
159 | if rcv != nil {
160 | panic(*rcv)
161 | }
162 | }
163 |
164 | func (rt *Runtime) Shutdown() {
165 | rt.lg.Info("Shutting down")
166 | for i := uint(0); i < rt.started; i += 1 {
167 | rt.stop <- true
168 | }
169 | rt.wg.Wait()
170 | rt.SlackWH.Stop()
171 | rt.Stats.Stop()
172 | rt.Cache.Stop()
173 | rt.DB.Close()
174 | rt.lg.Info("Shutdown complete")
175 | }
176 |
177 | func (rt *Runtime) DBGet(key []byte) (value []byte, err error) {
178 | rt.Stats.Tick("database", "get")
179 | value, err = rt.DB.Get(key, nil)
180 | if err != nil && err != errors.ErrNotFound {
181 | rt.Stats.Tick("database", "get_error")
182 | }
183 | return value, err
184 | }
185 |
186 | func (rt *Runtime) DBPut(key, value []byte) (err error) {
187 | rt.Stats.Tick("database", "put")
188 | err = rt.DB.Put(key, value, nil)
189 | if err != nil {
190 | rt.Stats.Tick("database", "put_error")
191 | }
192 | return err
193 | }
194 |
--------------------------------------------------------------------------------
/stresser/stresser.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "encoding/csv"
7 | "flag"
8 | "fmt"
9 | "io"
10 | "io/ioutil"
11 | "math/rand"
12 | "net"
13 | "net/http"
14 | "os"
15 | "runtime"
16 | "strconv"
17 | "strings"
18 | "sync"
19 | "time"
20 |
21 | "github.com/miekg/dns"
22 | "github.com/sirupsen/logrus"
23 | "github.com/tenta-browser/tenta-dns/log"
24 | )
25 |
26 | var (
27 | ip = flag.String("ip", "127.0.0.1", "IP address to stress")
28 | port = flag.Uint("port", 53, "Port to test")
29 | tcp = flag.Bool("tcp", false, "Whether to use TCP mode")
30 | tls = flag.Bool("tls", false, "Whether to use TLS (implies -tcp)")
31 | workers = flag.Uint("workers", 0, "Number of simultaneous test workers to run (0 => autoselect)")
32 | quiet = flag.Bool("quiet", false, "Don't produce any output to the terminal")
33 | verbose = flag.Bool("verbose", false, "Produce lots of output to the terminal (overrides the -quiet flag)")
34 | limit = flag.Uint("limit", 1000, "How many domain names to use in the test (max 1000000)")
35 | errfile = flag.String("errfile", "", "If specified, write errors to this file")
36 | printver = flag.Bool("version", false, "Print the version and exit")
37 | )
38 |
39 | var version string
40 |
41 | const (
42 | maxLimit = 1000000
43 | alexaDir = "https://s3.amazonaws.com/alexa-static"
44 | alexaFile = "top-1m.csv.zip"
45 | )
46 |
47 | func usage() {
48 | fmt.Println("Stress tester for Tenta DNS\n" +
49 | "Options:")
50 | flag.PrintDefaults()
51 | }
52 |
53 | func main() {
54 | log.SetLogLevel(logrus.InfoLevel)
55 | flag.Usage = usage
56 | flag.Parse()
57 |
58 | if *printver {
59 | fmt.Println(version)
60 | os.Exit(0)
61 | }
62 | if *limit > maxLimit {
63 | fmt.Printf("limit must be < %d", maxLimit)
64 | os.Exit(1)
65 | }
66 | if *quiet {
67 | log.SetLogLevel(logrus.FatalLevel)
68 | }
69 | if *verbose {
70 | log.SetLogLevel(logrus.DebugLevel)
71 | }
72 | if *workers < 1 {
73 | *workers = uint(runtime.NumCPU())
74 | }
75 |
76 | lg := log.GetLogger("stresser")
77 | start := time.Now()
78 | defer func() {
79 | lg.Infof("Finished in %s", time.Now().Sub(start))
80 | }()
81 | lg.Infof("Starting test with %d workers", *workers)
82 | lg.Infof("Resolving up to %d domain names", *limit)
83 |
84 | resp, err := http.Get(alexaDir + "/" + alexaFile)
85 | if err != nil {
86 | lg.Errorf("Unable to download domain listing: %v", err)
87 | os.Exit(1)
88 | }
89 | defer resp.Body.Close()
90 | b, err := ioutil.ReadAll(resp.Body)
91 | if err != nil {
92 | lg.Errorf("Unable to read domain listing body: %v", err)
93 | os.Exit(1)
94 | }
95 | r, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
96 | if err != nil {
97 | lg.Errorf("Unable to decode domain listing data as zip: %v", err)
98 | os.Exit(1)
99 | }
100 | lg.Debugf("Got zip file")
101 | for _, f := range r.File {
102 | lg.Debugf("Found file %s", f.Name)
103 | if f.Name != strings.TrimSuffix(alexaFile, ".zip") {
104 | continue
105 | }
106 | fr, err := f.Open()
107 | if err != nil {
108 | lg.Errorf("Failure while opening file %s", f.Name)
109 | os.Exit(1)
110 | }
111 | defer fr.Close()
112 | testRecords(csv.NewReader(fr), *workers, lg)
113 | return
114 | }
115 | }
116 |
117 | type result struct {
118 | name string
119 | time time.Duration
120 | err error
121 | }
122 |
123 | type worker struct {
124 | dc *dns.Client
125 | lg *logrus.Entry
126 | wg *sync.WaitGroup
127 | work <-chan string
128 | results chan<- result
129 | }
130 |
131 | func dnsMsg(name string) *dns.Msg {
132 | m := &dns.Msg{
133 | MsgHdr: dns.MsgHdr{
134 | Id: dns.Id(),
135 | RecursionDesired: true,
136 | CheckingDisabled: false,
137 | },
138 | Question: []dns.Question{
139 | {
140 | Name: dns.Fqdn(name),
141 | Qtype: dns.TypeA,
142 | Qclass: dns.ClassINET,
143 | },
144 | },
145 | }
146 | m.SetEdns0(4096, false)
147 | return m
148 | }
149 |
150 | func (w *worker) doWork() {
151 | defer w.wg.Done()
152 | for name := range w.work {
153 | start := time.Now()
154 | in, _, err := w.dc.Exchange(dnsMsg(name), net.JoinHostPort(*ip, strconv.Itoa(int(*port))))
155 | var queryErr error
156 | switch {
157 | case err != nil:
158 | queryErr = fmt.Errorf("Error querying %q: %v", name, err)
159 | case in.Rcode != dns.RcodeSuccess:
160 | queryErr = fmt.Errorf("Got a bad RCode for %q: %s", name, dns.RcodeToString[in.Rcode])
161 | }
162 | r := result{name: name, time: time.Since(start), err: queryErr}
163 | w.results <- r
164 | // Create some skew in the worker pacing.
165 | time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
166 | }
167 | }
168 |
169 | func dnsClient() *dns.Client {
170 | c := &dns.Client{
171 | Net: "udp",
172 | SingleInflight: true,
173 | DialTimeout: time.Second,
174 | WriteTimeout: 3 * time.Second,
175 | ReadTimeout: 30 * time.Second,
176 | }
177 | if *tcp {
178 | c.Net = "tcp"
179 | }
180 | if *tls {
181 | c.Net = "tcp-tls"
182 | }
183 | return c
184 | }
185 |
186 | func testRecords(r *csv.Reader, num uint, lg *logrus.Entry) {
187 | wg := &sync.WaitGroup{}
188 | work := make(chan string, num*50)
189 | rc := make(chan result, num*30)
190 | wg.Add(int(num))
191 | for i := uint(1); i <= num; i++ {
192 | wrk := worker{
193 | wg: wg,
194 | lg: lg,
195 | work: work,
196 | results: rc,
197 | dc: dnsClient(),
198 | }
199 | go wrk.doWork()
200 | }
201 | lg.Debugf("Started %d workers", num)
202 | go func() {
203 | defer lg.Debug("All workers finished")
204 | wg.Wait()
205 | close(rc)
206 | }()
207 | go func() {
208 | defer lg.Debug("Finished dispatching work")
209 | // Feed the work queue up to the user-provided limit.
210 | for i := uint(0); i < *limit; i++ {
211 | rec, err := r.Read()
212 | if err == io.EOF {
213 | lg.Debugf("Reached EOF after %d records", i)
214 | break
215 | }
216 | if err != nil || len(rec) == 0 {
217 | lg.Errorf("Failed to read record")
218 | continue
219 | }
220 | work <- rec[1]
221 | }
222 | close(work)
223 | }()
224 |
225 | results := make(map[string]result)
226 | var total int
227 | errs := []string{}
228 | for res := range rc {
229 | total++
230 | if res.err != nil {
231 | lg.Debugf("%s: failure: %v", res.name, res.err)
232 | errs = append(errs, res.err.Error())
233 | continue
234 | }
235 | results[res.name] = res
236 | }
237 | if *errfile != "" && len(errs) > 0 {
238 | if err := ioutil.WriteFile(*errfile, []byte(strings.Join(errs, "\n")), os.ModePerm); err != nil {
239 | lg.Errorf("Unable to write errors to file %s: %v", *errfile, err)
240 | }
241 | }
242 | success := total - len(errs)
243 | lg.Infof("%d out of %d queries succeeded (%0.02f%%)", success, total, (float64(success)/float64(total))*100)
244 | }
245 |
--------------------------------------------------------------------------------
/runtime/geoupdater.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * geoupdater.go: Geo database updater
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "archive/tar"
27 | "bytes"
28 | "compress/gzip"
29 | "encoding/binary"
30 | "errors"
31 | "fmt"
32 | "github.com/tenta-browser/tenta-dns/log"
33 | "io"
34 | "io/ioutil"
35 | "net/http"
36 | "os"
37 | "path/filepath"
38 | "regexp"
39 | "time"
40 |
41 | "github.com/syndtr/goleveldb/leveldb"
42 | )
43 |
44 | const URL_TEMPLATE = "https://download.maxmind.com/app/geoip_download?edition_id=%s&suffix=%s&license_key=%s"
45 | const DB_TEMPLATE_VERSION = "geo-key-%s"
46 |
47 | func geoupdater(cfg Config, rt *Runtime) {
48 | defer rt.wg.Done()
49 | lg := log.GetLogger("geoupdater")
50 | products := [2]string{"GeoIP2-City", "GeoIP2-ISP"}
51 | lg.Debug("Starting up")
52 | ticker := time.NewTicker(time.Hour)
53 | for {
54 | successful := 0
55 | for _, product := range products {
56 | var err error
57 | var url, dbkey, dbfilename string
58 | var newmd5, oldmd5 []byte
59 | var resp *http.Response
60 | var archive *gzip.Reader
61 | var tr *tar.Reader
62 | lg.Debugf("Checking for updates to %s", product)
63 | url = fmt.Sprintf(URL_TEMPLATE, product, "tar.gz.md5", cfg.MaxmindKey)
64 | lg.Debugf("Checking %s", url)
65 | resp, err = http.Get(url)
66 | if err != nil {
67 | lg.Warnf("Failed fetching %s: %s", url, err.Error())
68 | goto DONE
69 | }
70 | newmd5, err = ioutil.ReadAll(resp.Body)
71 | resp.Body.Close()
72 | if err != nil {
73 | lg.Warnf("Failed reading %s: %s", url, err.Error())
74 | goto DONE
75 | }
76 | if string(newmd5) == "Invalid license key\n" {
77 | lg.Warnf("Invalid license key for %s", product)
78 | err = errors.New("Invalid license key")
79 | goto DONE
80 | }
81 | lg.Debugf("New hash for %s is %s", product, newmd5)
82 | dbkey = fmt.Sprintf(DB_TEMPLATE_VERSION, product)
83 | lg.Debugf("Checking database for %s", dbkey)
84 | oldmd5, err = rt.DBGet([]byte(dbkey))
85 | if err != nil {
86 | if err != leveldb.ErrNotFound {
87 | lg.Warnf("Error reading database: %s", err.Error())
88 | goto DONE
89 | } else {
90 | lg.Debugf("No existing record found in the database for %s", product)
91 | oldmd5 = make([]byte, 0)
92 | err = nil
93 | }
94 | }
95 | dbfilename = filepath.Join(cfg.GeoDBPath, fmt.Sprintf("%s-%s.mmdb", product, newmd5))
96 | if bytes.Compare(oldmd5, newmd5) == 0 {
97 | if _, err := os.Stat(dbfilename); err == nil {
98 | lg.Debugf("Nothing to do, %s is up to date", product)
99 | goto DONE
100 | }
101 | lg.Warnf("Database isn't updated, but %s is missing", dbfilename)
102 | }
103 | lg.Debugf("Need to update the underlying database %s", dbfilename)
104 | url = fmt.Sprintf(URL_TEMPLATE, product, "tar.gz", cfg.MaxmindKey)
105 | lg.Debugf("Fetching from %s", url)
106 | resp, err = http.Get(url)
107 | if err != nil {
108 | lg.Warnf("Failed to download database %s from %s: %s", dbfilename, url, err.Error())
109 | goto DONE
110 | }
111 | archive, err = gzip.NewReader(resp.Body)
112 | if err != nil {
113 | lg.Warnf("Failed to open return data as a gzip file %s: %s", url, err.Error())
114 | resp.Body.Close()
115 | goto DONE
116 | }
117 | tr = tar.NewReader(archive)
118 |
119 | for {
120 | header, innerErr := tr.Next()
121 | if innerErr == io.EOF {
122 | goto DONE
123 | }
124 | if innerErr != nil {
125 | err = innerErr
126 | goto DONE
127 | }
128 |
129 | if matched, _ := regexp.MatchString("^.*mmdb$", header.Name); matched {
130 | lg.Debugf("Found DB File: %s (%d bytes), writing to %s", header.Name, header.Size, dbfilename)
131 |
132 | fhandle, innerErr := os.OpenFile(dbfilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
133 | if innerErr != nil {
134 | lg.Warnf("Error opening geo database file %s for writing: %s", dbfilename, innerErr.Error())
135 | err = innerErr
136 | goto DONE
137 | }
138 |
139 | size, innerErr := io.Copy(fhandle, tr)
140 | if innerErr != nil {
141 | fhandle.Close()
142 | err = innerErr
143 | lg.Warnf("Error writing out geo database file %s: %s", dbfilename, innerErr.Error())
144 | goto DONE
145 | }
146 |
147 | lg.Debugf("Successfully updated %d bytes into %s", size, dbfilename)
148 | fhandle.Close()
149 |
150 | innerErr = rt.DBPut([]byte(dbkey), newmd5)
151 | if innerErr != nil {
152 | err = innerErr
153 | lg.Warnf("Error saving geo file %s version to DB %s: %s", dbfilename, dbkey, innerErr.Error())
154 | goto DONE
155 | }
156 |
157 | if bytes.Compare(oldmd5, newmd5) != 0 {
158 | oldfilename := filepath.Join(cfg.GeoDBPath, fmt.Sprintf("%s-%s.mmdb", product, oldmd5))
159 |
160 | if _, innerErr := os.Stat(oldfilename); innerErr == nil {
161 | lg.Debugf("Removing old geo database file %s", oldfilename)
162 | innerErr = os.Remove(oldfilename)
163 | if innerErr != nil {
164 | lg.Warnf("Error removing old geo database file %s: %s", oldfilename, innerErr.Error())
165 | }
166 | }
167 | }
168 | break
169 | }
170 | }
171 | successful += 1
172 | DONE:
173 | if archive != nil {
174 | archive.Close()
175 | }
176 | if resp != nil && resp.Body != nil {
177 | resp.Body.Close()
178 | }
179 | if err != nil {
180 | lg.Errorf("Failed updating %s: %s", product, err.Error())
181 | }
182 | }
183 | if successful > 0 {
184 | // TODO: Count number of databases desired and determine success based on that, not just any success as it is now
185 | lg.Debugf("Did a successful update, notifying Geo and updating database")
186 | rt.Geo.Reload()
187 | startTimeBytes := make([]byte, 8)
188 | binary.LittleEndian.PutUint64(startTimeBytes, uint64(time.Now().Unix()))
189 | if err := rt.DBPut([]byte(KEY_GEODB_UPDATED), startTimeBytes); err != nil {
190 | lg.Warnf("Unable to write to DB %s: %s", KEY_GEODB_UPDATED, err.Error())
191 | }
192 | }
193 | select {
194 | case <-ticker.C:
195 | // Nothing to do here, go to the top of the loop and check for updates
196 | case <-rt.stop:
197 | ticker.Stop()
198 | lg.Debug("Shutting down")
199 | return
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/runtime/geo.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * geo.go: Geo interface functionality
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "fmt"
27 | "github.com/tenta-browser/tenta-dns/common"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "net"
30 | "path/filepath"
31 | "time"
32 |
33 | "github.com/oschwald/maxminddb-golang"
34 | "github.com/sirupsen/logrus"
35 | )
36 |
37 | type responsewrapper struct {
38 | response *common.GeoLocation
39 | err error
40 | }
41 |
42 | type Query struct {
43 | dummy bool
44 | ip string
45 | resp chan *responsewrapper
46 | valid bool
47 | }
48 |
49 | type ErrResponseTimeout struct {
50 | s string
51 | }
52 |
53 | func (e *ErrResponseTimeout) Error() string {
54 | return e.s
55 | }
56 |
57 | type ErrNotStarted struct{}
58 |
59 | func (e *ErrNotStarted) Error() string {
60 | return "The geo service was configured not to start"
61 | }
62 |
63 | type Geo struct {
64 | dummy bool
65 | loaded bool
66 | reload chan bool
67 | queries chan *Query
68 | citydb *maxminddb.Reader
69 | ispdb *maxminddb.Reader
70 | tordb *TorHash
71 | lg *logrus.Entry
72 | }
73 |
74 | func StartGeo(cfg Config, rt *Runtime, dummy bool) *Geo {
75 | g := new(Geo)
76 |
77 | if !dummy {
78 | g.lg = log.GetLogger("geo")
79 | g.loaded = false
80 | g.reload = make(chan bool, 2) // Startup reload + after the updater runs, we might have one pending
81 | g.queries = make(chan *Query, 1024)
82 |
83 | rt.AddService()
84 | go geolisten(cfg, rt, g)
85 |
86 | g.Reload()
87 | } else {
88 | g.dummy = true
89 | }
90 | return g
91 | }
92 |
93 | func (g *Geo) Reload() {
94 | if g.dummy {
95 | return
96 | }
97 | g.lg.Debug("Config reload requested")
98 | g.reload <- true
99 | }
100 |
101 | func (g *Geo) Query(ip string) *Query {
102 | q := new(Query)
103 | if !g.dummy {
104 | q.ip = ip
105 | q.resp = make(chan *responsewrapper, 1)
106 | q.valid = true
107 | g.queries <- q
108 | } else {
109 | q.dummy = true
110 | }
111 | return q
112 | }
113 |
114 | func (q *Query) Response() (*common.GeoLocation, error) {
115 | if q.dummy {
116 | return nil, &ErrNotStarted{}
117 | }
118 | ticker := time.NewTicker(50 * time.Millisecond)
119 | defer ticker.Stop()
120 | select {
121 | case wrap := <-q.resp:
122 | return wrap.response, wrap.err
123 | case <-ticker.C:
124 | q.valid = false
125 | return nil, &ErrResponseTimeout{"Timed out waiting for geo response"}
126 | }
127 | }
128 |
129 | func geolisten(cfg Config, rt *Runtime, g *Geo) {
130 | defer rt.wg.Done()
131 | g.lg.Debug("Started listener")
132 | defer func() {
133 | g.lg.Debug("Shut down")
134 | }()
135 | for {
136 | if g.loaded {
137 | select {
138 | case q := <-g.queries:
139 | if q.valid {
140 | doQuery(q, g)
141 | } else {
142 | g.lg.Debug("Query is no longer valid")
143 | }
144 | case <-g.reload:
145 | g.lg.Debug("Got a command to reload in loaded state")
146 | doReload(cfg, rt, g)
147 | case <-rt.stop:
148 | g.lg.Debug("Got shutdown command in loaded state")
149 | return
150 | }
151 | } else {
152 | select {
153 | case <-g.reload:
154 | g.lg.Debug("Got a command to reload in unloaded state")
155 | doReload(cfg, rt, g)
156 | case <-rt.stop:
157 | g.lg.Debug("Got shutdown command in unloaded state")
158 | return
159 | }
160 | }
161 | }
162 | }
163 |
164 | func doReload(cfg Config, rt *Runtime, g *Geo) {
165 | g.loaded = false
166 | g.lg.Info("Doing a reload")
167 | success := 0
168 |
169 | citykey := fmt.Sprintf(DB_TEMPLATE_VERSION, "GeoIP2-City")
170 | cityver, err := rt.DBGet([]byte(citykey))
171 | if err == nil {
172 | cityfile := filepath.Join(cfg.GeoDBPath, fmt.Sprintf("%s-%s.mmdb", "GeoIP2-City", cityver))
173 | g.lg.Debugf("Geo: Opening city file %s", cityfile)
174 | r, err := maxminddb.Open(cityfile)
175 | if err == nil {
176 | g.citydb = r
177 | success += 1
178 | } else {
179 | g.lg.Errorf("Failed to open city database %s: %s", cityfile, err.Error())
180 | }
181 | }
182 |
183 | ispkey := fmt.Sprintf(DB_TEMPLATE_VERSION, "GeoIP2-ISP")
184 | ispver, err := rt.DBGet([]byte(ispkey))
185 | if err == nil {
186 | ispfile := filepath.Join(cfg.GeoDBPath, fmt.Sprintf("%s-%s.mmdb", "GeoIP2-ISP", ispver))
187 | g.lg.Debugf("Opening isp file %s", ispfile)
188 | r, err := maxminddb.Open(ispfile)
189 | if err == nil {
190 | g.ispdb = r
191 | success += 1
192 | } else {
193 | g.lg.Errorf("Failed to open isp database %s: %s", ispfile, err.Error())
194 | }
195 | }
196 |
197 | if success == 2 {
198 | g.loaded = true
199 | g.lg.Info("Reloaded Successfully")
200 | } else {
201 | g.lg.Error("Reload failure")
202 | }
203 | }
204 |
205 | func shouldIncludeSubdivision(iso string) bool {
206 | if iso == "US" || iso == "CA" || iso == "MX" || iso == "IN" || iso == "CN" {
207 | return true
208 | }
209 | return false
210 | }
211 |
212 | func doQuery(q *Query, g *Geo) {
213 | ip := net.ParseIP(q.ip)
214 | ret := &common.GeoLocation{
215 | ISP: &common.ISP{},
216 | LocationI18n: make(map[string]string, 0),
217 | }
218 |
219 | ispErr := g.ispdb.Lookup(ip, &ret.ISP)
220 | if ispErr != nil {
221 | g.lg.Warnf("ISP error: %s", ispErr.Error())
222 | ret.ISP = nil
223 | }
224 | var record struct {
225 | Position common.Position `maxminddb:"location"`
226 | City struct {
227 | Names map[string]string `maxminddb:"names"`
228 | } `maxminddb:"city"`
229 | Subdivisions []struct {
230 | Names map[string]string `maxminddb:"names"`
231 | } `maxminddb:"subdivisions"`
232 | Country struct {
233 | Names map[string]string `maxminddb:"names"`
234 | ISOCode string `maxminddb:"iso_code"`
235 | } `maxminddb:"country"`
236 | }
237 | lookupError := g.citydb.Lookup(ip, &record)
238 | if lookupError != nil {
239 | g.lg.Warnf("Lookup error: %s", lookupError.Error())
240 | ret = nil
241 | } else {
242 |
243 | ret.Position = &record.Position
244 |
245 | if country, ok := record.Country.Names["en"]; ok {
246 | ret.Country = country
247 | for lang, countryName := range record.Country.Names {
248 | var subdivision string = ""
249 | if shouldIncludeSubdivision(record.Country.ISOCode) && len(record.Subdivisions) > 0 {
250 | subdivision, ok = record.Subdivisions[0].Names[lang]
251 | if !ok {
252 | subdivision, ok = record.Subdivisions[0].Names["en"]
253 | if !ok {
254 | subdivision = ""
255 | }
256 | }
257 | }
258 | var cityName = ""
259 | cityName, ok = record.City.Names[lang]
260 | if !ok {
261 | cityName, ok = record.City.Names["en"]
262 | if !ok {
263 | cityName = ""
264 | }
265 | }
266 | if subdivision != "" {
267 | countryName = fmt.Sprintf("%s, %s", subdivision, countryName)
268 | }
269 | if cityName != "" {
270 | countryName = fmt.Sprintf("%s, %s", cityName, countryName)
271 | }
272 | ret.LocationI18n[lang] = countryName
273 | }
274 | }
275 | if city, ok := record.City.Names["en"]; ok {
276 | ret.City = city
277 | }
278 | if location, ok := ret.LocationI18n["en"]; ok {
279 | ret.Location = location
280 | }
281 | ret.CountryISO = record.Country.ISOCode
282 | }
283 |
284 | if g.tordb != nil {
285 | if nodeid, present := g.tordb.Exists(q.ip); present {
286 | ret.TorNode = &nodeid
287 | } else {
288 | ret.TorNode = nil
289 | }
290 | }
291 |
292 | wrap := &responsewrapper{
293 | response: ret,
294 | err: lookupError,
295 | }
296 | q.resp <- wrap
297 | }
298 |
--------------------------------------------------------------------------------
/anycast/anycast.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * anycast.go: Routine collection for Anycast addressing
21 | */
22 |
23 | package anycast
24 |
25 | import (
26 | "fmt"
27 | "github.com/tenta-browser/tenta-dns/common"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "net"
30 | "strconv"
31 | "strings"
32 | "sync"
33 | "time"
34 |
35 | api "github.com/osrg/gobgp/api"
36 | bgpconfig "github.com/osrg/gobgp/config"
37 | "github.com/osrg/gobgp/packet/bgp"
38 | gobgp "github.com/osrg/gobgp/server"
39 | "github.com/osrg/gobgp/table"
40 | "github.com/sirupsen/logrus"
41 | )
42 |
43 | func watchRoutes(srv *gobgp.BgpServer, stop chan bool, wg *sync.WaitGroup, lg *logrus.Entry) {
44 | wg.Add(1)
45 | defer wg.Done()
46 | lg.Info("Watching routes")
47 | w := srv.Watch(gobgp.WatchBestPath(true))
48 | run := true
49 | for run {
50 | select {
51 | case <-stop:
52 | run = false
53 | break
54 | case ev := <-w.Event():
55 | switch msg := ev.(type) {
56 | case *gobgp.WatchEventBestPath:
57 | for _, path := range msg.PathList {
58 | // do something useful
59 | lg.Debug(path)
60 | }
61 | }
62 | }
63 | }
64 | lg.Debug("Stopping route watcher")
65 | }
66 |
67 | func watchPeerState(srv *gobgp.BgpServer, stop chan bool, wg *sync.WaitGroup, lg *logrus.Entry) {
68 | wg.Add(1)
69 | defer wg.Done()
70 | lg.Info("Watching peers")
71 | w := srv.Watch(gobgp.WatchPeerState(true))
72 | run := true
73 | for run {
74 | select {
75 | case <-stop:
76 | run = false
77 | break
78 | case ev := <-w.Event():
79 | switch msg := ev.(type) {
80 | case *gobgp.WatchEventPeerState:
81 | lg.Debugf("Peer %s has state %s", msg.PeerID, msg.State.String())
82 | }
83 | }
84 | }
85 | lg.Debug("Stopping peer watcher")
86 | }
87 |
88 | func AdvertiseRoutes(b BGPConfig, peers map[string]Peer, netblocks map[string]common.Netblock, interfaces []common.Interface) (chan RouteUpdate, chan bool, *sync.WaitGroup) {
89 | notify := make(chan RouteUpdate, 1024)
90 | stop := make(chan bool, 1)
91 | lg := log.GetLogger("bgp")
92 | wg := &sync.WaitGroup{}
93 | go func() {
94 | dummies := make([]string, 0)
95 | enable := b.Enabled() && common.AnycastSupported()
96 | defer func(createdifaces *[]string) {
97 | if rcv := recover(); rcv != nil {
98 | lg.Errorf("Got a panic from the bgp subsystem. Sending a fatal failure: %s", rcv)
99 | notify <- RouteUpdate{"", RouteStatusCriticalFailure}
100 | }
101 | if enable {
102 | removeLinks(createdifaces, lg)
103 | }
104 | wg.Done()
105 | }(&dummies)
106 | wg.Add(1)
107 | if enable {
108 | doAdvertiseRoutes(b, peers, netblocks, interfaces, &dummies, notify, stop, lg)
109 | } else {
110 | lg.Debug("Not starting BGP server, since it's not enabled or supported")
111 | }
112 | }()
113 | return notify, stop, wg
114 | }
115 |
116 | func doAdvertiseRoutes(cfg BGPConfig, peers map[string]Peer, netblocks map[string]common.Netblock, interfaces []common.Interface, dummies *[]string, notify chan RouteUpdate, stop chan bool, lg *logrus.Entry) {
117 | lg.Infof("Strting BGP server as AS%d", cfg.AS)
118 |
119 | // TODO: Probably need to do a double function wrap to catch panics here
120 | srv := gobgp.NewBgpServer()
121 | go srv.Serve()
122 |
123 | global := &bgpconfig.Global{
124 | Config: bgpconfig.GlobalConfig{
125 | As: cfg.AS,
126 | RouterId: cfg.IPv4,
127 | Port: -1,
128 | },
129 | }
130 |
131 | if err := srv.Start(global); err != nil {
132 | lg.Errorf("Failed to start the BGP server: %s", err.Error())
133 | panic(err.Error())
134 | }
135 |
136 | var g *api.Server
137 | if cfg.EnableRPC {
138 | lg.Infof("Starting bgp rpc server on 127.0.0.1:%d", cfg.RPCPort)
139 | // TODO: Offer multiple addresses? Multiple ports? ...?
140 | g = api.NewGrpcServer(srv, fmt.Sprintf("127.0.0.1:%d", cfg.RPCPort))
141 | go g.Serve()
142 | }
143 |
144 | started := uint(0)
145 | stopper := make(chan bool, 1024)
146 | wg := &sync.WaitGroup{}
147 |
148 | go watchRoutes(srv, stopper, wg, lg)
149 | started += 1
150 |
151 | go watchPeerState(srv, stopper, wg, lg)
152 | started += 1
153 |
154 | for name, p := range peers {
155 | neighbor := &bgpconfig.Neighbor{
156 | Config: bgpconfig.NeighborConfig{
157 | NeighborAddress: p.IP,
158 | PeerAs: p.AS,
159 | Description: p.Description,
160 | },
161 | }
162 | if len(p.Password) > 0 {
163 | neighbor.Config.AuthPassword = p.Password
164 | }
165 | if cfg.EnableCommunities {
166 | neighbor.Config.SendCommunity = bgpconfig.COMMUNITY_TYPE_STANDARD
167 | }
168 | lg.Debugf("Adding peer %s [AS%d] at %s", name, p.AS, p.IP)
169 | if err := srv.AddNeighbor(neighbor); err != nil {
170 | lg.Errorf("Failed to add peer %s: %s", name, err.Error())
171 | panic(err.Error())
172 | }
173 | }
174 |
175 | advblocks := make(map[string]common.Netblock)
176 | advifaces := make(map[string]common.Netblock)
177 |
178 | // Run through the netblocks and add any that we must advertise because they're forced
179 | for name, b := range netblocks {
180 | if b.Force {
181 | advblocks[name] = b
182 | }
183 | }
184 |
185 | // Run through the interfaces and identify any that need to be announced via BGP
186 | for _, i := range interfaces {
187 | for name, b := range netblocks {
188 | _, bnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", b.IP, b.Netmask))
189 | if err != nil {
190 | lg.Error("Unable to parse netblock")
191 | panic("Unable to parse netblock")
192 | }
193 | if bnet.Contains(i.IP) {
194 | lg.Debugf("%s is within %s [%s], announcing it", i.IP.String(), name, bnet.String())
195 | advblocks[name] = b
196 | if i.IP.To4() != nil {
197 | // We use the sting as the key here (and not the ID), since multiple service IDs may share the same IP
198 | advifaces[i.IP.String()] = common.Netblock{IP: i.IP.String(), Netmask: 32, Force: true}
199 | } else {
200 | advifaces[i.IP.String()] = common.Netblock{IP: i.IP.String(), Netmask: 128, Force: true}
201 | }
202 | }
203 | }
204 | }
205 |
206 | for _, b := range advblocks {
207 | advertisNetblock(cfg, srv, b, lg)
208 | }
209 |
210 | cnt := uint(0)
211 | for _, b := range advifaces {
212 | advertisNetblock(cfg, srv, b, lg)
213 | addLink(dummies, b, &cnt, lg)
214 | notify <- RouteUpdate{IP: b.IP, Status: RouteStatusAnnounced}
215 | }
216 |
217 | for _, d := range *dummies {
218 | lg.Infof("Added link %s", d)
219 | }
220 |
221 | select {
222 | case <-stop:
223 | break
224 | }
225 |
226 | lg.Debug("Shutting down")
227 |
228 | for i := uint(0); i < started; i += 1 {
229 | stopper <- true
230 | }
231 | wg.Wait()
232 |
233 | if g != nil {
234 | // TODO: Talk to GoBGP team about why this takes parameters which it doesn't use
235 | g.StopServer(nil, nil)
236 | }
237 | srv.Stop()
238 |
239 | lg.Debug("Stopped")
240 | }
241 |
242 | func advertisNetblock(cfg BGPConfig, srv *gobgp.BgpServer, b common.Netblock, lg *logrus.Entry) {
243 | lg.Debugf("Advertising %s/%d", b.IP, b.Netmask)
244 | _, ipnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", b.IP, b.Netmask))
245 | if err != nil {
246 | lg.Errorf("Unable to parse CIDR for %s/%d", b.IP, b.Netmask)
247 | return
248 | }
249 | if ipnet.IP.To4() != nil {
250 | attrs4 := []bgp.PathAttributeInterface{
251 | bgp.NewPathAttributeOrigin(0),
252 | bgp.NewPathAttributeNextHop(cfg.IPv4),
253 | bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{cfg.AS})}),
254 | }
255 | if len(b.Communities) > 0 {
256 | comm := make([]uint32, 0)
257 | for _, c := range b.Communities {
258 | parts := strings.Split(c, ":")
259 | if len(parts) != 2 {
260 | lg.Warnf("Error parsing community %s", c)
261 | continue
262 | }
263 | as, err := strconv.ParseUint(parts[0], 10, 16)
264 | if err != nil {
265 | lg.Warnf("Error parsing community %s", c)
266 | continue
267 | }
268 | cmn, err := strconv.ParseUint(parts[1], 10, 16)
269 | comm = append(comm, uint32(as<<16+cmn))
270 | }
271 | attrs4 = append(attrs4, bgp.NewPathAttributeCommunities(comm))
272 | }
273 | if _, err := srv.AddPath("", []*table.Path{table.NewPath(nil, bgp.NewIPAddrPrefix(b.Netmask, b.IP), false, attrs4, time.Now(), false)}); err != nil {
274 | lg.Errorf("Unable to add %s/%d to path", b.IP, b.Netmask)
275 | }
276 | } else {
277 | lg.Debugf("Don't currently know how to add IPv6 to the path")
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/monitor/monitor.go:
--------------------------------------------------------------------------------
1 | /*
2 | ** Monitor
3 | ** Basic operating principle:
4 | ** It can either read a config file, or ingest command line arguments (read flag definitions for more info)
5 | ** Firstly, it reads the configs of the running Tenta DNS instance, and parses the location of the module configs (recursor, nsnitch)
6 | ** Secondly, reads the module configs and parses all DNS Recursor IP addresses, and all Authoritative DNS addresses (and zone information it should serve)
7 | ** Thirdly, sets up TLS listener with the given parameters, and starts to listen for incoming requests
8 | ** Lastly, when a request comes in, it launches all preconfigured domains to be resolved by the recursor(s) within a predefined time interval
9 | */
10 | package main
11 |
12 | import (
13 | "flag"
14 | "fmt"
15 | "net"
16 | "net/http"
17 | "os"
18 | "os/signal"
19 | "strings"
20 | "sync"
21 | "syscall"
22 | "time"
23 |
24 | "github.com/BurntSushi/toml"
25 | "github.com/miekg/dns"
26 | "github.com/sirupsen/logrus"
27 | "github.com/tenta-browser/tenta-dns/log"
28 | "github.com/tenta-browser/tenta-dns/runtime"
29 | "github.com/tenta-browser/tenta-dns/common"
30 | )
31 |
32 | const (
33 | CHECK_HISTORY_LENGTH = 5
34 | TESTING_PERIOD = 30
35 | )
36 |
37 | var (
38 | config = flag.String("config", "", "Path to the configuration file for monitoring service")
39 | dnsconf = flag.String("dnsconf", "", "Path to Tenta DNS main configuration file (Tenta DNS launch parameter)")
40 | exclude = flag.String("exclude", "", "Comma separated list of IPs to exclude from testing")
41 | ip = flag.String("ip", "", "Local IP address to bind service")
42 | domain = flag.String("domain", "", "TLS domain name to serve API from")
43 | cert = flag.String("cert", "", "Path to TLS certificate file for domain")
44 | key = flag.String("key", "", "Path to TLS key file for domain")
45 | target = flag.String("target", "example.com,example.org", "Comma separated list of domains to resolve")
46 | timeout = flag.Int("timeout", 15, "Maximum duration the DNS server should finish a single query")
47 | systemd = flag.Bool("systemd", false, "Set only if daemon is run via systemd")
48 | )
49 |
50 | var version string
51 |
52 | type monitorConfig struct {
53 | DnsConf, Ip, Domain, Cert, Key string
54 | Target, Exclude []string
55 | Timeout int
56 | Systemd bool
57 | }
58 |
59 | type dnsInstance struct {
60 | ip, net, hostname string
61 | port int
62 | }
63 |
64 | type monitorRuntime struct {
65 | c *monitorConfig
66 | d []*dnsInstance
67 | t *time.Ticker
68 | m *sync.Mutex
69 | r [20]bool
70 | }
71 |
72 | func checkExcludedIP(rt *monitorRuntime, ip string) bool {
73 | for _, eip := range rt.c.Exclude {
74 | if eip == ip {
75 | return true
76 | }
77 | }
78 | return false
79 | }
80 |
81 | func parseDNSConfig(rt *monitorRuntime, holder runtime.ConfigHolder) error {
82 | for _, r := range holder.Recursors {
83 | for _, d := range r.Domains {
84 | if checkExcludedIP(rt, d.IPv4) {
85 | continue
86 | }
87 | if d.DnsTcpPort != runtime.PORT_UNSET {
88 | rt.d = append(rt.d, &dnsInstance{d.IPv4, "tcp", d.HostName, d.DnsTcpPort})
89 | }
90 | if d.DnsUdpPort != runtime.PORT_UNSET {
91 | rt.d = append(rt.d, &dnsInstance{d.IPv4, "udp", d.HostName, d.DnsUdpPort})
92 | }
93 | if d.DnsTlsPort != runtime.PORT_UNSET {
94 | rt.d = append(rt.d, &dnsInstance{d.IPv4, "tcp-tls", d.HostName, d.DnsTlsPort})
95 | }
96 | }
97 | }
98 | return nil
99 | }
100 |
101 | func pingdomWrapper(rt *monitorRuntime) http.HandlerFunc {
102 | return func(w http.ResponseWriter, r *http.Request) {
103 | var response string
104 | rt.m.Lock()
105 | defer rt.m.Unlock()
106 |
107 | for i := CHECK_HISTORY_LENGTH - 1; i >= 0; i-- {
108 | if rt.r[i] == false {
109 | response = fmt.Sprintf("\n" +
110 | " FAIL\n" +
111 | " 1\n" +
112 | "")
113 | return
114 | }
115 | }
116 | response = fmt.Sprintf("\n" +
117 | " OK\n" +
118 | " 1\n" +
119 | "")
120 |
121 | w.Header().Set("Content-Disposition", "attachment; filename=status.xml")
122 | w.Header().Set("Content-Type", "text/xml")
123 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(response)))
124 | w.Write([]byte(response))
125 |
126 | }
127 | }
128 |
129 | func sitrepWrapper(rt *monitorRuntime) http.HandlerFunc {
130 | return func(w http.ResponseWriter, r *http.Request) {
131 | rt.m.Lock()
132 | defer rt.m.Unlock()
133 | for i := CHECK_HISTORY_LENGTH - 1; i >= 0; i-- {
134 | if rt.r[i] == false {
135 | w.Write([]byte("FAIL"))
136 | return
137 | }
138 | }
139 | w.Write([]byte("OK"))
140 | }
141 | }
142 |
143 | func createDialer(ip, network string) *net.Dialer {
144 | switch network {
145 | case "udp":
146 | return &net.Dialer{LocalAddr: &net.UDPAddr{IP: net.ParseIP(ip)}}
147 | break
148 | case "tcp":
149 | case "tcp-tls":
150 | return &net.Dialer{LocalAddr: &net.TCPAddr{IP: net.ParseIP(ip)}}
151 | break
152 | }
153 | return nil
154 | }
155 |
156 | func testDNS(rt *monitorRuntime, log *logrus.Entry) {
157 | allOK := true
158 | wg := &sync.WaitGroup{}
159 | for _, _d := range rt.d {
160 | wg.Add(1)
161 | d := _d
162 | go func() {
163 | //log.Infof("Launching resolve for %s/%s", d.ip, d.net)
164 | c := dns.Client{
165 | Net: d.net,
166 | SingleInflight: true,
167 | }
168 | if di := createDialer(rt.c.Ip, d.net); d != nil {
169 | c.Dialer = di
170 | }
171 | if d.net == "tcp-tls" {
172 | c.TLSConfig = common.TLSConfigDNS()
173 | }
174 |
175 | for _, testDomain := range rt.c.Target {
176 | m := new(dns.Msg)
177 | m.SetQuestion(dns.Fqdn(testDomain), dns.TypeA)
178 | m.SetEdns0(4096, true)
179 | r, rtt, e := c.Exchange(m, fmt.Sprintf("%s:%d", d.ip, d.port))
180 |
181 | if e != nil {
182 | log.Warnf("An error occured during DNS exchange. Setup: %s/%s [%s]. Cause [%s]", d.ip, d.net, testDomain, e.Error())
183 | continue
184 | }
185 | if r.Rcode != dns.RcodeSuccess {
186 | log.Warnf("DNS query %s/%s about %s returned non-success rcode [%s]. Failure notice sent.", d.ip, d.net, testDomain, dns.RcodeToString[r.Rcode])
187 | allOK = false
188 | break
189 | }
190 | if rtt > time.Duration(rt.c.Timeout)*time.Millisecond {
191 | log.Warnf("Querying %s/%s about %s exceeded rtt threshold [%v]. Retrying.", d.ip, d.net, testDomain, rtt)
192 |
193 | r, rtt, e = c.Exchange(m, fmt.Sprintf("%s:%d", d.ip, d.port))
194 |
195 | if e != nil {
196 | log.Warnf("An error occured during DNS exchange. Setup: %s/%s [%s]. Cause [%s]", d.ip, d.net, testDomain, e.Error())
197 | allOK = false
198 | break
199 | }
200 | if r.Rcode != dns.RcodeSuccess {
201 | log.Warnf("DNS query %s/%s about %s returned non-success rcode [%s]. Failure notice sent.", d.ip, d.net, testDomain, dns.RcodeToString[r.Rcode])
202 | allOK = false
203 | break
204 | }
205 | if rtt > time.Duration(rt.c.Timeout)*time.Millisecond {
206 | log.Warnf("Querying %s/%s about %s exceeded rtt threshold [%v]. Failure notice sent.", d.ip, d.net, testDomain, rtt)
207 | allOK = false
208 | break
209 | }
210 | }
211 | }
212 | wg.Done()
213 | }()
214 | }
215 | wg.Wait()
216 |
217 | if allOK {
218 | log.Infof("SUCCESS for this round")
219 | } else {
220 | log.Infof("FAILURE for this round")
221 | }
222 |
223 | rt.m.Lock()
224 | defer rt.m.Unlock()
225 |
226 | for i := 18; i >= 0; i-- {
227 | rt.r[i+1] = rt.r[i]
228 | }
229 | rt.r[0] = allOK
230 |
231 | }
232 |
233 | func usage() {
234 | fmt.Printf("Tenta DNS Monitorization - build %s\nOptions:", version)
235 | flag.PrintDefaults()
236 | }
237 |
238 | func main() {
239 | log.SetLogLevel(logrus.InfoLevel)
240 | lg := log.GetLogger("dnsmon")
241 | /// Step 1. Make sure either we have command-line arguments or a config file (command line args take precedence)
242 | flag.Usage = usage
243 | flag.Parse()
244 |
245 | rt := &monitorRuntime{m: &sync.Mutex{}}
246 | cfg := &monitorConfig{}
247 |
248 | for i := range rt.r {
249 | rt.r[i] = true
250 | }
251 |
252 | if config == nil {
253 | lg.Infof("Attempting to construct context from commandline arguments")
254 | if dnsconf == nil || ip == nil || domain == nil || cert == nil || key == nil {
255 | lg.Errorf("Service halted. Missing commandline arguments")
256 | os.Exit(1)
257 | }
258 |
259 | cfg = &monitorConfig{
260 | DnsConf: *dnsconf,
261 | Exclude: strings.Split(*exclude, ","),
262 | Ip: *ip,
263 | Domain: *domain,
264 | Cert: *cert,
265 | Key: *key,
266 | Target: strings.Split(*target, ","),
267 | Timeout: *timeout,
268 | Systemd: *systemd,
269 | }
270 | } else {
271 | lg.Infof("Attempting to construct context from config file [%s]", *config)
272 | if _, err := toml.DecodeFile(*config, cfg); err != nil {
273 | lg.Errorf("Service halted. Configuration file error. [%s]", err.Error())
274 | os.Exit(1)
275 | }
276 | }
277 | rt.c = cfg
278 | tentaHolder := runtime.ParseConfig(cfg.DnsConf, false, false)
279 | if e := parseDNSConfig(rt, tentaHolder); e != nil {
280 | lg.Errorf("Cannot parse DNS configuration. [%s]", e.Error())
281 | os.Exit(1)
282 | }
283 | lg.Infof("Parsed Tenta DNS configurations")
284 |
285 | rt.t = time.NewTicker(TESTING_PERIOD * time.Second)
286 | mux := http.NewServeMux()
287 | mux.HandleFunc("/api/v1/checkup", sitrepWrapper(rt))
288 | mux.HandleFunc("/api/v1/pingdom", pingdomWrapper(rt))
289 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
290 | w.WriteHeader(http.StatusNotFound)
291 | })
292 |
293 | go func() {
294 | srv := &http.Server{
295 | Addr: net.JoinHostPort(rt.c.Ip, "80"),
296 | Handler: mux,
297 | }
298 | if e := srv.ListenAndServe(); e != nil {
299 | lg.Errorf("HTTP listener error [%s]", e.Error())
300 | }
301 | }()
302 |
303 | go func() {
304 | srv := &http.Server{
305 | Addr: net.JoinHostPort(rt.c.Ip, "443"),
306 | Handler: mux,
307 | TLSConfig: common.TLSConfigModernHTTPS(),
308 | }
309 | if e := srv.ListenAndServeTLS(rt.c.Cert, rt.c.Key); e != nil {
310 | lg.Errorf("HTTPS listener error [%s]", e.Error())
311 | }
312 | }()
313 |
314 | sig := make(chan os.Signal)
315 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
316 |
317 | for {
318 | select {
319 | case rs := <-sig:
320 | lg.Infof("Received termination signal [%s]. Exiting.", rs)
321 | os.Exit(0)
322 | break
323 | case <-rt.t.C:
324 | lg.Debugf("Received test signal. Working.")
325 | testDNS(rt, lg)
326 | break
327 | }
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/runtime/stats.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Tenta DNS Server
3 | *
4 | * Copyright 2017 Tenta, LLC
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | * For any questions, please contact developer@tenta.io
19 | *
20 | * stats.go: Stats subsystem
21 | */
22 |
23 | package runtime
24 |
25 | import (
26 | "encoding/binary"
27 | "fmt"
28 | "github.com/tenta-browser/tenta-dns/log"
29 | "strconv"
30 | "strings"
31 | "sync"
32 | "time"
33 |
34 | "github.com/sasha-s/go-hll"
35 | "github.com/sirupsen/logrus"
36 | "github.com/syndtr/goleveldb/leveldb/errors"
37 | "github.com/syndtr/goleveldb/leveldb/util"
38 | "github.com/tenta-browser/go-highway"
39 | )
40 |
41 | type EventType uint8
42 |
43 | const (
44 | EvtTypeCount = 1
45 | EvtTypeCard = 2
46 | EvtTypeLatency = 3
47 | )
48 |
49 | type Event struct {
50 | Key string
51 | Type EventType
52 | Count uint64
53 | Value []byte
54 | Time uint64
55 | }
56 |
57 | type PerSecond struct {
58 | Component string
59 | Type string
60 | Count uint64
61 | }
62 |
63 | type Stats struct {
64 | queue chan *Event
65 | persec chan *PerSecond
66 | stop chan bool
67 | lock *sync.Mutex
68 | wg *sync.WaitGroup
69 | lanes *highway.Lanes
70 | lg *logrus.Entry
71 | broadcast []chan map[string]interface{}
72 | }
73 |
74 | func StartStats(rt *Runtime) *Stats {
75 | s := new(Stats)
76 | s.lg = log.GetLogger("stats")
77 | s.queue = make(chan *Event, 4096)
78 | s.persec = make(chan *PerSecond, 8192)
79 | s.stop = make(chan bool)
80 | s.broadcast = make([]chan map[string]interface{}, 0)
81 | var wg sync.WaitGroup
82 | s.wg = &wg
83 | s.wg.Add(1)
84 | var lock sync.Mutex
85 | s.lock = &lock
86 |
87 | s.lanes = &highway.Lanes{0x0706050403020100, 0x0F0E0D0C0B0A0908, 0x1716151413121110, 0x1F1E1D1C1B1A1918}
88 |
89 | go processstats(rt, s)
90 |
91 | return s
92 | }
93 |
94 | func (s *Stats) Stop() {
95 | s.lock.Lock()
96 | defer s.wg.Wait()
97 | defer s.lock.Unlock()
98 | s.stop <- true
99 | s.lg.Debug("Sent stop signal")
100 | }
101 |
102 | func (s *Stats) Count(key string) {
103 | s.CountN(key, 1)
104 | }
105 |
106 | func (s *Stats) CountN(key string, increment uint64) {
107 | e := &Event{
108 | Key: key,
109 | Type: EvtTypeCount,
110 | Count: increment,
111 | }
112 | s.queue <- e
113 | }
114 |
115 | func (s *Stats) Card(key, value string) {
116 | e := &Event{
117 | Key: key,
118 | Type: EvtTypeCard,
119 | Value: []byte(value),
120 | }
121 | s.queue <- e
122 | }
123 |
124 | func (s *Stats) Latency(key string, value uint64) {
125 | e := &Event{
126 | Key: key,
127 | Type: EvtTypeLatency,
128 | Time: value,
129 | }
130 | s.queue <- e
131 | }
132 |
133 | func (s *Stats) Tick(component, action_type string) {
134 | s.TickN(component, action_type, 1)
135 | }
136 | func (s *Stats) TickN(component string, action_type string, count uint64) {
137 | p := &PerSecond{
138 | Component: component,
139 | Type: action_type,
140 | Count: count,
141 | }
142 | s.persec <- p
143 | }
144 |
145 | func (s *Stats) AddBroadcastWatcher() chan map[string]interface{} {
146 | ret := make(chan map[string]interface{}, 4096)
147 | s.broadcast = append(s.broadcast, ret)
148 | return ret
149 | }
150 |
151 | func processstats(rt *Runtime, s *Stats) {
152 | defer s.wg.Done()
153 | s.lg.Debug("Starting Stats Service")
154 | ticker := time.NewTicker(time.Millisecond * 10)
155 | defer ticker.Stop()
156 | buffer := make([]*Event, 0)
157 | defer func() {
158 | s.lg.Debug("Stopped Stats Service")
159 | }()
160 | currentPerSecondSecond := time.Now().Unix()
161 | lastQueueUpdate := currentPerSecondSecond
162 | // lastPerSecondMinute := int64(0)
163 | register := make(map[int64]map[string]uint64)
164 | lastAverage := int64(0)
165 | for {
166 | select {
167 | case <-s.stop:
168 | // Check to see if we got a stop message
169 | s.lg.Debug("Got stop command")
170 | if len(buffer) > 0 {
171 | s.lg.Debugf("Inserting %d final stats", len(buffer))
172 | insertStats(rt, s, buffer)
173 | }
174 | return
175 | case p := <-s.persec:
176 | // Process "per second" ticks
177 | //var created_register = ""
178 | //var created_subkey = ""
179 | if _, ok := register[currentPerSecondSecond]; !ok {
180 | register[currentPerSecondSecond] = make(map[string]uint64)
181 | // created_register = "created register"
182 | }
183 | register_key := strings.Join([]string{p.Component, p.Type}, ":")
184 | if _, ok := register[currentPerSecondSecond][register_key]; !ok {
185 | register[currentPerSecondSecond][register_key] = 0
186 | // created_subkey = "created subkey"
187 | }
188 | register[currentPerSecondSecond][register_key] += p.Count
189 | //fmt.Printf(" + %s %d %s %s\n", register_key, p.Count, created_register, created_subkey)
190 | case e := <-s.queue:
191 | // Empty the stats queue into a buffer
192 | buffer = append(buffer, e)
193 | case <-ticker.C:
194 | // Run on a tick
195 | now := time.Now().Unix()
196 | if now > currentPerSecondSecond {
197 | if _, ok := register[now-16*60]; ok { // Delete from 16 minutes ago
198 | register[now-16*60] = nil
199 | }
200 | currentPerSecondSecond = now
201 | }
202 | if now-lastQueueUpdate > 1 {
203 | if len(buffer) > 1 {
204 | insertStats(rt, s, buffer)
205 | buffer = nil // Note, this doesn't go away with the slice, it just empties it
206 | lastQueueUpdate = now
207 | }
208 | }
209 | if (now%60) == 0 && lastAverage != now {
210 | averages := make(map[string]uint64)
211 | totals := make(map[string]uint64)
212 | for i := now - 60; i < now; i += 1 {
213 | for key, ticks := range register[i] {
214 | averages[key] += ticks
215 | totals[key] += ticks
216 | }
217 | }
218 | fmap := make(map[string]interface{})
219 | for key, ticks := range averages {
220 | fmap[key] = strconv.FormatUint(totals[key], 10)
221 | fmap[fmt.Sprintf("%s/average", key)] = strconv.FormatUint(ticks/60, 10)
222 | }
223 | for _, b := range s.broadcast {
224 | select {
225 | case b <- fmap:
226 | break
227 | case <-time.After(time.Millisecond * 5):
228 | break
229 | }
230 | }
231 | s.lg.WithFields(logrus.Fields(fmap)).Debug("60 second averages")
232 | lastAverage = now
233 | }
234 | // TODO: 5 minute and 15 minute rolling averages
235 | }
236 | }
237 | }
238 |
239 | func insertStats(rt *Runtime, s *Stats, buffer []*Event) {
240 |
241 | now := time.Now()
242 | timestamp := fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute())
243 | counters := make(map[string]uint64)
244 | pfcounters := make(map[string]hll.HLL)
245 | latencies := make(map[string][]uint64)
246 | allkeys := make(map[string]bool)
247 | trueval := []byte("true")
248 |
249 | for _, e := range buffer {
250 | switch e.Type {
251 | case EvtTypeCount:
252 | // Preparing counters
253 | if _, ok := counters[e.Key]; !ok {
254 | counters[e.Key] = 0
255 | }
256 | counters[e.Key] += e.Count
257 | allkeys[e.Key] = true
258 | break
259 | case EvtTypeCard:
260 | // Preparing HLLs
261 | if _, ok := pfcounters[e.Key]; !ok {
262 | pfsize, err := hll.SizeByP(14)
263 | if err != nil {
264 | continue
265 | }
266 | pfcounters[e.Key] = make(hll.HLL, pfsize)
267 | }
268 | pfcounters[e.Key].Add(highway.Hash(*s.lanes, e.Value))
269 | allkeys[e.Key] = true
270 | break
271 | case EvtTypeLatency:
272 | if _, ok := latencies[e.Key]; !ok {
273 | latencies[e.Key] = make([]uint64, 4)
274 | }
275 | latencies[e.Key] = append(latencies[e.Key], e.Time)
276 | allkeys[e.Key] = true
277 | break
278 | default:
279 | s.lg.Warnf("Event %s has unknown type %d", e.Key, e.Type)
280 | break
281 | }
282 | }
283 |
284 | s.lock.Lock()
285 | defer s.lock.Unlock()
286 |
287 | fetched := uint64(64)
288 | put := uint64(0)
289 | trans, err := rt.DB.OpenTransaction()
290 | if err != nil {
291 | s.lg.Warnf("Unable to open transaction to perist stats: %s", err.Error())
292 | return
293 | }
294 |
295 | for key, value := range counters {
296 | for _, dtype := range []string{"total", timestamp} {
297 | var count uint64 = 0
298 | dbkey := []byte(fmt.Sprintf("stats:counter:%s:%s", key, dtype))
299 | count_bytes, err := trans.Get(dbkey, nil)
300 | if err == nil {
301 | count = binary.LittleEndian.Uint64(count_bytes)
302 | } else if err == errors.ErrNotFound {
303 | count_bytes = make([]byte, 8)
304 | } else {
305 | s.lg.Warnf("Failed fetching existing counter %s: %s", key, err.Error())
306 | continue
307 | }
308 | fetched += 1
309 | count += value
310 | binary.LittleEndian.PutUint64(count_bytes, count)
311 | err = trans.Put(dbkey, count_bytes, nil)
312 | if err != nil {
313 | s.lg.Warnf("Failed adding counter %s", dbkey)
314 | }
315 | put += 1
316 | }
317 | }
318 |
319 | for key, pfcounter := range pfcounters {
320 | for _, dtype := range []string{"total", timestamp} {
321 | dbkey := []byte(fmt.Sprintf("stats:hll:%s:%s", key, dtype))
322 | counter, err := trans.Get(dbkey, nil)
323 | if err == nil {
324 | hll.HLL(counter).Merge(pfcounter)
325 | } else if err == errors.ErrNotFound {
326 | counter = []byte(pfcounter)
327 | } else {
328 | s.lg.Warnf("Failed fetching existing HLL %s: %s", dbkey, err.Error())
329 | }
330 | fetched += 1
331 | err = trans.Put(dbkey, counter, nil)
332 | if err != nil {
333 | s.lg.Warnf("Failed adding HLL %s", dbkey)
334 | }
335 | put += 1
336 | }
337 | }
338 |
339 | for key := range allkeys {
340 | dbkey := []byte(fmt.Sprintf("stats:allkeys:%s", key))
341 | err = trans.Put(dbkey, trueval, nil)
342 | if err != nil {
343 | s.lg.Warnf("Failed adding key %s to allkeys", dbkey)
344 | }
345 | put += 1
346 | }
347 |
348 | for key, latency := range latencies {
349 | s.lg.Debugf("%s latency (size %d)\n", key, len(latency))
350 | }
351 |
352 | err = trans.Commit()
353 | if err != nil {
354 | s.lg.Errorf("Error committing transaction %s", err.Error())
355 | s.TickN("database", "get", fetched)
356 | s.TickN("database", "put", put)
357 | s.TickN("database", "get_error", fetched)
358 | s.TickN("database", "put_error", put)
359 | }
360 | s.TickN("database", "get", fetched)
361 | s.TickN("database", "put", put)
362 | }
363 |
364 | func (s *Stats) ListKeys(rt *Runtime) (*[]string, error) {
365 | ret := make([]string, 0)
366 | iter := rt.DB.NewIterator(util.BytesPrefix([]byte("stats:allkeys:")), nil)
367 | cnt := 0
368 | for iter.Next() {
369 | ret = append(ret, string(iter.Key()[14:])) // strip of stats:allkeys:
370 | cnt += 1
371 | }
372 | s.TickN("database", "get", uint64(cnt))
373 | if err := iter.Error(); err != nil {
374 | s.Tick("database", "get_error")
375 | return nil, err
376 | }
377 | iter.Release()
378 | return &ret, nil
379 | }
380 |
--------------------------------------------------------------------------------