├── 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 | [![Build Status](https://travis-ci.org/tenta-browser/tenta-dns.svg?branch=master)](https://travis-ci.org/tenta-browser/tenta-dns) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/tenta-browser/tenta-dns)](https://goreportcard.com/report/github.com/tenta-browser/tenta-dns) 6 | [![GoDoc](https://godoc.org/github.com/tenta-browser/tenta-dns?status.svg)](https://godoc.org/github.com/tenta-browser/tenta-dns) 7 | 8 | ![Tenta Gopher](logo.png?raw=true "Tenta Gopher") 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 | --------------------------------------------------------------------------------