├── .gitignore ├── .travis.yml ├── AUTHORS ├── ChangeLog.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── trustydns-dig │ ├── Makefile │ ├── config.go │ ├── main.go │ ├── main_test.go │ ├── usage.go │ └── usage_test.go ├── trustydns-proxy │ ├── Makefile │ ├── config.go │ ├── main.go │ ├── main_test.go │ ├── reporter.go │ ├── reporter_test.go │ ├── server.go │ ├── server_test.go │ ├── state.go │ ├── testdata │ │ ├── emptyfile │ │ └── resolv.conf │ ├── usage.go │ └── usage_test.go └── trustydns-server │ ├── Makefile │ ├── config.go │ ├── main.go │ ├── main_test.go │ ├── reporter.go │ ├── reporter_test.go │ ├── server.go │ ├── server_test.go │ ├── state.go │ ├── testdata │ ├── emptyfile │ ├── resolv.conf │ ├── rootCA.cert │ ├── server.cert │ └── server.key │ ├── usage.go │ └── usage_test.go ├── docs ├── ECS.md ├── TODO.md ├── unbound.md └── windows.md ├── go.mod ├── go.sum ├── internal ├── bestserver │ ├── base.go │ ├── base_test.go │ ├── doc.go │ ├── latency.go │ ├── latency_test.go │ ├── manager.go │ ├── traditional.go │ └── traditional_test.go ├── concurrencytracker │ ├── counter.go │ └── counter_test.go ├── connectiontracker │ ├── reporter.go │ ├── reporter_test.go │ ├── tracker.go │ └── tracker_test.go ├── constants │ ├── constants.go │ └── constants_test.go ├── dnsutil │ ├── compact.go │ ├── compact_test.go │ ├── msg.go │ ├── msg_test.go │ ├── padding.go │ └── padding_test.go ├── flagutil │ ├── stringvalue.go │ └── stringvalue_test.go ├── osutil │ ├── allowed_linux.go │ ├── allowed_unix.go │ ├── constrain.go │ ├── constrain_test.go │ ├── constrain_windows.go │ ├── signal_unix.go │ └── signal_windows.go ├── reporter │ └── reporter.go ├── resolver │ ├── doh │ │ ├── config.go │ │ ├── reporter.go │ │ ├── reporter_test.go │ │ ├── resolver.go │ │ └── resolver_test.go │ ├── local │ │ ├── config.go │ │ ├── reporter.go │ │ ├── reporter_test.go │ │ ├── resolver.go │ │ ├── resolver_test.go │ │ └── testdata │ │ │ ├── empty.resolv.conf │ │ │ ├── resolv.conf │ │ │ ├── simplest.resolv.conf │ │ │ ├── timeout.resolv.conf │ │ │ └── two.resolv.conf │ └── resolver.go └── tlsutil │ ├── client.go │ ├── client_test.go │ ├── loadroots.go │ ├── loadroots_test.go │ ├── server.go │ ├── server_test.go │ └── testdata │ ├── proxy.cert │ ├── proxy.key │ └── rootCA.cert ├── openssl ├── README.md ├── make_proxy_cert ├── make_rootca_cert ├── make_server_cert └── site.conf └── tools ├── README.md ├── daily-proxy-stats ├── daily-server-stats ├── tdt-analyze-proxylog ├── tdt-analyze-serverlog └── tdt-cat-yesterday-multilogs /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dev environment 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.19 5 | - tip 6 | 7 | before_install: 8 | - go get -t -v ./... 9 | 10 | script: 11 | - go test -race -coverprofile=coverage.txt -covermode=atomic ./... 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Mark Delany 2 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Trustydns Change Log 2 | ### v0.3.0 -- 2021-11-04 3 | * Add --gops flag to server and proxy 4 | ### v0.2.1 -- 2021-03-23 5 | * Make sure all version values match - should probably make this more robust 6 | * Add missing changes to v0.2.0 log entry 7 | * Remove 'updatepackages' make target 8 | ### v0.2.0 -- 2021-03-21 9 | * Cross-compiles and runs on Windows 10 | * Pull request #3 "Use time.Since" from @muesli 11 | * Set trustydns-proxy default padding option (-p) to false as documented 12 | * Move to go 1.16 and use go modules with semantic version tagging 13 | For now I'll retain the 'updatepackages' make target but it's superflous with go modules 14 | ### v0.1.0 -- 2019-06-28 15 | * Initial public release. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, 2020 Mark Delany 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This is just a dumb makefile that provides a few short-hand build targets and install 2 | # destinations. It's not meant to be robust or efficient or even particularly flexible, but 3 | # hopefully the simplicity makes it easy for you to use and modify to suit your own needs. 4 | # 5 | # This makefile also has a few examples of how to cross-compile the executables for other 6 | # architectures, such as home routers. 7 | 8 | daemondest=/usr/local/sbin 9 | cmddest=/usr/local/bin 10 | cmddirs=cmd/trustydns-dig cmd/trustydns-proxy cmd/trustydns-server 11 | commands=cmd/trustydns-server/trustydns-server cmd/trustydns-proxy/trustydns-proxy cmd/trustydns-dig/trustydns-dig 12 | 13 | targets: 14 | @echo "Installation targets: 'clean', 'all', and 'install'" 15 | @echo "Developer targets: 'clean', 'fmt' and 'test'" 16 | @echo "Cross-platform targets: 'mips64', 'debian64', 'pi3b', 'freebsdarm64', 'freebsd64', 'windowsamd64' and 'windows386'" 17 | 18 | .PHONY: all 19 | all: $(commands) 20 | 21 | cmd/trustydns-server/trustydns-server cmd/trustydns-proxy/trustydns-proxy cmd/trustydns-dig/trustydns-dig: 22 | $(MAKE) -C `dirname $@` all 23 | 24 | .PHONY: race 25 | race: 26 | @for dir in $(cmddirs); do echo $$dir; $(MAKE) -C $$dir $@; done 27 | 28 | .PHONY: clean vet test 29 | clean vet test: 30 | go $@ ./... 31 | 32 | .PHONY: testrace 33 | testrace: 34 | go test -race ./... 35 | 36 | .PHONY: critic 37 | critic: 38 | gocritic check ./... 39 | 40 | .PHONY: fmt 41 | fmt: 42 | gofmt -s -w `find . -name '*.go' -type f -print` 43 | 44 | .PHONY: install 45 | install: $(commands) 46 | install -d -o 0 -g 0 -m a=rx $(daemondest) $(cmddest) 47 | install -p -o 0 -g 0 -m a=rx cmd/trustydns-server/trustydns-server $(daemondest) 48 | install -p -o 0 -g 0 -m a=rx cmd/trustydns-proxy/trustydns-proxy $(daemondest) 49 | install -p -o 0 -g 0 -m a=rx cmd/trustydns-dig/trustydns-dig $(cmddest) 50 | 51 | .PHONY: mips64 52 | mips64: clean 53 | @echo 'Building for mips64 Linux targets (particularly Ubiquiti er3 and er6)' 54 | @GOOS=linux GOARCH=mips64 $(MAKE) all 55 | @file $(commands) 56 | 57 | .PHONY: debian64 58 | debian64: clean 59 | @echo 'Building for amd64 Debian (as the Debian go package is antideluvian)' 60 | @GOOS=linux GOARCH=amd64 $(MAKE) all 61 | @file $(commands) 62 | 63 | .PHONY: pi3b 64 | pi3b: clean 65 | @echo 'Building for Raspberry Pi3 Model B (32-bit armv71)' 66 | @GOOS=linux GOARCH=arm $(MAKE) all 67 | @file $(commands) 68 | 69 | .PHONY: freebsdarm64 70 | freebsdarm64: clean 71 | @echo 'Building for aarch64 Freebsd targets (particularly Pi4 Model B (64-bit armv8)' 72 | @GOOS=freebsd GOARCH=arm64 $(MAKE) all 73 | @file $(commands) 74 | 75 | .PHONY: freebsd64 76 | freebsd64: clean 77 | @echo Building for amd64 FreeBSD 78 | @GOOS=freebsd GOARCH=amd64 $(MAKE) all 79 | @file $(commands) 80 | 81 | .PHONY: windowsamd64 82 | windowsamd64: clean 83 | @echo Building for amd64 Windows 84 | @GOOS=windows GOARCH=amd64 $(MAKE) all 85 | 86 | .PHONY: windows386 87 | windows386: clean 88 | @echo Building for 386 Windows 89 | @GOOS=windows GOARCH=386 $(MAKE) all 90 | -------------------------------------------------------------------------------- /cmd/trustydns-dig/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all race clean test 2 | 3 | all: 4 | go build 5 | 6 | race: 7 | CGO_ENABLED=1 go build -race 8 | 9 | clean: 10 | go clean 11 | 12 | test: 13 | go test 14 | -------------------------------------------------------------------------------- /cmd/trustydns-dig/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/markdingo/trustydns/internal/flagutil" 7 | "github.com/markdingo/trustydns/internal/resolver/doh" 8 | ) 9 | 10 | type config struct { 11 | help bool 12 | parallel bool 13 | short bool 14 | version bool 15 | 16 | repeatCount int 17 | requestTimeout time.Duration 18 | ecsSet string 19 | 20 | tlsClientCertFile string 21 | tlsClientKeyFile string 22 | tlsCAFiles flagutil.StringValue // Non-system root CAs 23 | tlsUseSystemRootCAs bool // Do/Do not use system root CAs 24 | 25 | dohConfig doh.Config 26 | } 27 | -------------------------------------------------------------------------------- /cmd/trustydns-dig/main.go: -------------------------------------------------------------------------------- 1 | // Issue a DoH DNS query to a trustydns-server 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "time" 15 | 16 | "github.com/markdingo/trustydns/internal/constants" 17 | "github.com/markdingo/trustydns/internal/resolver" 18 | "github.com/markdingo/trustydns/internal/resolver/doh" 19 | "github.com/markdingo/trustydns/internal/tlsutil" 20 | 21 | "github.com/miekg/dns" 22 | "golang.org/x/net/http2" 23 | ) 24 | 25 | // Program-wide variables 26 | var ( 27 | consts = constants.Get() 28 | cfg *config 29 | 30 | stdout io.Writer 31 | stderr io.Writer 32 | 33 | flagSet *flag.FlagSet 34 | ) 35 | 36 | ////////////////////////////////////////////////////////////////////// 37 | 38 | func fatal(args ...interface{}) int { 39 | fmt.Fprint(stderr, "Fatal: ", consts.DigProgramName, ": ") 40 | fmt.Fprintln(stderr, args...) 41 | 42 | return 1 43 | } 44 | 45 | ////////////////////////////////////////////////////////////////////// 46 | // main is a wrapper for mainExecute() so tests can call mainExecute() 47 | ////////////////////////////////////////////////////////////////////// 48 | 49 | func mainInit(out io.Writer, err io.Writer) { 50 | cfg = &config{} 51 | stdout = out 52 | stderr = err 53 | } 54 | 55 | func main() { 56 | mainInit(os.Stdout, os.Stderr) 57 | os.Exit(mainExecute(os.Args)) 58 | } 59 | 60 | func mainExecute(args []string) int { 61 | flagSet = flag.NewFlagSet(args[0], flag.ContinueOnError) 62 | flagSet.SetOutput(stderr) 63 | err := parseCommandLine(args) 64 | if err != nil { 65 | return 1 // Error already printed by the flag package 66 | } 67 | if cfg.help { 68 | usage(stdout) 69 | return 0 70 | } 71 | if cfg.version { 72 | fmt.Fprintln(stdout, consts.DigProgramName, "Version:", consts.Version) 73 | return 0 74 | } 75 | 76 | // Validate repeat count 77 | 78 | if cfg.repeatCount < 0 { 79 | return fatal("Repeat count (-r) must be GE zero, not", cfg.repeatCount) 80 | } 81 | 82 | // Validate ECS settings 83 | 84 | var ecsIPNet *net.IPNet 85 | if len(cfg.ecsSet) > 0 { 86 | var err error 87 | _, ecsIPNet, err = net.ParseCIDR(cfg.ecsSet) 88 | if err != nil { 89 | return fatal("--ecs-set", err) 90 | } 91 | if cfg.dohConfig.ECSRequestIPv4PrefixLen != 0 || cfg.dohConfig.ECSRequestIPv6PrefixLen != 0 { 92 | return fatal("Cannot have both --ecs-set and --ecs-request-* options set at the same time") 93 | } 94 | } 95 | 96 | if cfg.dohConfig.ECSRequestIPv4PrefixLen < 0 || cfg.dohConfig.ECSRequestIPv4PrefixLen > 32 { 97 | return fatal("--ecs-request-ipv4-prefixlen", cfg.dohConfig.ECSRequestIPv4PrefixLen, 98 | "must be between 0 and 32") 99 | } 100 | if cfg.dohConfig.ECSRequestIPv6PrefixLen < 0 || cfg.dohConfig.ECSRequestIPv6PrefixLen > 128 { 101 | return fatal("--ecs-request-ipv6-prefixlen", cfg.dohConfig.ECSRequestIPv6PrefixLen, 102 | "must be between 0 and 128") 103 | } 104 | 105 | remainingOptions := flagSet.NArg() // Track command line options 106 | optionIndex := 0 107 | 108 | // Validate DoH from command line: DoHServer qName [qType] 109 | 110 | if remainingOptions < 1 { 111 | return fatal("Require DoH Server URL on command line. Consider -h") 112 | } 113 | dohServerURL := flagSet.Arg(optionIndex) 114 | if len(dohServerURL) == 0 { 115 | return fatal("DoH Server URL cannot be an empty string") 116 | } 117 | optionIndex++ 118 | remainingOptions-- 119 | u, err := url.Parse(dohServerURL) 120 | if err != nil { 121 | return fatal(err) 122 | } 123 | if len(u.Scheme) == 0 && len(u.Host) == 0 && len(u.Path) > 0 { // A plain FQDN looks like this 124 | u.Host = u.Path 125 | u.Path = "" 126 | } 127 | if len(u.Host) == 0 { 128 | return fatal(dohServerURL, "does not contain a hostname") 129 | } 130 | if len(u.Scheme) == 0 { 131 | u.Scheme = "https" 132 | } 133 | dohServerURL = u.String() // Put possibly modified URL back into the config 134 | 135 | // Validate qName 136 | 137 | if remainingOptions < 1 { 138 | return fatal("Require qName on command line. Consider -h") 139 | } 140 | 141 | qName := dns.Fqdn(flagSet.Arg(optionIndex)) 142 | optionIndex++ 143 | remainingOptions-- 144 | 145 | // Validate qType - if present 146 | 147 | qTypeString := dns.TypeToString[dns.TypeA] // Default to an "A" query 148 | if remainingOptions > 0 { 149 | qTypeString = strings.ToUpper(flagSet.Arg(optionIndex)) 150 | optionIndex++ 151 | remainingOptions-- 152 | } 153 | qType, ok := dns.StringToType[qTypeString] // Does miekg know about this type? 154 | if !ok { 155 | return fatal("Unrecognized qType of", qTypeString) 156 | } 157 | 158 | // Make sure there is no residual goop on the command line 159 | 160 | if remainingOptions > 0 { 161 | return fatal("Don't know what to do with residual goop on command line:", flagSet.Arg(optionIndex)) 162 | } 163 | 164 | // Create TLS configuration for constructing HTTPS transport. This is where we set up 165 | // verification of server certs and activate http2. 166 | 167 | client := &http.Client{Timeout: cfg.requestTimeout} 168 | tlsConfig, err := tlsutil.NewClientTLSConfig(cfg.tlsUseSystemRootCAs, cfg.tlsCAFiles.Args(), 169 | cfg.tlsClientCertFile, cfg.tlsClientKeyFile) 170 | if err != nil { 171 | return fatal(err) 172 | } 173 | 174 | tr := &http.Transport{TLSClientConfig: tlsConfig} 175 | if err := http2.ConfigureTransport(tr); err != nil { // Use latest http2 support - is this still needed? 176 | return fatal(err) 177 | } 178 | client.Transport = tr 179 | 180 | // Complete doh Config settings and construct the DoH resolver 181 | cfg.dohConfig.ECSSetCIDR = ecsIPNet 182 | cfg.dohConfig.ServerURLs = []string{dohServerURL} 183 | 184 | dohResolver, err := doh.New(cfg.dohConfig, client) 185 | if err != nil { 186 | return fatal(err) 187 | } 188 | 189 | // Verify that the remote resolver handles this FQDN 190 | if !dohResolver.InBailiwick(qName) { 191 | return fatal("qName cannot be resolved remotely. Is it a valid FQDN?", qName) 192 | } 193 | 194 | // Issue the query the requested number of times 195 | 196 | chOut := make(chan string, 1) // Queries write to a chan so we can parallelize 197 | chErr := make(chan string, 1) // and reap and print the outputs without interleaving. 198 | if cfg.parallel { 199 | for qx := 0; qx < cfg.repeatCount; qx++ { 200 | go doQuery(chOut, chErr, dohResolver, qName, qType, cfg.short) 201 | } 202 | for qx := 0; qx < cfg.repeatCount; qx++ { 203 | s := <-chOut 204 | fmt.Fprint(stdout, s) 205 | s = <-chErr 206 | fmt.Fprint(stderr, s) 207 | } 208 | } else { 209 | for qx := 0; qx < cfg.repeatCount; qx++ { 210 | doQuery(chOut, chErr, dohResolver, qName, qType, cfg.short) 211 | s := <-chOut 212 | fmt.Fprint(stdout, s) 213 | s = <-chErr 214 | fmt.Fprint(stderr, s) 215 | } 216 | } 217 | 218 | return 0 219 | } 220 | 221 | ////////////////////////////////////////////////////////////////////// 222 | 223 | func doQuery(chOut, chErr chan string, dohResolver resolver.Resolver, qName string, qType uint16, short bool) { 224 | outBuf := &bytes.Buffer{} 225 | errBuf := &bytes.Buffer{} 226 | defer func() { 227 | chOut <- outBuf.String() 228 | chErr <- errBuf.String() 229 | }() 230 | query := &dns.Msg{} 231 | query.SetQuestion(dns.Fqdn(qName), qType) 232 | resp, respMeta, err := dohResolver.Resolve(query, nil) 233 | if err != nil { 234 | fmt.Fprintln(errBuf, "Error:", err) 235 | return 236 | } 237 | 238 | if short { 239 | for _, rr := range resp.Answer { 240 | fmt.Fprintln(outBuf, rr.String()) 241 | } 242 | } else { 243 | fmt.Fprintln(outBuf, resp) 244 | 245 | fmt.Fprintf(outBuf, ";; Query Time: %s/%s\n", 246 | respMeta.TransportDuration.Truncate(time.Millisecond).String(), 247 | respMeta.ResolutionDuration.Truncate(time.Millisecond).String()) 248 | fmt.Fprintf(outBuf, ";; Final Server: %s\n", respMeta.FinalServerUsed) 249 | fmt.Fprintf(outBuf, ";; Tries: %d(queries) %d(servers)\n", respMeta.QueryTries, respMeta.ServerTries) 250 | fmt.Fprintf(outBuf, ";; Payload Size: %d\n", respMeta.PayloadSize) 251 | fmt.Fprintln(outBuf) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /cmd/trustydns-dig/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type testCase struct { 11 | args []string 12 | stdout []string 13 | stderr string 14 | } 15 | 16 | var mainTestCases = []testCase{ 17 | {[]string{"http://localhost:63080", "example.net"}, []string{}, "connection refused"}, 18 | {[]string{"-r", "2", "http://localhost:63080", "example.net"}, []string{}, "connection refused"}, 19 | {[]string{"-p", "-r", "2", "http://localhost:63080", "example.net"}, []string{}, "connection refused"}, 20 | {[]string{"-g", "http://localhost:63080", "example.net"}, []string{}, "connection refused"}, 21 | {[]string{"--ecs-set", "10.0.120.0/24", "http://localhost:63080", "example.net"}, []string{}, 22 | "connection refused"}, 23 | 24 | {[]string{"localhost", "example.net"}, []string{}, "connection refused"}, 25 | 26 | {[]string{"-t", "xx", "http://localhost:63080", "example.net"}, []string{}, "invalid value"}, 27 | {[]string{"--tls-cert", "/dev/null", "http://localhost:63080", "example.net"}, []string{}, 28 | "key file missing"}, 29 | 30 | // These tests may or may not work depending on whether the public server is accessible 31 | 32 | {[]string{"https://mozilla.cloudflare-dns.com/dns-query", "time-osx.g.aaplimg.com"}, 33 | []string{"Query Time", "17.", "status: NOERROR"}, ""}, 34 | {[]string{"--short", "https://mozilla.cloudflare-dns.com/dns-query", "time-osx.g.aaplimg.com"}, 35 | []string{"time-osx.g.aaplimg.com", "IN", "17."}, ""}, 36 | } 37 | 38 | func TestMain(t *testing.T) { 39 | for tx, tc := range mainTestCases { 40 | runTest(t, tx, tc) 41 | } 42 | } 43 | 44 | // This function is used by usage_test.go as well 45 | func runTest(t *testing.T, tx int, tc testCase) { 46 | t.Run(fmt.Sprintf("%d", tx), func(t *testing.T) { 47 | args := append([]string{"trustydns-dig"}, tc.args...) 48 | out := &bytes.Buffer{} 49 | err := &bytes.Buffer{} 50 | mainInit(out, err) 51 | ec := mainExecute(args) 52 | 53 | outStr := out.String() 54 | errStr := err.String() 55 | 56 | if ec != 0 && len(tc.stderr) == 0 { 57 | t.Error("Unexpected non-zero exit code", ec, outStr, errStr) 58 | } 59 | 60 | if len(errStr) > 0 && len(tc.stderr) == 0 { 61 | t.Error("Did not expect stderr:", errStr) 62 | } 63 | if len(tc.stderr) > 0 && !strings.Contains(errStr, tc.stderr) { 64 | t.Error("Stderr expected:\n", tc.stderr, "Got:\n", errStr, args) 65 | } 66 | for _, o := range tc.stdout { 67 | if !strings.Contains(outStr, o) { 68 | t.Error("Stdout expected:\n", o, "Got:\n", outStr, args) 69 | } 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/trustydns-dig/usage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "text/template" 7 | "time" 8 | ) 9 | 10 | // The "flag" package is not tty aware so we've arbitrarily picked 100 columns as a conservative tty 11 | // width for the usage output. 12 | 13 | const usageMessageTemplate = ` 14 | NAME 15 | {{.DigProgramName}} -- a DNS Over HTTPS query program 16 | 17 | SYNOPSIS 18 | {{.DigProgramName}} [options] DoH-server-URL FQDN [DNS-qType] 19 | 20 | DESCRIPTION 21 | {{.DigProgramName}} issues DNS over HTTPS queries to {{.ServerProgramName}}. Some options generate 22 | specific request features that are unlikely to be available in normal DoH servers. 23 | Only qClass=IN is supported. If a DNS-Type is not supplied then qType=A is used. 24 | 25 | The primary purpose of {{.DigProgramName}} is to issue queries exactly as they are issued 26 | by {{.ProxyProgramName}} and thus test the feature exchange between it and the {{.ServerProgramName}}. 27 | In fact {{.DigProgramName}} purposely uses the same packages as {{.ProxyProgramName}}. 28 | 29 | ********** 30 | Production Use Alert: {{.DigProgramName}} is a diagnostic program which will almost certainly 31 | change with each new package release. Please do not rely on its current behaviour 32 | or output format and definitely do not use it in a shell script. 33 | ********** 34 | 35 | EXAMPLES 36 | When using an instance of {{.ServerProgramName}}: 37 | 38 | $ {{.DigProgramName}} \ 39 | --ecs-request-ipv4-prefixlen 24 --ecs-request-ipv6-prefixlen 64 \ 40 | https://trustydns-server.example.net/dns-query yahoo.com MX 41 | 42 | When using a third-party DoH server from, say, Mozilla or quad9: 43 | 44 | $ {{.DigProgramName}} https://mozilla.cloudflare-dns.com/dns-query yahoo.com MX 45 | $ {{.DigProgramName}} --ecs-set 17.0.0.0/18 https://dns.quad9.net/dns-query yahoo.com 46 | 47 | OPTIONS 48 | [-ghp] [--short] 49 | 50 | [-r repeat count] [-t remote request timeout] 51 | 52 | [--ecs-remove] 53 | [ **Either** 54 | [--ecs-request-ipv4-prefixlen prefix-len] 55 | [--ecs-request-ipv6-prefixlen prefix-len] 56 | | **Or** 57 | [--ecs-set CIDR] 58 | ] 59 | 60 | [--padding] 61 | [--tls-cert TLS Client Certificate file] 62 | [--tls-key TLS Client Key file] 63 | [--tls-other-roots TLS Root Certificate file...] 64 | [--tls-use-system-roots] 65 | [--version] 66 | ` 67 | 68 | ////////////////////////////////////////////////////////////////////// 69 | 70 | func usage(out io.Writer) { 71 | tmpl, err := template.New("usage").Parse(usageMessageTemplate) 72 | if err != nil { 73 | panic(err) // We've messed up our template 74 | } 75 | err = tmpl.Execute(out, consts) 76 | if err != nil { 77 | panic(err) // We've messed up our template 78 | } 79 | flagSet.SetOutput(out) 80 | flagSet.PrintDefaults() 81 | fmt.Fprintln(out, "\nVersion:", consts.Version) 82 | } 83 | 84 | // parseCommandLine sets up the flags-to-config mapping and parses the supplied command line 85 | // arguments. It starts from scratch each time to make it eaiser for test wrappers to use. 86 | func parseCommandLine(args []string) error { 87 | flagSet.BoolVar(&cfg.dohConfig.UseGetMethod, "g", false, "Use HTTP GET with the 'dns' query parameter (instead of POST)") 88 | flagSet.BoolVar(&cfg.help, "h", false, "Print usage message to Stdout then exit(0)") 89 | flagSet.BoolVar(&cfg.parallel, "p", false, "Issue all queries in parallel") 90 | flagSet.IntVar(&cfg.repeatCount, "r", 1, "`Number` of times to issue the query (GE zero)") 91 | 92 | flagSet.BoolVar(&cfg.short, "short", false, "Generate short output showing only Answer RRs") 93 | 94 | flagSet.DurationVar(&cfg.requestTimeout, "t", time.Second*15, "Remote request `timeout`") 95 | 96 | flagSet.BoolVar(&cfg.dohConfig.ECSRemove, "ecs-remove", false, "Remove inbound ECS") 97 | flagSet.IntVar(&cfg.dohConfig.ECSRequestIPv4PrefixLen, "ecs-request-ipv4-prefixlen", 0, 98 | "Server-side IPv4 ECS synthesis `Prefix-Length` (normally 24 when used)") 99 | flagSet.IntVar(&cfg.dohConfig.ECSRequestIPv6PrefixLen, "ecs-request-ipv6-prefixlen", 0, 100 | "Server-side IPv6 ECS synthesis `Prefix-Length` (normally 64 when used)") 101 | flagSet.StringVar(&cfg.ecsSet, "ecs-set", "", "`CIDR` to set ECS IP Address and Prefix Length") 102 | 103 | flagSet.BoolVar(&cfg.dohConfig.GeneratePadding, "padding", true, "Add RFC8467 recommended padding to queries") 104 | 105 | flagSet.StringVar(&cfg.tlsClientCertFile, "tls-cert", "", "TLS Client Certificate `file`") 106 | flagSet.StringVar(&cfg.tlsClientKeyFile, "tls-key", "", "TLS Client Key `file`") 107 | flagSet.Var(&cfg.tlsCAFiles, "tls-other-roots", "Non-system Root CA `file` used to validate HTTPS endpoint") 108 | flagSet.BoolVar(&cfg.tlsUseSystemRootCAs, "tls-use-system-roots", true, 109 | "Validate HTTPS endpoints with root CAs") 110 | 111 | flagSet.BoolVar(&cfg.version, "version", false, "Print version and exit") 112 | 113 | return flagSet.Parse(args[1:]) 114 | } 115 | -------------------------------------------------------------------------------- /cmd/trustydns-dig/usage_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var usageTestCases = []testCase{ 8 | {[]string{}, []string{}, "Fatal: trustydns-dig: Require DoH Server URL on command line. Consider -h"}, 9 | {[]string{"-h"}, []string{"NAME", "SYNOPSIS", "OPTIONS", "Version: v"}, ""}, 10 | {[]string{"--version"}, []string{"Version: v"}, ""}, 11 | {[]string{"-badopt"}, []string{}, "flag provided but not defined"}, 12 | 13 | {[]string{"--ecs-set", "10.0.120.XXX/24", "http://localhost:63080", "example.net"}, []string{}, 14 | "invalid CIDR address"}, 15 | {[]string{"--ecs-set", "10.0.120.0/24", "--ecs-request-ipv4-prefixlen", "24", 16 | "http://localhost:63080", "example.net"}, []string{}, 17 | "Cannot have both --ecs-set and --ecs-request"}, 18 | 19 | {[]string{"--ecs-set", "10.0.120.0/24", "--ecs-request-ipv6-prefixlen", "66", 20 | "http://localhost:63080", "example.net"}, []string{}, 21 | "Cannot have both --ecs-set and --ecs-request"}, 22 | {[]string{"--ecs-request-ipv6-prefixlen", "200", 23 | "http://localhost:63080", "example.net"}, []string{}, 24 | "must be between 0 and 128"}, 25 | {[]string{"--ecs-request-ipv4-prefixlen", "200", 26 | "http://localhost:63080", "example.net"}, []string{}, 27 | "must be between 0 and 32"}, 28 | 29 | {[]string{"", "example.net"}, []string{}, "URL cannot be an empty string"}, 30 | {[]string{"htts://localhost", "example.net"}, []string{}, "unsupported"}, 31 | {[]string{"http://", "example.net"}, []string{}, "does not contain a hostname"}, 32 | {[]string{"httpX://localhost/xxx", "example.net"}, []string{}, "unsupported protocol scheme"}, 33 | {[]string{"://localhost/xxx", "example.net"}, []string{}, "missing protocol scheme"}, 34 | {[]string{"http://localhost:63080"}, []string{}, "Require qName on command"}, 35 | {[]string{"http://localhost:63080", "example.net", "BADTYPE"}, []string{}, "Unrecognized qType"}, 36 | {[]string{"http://localhost:63080", "example.net", "AAAA", "goop"}, []string{}, "know what to do"}, 37 | 38 | {[]string{"-t", "xx", "http://localhost:63080", "example.net"}, []string{}, "invalid value"}, 39 | {[]string{"--tls-cert", "/dev/null", "http://localhost:63080", "example.net"}, []string{}, 40 | "key file missing"}, 41 | {[]string{"--tls-key", "/dev/null", "http://localhost:63080", "example.net"}, []string{}, 42 | "cert file missing"}, 43 | 44 | {[]string{"http://localhost:63080", "example.."}, []string{}, "Is it a valid FQDN"}, 45 | 46 | {[]string{"-r", "-1", "http://localhost:63080", "example.net"}, []string{}, "Repeat count"}, 47 | } 48 | 49 | func TestUsage(t *testing.T) { 50 | for tx, tc := range usageTestCases { 51 | runTest(t, tx, tc) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all race clean test 2 | 3 | all: 4 | go build 5 | 6 | race: 7 | CGO_ENABLED=1 go build -race 8 | 9 | clean: 10 | go clean 11 | 12 | test: 13 | go test 14 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/markdingo/trustydns/internal/flagutil" 7 | "github.com/markdingo/trustydns/internal/resolver/doh" 8 | ) 9 | 10 | type config struct { 11 | gops bool 12 | help bool 13 | tcp bool // Listen on TCP 14 | udp bool // Listen on UDP 15 | verbose bool 16 | version bool 17 | 18 | listenAddresses flagutil.StringValue // Listen address for inbound DNS queries 19 | 20 | localResolvConf string 21 | localDomains flagutil.StringValue // In addition to those in resolv.conf 22 | statusInterval time.Duration 23 | 24 | maximumRemoteConnections int 25 | requestTimeout time.Duration 26 | ecsSet string 27 | 28 | logAll bool // Turns on all other log options 29 | logClientIn bool // Print the DNS query arriving from the client 30 | logClientOut bool // Print the DNS response returned to the client 31 | logTLSErrors bool // Print x509 errors returned from the DoH Resolver 32 | 33 | tlsClientCertFile string // Connect to the DoH Server using these credentials 34 | tlsClientKeyFile string 35 | tlsCAFiles flagutil.StringValue // Non-system root CAs to validate DoH Servers 36 | tlsUseSystemRootCAs bool // Do/Do not use system root CAs to validate DoH Servers 37 | 38 | dohConfig doh.Config 39 | 40 | cpuprofile, memprofile string 41 | 42 | setuidName, setgidName, chrootDir string // Process constraint settings 43 | } 44 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | "syscall" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // We use a bytes.Buffer as stdout, stderr which is shared across multiple go-routines so we need to 15 | // protected it from concurrent access. This is test-only code but -race doesn't know that. 16 | type mutexBytesBuffer struct { 17 | mu sync.Mutex 18 | buffer bytes.Buffer 19 | } 20 | 21 | func (t *mutexBytesBuffer) Write(p []byte) (n int, err error) { 22 | t.mu.Lock() 23 | defer t.mu.Unlock() 24 | 25 | return t.buffer.Write(p) 26 | } 27 | 28 | func (t *mutexBytesBuffer) String() string { 29 | t.mu.Lock() 30 | defer t.mu.Unlock() 31 | 32 | return t.buffer.String() 33 | } 34 | 35 | ////////////////////////////////////////////////////////////////////// 36 | 37 | type mainTestCase struct { 38 | description string 39 | needsRoot bool // Only run if we're setuid 0 40 | willRunFor time.Duration // trustydns-proxy should run for this amount of time before being terminated 41 | args []string // ARGV - not counting command 42 | stdout []string // Expected stdout strings 43 | stderr string // Expected stderr string 44 | } 45 | 46 | // The -A 255.... arguments are present to cause mainExecute() to fail when it starts *after* 47 | // exercising the code coverage area intended. 48 | 49 | var mainTestCases = []mainTestCase{ 50 | {"ecs-set", 51 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62081", "-v", 52 | "--ecs-set", "10.0.120.0/24", "http://localhost:63080"}, []string{"Starting"}, ""}, 53 | 54 | {"ecs-request-prefix", 55 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62082", "-v", 56 | "--ecs-request-ipv4-prefixlen", "20", "--ecs-request-ipv6-prefixlen", "56", 57 | "http://localhost:63080"}, []string{"Starting"}, ""}, 58 | 59 | {"URL mangling", // Silently runs with a mangled URL 60 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62083", "localhost"}, []string{}, ""}, 61 | 62 | {"URL syntax", // Silently runs with a dodgy (but legal) URL 63 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62084", "///localhost"}, []string{}, ""}, 64 | 65 | {"Good URL with scheme", 66 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62085", "-v", "http://localhost"}, 67 | []string{"Starting", "Exiting"}, ""}, 68 | 69 | {"Good URL No Scheme", 70 | false, 100 * time.Millisecond, []string{"-g", "-v", "-A", "127.0.0.1:62086", "localhost"}, 71 | []string{"Starting", "Exiting"}, ""}, 72 | 73 | {"Good local resolver config", 74 | false, 100 * time.Millisecond, []string{"-v", "-A", "127.0.0.1:62087", 75 | "-c", "testdata/resolv.conf", "http://localhost"}, 76 | []string{"Starting", "Exiting"}, ""}, 77 | 78 | {"log-all", 79 | false, 100 * time.Millisecond, 80 | []string{"-v", "--log-all", "-A", "127.0.0.1:62088", 81 | "-c", "testdata/resolv.conf", "http://localhost"}, 82 | []string{"Starting", "Exiting"}, ""}, 83 | 84 | {"Status report", 85 | false, 2 * time.Second, []string{"-v", "-i", "1s", "-A", "127.0.0.1:62089", "http://localhost"}, 86 | []string{"Status Server:"}, ""}, 87 | 88 | {"CPU Profile", 89 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62090", "--cpu-profile", "testdata/cpu", 90 | "http://localhost"}, []string{}, ""}, 91 | {"Mem Profile", 92 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:62091", "--mem-profile", "testdata/mem", 93 | "http://localhost"}, []string{}, ""}, 94 | 95 | {"Wildcard listen address", 96 | true, 100 * time.Millisecond, []string{"http://localhost"}, []string{}, ""}, 97 | } 98 | 99 | // TestMain tests legitimate usage invocations 100 | func TestMain(t *testing.T) { 101 | uid := os.Getuid() 102 | for _, tc := range mainTestCases { 103 | t.Run(tc.description, func(t *testing.T) { 104 | if tc.needsRoot && uid != 0 { 105 | t.Skip("Skipping setuid=0 test as not running as root") 106 | return 107 | } 108 | args := append([]string{"trustydns-proxy"}, tc.args...) 109 | out := &mutexBytesBuffer{} 110 | err := &mutexBytesBuffer{} 111 | mainInit(out, err) 112 | done := make(chan error) 113 | go func() { 114 | done <- waitForMainExecute(t, tc.willRunFor) 115 | }() 116 | ec := mainExecute(args) 117 | e := <-done // Get waitForMainExecute results 118 | if e != nil { 119 | t.Log("wfmeO:", out.String()) 120 | t.Log("wfmeE:", err.String()) 121 | t.Fatal(e) 122 | } 123 | if ec == 0 && tc.willRunFor == 0 { 124 | t.Error("Non-zero Exit code expected") 125 | } 126 | if ec != 0 && tc.willRunFor > 0 { 127 | t.Error("Zero Exit code expected, not:", ec) 128 | } 129 | 130 | outStr := out.String() 131 | errStr := err.String() 132 | if len(errStr) > 0 && len(tc.stderr) == 0 { 133 | t.Error("Did not expect a fatal error:", errStr) 134 | } 135 | if !strings.Contains(errStr, tc.stderr) { 136 | t.Error("Stderr expected:", tc.stderr, "Got:", errStr) 137 | } 138 | 139 | for _, o := range tc.stdout { 140 | if !strings.Contains(outStr, o) { 141 | t.Error("Stdout expected:", o, "Got:", outStr) 142 | } 143 | } 144 | }) 145 | } 146 | } 147 | 148 | func TestNextInterval(t *testing.T) { 149 | tt := []struct { 150 | now time.Time 151 | interval time.Duration 152 | nextIn time.Duration 153 | }{ 154 | // mod(01:01:01, minute)++ -> 01:02:00 needs 59s 155 | {time.Date(2019, 5, 7, 1, 1, 1, 0, time.UTC), time.Minute, time.Second * 59}, 156 | // mod(01:13:58, 15m)++ -> 01:15:00 needs 1m2s 157 | {time.Date(2019, 5, 7, 1, 13, 58, 0, time.UTC), time.Minute * 15, time.Minute + time.Second*2}, 158 | // mod(01:01:01, hour)++ -> 02:00:00 needs 58m59s 159 | {time.Date(2019, 5, 7, 1, 1, 1, 0, time.UTC), time.Hour, time.Minute*58 + time.Second*59}, 160 | } 161 | 162 | for tx, tc := range tt { 163 | t.Run(fmt.Sprintf("%d", tx), func(t *testing.T) { 164 | nextIn := nextInterval(tc.now, tc.interval) 165 | if nextIn != tc.nextIn { 166 | t.Error("nextIn NE:now", tc.now, "Int", tc.interval, "Want", tc.nextIn, "Got", nextIn) 167 | } 168 | }) 169 | } 170 | } 171 | 172 | // Test that SIGUSR1 causes a stats report 173 | func TestUSR1(t *testing.T) { 174 | out := &mutexBytesBuffer{} 175 | err := &mutexBytesBuffer{} 176 | args := []string{"trustydns-proxy", "-A", "127.0.0.1:5356", "http://localhost"} 177 | mainInit(out, err) // Start up quietly 178 | go func() { 179 | stopChannel <- syscall.SIGUSR1 180 | time.Sleep(time.Millisecond * 200) // Give it time to process 181 | stopMain() 182 | }() 183 | ec := mainExecute(args) 184 | outStr := out.String() 185 | errStr := err.String() 186 | if ec != 0 { 187 | t.Error("Expected zero exit return, not", ec, errStr) 188 | } 189 | if !strings.Contains(outStr, "User1 Server") { 190 | t.Error("Expected User1 Server", outStr) 191 | } 192 | } 193 | 194 | // waitForMainExecute is a helper routine which makes sure that main mainExecute() function starts up and 195 | // terminates as expected. If not, t.Fatal() 196 | func waitForMainExecute(t *testing.T, howLong time.Duration) error { 197 | for ix := 0; ix < 10; ix++ { // Wait for up to two seconds for main to get running 198 | if isMain(started) { 199 | break 200 | } 201 | time.Sleep(time.Millisecond * 200) 202 | } 203 | if !isMain(started) { 204 | return fmt.Errorf("mainStarted did not get set after two seconds") 205 | } 206 | time.Sleep(howLong) // Give it the designated time to complete 207 | stopMain() // Then ask it to finished up 208 | for ix := 0; ix < 10; ix++ { // Wait for up to two seconds for main to terminate 209 | if isMain(stopped) { 210 | break 211 | } 212 | time.Sleep(time.Millisecond * 200) 213 | } 214 | if !isMain(stopped) { 215 | return fmt.Errorf("mainStopped did not get set two seconds after stopMain() call for %s", t.Name()) 216 | } 217 | 218 | return nil 219 | } 220 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/reporter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | ////////////////////////////////////////////////////////////////////// 9 | // reporter implementation 10 | ////////////////////////////////////////////////////////////////////// 11 | 12 | // addSuccessStats transfers successful ServerDNS query stats to longer-term server stats. 13 | func (t *server) addSuccessStats(latency time.Duration, evs events) { 14 | t.mu.Lock() 15 | defer t.mu.Unlock() 16 | 17 | t.successCount++ 18 | t.totalLatency += latency 19 | for ix := 0; ix < len(evs); ix++ { 20 | if evs[ix] { 21 | t.eventCounters[ix]++ 22 | } 23 | } 24 | } 25 | 26 | // addFailureStats transfers stats from a failed ServerDNS query to longer-term server stats. 27 | func (t *server) addFailureStats(ix int, evs events) { 28 | t.mu.Lock() 29 | defer t.mu.Unlock() 30 | 31 | t.failureCounters[ix]++ 32 | for ix := 0; ix < len(evs); ix++ { 33 | if evs[ix] { 34 | t.eventCounters[ix]++ 35 | } 36 | } 37 | } 38 | 39 | func (t *server) Name() string { 40 | return "Server: (on " + t.listenAddress + "/" + t.transport + ")" 41 | } 42 | 43 | func (t *server) Report(resetCounters bool) string { 44 | if resetCounters { 45 | t.mu.Lock() 46 | defer t.mu.Unlock() 47 | } else { 48 | t.mu.RLock() 49 | defer t.mu.RUnlock() 50 | } 51 | 52 | errs := 0 53 | for _, v := range t.failureCounters { 54 | errs += v 55 | } 56 | req := t.successCount + errs 57 | 58 | var al float64 59 | if t.successCount > 0 { 60 | al = t.totalLatency.Seconds() / float64(t.successCount) 61 | } 62 | 63 | s := fmt.Sprintf("req=%d ok=%d (%s) al=%0.3f errs=%d (%s) Concurrency=%d", 64 | req, t.successCount, formatCounters("%d", "/", t.eventCounters[:]), al, 65 | errs, formatCounters("%d", "/", t.failureCounters[:]), 66 | t.cct.Peak(resetCounters)) 67 | 68 | if resetCounters { 69 | t.stats = stats{} 70 | } 71 | 72 | return s 73 | } 74 | 75 | // formatCounters returns a nice %d/%d/%d format for an array of ints. This is less error-prone than 76 | // hard-coding one big ol' Sprintf string but obviously slower. Not relevant in this context. 77 | func formatCounters(vfmt string, delim string, vals []int) string { 78 | res := "" 79 | for ix, v := range vals { 80 | if ix > 0 { 81 | res += delim 82 | } 83 | res += fmt.Sprintf(vfmt, v) 84 | } 85 | 86 | return res 87 | } 88 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/reporter_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | expect1 = "req=5 ok=2 (0/0) al=0.450 errs=3 (1/2) Concurrency=0" 12 | expect2 = "req=5 ok=2 (1/1) al=0.450 errs=3 (1/2) Concurrency=0" 13 | ) 14 | 15 | func TestReporter(t *testing.T) { 16 | var evs events 17 | s := &server{stdout: os.Stdout, listenAddress: "127.0.0.1", transport: "udp"} 18 | name := s.Name() 19 | if !strings.Contains(name, "127.0.0.1/udp") { 20 | t.Error("Name does not contain IP address", name) 21 | } 22 | 23 | rep1 := s.Report(false) 24 | s.addSuccessStats(time.Millisecond*300, evs) 25 | rep2 := s.Report(true) 26 | if rep2 == rep1 { 27 | t.Error("Report should changed with counter updates", rep1, rep2) 28 | } 29 | rep2 = s.Report(false) 30 | if rep2 != rep1 { 31 | t.Error("Reset Counters report should equal initial report", rep1, rep2) 32 | } 33 | 34 | s.addSuccessStats(time.Millisecond*400, evs) 35 | s.addSuccessStats(time.Millisecond*500, evs) // (400+500) / 2 = 0.450ms average latency 36 | s.addFailureStats(serNoResponse, evs) 37 | s.addFailureStats(serDNSWriteFailed, evs) 38 | evs[evInTruncated] = true 39 | evs[evOutTruncated] = true 40 | s.addFailureStats(serDNSWriteFailed, evs) 41 | rep1 = s.Report(false) 42 | rep2 = s.Report(false) 43 | 44 | if rep1 != rep2 { 45 | t.Error("Report should not have reset", rep1, rep2) 46 | } 47 | if rep1 != expect2 { 48 | t.Error("Report should not have changed. Expected:", expect2, "Got:", rep1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/state.go: -------------------------------------------------------------------------------- 1 | // Manage main state transitions for unit tests. Not used in production code path. 2 | package main 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | type mainStateType int 9 | 10 | const ( 11 | initial mainStateType = iota // Never been started 12 | started // Running 13 | stopped // Previously started, now stopped 14 | ) 15 | 16 | var ( 17 | stateMutex sync.Mutex 18 | state mainStateType = initial 19 | ) 20 | 21 | func mainState(newState mainStateType) { 22 | stateMutex.Lock() 23 | defer stateMutex.Unlock() 24 | state = newState 25 | 26 | } 27 | 28 | func isMain(wantedState mainStateType) bool { 29 | stateMutex.Lock() 30 | defer stateMutex.Unlock() 31 | return state == wantedState 32 | } 33 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/testdata/emptyfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdingo/trustydns/766fc0e6b83bb8aaa9cf4035e3f26fc1d5141c59/cmd/trustydns-proxy/testdata/emptyfile -------------------------------------------------------------------------------- /cmd/trustydns-proxy/testdata/resolv.conf: -------------------------------------------------------------------------------- 1 | domain dom.example.org 2 | search search1.example.net search2.exAmple.net 3 | nameserver 192.168.1.1 4 | nameserver 10.0.0.1 5 | nameserver 10.0.0.2 6 | nameserver 10.0.0.3 7 | options timeout:1 attempts:3 8 | -------------------------------------------------------------------------------- /cmd/trustydns-proxy/usage_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | ////////////////////////////////////////////////////////////////////// 11 | 12 | type usageTestCase struct { 13 | expectToRun bool // waitForExecute should not return an error if this is true 14 | args []string // ARGV - not counting command 15 | stdout []string // Expected stdout strings 16 | stderr string // Expected stderr string 17 | } 18 | 19 | var usageTestCases = []usageTestCase{ 20 | {false, []string{"--version"}, []string{"trustydns-proxy", "Version:"}, ""}, 21 | {false, []string{"-h"}, []string{"NAME", "SYNOPSIS", "OPTIONS", "Version: v"}, ""}, 22 | {false, []string{}, []string{}, "Fatal: trustydns-proxy: Must supply at least one DoH server URL on the command line"}, 23 | {false, []string{"-badopt"}, []string{}, "flag provided but not defined"}, 24 | {false, []string{"-v", "-A", "255.254.253.252", "http://localhost:63080"}, []string{"Starting:"}, 25 | "assign requested address"}, 26 | 27 | // -e local domains without resolv.conf 28 | {false, []string{"-e", "example.net", "http://localhost"}, []string{}, "Local Domains"}, 29 | 30 | // Bad ecs-set 31 | {false, []string{"--ecs-set", "10.0.120.XXX/24", "http://localhost:63080"}, []string{}, "invalid CIDR"}, 32 | {false, []string{"--ecs-set", "10.0.120.0/24", "--ecs-request-ipv4-prefixlen", "24", 33 | "http://localhost"}, []string{}, "Cannot have both --ecs-set and --ecs-request"}, 34 | {false, []string{"--ecs-set", "10.0.120.0/24", "--ecs-request-ipv6-prefixlen", "66", 35 | "http://localhost"}, []string{}, "Cannot have both --ecs-set and --ecs-request"}, 36 | {false, []string{"--ecs-request-ipv6-prefixlen", "200", "http://localhost:63080"}, []string{}, 37 | "must be between 0 and 128"}, 38 | {false, []string{"--ecs-request-ipv4-prefixlen", "200", "http://localhost:63080"}, []string{}, 39 | "must be between 0 and 32"}, 40 | {false, []string{"--ecs-request-ipv6-prefixlen", "-2", "http://localhost:63080"}, []string{}, 41 | "must be between 0 and 128"}, 42 | {false, []string{"--ecs-request-ipv4-prefixlen", "-1", "http://localhost:63080"}, []string{}, 43 | "must be between 0 and 32"}, 44 | 45 | // Transport 46 | {false, []string{"--udp=false", "--tcp=false", "http://localhost:63080"}, []string{}, 47 | "Must have one of"}, 48 | 49 | // ECS with GET 50 | {false, []string{"-g", "--ecs-set", "10.0.120.0/24", "http://localhost:63080"}, []string{}, "any ECS synthesis"}, 51 | 52 | // Test URL mangling code paths 53 | {false, []string{"http://"}, []string{}, "does not contain a hostname"}, 54 | {false, []string{"://localhost/xxx"}, []string{}, "missing protocol scheme"}, 55 | 56 | // Bad options 57 | {false, []string{"-t", "xxs", "http://localhost"}, []string{}, "invalid value"}, 58 | {false, []string{"-i", "xxs", "http://localhost"}, []string{}, "invalid value"}, 59 | {false, []string{"-r", "0", "http://localhost:63080"}, []string{}, "Minimum remote concurrency"}, 60 | 61 | // Bad local resolver config 62 | {false, []string{"-c", "testdata/emptyfile", "http://localhost"}, []string{}, "No servers"}, 63 | 64 | // tls 65 | {false, []string{"--tls-cert", "testdata/emptyfile", "http://localhost"}, []string{}, "key file missing"}, 66 | {false, []string{"--tls-key", "testdata/emptyfile", "http://localhost"}, []string{}, "cert file missing"}, 67 | } 68 | 69 | func TestUsage(t *testing.T) { 70 | for tx, tc := range usageTestCases { 71 | t.Run(fmt.Sprintf("%d", tx), func(t *testing.T) { 72 | args := append([]string{"trustydns-proxy"}, tc.args...) 73 | out := &mutexBytesBuffer{} 74 | err := &mutexBytesBuffer{} 75 | mainInit(out, err) 76 | done := make(chan error) 77 | go func() { 78 | done <- waitForMainExecute(t, time.Millisecond*200) 79 | }() 80 | ec := mainExecute(args) 81 | e := <-done // Get waitForExecute results 82 | outStr := out.String() 83 | errStr := err.String() 84 | 85 | if e != nil && tc.expectToRun { 86 | t.Fatal("Expected to run, but", e, errStr, outStr) 87 | } 88 | if ec == 0 && len(tc.stderr) > 0 { 89 | t.Error("Expected error exit from Execute() with stderr", tc.stderr) 90 | } 91 | 92 | if len(errStr) > 0 && len(tc.stderr) == 0 { 93 | t.Error("Did not expect a fatal error:", errStr) 94 | } 95 | if !strings.Contains(errStr, tc.stderr) { 96 | t.Error("Stderr expected:", tc.stderr, "Got:", errStr) 97 | } 98 | 99 | for _, o := range tc.stdout { 100 | if !strings.Contains(outStr, o) { 101 | t.Error("Stdout expected:", o, "Got:", outStr) 102 | } 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cmd/trustydns-server/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all race clean test 2 | 3 | all: 4 | go build 5 | 6 | race: 7 | CGO_ENABLED=1 go build -race 8 | 9 | clean: 10 | go clean 11 | 12 | test: 13 | go test 14 | -------------------------------------------------------------------------------- /cmd/trustydns-server/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/markdingo/trustydns/internal/flagutil" 7 | ) 8 | 9 | type config struct { 10 | gops bool 11 | help bool 12 | verbose bool 13 | verifyClientCerts bool 14 | version bool 15 | 16 | listenAddresses flagutil.StringValue // Addresses for inbound HTTP requests 17 | 18 | resolvConf string 19 | statusInterval time.Duration 20 | requestTimeout time.Duration 21 | 22 | ecsRemove bool // Remove inbound ECS 23 | ecsSet bool 24 | ecsSetIPv4PrefixLen int 25 | ecsSetIPv6PrefixLen int 26 | 27 | logAll bool // Turns on all other log options 28 | logClientIn bool // Compact print of DNS query arriving from the HTTPS client 29 | logClientOut bool // Compact print of DNS response returned to the HTTPS client 30 | logHTTPIn bool // Compact print of HTTP query arriving from the HTTPS client 31 | logHTTPOut bool // Compact print of HTTP response returned to the HTTPS client 32 | logLocalIn bool // Compact print of DNS response returned by the local resolver 33 | logLocalOut bool // Compact print of DNS query sent to the local resolver 34 | logTLSErrors bool // Print Client TLS verification failures 35 | 36 | tlsServerCertFiles flagutil.StringValue 37 | tlsServerKeyFiles flagutil.StringValue 38 | tlsCAFiles flagutil.StringValue // Non-system root CAs 39 | tlsUseSystemRootCAs bool // Do/Do not use system root CAs 40 | 41 | cpuprofile, memprofile string 42 | 43 | setuidName, setgidName, chrootDir string // Process constraint settings 44 | } 45 | -------------------------------------------------------------------------------- /cmd/trustydns-server/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | "syscall" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // We use a bytes.Buffer as stdout, stderr which is shared across multiple go-routines so we need to 15 | // protected it from concurrent access. This is test-only code but -race doesn't know that. 16 | type mutexBytesBuffer struct { 17 | mu sync.Mutex 18 | buffer bytes.Buffer 19 | } 20 | 21 | func (t *mutexBytesBuffer) Write(p []byte) (n int, err error) { 22 | t.mu.Lock() 23 | defer t.mu.Unlock() 24 | 25 | return t.buffer.Write(p) 26 | } 27 | 28 | func (t *mutexBytesBuffer) String() string { 29 | t.mu.Lock() 30 | defer t.mu.Unlock() 31 | 32 | return t.buffer.String() 33 | } 34 | 35 | ////////////////////////////////////////////////////////////////////// 36 | 37 | type mainTestCase struct { 38 | description string 39 | needsRoot bool // Only run if we're setuid 0 40 | willRunFor time.Duration // trustydns-server should run for this amount of time before being terminated 41 | args []string // ARGV - not counting command 42 | stdout []string // Expected stdout strings 43 | stderr string // Expected stderr string 44 | } 45 | 46 | // The -A 255.... arguments are present to cause mainExecute() to fail when it starts *after* 47 | // exercising the code coverage area intended. Yeah a bit of a hack, but the go testing framework is 48 | // not well suited to running command-line tests that involve running then killing. 49 | 50 | var mainTestCases = []mainTestCase{ 51 | {"ecs-set", 52 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:63081", "-v", "--ecs-set"}, 53 | []string{"Starting", "Exiting"}, ""}, 54 | {"ecs-*prefixlen", 55 | false, 100 * time.Millisecond, []string{"-A", "127.0.0.1:63082", "-v", 56 | "--ecs-remove", "--ecs-set", "--ecs-set-ipv4-prefixlen", "20", "--ecs-set-ipv6-prefixlen", "56"}, 57 | []string{"Starting", "Exiting"}, ""}, 58 | 59 | {"Good tls files", 60 | false, 100 * time.Millisecond, []string{"-v", "-A", "127.0.0.1:63083", 61 | "--tls-cert", "testdata/server.cert", "--tls-key", "testdata/server.key"}, 62 | []string{"Starting", "Exiting"}, ""}, 63 | 64 | {"Good local resolver config", 65 | false, 100 * time.Millisecond, []string{"-v", "-A", "127.0.0.1:63084", "-c", "testdata/resolv.conf"}, 66 | []string{"Starting", "Exiting"}, ""}, 67 | 68 | {"Good profile files", 69 | false, 100 * time.Millisecond, []string{"--cpu-profile", "testdata/cpu", 70 | "--mem-profile", "testdata/mem", "-v", "-A", "127.0.0.1:63085", "-c", "testdata/resolv.conf"}, 71 | []string{"Starting", "Exiting"}, ""}, 72 | 73 | {"Logging", 74 | false, 100 * time.Millisecond, 75 | []string{"-v", "--log-all", "-A", "127.0.0.1:63086", "-c", "testdata/resolv.conf"}, 76 | []string{"Starting", "Exiting"}, ""}, 77 | 78 | {"Status report", 79 | false, 2 * time.Second, []string{"-v", "-i", "1s", "-A", "127.0.0.1:63087"}, 80 | []string{"Listening: (HTTP on"}, ""}, 81 | 82 | {"Wildcard listen address - may not work on some systems", 83 | true, time.Millisecond, []string{}, []string{}, ""}, 84 | } 85 | 86 | func TestMain(t *testing.T) { 87 | uid := os.Getuid() 88 | for tx, tc := range mainTestCases { 89 | t.Run(fmt.Sprintf("%d %s", tx, tc.description), func(t *testing.T) { 90 | if tc.needsRoot && uid != 0 { 91 | t.Skip("Skipping setuid=0 test as not running as root") 92 | return 93 | } 94 | 95 | args := append([]string{"trustydns-server"}, tc.args...) 96 | out := &mutexBytesBuffer{} 97 | err := &mutexBytesBuffer{} 98 | mainInit(out, err) 99 | done := make(chan error) 100 | go func() { 101 | done <- waitForMainExecute(t, tc.willRunFor) 102 | }() 103 | ec := mainExecute(args) 104 | e := <-done // Get waitForMainExecute results 105 | if e != nil { 106 | t.Fatal(e) 107 | } 108 | if ec == 0 && tc.willRunFor == 0 { 109 | t.Error("Non-zero Exit code expected") 110 | } 111 | if ec != 0 && tc.willRunFor > 0 { 112 | t.Error("Zero Exit code expected, not:", ec) 113 | } 114 | 115 | outStr := out.String() 116 | errStr := err.String() 117 | if len(errStr) > 0 && len(tc.stderr) == 0 { 118 | t.Error("Did not expect a fatal error:", errStr) 119 | } 120 | if !strings.Contains(errStr, tc.stderr) { 121 | t.Error("Stderr expected:", tc.stderr, "Got:", errStr) 122 | } 123 | 124 | for _, o := range tc.stdout { 125 | if !strings.Contains(outStr, o) { 126 | t.Error("Stdout expected:", o, "Got:", outStr) 127 | } 128 | } 129 | }) 130 | } 131 | } 132 | 133 | // waitForMainExecute is a helper routine which makes sure that main mainExecute() function starts up and 134 | // terminates as expected. If not, t.Fatal() 135 | func waitForMainExecute(t *testing.T, howLong time.Duration) error { 136 | for ix := 0; ix < 10; ix++ { // Wait for up to two seconds for main to get running 137 | if isMain(started) { 138 | break 139 | } 140 | time.Sleep(time.Millisecond * 200) 141 | } 142 | if !isMain(started) { 143 | return fmt.Errorf("mainStarted did not get set after a second for %s", t.Name()) 144 | } 145 | time.Sleep(howLong) // Give it the designated time to complete 146 | stopMain() // Then ask it to finished up 147 | for ix := 0; ix < 10; ix++ { // Wait for up to two seconds for main to terminate 148 | if isMain(stopped) { 149 | break 150 | } 151 | time.Sleep(time.Millisecond * 200) 152 | } 153 | if !isMain(stopped) { 154 | return fmt.Errorf("mainStopped did not get set two seconds after stopMain() call for %s", t.Name()) 155 | } 156 | 157 | return nil 158 | } 159 | 160 | func TestNextInterval(t *testing.T) { 161 | tt := []struct { 162 | now time.Time 163 | interval time.Duration 164 | nextIn time.Duration 165 | }{ 166 | // mod(01:01:01, minute)++ -> 01:02:00 needs 59s 167 | {time.Date(2019, 5, 7, 1, 1, 1, 0, time.UTC), time.Minute, time.Second * 59}, 168 | // mod(01:13:58, 15m)++ -> 01:15:00 needs 1m2s 169 | {time.Date(2019, 5, 7, 1, 13, 58, 0, time.UTC), time.Minute * 15, time.Minute + time.Second*2}, 170 | // mod(01:01:01, hour)++ -> 02:00:00 needs 58m59s 171 | {time.Date(2019, 5, 7, 1, 1, 1, 0, time.UTC), time.Hour, time.Minute*58 + time.Second*59}, 172 | } 173 | 174 | for tx, tc := range tt { 175 | t.Run(fmt.Sprintf("%d", tx), func(t *testing.T) { 176 | nextIn := nextInterval(tc.now, tc.interval) 177 | if nextIn != tc.nextIn { 178 | t.Error("nextIn NE:now", tc.now, "Int", tc.interval, "Want", tc.nextIn, "Got", nextIn) 179 | } 180 | }) 181 | } 182 | } 183 | 184 | // Test that SIGUSR1 causes a stats report 185 | func TestUSR1(t *testing.T) { 186 | out := &mutexBytesBuffer{} 187 | err := &mutexBytesBuffer{} 188 | args := []string{"trustydns-server", "-A", "127.0.0.1:60443"} 189 | mainInit(out, err) // Start up quietly 190 | go func() { 191 | stopChannel <- syscall.SIGUSR1 192 | time.Sleep(time.Millisecond * 200) // Give it time to process 193 | stopMain() 194 | }() 195 | ec := mainExecute(args) 196 | outStr := out.String() 197 | errStr := err.String() 198 | if ec != 0 { 199 | t.Error("Expected zero exit return, not", ec, errStr) 200 | } 201 | if !strings.Contains(outStr, "User1 Listener:") { 202 | t.Error("Expected 'User1 Listener:', got", outStr) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /cmd/trustydns-server/reporter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // addSuccessStats bumps the success counter as well as total duration which are used to generate 9 | // reports. All event settings for the request are transferred to counters. 10 | func (t *server) addSuccessStats(latency time.Duration, evs events) { 11 | t.mu.Lock() 12 | defer t.mu.Unlock() 13 | 14 | t.successCount++ 15 | t.totalLatency += latency 16 | for ix := 0; ix < len(evs); ix++ { 17 | if evs[ix] { 18 | t.eventCounters[ix]++ 19 | } 20 | } 21 | } 22 | 23 | // addFailureStats bumps the failure counter 24 | func (t *server) addFailureStats(ix serFailureIndex, evs events) { 25 | t.mu.Lock() 26 | defer t.mu.Unlock() 27 | 28 | t.failureCounters[ix]++ 29 | for ix := 0; ix < len(evs); ix++ { 30 | if evs[ix] { 31 | t.eventCounters[ix]++ 32 | } 33 | } 34 | } 35 | 36 | func (t *server) Name() string { 37 | return "Listener" 38 | } 39 | 40 | func (t *server) listenName() string { 41 | s := "(" 42 | if cfg.tlsServerKeyFiles.NArg() > 0 { 43 | s += "HTTPS on " 44 | } else { 45 | s += "HTTP on " 46 | } 47 | s += t.listenAddress + ")" 48 | 49 | return s 50 | } 51 | 52 | /* 53 | 54 | Reporter Output: 55 | Error Counters 56 | req=1 ok=0 (0/0/120/120/0/120) al=0.000 errs=1 (0/1/0/0/0/0/0/0/0/0/0/0) Concurrency=1 listenName 57 | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 58 | | | | | | | | | | | | | | | | | | | | | | | | 59 | | | | | | | | | | | | | | | | | | | | | | | +--Peak inbound HTTP 60 | | | | | | | | | | | | | | | | | | | | | | +--QueryParamMissing 61 | | | | | | | | | | | | | | | | | | | | | +--LocalResolutionFailed 62 | | | | | | | | | | | | | | | | | | | | +--HTTPWriterFailed 63 | | | | | | | | | | | | | | | | | | | +--FailureListSize 64 | | | | | | | | | | | | | | | | | | +--ECSSynthesisFailed 65 | | | | | | | | | | | | | | | | | +--DNSUnpackRequestFailed 66 | | | | | | | | | | | | | | | | +--DNSPackResponseFailed 67 | | | | | | | | | | | | | | | +--ClientTLSBad 68 | | | | | | | | | | | | | | +--BodyReadError 69 | | | | | | | | | | | | | +--BadQueryParamDecode 70 | | | | | | | | | | | | +--BadPrefixLengths 71 | | | | | | | | | | | +--BadContentType 72 | | | | | | | | | | +--Total Bad Requests 73 | | | | | | | | | +--Average resolution latency 74 | | | | | | | | +--evPadding 75 | | | | | | | +--evECSv6Synth 76 | | | | | | +--evECSv4Synth 77 | | | | | +--evEDNS0Removed 78 | | | | +--evTsig 79 | | | +--evGet 80 | | +--Good Requests 81 | +--Total Requests 82 | 83 | */ 84 | 85 | func (t *server) Report(resetCounters bool) string { 86 | if resetCounters { 87 | t.mu.Lock() 88 | defer t.mu.Unlock() 89 | } else { 90 | t.mu.RLock() 91 | defer t.mu.RUnlock() 92 | } 93 | 94 | errs := 0 95 | for _, v := range t.failureCounters { 96 | errs += v 97 | } 98 | req := t.successCount + errs 99 | 100 | var al float64 101 | if t.successCount > 0 { 102 | al = t.totalLatency.Seconds() / float64(t.successCount) 103 | } 104 | s := fmt.Sprintf("req=%d ok=%d (%s) al=%0.3f errs=%d (%s) Concurrency=%d %s\n", 105 | req, t.successCount, formatCounters("%d", "/", t.eventCounters[:]), al, 106 | errs, formatCounters("%d", "/", t.failureCounters[:]), 107 | t.ccTrk.Peak(resetCounters), t.listenName()) 108 | 109 | if resetCounters { 110 | t.stats = stats{} 111 | } 112 | 113 | return s 114 | } 115 | 116 | // formatCounters returns a nice %d/%d/%d format for an array of ints. This is less error-prone than 117 | // hard-coding one big ol' Sprintf string but obviously slower. Not relevant in this context. 118 | func formatCounters(vfmt string, delim string, vals []int) string { 119 | res := "" 120 | for ix, v := range vals { 121 | if ix > 0 { 122 | res += delim 123 | } 124 | res += fmt.Sprintf(vfmt, v) 125 | } 126 | 127 | return res 128 | } 129 | -------------------------------------------------------------------------------- /cmd/trustydns-server/reporter_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const expect1 = "req=14 ok=2 (0/0/0/0/0/0) al=0.750 errs=12 (1/1/1/1/1/1/1/1/1/1/1/1) Concurrency=0" 11 | 12 | func TestReporter(t *testing.T) { 13 | mainInit(os.Stdout, os.Stderr) // Make sure cfg is initialized 14 | s := &server{stdout: stdout, listenAddress: "127.0.0.1"} 15 | name := s.Name() 16 | if !strings.Contains(name, "Listener") { 17 | t.Error("Name does not contain 'Listener'", name) 18 | } 19 | rep1 := s.Report(false) 20 | if !strings.Contains(rep1, "127.0.0.1") { 21 | t.Error("Report does not contain IP address 127.0.0.1", rep1) 22 | } 23 | 24 | var evs events 25 | s.addSuccessStats(time.Second, evs) 26 | rep2 := s.Report(true) 27 | if rep2 == rep1 { 28 | t.Error("Report should changed with counter updates", rep1, rep2) 29 | } 30 | rep2 = s.Report(false) 31 | if rep2 != rep1 { 32 | t.Error("Reset Counters report should equal initial report", rep1, rep2) 33 | } 34 | s.addSuccessStats(time.Second, evs) 35 | s.addSuccessStats(time.Millisecond*500, evs) // ok=2, al=1.5/2 = 0.750 36 | s.addFailureStats(serBadContentType, evs) 37 | s.addFailureStats(serBadMethod, evs) 38 | s.addFailureStats(serBadPrefixLengths, evs) 39 | s.addFailureStats(serBadQueryParamDecode, evs) 40 | s.addFailureStats(serBodyReadError, evs) 41 | s.addFailureStats(serClientTLSBad, evs) 42 | s.addFailureStats(serDNSPackResponseFailed, evs) 43 | s.addFailureStats(serDNSUnpackRequestFailed, evs) 44 | s.addFailureStats(serECSSynthesisFailed, evs) 45 | s.addFailureStats(serHTTPWriterFailed, evs) 46 | s.addFailureStats(serLocalResolutionFailed, evs) 47 | s.addFailureStats(serQueryParamMissing, evs) // errs=12 48 | 49 | rep1 = s.Report(false) 50 | rep2 = s.Report(false) 51 | 52 | if rep1 != rep2 { 53 | t.Error("Report should not have reset", rep1, rep2) 54 | } 55 | if !strings.Contains(rep1, expect1) { 56 | t.Error("Report should not have changed. Expected:", expect1, "Got:", rep1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cmd/trustydns-server/state.go: -------------------------------------------------------------------------------- 1 | // Manage main state transitions for unit tests. Not used in production code path. 2 | package main 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | type mainStateType int 9 | 10 | const ( 11 | initial mainStateType = iota // Never been started 12 | started // Running 13 | stopped // Previously started, now stopped 14 | ) 15 | 16 | var ( 17 | stateMutex sync.Mutex 18 | state mainStateType = initial 19 | ) 20 | 21 | func mainState(newState mainStateType) { 22 | stateMutex.Lock() 23 | defer stateMutex.Unlock() 24 | state = newState 25 | 26 | } 27 | 28 | func isMain(wantedState mainStateType) bool { 29 | stateMutex.Lock() 30 | defer stateMutex.Unlock() 31 | return state == wantedState 32 | } 33 | -------------------------------------------------------------------------------- /cmd/trustydns-server/testdata/emptyfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdingo/trustydns/766fc0e6b83bb8aaa9cf4035e3f26fc1d5141c59/cmd/trustydns-server/testdata/emptyfile -------------------------------------------------------------------------------- /cmd/trustydns-server/testdata/resolv.conf: -------------------------------------------------------------------------------- 1 | domain dom.example.org 2 | search search1.example.net search2.exAmple.net 3 | nameserver 192.168.1.1 4 | nameserver 10.0.0.1 5 | nameserver 10.0.0.2 6 | nameserver 10.0.0.3 7 | options timeout:1 attempts:3 8 | -------------------------------------------------------------------------------- /cmd/trustydns-server/testdata/rootCA.cert: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 1 (0x1) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=AU, ST=QLD, O=trustydns1, CN=bm.bushwire.net/emailAddress=test@example.com 7 | Validity 8 | Not Before: Jun 13 09:11:27 2019 GMT 9 | Not After : Jun 20 09:11:27 2029 GMT 10 | Subject: C=AU, ST=QLD, O=trustydns1, CN=bm.bushwire.net/emailAddress=test@example.com 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:ab:65:d8:72:d3:e6:37:f0:b4:81:8d:a3:30:6f: 16 | f0:07:2e:60:71:1f:fa:a8:71:f9:e8:10:0e:61:8e: 17 | 6f:32:47:fc:2f:0f:db:69:bf:53:bc:8f:ee:86:2e: 18 | da:2d:4f:40:e0:3b:77:70:9b:46:d7:b9:70:b5:77: 19 | 64:7b:ae:92:63:7b:81:19:28:8e:ec:ba:a4:9d:0f: 20 | 19:77:63:1f:1b:a1:71:91:23:ea:ba:9f:56:15:5f: 21 | ee:72:c2:0f:e1:c1:45:e1:49:ab:b9:61:17:92:36: 22 | c4:fb:30:3b:d3:ae:59:ca:a9:82:51:d7:83:14:53: 23 | ef:72:b1:f5:8b:74:3f:84:d4:fa:83:fa:66:f2:76: 24 | 87:14:93:4d:8d:ee:39:39:3a:28:57:cf:88:92:ec: 25 | c9:47:25:02:8d:0a:2a:02:c2:34:9d:0c:5f:da:9b: 26 | 24:4c:d3:88:03:85:0b:45:78:d9:83:d4:a9:3e:91: 27 | c1:fb:d7:d2:82:ad:5a:28:00:83:4e:c0:18:81:af: 28 | 3f:9f:3e:51:21:4b:48:21:d9:ea:0e:51:5a:e4:08: 29 | f8:0a:d1:97:36:05:43:08:ad:3b:64:7d:9b:e7:52: 30 | 27:29:9b:32:ae:5f:28:07:60:13:58:4e:03:1d:10: 31 | ce:a2:ec:18:70:25:3d:b7:93:a9:5c:7e:6e:04:0f: 32 | 1e:b1 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Subject Key Identifier: 36 | 62:A4:93:9F:E5:43:33:EB:0A:9C:41:4A:36:21:F9:B2:13:69:C2:E7 37 | X509v3 Authority Key Identifier: 38 | keyid:62:A4:93:9F:E5:43:33:EB:0A:9C:41:4A:36:21:F9:B2:13:69:C2:E7 39 | 40 | X509v3 Basic Constraints: 41 | CA:TRUE 42 | Signature Algorithm: sha256WithRSAEncryption 43 | 94:61:20:e3:3f:86:f2:16:71:17:c8:79:3b:a8:9d:81:53:63: 44 | de:5e:b7:8a:b3:8d:b8:28:00:f6:50:af:ee:bf:00:5c:91:02: 45 | 2e:7d:b3:81:63:aa:0d:49:21:a8:40:b9:34:26:61:eb:c5:22: 46 | 89:da:73:0e:a6:27:50:59:18:34:67:a2:fc:73:ee:c3:f9:6e: 47 | 2b:db:12:cf:c2:75:bd:74:2b:24:1c:a8:b6:de:73:09:73:93: 48 | 43:93:75:25:3e:09:d3:7c:0a:de:8b:3b:b6:48:ff:27:98:77: 49 | e9:ba:eb:d2:92:67:e9:f2:82:38:f0:6f:78:a4:73:64:68:d2: 50 | 49:0a:7f:0f:a7:69:ff:8f:16:97:f6:59:f1:cb:39:c3:94:58: 51 | a8:c5:bb:d6:d2:34:b0:f8:f6:bd:76:d0:7e:71:ec:8c:0a:c6: 52 | 1d:5d:d8:05:d4:17:e4:51:61:69:47:67:b4:c7:a0:e7:7a:5e: 53 | 25:72:bb:06:4d:10:f7:ba:8d:ae:1f:79:87:53:0b:dd:15:8e: 54 | 28:3a:79:d3:6a:ff:5f:ce:b9:91:31:9b:7b:3d:67:b8:cb:77: 55 | a9:e4:8f:80:49:82:35:e7:e7:2e:1b:e1:d1:f6:27:30:3a:81: 56 | 2c:d5:ef:d7:90:97:31:a9:0f:c2:06:21:3a:62:15:33:30:da: 57 | 6d:39:71:35 58 | -----BEGIN CERTIFICATE----- 59 | MIIDoTCCAomgAwIBAgIBATANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJBVTEM 60 | MAoGA1UECAwDUUxEMRMwEQYDVQQKDAp0cnVzdHlkbnMxMRgwFgYDVQQDDA9ibS5i 61 | dXNod2lyZS5uZXQxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcN 62 | MTkwNjEzMDkxMTI3WhcNMjkwNjIwMDkxMTI3WjBrMQswCQYDVQQGEwJBVTEMMAoG 63 | A1UECAwDUUxEMRMwEQYDVQQKDAp0cnVzdHlkbnMxMRgwFgYDVQQDDA9ibS5idXNo 64 | d2lyZS5uZXQxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wggEiMA0G 65 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrZdhy0+Y38LSBjaMwb/AHLmBxH/qo 66 | cfnoEA5hjm8yR/wvD9tpv1O8j+6GLtotT0DgO3dwm0bXuXC1d2R7rpJje4EZKI7s 67 | uqSdDxl3Yx8boXGRI+q6n1YVX+5ywg/hwUXhSau5YReSNsT7MDvTrlnKqYJR14MU 68 | U+9ysfWLdD+E1PqD+mbydocUk02N7jk5OihXz4iS7MlHJQKNCioCwjSdDF/amyRM 69 | 04gDhQtFeNmD1Kk+kcH719KCrVooAINOwBiBrz+fPlEhS0gh2eoOUVrkCPgK0Zc2 70 | BUMIrTtkfZvnUicpmzKuXygHYBNYTgMdEM6i7BhwJT23k6lcfm4EDx6xAgMBAAGj 71 | UDBOMB0GA1UdDgQWBBRipJOf5UMz6wqcQUo2IfmyE2nC5zAfBgNVHSMEGDAWgBRi 72 | pJOf5UMz6wqcQUo2IfmyE2nC5zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA 73 | A4IBAQCUYSDjP4byFnEXyHk7qJ2BU2PeXreKs424KAD2UK/uvwBckQIufbOBY6oN 74 | SSGoQLk0JmHrxSKJ2nMOpidQWRg0Z6L8c+7D+W4r2xLPwnW9dCskHKi23nMJc5ND 75 | k3UlPgnTfAreizu2SP8nmHfpuuvSkmfp8oI48G94pHNkaNJJCn8Pp2n/jxaX9lnx 76 | yznDlFioxbvW0jSw+Pa9dtB+ceyMCsYdXdgF1BfkUWFpR2e0x6Dnel4lcrsGTRD3 77 | uo2uH3mHUwvdFY4oOnnTav9fzrmRMZt7PWe4y3ep5I+ASYI15+cuG+HR9icwOoEs 78 | 1e/XkJcxqQ/CBiE6YhUzMNptOXE1 79 | -----END CERTIFICATE----- 80 | -------------------------------------------------------------------------------- /cmd/trustydns-server/testdata/server.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkwCAQEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCQVUxDDAKBgNV 3 | BAgMA1FMRDETMBEGA1UECgwKdHJ1c3R5ZG5zMTEYMBYGA1UEAwwPYm0uYnVzaHdp 4 | cmUubmV0MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTE5MDYx 5 | MzA5MTEyN1oXDTI5MDYyMDA5MTEyN1owgYQxCzAJBgNVBAYTAkFVMQwwCgYDVQQI 6 | DANRTEQxFzAVBgNVBAcMDlN1bnNoaW5lIENvYXN0MRMwEQYDVQQKDAp0cnVzdHlk 7 | bnMxMRgwFgYDVQQDDA9ibS5idXNod2lyZS5uZXQxHzAdBgkqhkiG9w0BCQEWEHRl 8 | c3RAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3 9 | wXCO23GxzBwEEQNcNYPbnlIdUFxZbm1Hbnecj7MZaMBOTSLjYuFYg79BdYP/EYUz 10 | c6zNEaTPWYoz6RPuEcdS7MGzh+BScYWV/ngDua8hcDeGNmS+VjhQ+J/ynTA5czIj 11 | VPn5/wYAblHR5p0kyBDHAGPdUKRFUSeCsKNCqKQop4M8HsnmHUPd31Xr4/ZQOb7E 12 | Bd4tpH6OjAm9uDlzS9tuvM1xbGrTqRNGML4aQpH6rSPKJ14bEmc7ueh01WyxSkBx 13 | XzonBe/MN+1Dxn3i2Smnbrklvbm2WkQLsrH5rczIEX2jdj6Oh+pJPRp+7JEJai/J 14 | AKlwHzkB3op/DwCT23MZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEkJ6ogryN3+ 15 | uUJ4WujaLuVMsG64ttrKY7R1sG7IucdqnyeIbifWqJZBuGpVCMhRl8KgOQkpEoF8 16 | qhY5rUi0o/V9wXl/D0AxynBY7QcbPzphF6ntGzRaQkjIzbcPDYe5o6fINU8AhBnC 17 | JXHZq3M+tZ5ovsnXtmY1FBkmq8sxjE3vja3oa+jBYsgjJB5IMeDrgDyJLNtuFC0r 18 | 5nExlt5hgL5It0qzB2b7XFHfIwHFq70B1lOXKWlLCFLwmuGMBFsdDWULJwayzTyG 19 | 5HqmNWOj5/KqRboc3tGwujR4msTRynNAI92kFeVnC77IECWTT80JnxD6uLr+D2H6 20 | nTXY53nQne4= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /cmd/trustydns-server/testdata/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC3wXCO23GxzBwE 3 | EQNcNYPbnlIdUFxZbm1Hbnecj7MZaMBOTSLjYuFYg79BdYP/EYUzc6zNEaTPWYoz 4 | 6RPuEcdS7MGzh+BScYWV/ngDua8hcDeGNmS+VjhQ+J/ynTA5czIjVPn5/wYAblHR 5 | 5p0kyBDHAGPdUKRFUSeCsKNCqKQop4M8HsnmHUPd31Xr4/ZQOb7EBd4tpH6OjAm9 6 | uDlzS9tuvM1xbGrTqRNGML4aQpH6rSPKJ14bEmc7ueh01WyxSkBxXzonBe/MN+1D 7 | xn3i2Smnbrklvbm2WkQLsrH5rczIEX2jdj6Oh+pJPRp+7JEJai/JAKlwHzkB3op/ 8 | DwCT23MZAgMBAAECggEAEEjkYMSx3r/n+7RGR/W2KdBuFor4pDRVGu9/SjCx/p55 9 | 7HaJkP1CW0XRvUtc104GL/kgZ0wY/wdAaDlPutl4gLDaub5g8u18mT5kBHCARMZd 10 | JVfMtdGOTB68jhPAIldDKj5tqOog7gjY13FIm+nfEsPGFeb/p+T5S2u8DDxF0BRh 11 | 0A4qyvYinlEvHFjYKwMWQY50yRkmQP1RjpSdd8WCOxucmN8OyV/AOwE+45fI4KN9 12 | 6K/SHLpIgm4yOvccxUGJ9Yq0lX85HZFjfuCr1BkIzOiRstPIjoaznofbU1CvayVI 13 | EtZYUkNdPRjk1gOvdN58dI7Iy4/KlAk/+tIylTQAwQKBgQDpvQNVulbedgLZWd5l 14 | 8Shm7RbmEDzFsVaCzwSa7epkTCyJPkA4ztqU+OQvtjdbrY1jCgMz2IcClDQ9fSu+ 15 | 4vsBOXzUimbgi6j0HYgCscGJUlzfGqJgL/FZBQ8OcMcYcf+LUZFF7YIqLQJxs3eO 16 | PwC+JeVj8JNzuL3T5wB+ylPD6wKBgQDJQcC0pnHf65KM5kyX9X3krocFLmv7ahk0 17 | mc0ujE1HJuPZgw487k23qDWNeOgbIY8+c17tww5V/UPgKVKYGoyjDKjTnpG4plq9 18 | MVgaXU7FcqETXsm++hnWV/DHturQurYfzWFOScN6+huLORkW3Ys8nEOWJOXFHfCU 19 | kDP/sqwYCwKBgFCbT3AcD+MuHXNpa6oKTZ2ZO+FhTiP7MVNxIyxuyfuGzYETB8DP 20 | jU/8uWy+0T57jpvOEyapEH5SL+XYqeJtkpRsh+EgTbQ4Va7CFGqhdJXv4nlKTR8Q 21 | yZGijfuz5uVGQxN/sLLF4rK6zPH7K0rR7Wal2QLrL16kIkrWijQvgE27AoGAC7M4 22 | 4seyYxQs5ugUl9j7wqmqy9BREsKuSHKQjR429+X45RJLZ5trBTxQMLNQuxMOYtEO 23 | OcBXOwSIR6XfWVxhxLDdt7/GNPfm2ozd1FqMU8pANwIRtHqRufZO1y15JT1VjS/B 24 | cm2zYZjctRFSthOXHqTvAPGQMg91hw2DGGBoxNUCgYBqVV0SAR7rHERA15gzeX1s 25 | ttPYrrSw4LEPfND9BvGzn2eZQTAnlTozQjUALkTcrMG92SCwI7/irOr9VB5T6jCA 26 | ZDlVGuNgxsPsIRhZEL9Aiq1Zo0qZ8UwFnmTgLo0PaLThk0y/hXXtmbe1Rlaybb2I 27 | a2LTXi/m4zp9AkpUzM0nMw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /cmd/trustydns-server/usage_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type testUsageCase struct { 11 | expectToRun bool // waitForExecute should not return an error if this is true 12 | args []string // ARGV - not counting command 13 | stdout []string // Expected stdout strings 14 | stderr string // Expected stderr string 15 | } 16 | 17 | var testUsageCases = []testUsageCase{ 18 | {false, []string{"--version"}, []string{"trustydns-server", "Version:"}, ""}, 19 | {false, []string{"-h"}, []string{"NAME", "SYNOPSIS", "OPTIONS", "Version: v"}, ""}, 20 | {false, []string{"-badopt"}, []string{}, "flag provided but not defined"}, 21 | {false, []string{"-v", "-A", "255.254.253.252"}, []string{"Starting"}, 22 | "assign requested address"}, 23 | {false, []string{"Command", "line", "goop"}, []string{}, "Unexpected parameters"}, 24 | 25 | // Bad ecs-set values 26 | {false, []string{"--ecs-set-ipv4-prefixlen", "200"}, []string{}, "must be between 0 and 32"}, 27 | {false, []string{"--ecs-set-ipv6-prefixlen", "200"}, []string{}, "must be between 0 and 128"}, 28 | {false, []string{"--ecs-set-ipv4-prefixlen", "-1"}, []string{}, "must be between 0 and 32"}, 29 | {false, []string{"--ecs-set-ipv6-prefixlen", "-2"}, []string{}, "must be between 0 and 128"}, 30 | 31 | // Bad local resolver config 32 | {false, []string{"-c", ""}, []string{}, "Must supplied a resolv.conf"}, 33 | {false, []string{"-c", "testdata/emptyfile"}, []string{}, "No servers"}, 34 | 35 | // tls 36 | {false, []string{"--tls-cert", "testdata/nosuchfile"}, []string{}, "Certificate file count"}, 37 | {false, []string{"--tls-key", "testdata/nosuchfile"}, []string{}, "key file count"}, 38 | } 39 | 40 | func TestUsage(t *testing.T) { 41 | for tx, tc := range testUsageCases { 42 | t.Run(fmt.Sprintf("%d", tx), func(t *testing.T) { 43 | args := append([]string{"trustydns-server"}, tc.args...) 44 | out := &mutexBytesBuffer{} 45 | err := &mutexBytesBuffer{} 46 | mainInit(out, err) 47 | done := make(chan error) 48 | go func() { 49 | done <- waitForMainExecute(t, time.Millisecond*200) 50 | }() 51 | ec := mainExecute(args) 52 | e := <-done // Get waitForExecute results 53 | outStr := out.String() 54 | errStr := err.String() 55 | 56 | if e != nil && tc.expectToRun { 57 | t.Fatal("Expected to run, but", e, errStr, outStr) 58 | } 59 | if ec == 0 && len(tc.stderr) > 0 { 60 | t.Error("Expected error exit from Execute() with stderr", tc.stderr) 61 | } 62 | 63 | if len(errStr) > 0 && len(tc.stderr) == 0 { 64 | t.Error("Did not expect a fatal error:", errStr) 65 | } 66 | if !strings.Contains(errStr, tc.stderr) { 67 | t.Error("Stderr expected:", tc.stderr, "Got:", errStr) 68 | } 69 | 70 | for _, o := range tc.stdout { 71 | if !strings.Contains(outStr, o) { 72 | t.Error("Stdout expected:", o, "Got:", outStr) 73 | } 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/ECS.md: -------------------------------------------------------------------------------- 1 | # DoH reduces GSLB effectiveness - ECS increases GSLB effectiveness 2 | 3 | [RFC8484](https://tools.ietf.org/html/rfc8484) is silent on the matter of EDNS0 Client Subnet 4 | [(ECS)](https://tools.ietf.org/html/rfc7871). This is understandable given the provenance of DoH 5 | which was borne into existence mostly as a simple secure DNS tunnel. But this silence is less 6 | explicable given ECS mitigates against one of the biggest performance disadvantages of DoH - that it 7 | results in sub-optimal answers from 8 | [GSLBs](https://www.a10networks.com/resources/articles/global-server-load-balancing) and 9 | [CDNs](https://en.wikipedia.org/wiki/Content_delivery_network). 10 | 11 | By their very nature DoH servers are likely to be topologically distant from their clients. That 12 | means GSLB answers well-suited to the DoH server location may not be well-suited to the client 13 | location. This answer discrepancy is the main reason ECS came into existence in the first place so 14 | it stands to reason that ECS is especially relevant to DoH. 15 | 16 | In the absence of guidance from RFC8484, trustydns has taken the liberty of adding a number of 17 | features which let you manipulate ECS to help overcome the performance disadvantages of DoH. 18 | 19 | ## ECS Synthesis 20 | 21 | The most beneficial feature is to enable ECS Synthesis in the `trustydns-server` by setting the 22 | `--ecs-set` option (and possibly the `--ecs-remove` option). With this option set, 23 | `trustydns-server` synthesizes an ECS option in the out-going query with the HTTPS client IP 24 | address. If the query makes it to a GSLB intact, then the GSLB will provide an answer best-suited to 25 | the client IP address. 26 | 27 | ECS Synthesis is performed on the server as it sees the true *routable address* of the client 28 | whereas a proxy or client on the inside of a local network may not have that ability, particularly if the 29 | local network is sitting behind a NAT or CG-NAT. 30 | 31 | ECS Sythesis can also be reguested by `trustydns-proxy` with the `ecs-request-*` options. In this 32 | case the proxy uses non-standard HTTP headers to signal the Synthesis Request to a 33 | `trustydns-server` instance. This approach lets each proxy deployment decide whether to use ECS or 34 | not rather than have the server arbitrarily synthesize ECS for all proxies. Naturally ECS Synthesis 35 | triggered by `trustydns-proxy` only works when it is sending DoH queries to a `trustydns-server`. 36 | 37 | ## Privacy 38 | 39 | The privacy implication of ECS Synthesis is that the client's IP address (or at least a masked 40 | version of it) is visible to the server-side resolvers and authoritative name servers used during 41 | resolution. This obvious exposure of the client's IP is a potential security risk. For that reason 42 | ECS Synthesis has to be specifically enabled by one of the aforementioned options. Furthermore, if 43 | there is any concern that local clients might be generated ECS options which you'd rather not 44 | expose, both the proxy and server have options to remove all ECS options prior to forwarding 45 | queries. 46 | 47 | ## Effectiveness 48 | 49 | Whether ECS Synthesis is effective or not depends on a number of factors. In particular whether the 50 | resolver used by the server forwards ECS options thru to authoritative servers. Many resolvers 51 | cannot forward ECS options. An exception is unbound so there is a [brief document](./unbound.md) 52 | explaining how to activate that feature. Also, ECS is only supported by some GSLBs or only supported 53 | on an opt-in basis. So all in all the effectiveness of ECS Synthesis may not be as significant as 54 | you might wish. To gain insights into ECS effectiveness the proxy and server produce periodic ECS 55 | statistics. 56 | 57 | 58 | ## A word of warning about IPv6 and GSLBs 59 | 60 | Many GSLBs rely on geo-IP databases such as those provided by [Akamai 61 | Edgescape](https://developer.akamai.com/edgescape), [Maxmind](https://www.maxmind.com/) and 62 | [IP2Location](https://www.ip2location.com). While these databases tends to have around an 80% level 63 | of accuracy for IPv4 they are much less accurate with IPv6 networks. In part this is because many 64 | end-user networks are using ISP-delegated IPv6 address space rather than purchasing their own 65 | portable allocations. This means the GEO IP database providers have no whois information to scrape - 66 | a significant source of their geo-IP location data. 67 | 68 | The net result from a DoH perspective is that even though ECS Synthesis can mitigate against the 69 | performance disadvantages of DoH, for IPv6 it may be some time before mitigation is on par with 70 | IPv4. 71 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO List for trustydns 2 | 3 | A dumping ground for unresolved issues and discussion topics. 4 | 5 | ## The Bootstrap Problem 6 | 7 | How should `trustydns-proxy` be deployed if the DNS lookups cannot be trusted in the first place? It 8 | has no easy way of determining the correct IP addresses for the DoH URLs. Most likely we'll need to 9 | introduce a configuration or command-line option specifying the IP addresses to use. We could 10 | annotate the URL with something like: https://dohserver/dns-query@10.1.2.3. This could work since we 11 | know that the DoH URL is heavily constrained anyway. 12 | 13 | Unfortunately there doesn't seem to be an easy way of achieving this with Go's net/http package 14 | unless we replace the RoundTripper which is a monster activity. What we really want to do is control 15 | the DNS lookup but that's embedded deep within the Transport package. Changing the 16 | http.Request.URL.Host to an IP doesn't work as that is used to form the URL 17 | https://IPAddress/dns-query which fails TLS unless the remote certificate also contains an "IP SANs" 18 | - whatever that is. 19 | 20 | ## Loopback Query Protection 21 | 22 | It's a relatively easy mistake to configure the proxy to send split-domain queries back to 23 | itself. To check for this, the proxy could issue a query of ${uuid}.example.net to the local 24 | resolver and see if it shows up on the listen side. This may not be foolproof if there are multiple 25 | nameservers entries only some of which point back to the proxy. There is also no easy way of 26 | encoding loop detection into the dns query as we risk that encoding leaking out to a "real" resolver 27 | and possibly causing undesirable responses. 28 | 29 | ## Multiple client Credentials for the proxy 30 | 31 | Strictly, there should be separate client TLS credentials for each private DoH server used by the 32 | proxy. As it stands the proxy can only load one set of credentials to use with all DoH 33 | servers. Let's see if multiple private DoH servers become common before worrying about this. 34 | 35 | ## Client Revocation 36 | 37 | I can't see a way to uniquely identify client certificates *and* access that unique identity on the 38 | server side thus there is no facility to revoke a client certificate without revoking the root CA 39 | and starting again. TLS must have a way of identify TLS clients - but is that accessible in Go? 40 | 41 | ## Solve Package Dependency with go mod 42 | 43 | We should upgrade to using `go mod` for package dependency. Fortunately there are few dependency 44 | thus far so this isn't critical. 45 | 46 | ## setuid/setgid is broken on Linux 47 | 48 | Not really a trustydns problem, more a victim of it, but on Linux programs written in Go cannot 49 | discard root privileges needed to open privileged network sockets. This is normal security practice 50 | on Unix. If you attempt a priviledge downgrade with the trustydns daemons (via `--user` and 51 | `--group`) they generate a warning but continue to run with elevated privileges. This is not a 52 | problem on any other Unix platform. 53 | 54 | ## miekg/dns.Client Optimization 55 | 56 | Should resolver/local have a pool of dns.Clients or net.Conns? Currently each Resolv() call results 57 | in a socket setup and teardown when there are typically a known (and small) set of local resolvers 58 | which are very amenable to connection pooling. 59 | 60 | ## go testing for main() is klunky and brittle 61 | 62 | The go testing frame-work is not well suited to testing executables. the main commands have been 63 | structured to make it possible to use the go framework to test things like usage use-cases but they 64 | are still more brittle than I would like. Perhaps an alternative testing framework should be 65 | considered for these tests? 66 | -------------------------------------------------------------------------------- /docs/unbound.md: -------------------------------------------------------------------------------- 1 | # Configuring unbound to forward ECS queries 2 | 3 | As discussed in [ECS](ECS.md), `trustydns-server` can be configured to add a synthetic ECS option to 4 | queries forwarded to its resolvers. Unfortunately most resolvers remove ECS options before 5 | forwarding queries to authoritative name servers which are the ones that actually act on 6 | them. Fortunately the popular resolver [unbound](https://nlnetlabs.nl/projects/unbound/about/) 7 | supports ECS forwarding. Unfortunately most distributions of unbound do not have that support 8 | compiled in so you'll most likely have to build from sources to get this functionalty. Fortunately 9 | that's pretty easy to do as witnessed by the brevity of this document. 10 | 11 | To build unbound from sources with ECS forwarding enabled: 12 | 13 | ```sh 14 | ./configure --enable-subnet 15 | make install 16 | ``` 17 | 18 | Then enable ECS in the unbound configuration with these configuration lines: 19 | 20 | ``` 21 | server: 22 | module-config: "subnetcache iterator" 23 | client-subnet-always-forward: "yes" 24 | ``` 25 | 26 | That's it. 27 | -------------------------------------------------------------------------------- /docs/windows.md: -------------------------------------------------------------------------------- 1 | # Running trustydns on Windows 2 | 3 | Warning: The authors are complete neophytes when it comes to Windows. Any help appreciated. The 4 | follow comments are based on testing with a 32bit Windows7 instance. 5 | 6 | The main message is that the trustydns *does* run on Windows, but it's not as pretty as we'd like. 7 | 8 | For cross-compiling the [Makefile](../Makefile) includes targets for 'windowsamd64' and 'windows386' 9 | which produce `.exe` files. These `.exe` files can then be transferred to your Windows 10 | system. Alternatively you can download `go` and compile natively. Both approaches are known to 11 | produce working executables. 12 | 13 | Regardless of the mechanism by which you create executables, the main issue runnng them is dealing 14 | with missing directories and files assumed to be present by the various commands. Specifically: 15 | 16 | * No TLS root certificate directory thus the need to run with `--tls-use-system-roots=false` for all commands 17 | * No resolv.conf file thus the need to create and identify one for `trustydns-server` using `-c resolv.conf` 18 | 19 | Our guess is that this data lives in the Registry and as such is not available via the file 20 | system. If anyone wants to offer code which accesses this data via the correct mechanism within 21 | Windows we will gladly accept it. 22 | 23 | A further limitation is that `go test` fails on Windows as the tests assume the presence of the 24 | aforementioned files and directories and they also currently assume a Unix signal 25 | environment. You'll have to take it on faith that a successful `go test` on a Unix system is 26 | sufficient. 27 | 28 | It also has to be said that all the commands are particularly "unixy" in that they still use 29 | `-short-option` and `--long-option` as opposed to Windows switches. This is due to the use of the 30 | standard go `flag` package. Is there an alternate package which use a platform-appropriate syntax 31 | rather than a hard-coded Unix syntax? That is one which accepts `/short-option` and `/long-option' 32 | for Windows? If so, let us know or better yet create a pull request with the patches to use it. 33 | 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/markdingo/trustydns 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/gops v0.3.28 7 | github.com/miekg/dns v1.1.62 8 | golang.org/x/net v0.38.0 9 | golang.org/x/sys v0.31.0 10 | ) 11 | 12 | require ( 13 | golang.org/x/mod v0.18.0 // indirect 14 | golang.org/x/sync v0.12.0 // indirect 15 | golang.org/x/text v0.23.0 // indirect 16 | golang.org/x/tools v0.22.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/gops v0.3.28 h1:2Xr57tqKAmQYRAfG12E+yLcoa2Y42UJo2lOrUFL9ark= 2 | github.com/google/gops v0.3.28/go.mod h1:6f6+Nl8LcHrzJwi8+p0ii+vmBFSlB4f8cOOkTJ7sk4c= 3 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 4 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 5 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 6 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 7 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 8 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 9 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 10 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 11 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 12 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 13 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 14 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 15 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 16 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 17 | -------------------------------------------------------------------------------- /internal/bestserver/base.go: -------------------------------------------------------------------------------- 1 | package bestserver 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | type algorithm string 9 | 10 | const ( 11 | LatencyAlgorithm algorithm = "latency" // Pick the fastest most reliable server 12 | TraditionalAlgorithm = "traditional" // Pick until fails - just as res_send() does 13 | ) 14 | 15 | // baseManager implements most of the Manager interface and provides helper routines that assist in 16 | // implementations meeting the Manager interface. Algorithms are encouraged to compose themselves 17 | // with baseManager as a way of providing most of the interface, though of course they are not 18 | // obliged to do so. 19 | type baseManager struct { 20 | algType algorithm // Set by Algorithm 21 | mu sync.RWMutex // Protects everything below here as well as implementation vars 22 | servers []Server 23 | serverCount int // Cache of len(servers) 24 | serverToIndex map[Server]int // Converts Server back to array index 25 | bestIndex int // Index of current 'best' server 26 | } 27 | 28 | // lock is a wrapper to encapsulate locking on behalf of all bestserver 29 | // implementations. Implementations must call lock|rlock/unlock to protect their 30 | // data structures from concurrent access. 31 | func (t *baseManager) lock() { 32 | t.mu.Lock() 33 | } 34 | 35 | // unlock is a wrapper to encapsulate locking on behalf of all implementations. 36 | func (t *baseManager) unlock() { 37 | t.mu.Unlock() 38 | } 39 | 40 | // rlock is a wrapper to encapsulate locking on behalf of all implementations. 41 | func (t *baseManager) rlock() { 42 | t.mu.RLock() 43 | } 44 | 45 | // rlock is a wrapper to encapsulate locking on behalf of all implementations. 46 | func (t *baseManager) runlock() { 47 | t.mu.RUnlock() 48 | } 49 | 50 | // init is called by the algorithm constructor to initialize the server variables. 51 | func (t *baseManager) init(algType algorithm, servers []Server) error { 52 | if len(servers) == 0 { 53 | return errors.New("bestserver:No servers in list") 54 | } 55 | t.algType = algType 56 | t.servers = servers 57 | t.serverCount = len(t.servers) 58 | 59 | t.serverToIndex = make(map[Server]int) 60 | for ix, s := range t.servers { 61 | if _, ok := t.serverToIndex[s]; ok { 62 | return errors.New("bestserver.New: Duplicate Server in list: " + s.Name()) 63 | } 64 | t.serverToIndex[s] = ix 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (t *baseManager) Algorithm() string { 71 | return string(t.algType) 72 | } 73 | 74 | func (t *baseManager) Best() (Server, int) { 75 | t.rlock() 76 | defer t.runlock() 77 | 78 | return t.servers[t.bestIndex], t.bestIndex 79 | } 80 | 81 | func (t *baseManager) Servers() []Server { 82 | servers := make([]Server, len(t.servers)) 83 | copy(servers, t.servers) 84 | 85 | return servers 86 | } 87 | 88 | func (t *baseManager) Len() int { 89 | return len(t.servers) 90 | } 91 | 92 | // defaultServer is the internal struct used to hold the server names provided to the NewFromNames() 93 | // constructor. 94 | type defaultServer struct { 95 | name string 96 | } 97 | 98 | // Name returns the name of the server returned by Best() 99 | func (t *defaultServer) Name() string { 100 | return t.name 101 | } 102 | 103 | // ServersFromNames is a helper function to construct a Server list for a string list. The order of 104 | // the returned list is the same as that of the supplied names. 105 | func ServersFromNames(names []string) []Server { 106 | servers := make([]Server, 0, len(names)) 107 | for _, n := range names { 108 | servers = append(servers, &defaultServer{name: n}) 109 | } 110 | 111 | return servers 112 | } 113 | -------------------------------------------------------------------------------- /internal/bestserver/base_test.go: -------------------------------------------------------------------------------- 1 | package bestserver 2 | 3 | import ( 4 | "strings" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var ( 11 | dupe = &defaultServer{name: "dupe"} 12 | unique = &defaultServer{name: "unique"} 13 | one = &defaultServer{name: "one"} 14 | two = &defaultServer{name: "two"} 15 | three = &defaultServer{name: "three"} 16 | ) 17 | 18 | func TestBaseInit(t *testing.T) { 19 | bm := &baseManager{} 20 | err := bm.init(LatencyAlgorithm, []Server{dupe, unique, dupe}) 21 | if err == nil { 22 | t.Error("Expected dupe server error") 23 | } 24 | if err != nil { 25 | if !strings.Contains(err.Error(), "Duplicate") { 26 | t.Error("Expected 'Duplicate' error, not", err) 27 | } 28 | } 29 | } 30 | 31 | func TestBaseName(t *testing.T) { 32 | bm := &baseManager{} 33 | err := bm.init(LatencyAlgorithm, []Server{one, two}) 34 | if err != nil { 35 | t.Fatal("Did not expect error during setup", err) 36 | } 37 | 38 | if bm.Algorithm() != string(LatencyAlgorithm) { 39 | t.Error("t.Name() mismatch. Expected", LatencyAlgorithm, "got", bm.Algorithm()) 40 | } 41 | } 42 | 43 | func TestBaseBest(t *testing.T) { 44 | bm := &baseManager{} 45 | err := bm.init(LatencyAlgorithm, []Server{one, two}) 46 | if err != nil { 47 | t.Fatal("Did not expect error during setup", err) 48 | } 49 | 50 | b, _ := bm.Best() 51 | if b.Name() != "one" { 52 | t.Error("Expected Best to be first cab off the rank, not", b) 53 | } 54 | } 55 | 56 | func TestBaseServers(t *testing.T) { 57 | bm := &baseManager{} 58 | origServers := []Server{one, two, three} 59 | err := bm.init(LatencyAlgorithm, origServers) 60 | if err != nil { 61 | t.Fatal("Did not expect error during setup", err) 62 | } 63 | 64 | sList := bm.Servers() 65 | if !sameServers(origServers, sList) { 66 | t.Error("server lists not the same", origServers, "and", sList) 67 | } 68 | 69 | if bm.Len() != 3 { 70 | t.Error("Len() did not return 3, got", bm.Len()) 71 | } 72 | } 73 | 74 | // Test reader/writer lock functions (just wrappers around mutex, but still). Any errors are fatal 75 | // as the lock is in an indeterminate state. 76 | func TestBaseLocking(t *testing.T) { 77 | bm := &baseManager{} 78 | err := bm.init(LatencyAlgorithm, []Server{one}) 79 | if err != nil { 80 | t.Fatal("Did not expect error during setup", err) 81 | } 82 | 83 | // Check writer lock 84 | bm.lock() 85 | var otherGotLock int64 86 | go func() { 87 | bm.lock() 88 | atomic.StoreInt64(&otherGotLock, 1) 89 | bm.unlock() 90 | }() 91 | 92 | time.Sleep(50 * time.Millisecond) 93 | if atomic.LoadInt64(&otherGotLock) != 0 { 94 | t.Fatal("writer lock didn't stop concurrent access") 95 | } 96 | bm.unlock() 97 | time.Sleep(50 * time.Millisecond) // Other go func should take lock and increment 98 | if atomic.LoadInt64(&otherGotLock) != 1 { 99 | t.Fatal("writer unlock did not allow other writer to lock") 100 | } 101 | 102 | // Check reader lock 103 | bm.rlock() // This may wait fractionally for the above go-routine to unlock, no matter 104 | atomic.StoreInt64(&otherGotLock, 0) 105 | go func() { 106 | bm.rlock() 107 | atomic.StoreInt64(&otherGotLock, 1) // Two readers should be fine 108 | bm.runlock() 109 | }() 110 | time.Sleep(50 * time.Millisecond) 111 | if atomic.LoadInt64(&otherGotLock) != 1 { 112 | t.Fatal("reader lock blocked second reader") 113 | } 114 | atomic.StoreInt64(&otherGotLock, 0) 115 | go func() { 116 | bm.lock() // Writer should block 117 | atomic.StoreInt64(&otherGotLock, 1) 118 | bm.unlock() 119 | }() 120 | time.Sleep(50 * time.Millisecond) 121 | if atomic.LoadInt64(&otherGotLock) == 1 { 122 | t.Fatal("reader lock did not block writer") 123 | } 124 | bm.runlock() 125 | time.Sleep(50 * time.Millisecond) 126 | if atomic.LoadInt64(&otherGotLock) != 1 { 127 | t.Fatal("reader unlock did not release blocked writer") 128 | } 129 | } 130 | 131 | func TestServersFromNames(t *testing.T) { 132 | sl := ServersFromNames([]string{"a", "b", "c", "a"}) 133 | if sl[0].Name() != "a" { 134 | t.Error("[0] name should EQ 'a', not", sl[0].Name()) 135 | } 136 | if sl[1].Name() != "b" { 137 | t.Error("[1] name should EQ 'b', not", sl[1].Name()) 138 | } 139 | if sl[2].Name() != "c" { 140 | t.Error("[2] name should EQ 'c', not", sl[2].Name()) 141 | } 142 | if sl[3].Name() != "a" { 143 | t.Error("[3] name should EQ 'a', not", sl[3].Name()) 144 | } 145 | } 146 | 147 | // A not very comprehesive matcher. We know that goodList has the correct entries which are also 148 | // promised to be unique so we can shortcut the comprehensive two-way comparison needed if the two 149 | // lists were completely unknown. 150 | func sameServers(goodList, newList []Server) bool { 151 | if len(goodList) != len(newList) { 152 | return false 153 | } 154 | 155 | found := 0 156 | for _, g := range goodList { 157 | matchNew: 158 | for _, n := range newList { 159 | if n == g { 160 | found++ 161 | break matchNew 162 | } 163 | } 164 | } 165 | 166 | return found == len(goodList) 167 | } 168 | -------------------------------------------------------------------------------- /internal/bestserver/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bestserver tracks the performance and reliability of each server for the purpose of 3 | identifying which server is the most reliable and has the lowest latency. This package *should* work 4 | for any sort of latency-based set of servers (or performance which can be expressed as a 5 | time.Duration) regardless of what they actually do. 6 | 7 | The bestserver structure contains a list of all available servers, what a server represents, is 8 | unknown to this package. It could be a URL, an IP address, the name of a racing pigeon... whatever. 9 | 10 | After a server is used by the application, the application calls this package to record 11 | success/failure and latency. That data is used internally to influence which server is chosen next. 12 | 13 | Typical usage looks like this: 14 | 15 | bs := bestServer.NewLatency(Config, ServerList...) // Construct a specific bestserver container 16 | for { 17 | server, _ := bs.Best() // Get current best server 18 | doStuffWithServer(server.Name()) // Use it 19 | bs.Result(server, success bool, when time.Time, latency time.Duration) // Say how it went 20 | } 21 | 22 | A call to Result() with the current best server causes a reassessment of the best server. Calls to 23 | Best() will always return the same server details if no intervening calls to Result() have been 24 | made. 25 | 26 | Calls to Result() with a server other than the current best result in accumulation of statistics 27 | but no reassessment of the current best. 28 | 29 | Callers must not cache returns from Best() as that distorts the reassessment algorithm. 30 | 31 | There are currently two types of "best servers" to choose from: 'latency' and 'traditional' which 32 | are created with the obviously named NewLatency() and NewTraditional() functions respectively. They 33 | each implement different algorithms when choosing a new best server. This package is structured to 34 | make it easy to add additional algorithms if the need arises. 35 | 36 | The 'latency' algorithm generally tries to gravitate towards the lowest latency server by 37 | opportunistically sampling all servers to collect statistics on their performance. The selection 38 | algorithm is: 39 | 40 | - the first server on the list starts as the 'best' server 41 | 42 | - a reassessment occurs if any of the following conditions are true: 43 | o the current 'best' server is given an unsuccessful result 44 | o the configured reassessment timer has expired 45 | o the configured number of Result() calls have been reached 46 | 47 | Reassessment chooses the server with the lowest weighted average latency to become the new 'best' 48 | server. 49 | 50 | To ensure there is latency data for all server, after a Result() call, Best() will periodically 51 | return a non-'best' server to gather performance information for that server. The default sample 52 | rate at which non-'best' servers are returned is approximately 5% of the time. 53 | 54 | Servers which are unsuccessful as indicated by Result() calls are excluded from this sampling 55 | process for a configured time period. 56 | 57 | The expectation is that there are a relatively small number of servers as much of the selection 58 | algorithm is a simple linear search of all entries and thus O(n). A server list of 10-20 is 59 | reasonable, 1,000-10,000 is probably not. 60 | 61 | The 'traditional' implementation created with NewTraditional() is intended to mimic nameserver 62 | selection by res_send(3) as described in RESOLVER(3). That is, the first server is used until it 63 | fails then the next server is used until it fails and so on. Once the end of the server list is 64 | reached, then the algorithm wraps around to the first server and the process repeats. 65 | 66 | Multiple goroutines can safely invoke all the Manager interface methods concurrently. 67 | */ 68 | package bestserver 69 | -------------------------------------------------------------------------------- /internal/bestserver/manager.go: -------------------------------------------------------------------------------- 1 | package bestserver 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Server is the interface used to create a bestserver collection. It is returned by Best() and 8 | // passed in to Result(). The underlying struct is supplied by the caller when they created a 9 | // bestserver collection with one of the New* functions. This struct can be either one created by 10 | // the caller or the default struct used by our NewFromNames() helper method. The application will 11 | // normally supply its own if it wants to track other things related to the server, such as stats or 12 | // server IP address or similar. 13 | type Server interface { 14 | Name() string 15 | } 16 | 17 | // Manager is the public interface for bestserver. 18 | type Manager interface { 19 | // Algorithm returns the name of the implementation 20 | Algorithm() string 21 | 22 | // Best returns the current best server (and its index into the Server 23 | // List) as determined by the underlying algorithm in use. It always 24 | // returns valid values. The returned index is an index to the server 25 | // list as originally supplied when this collection was created. 26 | Best() (Server, int) 27 | 28 | // Result updates internal statistics and *may* assess whether there is a 29 | // better choice for the current 'best' server. 30 | // 31 | // The Server passed into Result() must be exactly the value returned by 32 | // Best() as it is used as an index into a map. Result() requires the 33 | // Server parameter to be supplied rather than rely on the existing 34 | // "best" server as the "best" Server may have change between the two 35 | // calls by the action of another go-routine. 36 | // 37 | // Return false if Server is not part of this collection 38 | Result(server Server, success bool, now time.Time, latency time.Duration) bool 39 | 40 | // Servers returns a slice of all Servers in the order originally created. 41 | Servers() []Server 42 | 43 | // Len returns the count of servers 44 | Len() int 45 | } 46 | -------------------------------------------------------------------------------- /internal/bestserver/traditional.go: -------------------------------------------------------------------------------- 1 | package bestserver 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // TraditionalConfig defines all the public parameters that the calling application can 8 | // set. Currently this is just a place-holder for the API. But easier to add a field to an empty 9 | // struct than to add an additional parameter to an API in widespread use. 10 | type TraditionalConfig struct { 11 | } 12 | 13 | var ( 14 | defaultTraditionalConfig = TraditionalConfig{} 15 | ) 16 | 17 | type traditional struct { 18 | TraditionalConfig 19 | baseManager 20 | } 21 | 22 | func NewTraditional(config TraditionalConfig, servers []Server) (*traditional, error) { 23 | t := &traditional{} 24 | err := t.baseManager.init(TraditionalAlgorithm, servers) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return t, err 30 | } 31 | 32 | func (t *traditional) Result(server Server, success bool, now time.Time, latency time.Duration) bool { 33 | t.lock() 34 | defer t.unlock() 35 | 36 | ix, found := t.serverToIndex[server] 37 | if !found { 38 | return false 39 | } 40 | 41 | if success { 42 | return true 43 | } 44 | 45 | if ix == t.bestIndex { // If 'best' failed, move to next server. 46 | t.bestIndex = (t.bestIndex + 1) % t.serverCount 47 | } 48 | 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /internal/bestserver/traditional_test.go: -------------------------------------------------------------------------------- 1 | package bestserver 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTraditionalNew(t *testing.T) { 10 | _, err := NewTraditional(TraditionalConfig{}, []Server{first, second, third, fourth}) 11 | if err != nil { 12 | t.Fatal("Unexpected error with hen setting up for test", err) 13 | } 14 | 15 | _, err = NewTraditional(TraditionalConfig{}, []Server{}) 16 | if err == nil { 17 | t.Fatal("Expected an error with no servers") 18 | } 19 | if err != nil { 20 | if !strings.Contains(err.Error(), "No servers") { 21 | t.Error("Expected 'No servers' in error, not", err) 22 | } 23 | } 24 | } 25 | 26 | func TestTraditionalResult(t *testing.T) { 27 | bs, err := NewTraditional(TraditionalConfig{}, []Server{first, second, third, fourth}) 28 | if err != nil { 29 | t.Fatal("Unexpected error when setting up for test", err) 30 | } 31 | 32 | now := time.Now() 33 | s, _ := bs.Best() 34 | if s != first { 35 | t.Error("traditional did not return first server on first Best()", s) 36 | } 37 | 38 | s, _ = bs.Best() 39 | if s != first { 40 | t.Error("traditional did not return first server on second Best()", s) 41 | } 42 | 43 | bs.Result(first, true, now, time.Second) 44 | s, _ = bs.Best() 45 | if s != first { 46 | t.Error("traditional did not return first server on success Report()", s) 47 | } 48 | 49 | // Any sort of report on the non-Best should have no influence on current best 50 | bs.Result(second, false, now, time.Second) 51 | s, _ = bs.Best() 52 | if s != first { 53 | t.Error("traditional did not return first server on !success Report(second)", s) 54 | } 55 | 56 | bs.Result(s, false, now, time.Second) // Should move to second immediately 57 | s, _ = bs.Best() 58 | if s != second { 59 | t.Error("traditional did not return second server on !success Report of best", s) 60 | } 61 | bs.Result(s, false, now, time.Second) // Should move to third 62 | s, _ = bs.Best() 63 | if s != third { 64 | t.Error("traditional did not return third server on !success Report of best", s) 65 | } 66 | bs.Result(s, false, now, time.Second) // Should move to fourth 67 | s, _ = bs.Best() 68 | if s != fourth { 69 | t.Error("traditional did not return fourth server on !success Report of best", s) 70 | } 71 | 72 | bs.Result(s, false, now, time.Second) // Should wrap back to first 73 | s, _ = bs.Best() 74 | if s != first { 75 | t.Error("traditional did not loop back to first on !success Report of best", s) 76 | } 77 | 78 | ok := bs.Result(s, false, now, time.Second) 79 | if !ok { 80 | t.Error("Result did not return true with a legit server name") 81 | } 82 | ok = bs.Result(&defaultServer{name: "bogus"}, false, now, time.Second) 83 | if ok { 84 | t.Error("Result returned true with a bogus server name") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/concurrencytracker/counter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package concurrencytracker keeps track of how many concurrent requests are active. The purpose is 3 | simply to provide the ability to report peak concurrency over a reporting period. Typically usage: 4 | 5 | var ct concurrencytrack.Counter 6 | 7 | func ServeSomething() { 8 | cct.Add() 9 | defer cct.Done() 10 | ... do some work 11 | } 12 | 13 | and in some reporting function 14 | 15 | fmt.Println("Peak Concurrency", cct.Peak(true)) 16 | */ 17 | package concurrencytracker 18 | 19 | import ( 20 | "sync" 21 | ) 22 | 23 | // Counter is the core structure used by concurrencytracker 24 | type Counter struct { 25 | sync.Mutex 26 | current int // Count of pending Done() calls 27 | peak int // Max 'current' has ever reached 28 | } 29 | 30 | // Add increments 'current' and if a new peak has been reached, the peak value is updated. Return 31 | // true if the peak has increased as a result of this call. 32 | func (t *Counter) Add() (increased bool) { 33 | t.Lock() 34 | defer t.Unlock() // A tad silly to defer for a tiny func, but "idioms aint idioms for nuthin', Sol!" 35 | t.current++ 36 | if t.current > t.peak { 37 | t.peak = t.current 38 | increased = true 39 | } 40 | 41 | return 42 | } 43 | 44 | // Done decrements 'current'. Done() must only be called after an Add() call, otherwise a panic 45 | // ensues. 46 | func (t *Counter) Done() { 47 | t.Lock() 48 | defer t.Unlock() 49 | if t.current == 0 { 50 | panic("concurrencytracker.Done() lacks matching .Add()") // Someone goofed 51 | } 52 | t.current-- 53 | } 54 | 55 | // Peak returns the peak concurrency count and optionally resets the peak value to the current 56 | // concurrency value. Note that the current counter is *not* reset by this call. In fact that value 57 | // is never rest. The reset occurs *after* the return value is set so the impact of the reset is not 58 | // visible until a subsequent call to Peak(). 59 | func (t *Counter) Peak(resetCounters bool) (peak int) { 60 | t.Lock() 61 | defer t.Unlock() 62 | peak = t.peak 63 | if resetCounters { 64 | t.peak = t.current 65 | } 66 | 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /internal/concurrencytracker/counter_test.go: -------------------------------------------------------------------------------- 1 | package concurrencytracker 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAll(t *testing.T) { 8 | var cct Counter 9 | peak := cct.Peak(false) 10 | if peak != 0 { 11 | t.Error("Peak should start life at zero, not", peak) 12 | } 13 | cct.Add() // Should be: current=1, peak=1 14 | peak = cct.Peak(false) 15 | if peak != 1 { 16 | t.Error("Peak should reflect Add->1, not", peak) 17 | } 18 | cct.Add() // Should be: current=2, peak=2 19 | peak = cct.Peak(false) 20 | if peak != 2 { 21 | t.Error("Peak should reflect Add->2, not", peak) 22 | } 23 | 24 | cct.Done() // Should be: current=1, peak=2 25 | peak = cct.Peak(true) // true means peak=current. Should be: current=1, peak=1 26 | if peak != 2 { 27 | t.Error("Peak should not decrement until reset. Expect 2, not", peak) 28 | } 29 | peak = cct.Peak(false) // Should be: current=1, peak=1 30 | if peak != 1 { 31 | t.Error("Peak should have been reset down to current peak. Expect 1, not", peak) 32 | } 33 | 34 | cct.Done() // Should be: current=0, peak=1 35 | peak = cct.Peak(true) // Should be reset to: current=0, peak=0 36 | if peak != 1 { 37 | t.Error("Peak should have been reset down to current peak. Expect 1, not", peak) 38 | } 39 | peak = cct.Peak(false) 40 | if peak != 0 { 41 | t.Error("Peak should have been reset down to zero, not", peak) 42 | } 43 | } 44 | 45 | // Check that Add returns true when it increases peak 46 | func TestAddTrue(t *testing.T) { 47 | var cct Counter 48 | if !cct.Add() { // curr=1, peak=1 49 | t.Error("Expected first add to set new peak") 50 | } 51 | if !cct.Add() { // curr=2, peak=2 52 | t.Error("Expected second add to set new peak") 53 | } 54 | cct.Done() // curr=1, peak=2 55 | peak := cct.Peak(false) // Returns peak=2, After call curr=1, peak=2 56 | if cct.Add() { 57 | t.Error("Expected third add to not set new peak", peak, cct.Peak(false)) 58 | } 59 | } 60 | 61 | func TestPanic(t *testing.T) { 62 | gotPanic := false 63 | panicFunc(&gotPanic) 64 | if !gotPanic { 65 | t.Error("Expected a panic/recover sequence, but nadda") 66 | } 67 | } 68 | 69 | func panicFunc(gotPanic *bool) { 70 | var cct Counter 71 | cct.Add() 72 | cct.Done() 73 | defer func() { 74 | if x := recover(); x != nil { 75 | *gotPanic = true 76 | } 77 | }() 78 | cct.Done() // Should cause panic and set the gotPanic flag 79 | } 80 | -------------------------------------------------------------------------------- /internal/connectiontracker/reporter.go: -------------------------------------------------------------------------------- 1 | package connectiontracker 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Name implements the reporter interface 9 | func (t *Tracker) Name() string { 10 | return "Conn Track" 11 | } 12 | 13 | // Name Report implements the reporter interface 14 | func (t *Tracker) Report(resetCounters bool) string { 15 | t.mu.Lock() 16 | defer t.mu.Unlock() 17 | errs := 0 18 | for _, v := range t.errors { 19 | errs += v 20 | } 21 | report := fmt.Sprintf("curr=%d pk=%d sess=%d errs=%d (%s) connFor=%0.1fs activeFor=%0.1fs %s", 22 | len(t.connMap), t.peakConns, t.peakSessions, errs, formatCounters("%d", "/", t.errors[:]), 23 | t.connFor.Round(time.Millisecond*100).Seconds(), t.activeFor.Round(time.Millisecond*100).Seconds(), 24 | t.name) 25 | if resetCounters { 26 | t.trackerStats = trackerStats{} 27 | for _, v := range t.connMap { 28 | v.resetCounters() 29 | } 30 | } 31 | 32 | return report 33 | } 34 | 35 | // formatCounters returns a nice %d/%d/%d format from an array of ints. This is less error-prone 36 | // than hard-coding one big ol' Sprintf string but obviously slower which is irrelevant here. 37 | func formatCounters(vfmt string, delim string, vals []int) string { 38 | res := "" 39 | for ix, v := range vals { 40 | if ix > 0 { 41 | res += delim 42 | } 43 | res += fmt.Sprintf(vfmt, v) 44 | } 45 | 46 | return res 47 | } 48 | -------------------------------------------------------------------------------- /internal/connectiontracker/reporter_test.go: -------------------------------------------------------------------------------- 1 | package connectiontracker 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestReporterName(t *testing.T) { 11 | trk := New("Fido") 12 | if trk.Name() != "Conn Track" { 13 | t.Error("New not storing name correctly", trk.Name()) 14 | } 15 | rep := trk.Report(false) 16 | if !strings.Contains(rep, "Fido") { 17 | t.Error("New not reporting name correctly", rep) 18 | } 19 | } 20 | 21 | const ( 22 | zero = "curr=0 pk=0 sess=0 errs=0 (0/0/0/0/0/0) connFor=0.0s activeFor=0.0s Filo" 23 | one = "curr=1 pk=1 sess=0 errs=0 (0/0/0/0/0/0) connFor=0.0s activeFor=0.0s Filo" 24 | ) 25 | 26 | func TestReporterReport(t *testing.T) { 27 | trk := New("Filo") 28 | rep := trk.Report(false) 29 | if rep != zero { 30 | t.Error("Expected zero report", zero, "got", rep) 31 | } 32 | trk.ConnState("one", time.Now(), http.StateNew) 33 | rep = trk.Report(false) 34 | if rep != one { 35 | t.Error("Expected one report", one, "got", rep) 36 | } 37 | trk.ConnState("one", time.Now(), http.StateClosed) 38 | trk.Report(true) // Cause reset 39 | rep = trk.Report(false) // Get report *after* reset 40 | if rep != zero { 41 | t.Error("resetCounters did not produce zero report. Got", rep) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/connectiontracker/tracker.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package connectiontracker track connections for statistical purposes - ostensibly for inbound HTTP2 3 | connections - but it is a generic package that should apply to other connections. The goal is to 4 | determine occupancy and concurrency on a per-listen-address basis and within a given connection for 5 | those connections which support sessions. 6 | 7 | connectiontracker presents a reporter interface so its output can be periodically logged. 8 | 9 | Typically usage is to create a connectiontracker for a given listen or connect address then call it 10 | via the http.Server.ConnState function or moral equivalent, i.e: 11 | 12 | ct := connectiontracker.New("Name") 13 | s := http.Server{ConnState: func(c net.Conn, state ConnState) { 14 | ct.ConnState(c.RemoteAddr().String(), time.Now(), state) 15 | } 16 | 17 | ... time passes and requests occur 18 | fmt.Println(ct.Report(true)) 19 | 20 | If you are running a system in which connections can have multiple sessions such as HTTP2 then you 21 | should also call SessionAdd/SessionDone when sessions transition from active to closed. This is 22 | normally at the beginning of the request handler, such as: 23 | 24 | ct.SessionAdd(http.Request.RemoteAddr) 25 | defer ct.SessionDone(http.Request.RemoteAddr) 26 | 27 | The connection and session key can be any string you like so long as it is consistent and accurately 28 | reflects a unique connection endpoint. Normally it's a remote address/port and by virtue of the fact 29 | that a connectiontracker is associated with a server having a unique listen address the remote 30 | address/port/listen-address tuple makes the key appropriately unique. 31 | */ 32 | package connectiontracker 33 | 34 | import ( 35 | "net/http" 36 | "sync" 37 | "time" 38 | ) 39 | 40 | type connectionStats struct { 41 | connStart time.Time // When connection was first established 42 | activeStart time.Time // Last transition to active 43 | activeFor time.Duration // Sum of active periods 44 | currentSessions int 45 | peakSessions int 46 | } 47 | 48 | type connection struct { 49 | connectionStats 50 | } 51 | 52 | func (t *connection) resetCounters() { 53 | } 54 | 55 | type errIx int 56 | 57 | const ( 58 | errNoConnInMap errIx = iota // Connection not present for state change 59 | errNoConnForSession // No Connection found for session 60 | errDanglingConn // New when already active 61 | errNegativeConcurrency // More Idle than Active transitions 62 | errConnsLost // Close/hijack and concurrency greater than zero 63 | errUnknownState // We must be old relative to net/http 64 | errArSize 65 | ) 66 | 67 | type trackerStats struct { 68 | peakConns int 69 | peakSessions int 70 | connFor time.Duration // Total connections existence time (can easily be GT elapse) 71 | activeFor time.Duration // Total connections active time 72 | errors [errArSize]int 73 | } 74 | 75 | type Tracker struct { 76 | name string 77 | mu sync.Mutex 78 | 79 | connMap map[string]*connection // Indexed by address of connection 80 | trackerStats 81 | } 82 | 83 | // New constructs a tracker object - in particular the map used to track each connection key 84 | func New(name string) *Tracker { 85 | t := &Tracker{name: name} 86 | t.connMap = make(map[string]*connection) 87 | 88 | return t 89 | } 90 | 91 | // ConnState is called when a connection transitions to a new state. The key can be anything so long 92 | // as it is unique per-connection though normally it will be the net.Conn.RemoteAddr() provided by 93 | // http. So long as it's unique for a given connection tho, it's all good. 94 | // 95 | // ConnState checks that the new state makes sense for the connection and if it does, the connection 96 | // is updated and true is returned. If the new state doesn't make sense, the transition and internal 97 | // state are reconciled and false is returned. Reconciliation favours the current state over the 98 | // previous to avoid dangling connections. 99 | // 100 | // ConnState does not fastidiously check that all state transitions make sense, it merely checks 101 | // those which need to be correct for it to perform its function. This is a statistics gathering 102 | // function after all, not a logic validation monster; besideswhich this function does not really 103 | // know which transitions are legal in most cases. 104 | func (t *Tracker) ConnState(key string, now time.Time, state http.ConnState) bool { 105 | t.mu.Lock() 106 | defer t.mu.Unlock() 107 | 108 | cs, ok := t.connMap[key] 109 | if state == http.StateNew { // All other states must have a pre-existing connection 110 | cs := &connection{} // Always create a new and possibly over-write any dangling 111 | cs.connStart = now // connection. 112 | t.connMap[key] = cs 113 | if ok { // Dangling connection? Report it 114 | t.errors[errDanglingConn]++ 115 | } 116 | cc := len(t.connMap) 117 | if cc > t.peakConns { 118 | t.peakConns = cc 119 | } 120 | return !ok 121 | } 122 | 123 | if !ok { // If it's not a pre-existing connection then record the error and exit 124 | t.errors[errNoConnInMap]++ 125 | return false 126 | } 127 | 128 | switch state { 129 | case http.StateActive: 130 | cs.activeStart = now 131 | return true 132 | 133 | case http.StateIdle: 134 | if !cs.activeStart.IsZero() { 135 | cs.activeFor += now.Sub(cs.activeStart) 136 | cs.activeStart = time.Time{} 137 | } 138 | return true 139 | 140 | case http.StateHijacked, http.StateClosed: 141 | t.connFor += now.Sub(cs.connStart) 142 | if !cs.activeStart.IsZero() { // Capture last active period 143 | cs.activeFor += now.Sub(cs.activeStart) 144 | } 145 | t.activeFor += cs.activeFor 146 | 147 | delete(t.connMap, key) 148 | if cs.currentSessions > 0 { // Assuming this is an error for now, but it may not be 149 | t.errors[errConnsLost]++ 150 | return false 151 | } 152 | if cs.peakSessions > t.peakSessions { 153 | t.peakSessions = cs.peakSessions 154 | } 155 | return true 156 | } 157 | 158 | t.errors[errUnknownState]++ 159 | return false 160 | } 161 | 162 | // SessionAdd increments a session counter within a connection. Not all connections support multiple 163 | // sessions, but some such as HTTP2, do. Return false if the connection key is not know. 164 | func (t *Tracker) SessionAdd(key string) bool { 165 | t.mu.Lock() 166 | defer t.mu.Unlock() 167 | 168 | cs, ok := t.connMap[key] 169 | if !ok { 170 | t.errors[errNoConnForSession]++ 171 | return false 172 | } 173 | 174 | cs.currentSessions++ 175 | if cs.currentSessions > cs.peakSessions { 176 | cs.peakSessions = cs.currentSessions 177 | } 178 | 179 | return true 180 | } 181 | 182 | // SessionDone undoes SessionAdd. 183 | func (t *Tracker) SessionDone(key string) bool { 184 | t.mu.Lock() 185 | defer t.mu.Unlock() 186 | 187 | cs, ok := t.connMap[key] 188 | if !ok { 189 | t.errors[errNoConnForSession]++ 190 | return false 191 | } 192 | 193 | if cs.currentSessions <= 0 { 194 | t.errors[errNegativeConcurrency]++ 195 | return false 196 | 197 | } 198 | cs.currentSessions-- 199 | 200 | return true 201 | } 202 | -------------------------------------------------------------------------------- /internal/connectiontracker/tracker_test.go: -------------------------------------------------------------------------------- 1 | package connectiontracker 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // Test that unique connection IDs are tracked separately 11 | func TestUniqueConns(t *testing.T) { 12 | trk := New("Unique") 13 | var now time.Time 14 | res := trk.ConnState("1.2.3.4:5", now, http.StateNew) 15 | if !res { 16 | t.Error("Unexpected complaint from first StateNew") 17 | } 18 | 19 | res = trk.ConnState("1.2.3.5:5", now, http.StateNew) 20 | if !res { 21 | t.Error("Unexpected complaint from second StateNew") 22 | } 23 | 24 | rep := trk.Report(false) // Use reporter to check conn count 25 | if !strings.Contains(rep, "curr=2") { 26 | t.Error("Expected curr=2, got", rep) 27 | } 28 | 29 | res = trk.ConnState("1.2.3.4:5", now, http.StateClosed) 30 | if !res { 31 | t.Error("Unexpected complaint from first StateClosed") 32 | } 33 | 34 | res = trk.ConnState("1.2.3.5:5", now, http.StateClosed) 35 | if !res { 36 | t.Error("Unexpected complaint from second StateClosed") 37 | } 38 | 39 | rep = trk.Report(false) // Use reporter to check conn count 40 | if !strings.Contains(rep, "curr=0") { 41 | t.Error("Expected curr=0, got", rep) 42 | } 43 | } 44 | 45 | const ( 46 | exp = "curr=0 pk=2 sess=0 errs=0 (0/0/0/0/0/0) connFor=1260.0s activeFor=420.0s Active" 47 | ) 48 | 49 | // Check that the active times are accumlated correctly 50 | func TestDurations(t *testing.T) { 51 | trk := New("Active") 52 | var now time.Time 53 | now = now.Add(time.Hour * 12) 54 | trk.ConnState("one", now, http.StateNew) // Clock: 12:00 55 | trk.ConnState("two", now, http.StateNew) // Clock: 12:00 56 | 57 | now = now.Add(time.Minute) 58 | trk.ConnState("one", now, http.StateActive) // Clock: 12:01 59 | now = now.Add(time.Minute) 60 | trk.ConnState("two", now, http.StateActive) // Clock: 12:02 61 | 62 | now = now.Add(time.Minute * 2) 63 | trk.ConnState("one", now, http.StateIdle) // Clock: 12:04 64 | now = now.Add(time.Minute) 65 | trk.ConnState("two", now, http.StateIdle) // Clock: 12:05 66 | 67 | now = now.Add(time.Minute) 68 | trk.ConnState("two", now, http.StateActive) // Clock: 12:06 69 | now = now.Add(time.Minute) 70 | trk.ConnState("two", now, http.StateIdle) // Clock: 12:07 71 | 72 | now = now.Add(time.Minute * 3) 73 | trk.ConnState("one", now, http.StateClosed) // Clock: 12:10 74 | now = now.Add(time.Minute) 75 | trk.ConnState("two", now, http.StateHijacked) // Clock: 12:11 76 | 77 | // Elapse is 12:00-12:11 = 660s. 78 | // one exists for 12:00-12:10 = 600s. Active for 12:01-12:04 = 180s 79 | // two exists for 12:00-12:11 = 660s. Active for 12:02-12:05, 12:06-12:07 = 240s 80 | // Current should be zero, peak should be two. 81 | // connFor = 600+660=1260. activeFor=180+240=420. 82 | 83 | rep := trk.Report(false) // Use reporter to check results rather than peaking at struct 84 | if !strings.Contains(rep, exp) { 85 | t.Error("Expected", exp, "got", rep) 86 | } 87 | } 88 | 89 | const ( 90 | peakSession = "curr=0 pk=1 sess=2 errs=0 (0/0/0/0/0/0) connFor=0.0s activeFor=0.0s Sessions" 91 | ) 92 | 93 | func TestSessions(t *testing.T) { 94 | trk := New("Sessions") 95 | trk.ConnState("one", time.Now(), http.StateNew) 96 | trk.ConnState("one", time.Now(), http.StateActive) 97 | res := trk.SessionAdd("one") 98 | if res != true { 99 | t.Error("Unexpected false return from SessionAdd") 100 | } 101 | trk.SessionAdd("one") 102 | res = trk.SessionDone("one") 103 | if res != true { 104 | t.Error("Unexpected false return from SessionAdd") 105 | } 106 | trk.SessionDone("one") 107 | trk.ConnState("one", time.Now(), http.StateClosed) 108 | rep := trk.Report(false) 109 | if rep != peakSession { 110 | t.Error("Expected peak session", peakSession, "got", rep) 111 | } 112 | } 113 | 114 | // Exercise all the error paths when the supplied state doesn't match the internal state. 115 | func TestStateErrors(t *testing.T) { 116 | trk := New("State Errors") 117 | 118 | // Test creating a new key that needs to discard a dangling connection. 119 | 120 | trk.ConnState("one", time.Now(), http.StateNew) 121 | res := trk.ConnState("one", time.Now(), http.StateNew) // Dangling "one" 122 | if res { 123 | t.Error("Should not have got a true when replacing a dangling connection", trk) 124 | } 125 | 126 | rep := trk.Report(true) 127 | if !strings.Contains(rep, "curr=1") { // Should only have one Connection 128 | t.Error("Report should only have one connection, not", rep) 129 | } 130 | 131 | // Test referring to a key that doesn't exist, but should. 132 | 133 | res = trk.ConnState("two", time.Now(), http.StateClosed) // Should exist but doesn't 134 | if res { 135 | t.Error("Expected false return when referencing a non-existent key + Closed") 136 | } 137 | rep = trk.Report(true) 138 | if !strings.Contains(rep, "errs=1 (1/") { 139 | t.Error("Expected NoConnInMap error, got", rep) 140 | } 141 | 142 | // Test Closing a connection that has sessions active 143 | 144 | trk.ConnState("three", time.Now(), http.StateNew) 145 | trk.SessionAdd("three") 146 | res = trk.ConnState("three", time.Now(), http.StateClosed) 147 | if res { 148 | t.Error("Should have got a false return when closing connection with sessions", trk) 149 | } 150 | rep = trk.Report(true) 151 | if !strings.Contains(rep, "errs=1 (0/0/0/0/1/0)") { 152 | t.Error("Should have errNoConnForSession=1, not", rep) 153 | } 154 | 155 | // Test session count going negative 156 | 157 | trk.ConnState("four", time.Now(), http.StateNew) 158 | trk.SessionAdd("four") 159 | trk.SessionDone("four") 160 | res = trk.SessionDone("four") 161 | if res { 162 | t.Error("Expected false when decrementing sessions into negative") 163 | } 164 | rep = trk.Report(true) 165 | if !strings.Contains(rep, "errs=1 (0/0/0/1/0/0)") { 166 | t.Error("Should have errNegativeConcurrency=1, not", rep) 167 | } 168 | 169 | // Test referring to a session that doesn't exist 170 | 171 | res = trk.SessionAdd("five") 172 | if res { 173 | t.Error("Expected a false return for SessionAdd ref to non-existent connection", trk) 174 | } 175 | res = trk.SessionDone("five") 176 | if res { 177 | t.Error("Expected a false return for SessionDone ref to non-existent connection", trk) 178 | } 179 | 180 | // Test unknown state 181 | 182 | trk.ConnState("six", time.Now(), http.StateNew) 183 | res = trk.ConnState("six", time.Now(), http.StateNew+100) 184 | if res { 185 | t.Error("Invalid state should have returned false", trk) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package constants provides common values used across all trustydns packages. Usage is to call the 3 | global Get() function which returns the Constants by value ensuring that any modifications made 4 | (accidental or otherwise) will not affect other modules when they call Get(). 5 | 6 | Typically usage: 7 | 8 | consts := constants.Get() 9 | fmt.Println("I am", consts.ProxyProgramName, "based on", consts.RFC) 10 | 11 | The primary reason for making this a constructed struct rather than the more typical const () block 12 | is so that it can be fed directly into templating packages for printing usage messages. 13 | */ 14 | package constants 15 | 16 | // Constants contains the system-wide constants 17 | type Constants struct { 18 | DigProgramName string 19 | ProxyProgramName string // Package related constants 20 | ServerProgramName string 21 | Version string 22 | PackageName string 23 | PackageURL string 24 | RFC string 25 | 26 | HTTPSDefaultPort string // HTTP related constants 27 | AgeHeader string 28 | 29 | AcceptHeader string // Place in every request 30 | ContentTypeHeader string 31 | UserAgentHeader string 32 | 33 | TrustyDurationHeader string // Server header with time.Duration of server-side resolution 34 | TrustySynthesizeECSRequestHeader string // Proxy header with ipv4, ipv6 prefix length 35 | 36 | ConnectionValue string 37 | Rfc8484AcceptValue string 38 | 39 | Rfc8484Path string 40 | Rfc8484QueryParam string 41 | 42 | DNSDefaultPort string // DNS Related constants 43 | MinimumViableDNSMessage uint // MsgHdr + one Question with zero length name 44 | DNSTruncateThreshold int // A message larger than this size may be truncated unless EDNS0 45 | MaximumViableDNSMessage uint // RFC8484 defines an upper limit 46 | Rfc8467ClientPadModulo uint 47 | Rfc8467ServerPadModulo uint 48 | 49 | DNSUDPTransport string // Suitable for the "net" package, but just to make sure we're 50 | DNSTCPTransport string // consistent across the whole package. 51 | } 52 | 53 | var readOnlyConstants *Constants 54 | 55 | // createReadOnlyConstants creates a read-only copy of the Constants which is copied whenever a 56 | // caller asks for the constants set. The main reason for returning a struct is so that callers can 57 | // inspect and/or use packages that introspect - particularly */template packages. 58 | func createReadOnlyConstants() { 59 | readOnlyConstants = &Constants{ 60 | DigProgramName: "trustydns-dig", 61 | ProxyProgramName: "trustydns-proxy", 62 | ServerProgramName: "trustydns-server", 63 | Version: "v0.3.0", 64 | PackageName: "Trusty DNS Over HTTPS", 65 | PackageURL: "https://github.com/markdingo/trustydns", 66 | RFC: "RFC8484", 67 | 68 | HTTPSDefaultPort: "443", 69 | 70 | AgeHeader: "Age", 71 | 72 | AcceptHeader: "Accept", 73 | ContentTypeHeader: "Content-Type", 74 | UserAgentHeader: "User-Agent", 75 | 76 | TrustyDurationHeader: "X-trustydns-Duration", 77 | TrustySynthesizeECSRequestHeader: "X-trustydns-Synth", 78 | 79 | ConnectionValue: "Keep-Alive", 80 | Rfc8484AcceptValue: "application/dns-message", 81 | 82 | Rfc8484Path: "/dns-query", 83 | Rfc8484QueryParam: "dns", 84 | 85 | DNSDefaultPort: "53", 86 | MinimumViableDNSMessage: 16, // A legit binary DNS Message *cannot* be shorter than this 87 | DNSTruncateThreshold: 512, 88 | MaximumViableDNSMessage: 65535, 89 | Rfc8467ClientPadModulo: 128, 90 | Rfc8467ServerPadModulo: 468, 91 | 92 | DNSUDPTransport: "udp", 93 | DNSTCPTransport: "tcp", 94 | } 95 | } 96 | 97 | func init() { 98 | createReadOnlyConstants() 99 | } 100 | 101 | // Get returns a copy of the Constant struct. Return by value so internal values cannot be 102 | // inadvertently changed by callers. 103 | func Get() Constants { 104 | return *readOnlyConstants 105 | } 106 | -------------------------------------------------------------------------------- /internal/constants/constants_test.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPostGet(t *testing.T) { 8 | if readOnlyConstants == nil { 9 | t.Error("Expected readOnlyConstants to be set by init() prior to me") 10 | } 11 | } 12 | 13 | // TestValues tests that at least a few of the constants have been 14 | // initialized. Too tiresome to test them all and obviously of limited 15 | // value. 16 | func TestValues(t *testing.T) { 17 | consts := Get() 18 | if len(consts.ProxyProgramName) == 0 { 19 | t.Error("consts.ProxyProgramName should be set but it's zero length") 20 | } 21 | if len(consts.RFC) == 0 { 22 | t.Error("consts.RFC should be set but it's zero length") 23 | } 24 | 25 | if len(consts.HTTPSDefaultPort) == 0 { 26 | t.Error("consts.HTTPSDefaultPort should be set but it's zero length") 27 | } 28 | if len(consts.TrustySynthesizeECSRequestHeader) == 0 { 29 | t.Error("consts.TrustySynthesizeECSRequestHeader should be set but it's zero length") 30 | } 31 | 32 | if len(consts.DNSDefaultPort) == 0 { 33 | t.Error("consts.DNSDefaultPort should be set but it's zero length") 34 | } 35 | if consts.MinimumViableDNSMessage == 0 { 36 | t.Error("consts.MinimumViableDNSMessage should be set but it's zero") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/dnsutil/compact.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // CompactMsgString generates a relatively compact single-line, printable representation of most of 10 | // the useful data for DoH in dns.Msg. The output is intended to be well suited to printing to a log 11 | // or trace file. 12 | // 13 | // The generated format is: ID/Op/rcode (bits) IN/type/qname ACount/NCount/ECount Answers Auths Extras 14 | func CompactMsgString(m *dns.Msg) string { 15 | bits := "" 16 | if m.MsgHdr.Response { 17 | bits += "R" 18 | } 19 | if m.MsgHdr.Authoritative { 20 | bits += "A" 21 | } 22 | if m.MsgHdr.Truncated { 23 | bits += "T" 24 | } 25 | if m.MsgHdr.RecursionDesired { 26 | bits += "d" 27 | } 28 | if m.MsgHdr.RecursionAvailable { 29 | bits += "a" 30 | } 31 | if m.MsgHdr.Zero { 32 | bits += "Z" 33 | } 34 | if m.MsgHdr.AuthenticatedData { 35 | bits += "s" 36 | } 37 | if m.MsgHdr.CheckingDisabled { 38 | bits += "x" 39 | } 40 | 41 | qClass := "?" 42 | qType := "?" 43 | qName := "?" 44 | if len(m.Question) > 0 { 45 | q := m.Question[0] 46 | qClass = dns.ClassToString[q.Qclass] 47 | qType = dns.TypeToString[q.Qtype] 48 | qName = q.Name 49 | } 50 | opCode, ok := dns.OpcodeToString[m.MsgHdr.Opcode] 51 | if ok && len(opCode) >= 2 { 52 | opCode = opCode[0:2] 53 | } 54 | s := fmt.Sprintf("%d/%s/%d (%s) %s/%s/%s %d/%d/%d", 55 | m.MsgHdr.Id, opCode, m.MsgHdr.Rcode, bits, 56 | qClass, qType, qName, len(m.Answer), len(m.Ns), len(m.Extra)) 57 | s += " A:" + CompactRRsString(m.Answer) + " N:" + CompactRRsString(m.Ns) + " E:" + CompactRRsString(m.Extra) 58 | 59 | return s 60 | } 61 | 62 | // CompactRRsString generates a compact String() representation of an array of dns.RRs 63 | func CompactRRsString(rrs []dns.RR) string { 64 | s := "" 65 | sep := "" 66 | for _, interfaceRR := range rrs { 67 | s += sep 68 | sep = "/" 69 | switch rr := interfaceRR.(type) { 70 | case *dns.A: 71 | s += "A*" + rr.A.String() 72 | case *dns.AAAA: 73 | s += "AAAA*" + rr.AAAA.String() 74 | case *dns.MX: 75 | s += fmt.Sprintf("MX*%d-%s", rr.Preference, rr.Mx) 76 | case *dns.NS: 77 | s += "NS*" + rr.Ns 78 | case *dns.SRV: 79 | s += fmt.Sprintf("SRV*%d-%d-%s:%d", rr.Priority, rr.Weight, rr.Target, rr.Port) 80 | case *dns.OPT: 81 | s += fmt.Sprintf("OPT(%d,%d,%d:", rr.Version(), rr.ExtendedRcode(), rr.UDPSize()) 82 | subsep := "" 83 | for _, option := range rr.Option { 84 | s += subsep 85 | subsep = "," 86 | switch subOpt := option.(type) { 87 | case *dns.EDNS0_NSID: 88 | s += "NSID" 89 | case *dns.EDNS0_SUBNET: 90 | s += fmt.Sprintf("ECS[%d/%d]", subOpt.SourceNetmask, subOpt.SourceScope) 91 | case *dns.EDNS0_COOKIE: 92 | s += "COOKIE" 93 | case *dns.EDNS0_UL: 94 | s += "UL" 95 | case *dns.EDNS0_LLQ: 96 | s += "LLQ" 97 | case *dns.EDNS0_DAU: 98 | s += "DAU" 99 | case *dns.EDNS0_DHU: 100 | s += "DHU" 101 | case *dns.EDNS0_LOCAL: 102 | s += "LOCAL" 103 | case *dns.EDNS0_PADDING: 104 | s += "PAD" 105 | default: 106 | s += fmt.Sprintf("%d", option.Option()) 107 | } 108 | } 109 | s += ")" 110 | default: 111 | s += dns.TypeToString[interfaceRR.Header().Rrtype] 112 | } 113 | } 114 | 115 | return s 116 | } 117 | -------------------------------------------------------------------------------- /internal/dnsutil/compact_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | const allOpts = "NSID,ECS[24/16],COOKIE,UL,LLQ,DAU,DHU,7,LOCAL,PAD" 11 | 12 | func TestCompactString(t *testing.T) { 13 | a1, err := dns.NewRR("a.name.example.net. 300 IN A 1.2.3.4") // Create non-sensical but valid message 14 | checkFatal(t, err, "newRR a1") 15 | a2, err := dns.NewRR("a.name.example.net. 300 IN AAAA fe80::f0a2:46ff:feb5:3c98") 16 | checkFatal(t, err, "newRR a2") 17 | a3, err := dns.NewRR("compress.name.example.net. 300 IN TXT 'Some text'") 18 | checkFatal(t, err, "newRR a3") 19 | a4, err := dns.NewRR("service.example.net. 300 IN SRV 10 20 30 host1.example.net.") 20 | checkFatal(t, err, "newRR a4") 21 | n1, err := dns.NewRR("nocompress.example.com. 300 IN NS a.ns.example.net.") 22 | checkFatal(t, err, "newRR n1") 23 | n2, err := dns.NewRR("example.net. 600 IN NS b.ns.example.net.") 24 | checkFatal(t, err, "newRR n2") 25 | e1, err := dns.NewRR("example.com. 600 IN SOA internal.e hostmaster. 1554301415 16384 2048 1048576 480") 26 | checkFatal(t, err, "newRR e1") 27 | e2, err := dns.NewRR("example.net. 600 IN MX 10 smtp.example.net.") 28 | checkFatal(t, err, "newRR e2") 29 | 30 | m1 := &dns.Msg{ 31 | Answer: []dns.RR{a1, a2, a3, a4}, 32 | Ns: []dns.RR{n1, n2}, 33 | Extra: []dns.RR{e1, e2}, 34 | } 35 | 36 | m1.SetQuestion("a.name.example.net.", dns.TypeMX) 37 | s1 := CompactMsgString(m1) 38 | if !strings.Contains(s1, "AAAA*") { 39 | t.Error("Expected CompactMsgString to print out the AAAA", s1) 40 | } 41 | 42 | m1.MsgHdr.Response = true // Set all the bits to get the Ratsack decode 43 | m1.MsgHdr.Authoritative = true 44 | m1.MsgHdr.Truncated = true 45 | m1.MsgHdr.RecursionDesired = true 46 | m1.MsgHdr.RecursionAvailable = true 47 | m1.MsgHdr.Zero = true 48 | m1.MsgHdr.AuthenticatedData = true 49 | m1.MsgHdr.CheckingDisabled = true 50 | 51 | s1 = CompactMsgString(m1) 52 | if !strings.Contains(s1, "RATdaZsx") { 53 | t.Error("Expected CompactMsgString to generate 'RATdaZsx' to represent all header bits", s1) 54 | } 55 | 56 | // Create (almost) every OPT type on the planet! 57 | 58 | opt := NewOPT() // Use the official function to get/check legit OPT values 59 | opt.Option = append(opt.Option, 60 | &dns.EDNS0_NSID{}, 61 | &dns.EDNS0_SUBNET{SourceNetmask: 24, SourceScope: 16}, 62 | &dns.EDNS0_COOKIE{}, 63 | &dns.EDNS0_UL{}, 64 | &dns.EDNS0_LLQ{}, 65 | &dns.EDNS0_DAU{}, 66 | &dns.EDNS0_DHU{}, 67 | &dns.EDNS0_N3U{}, // This is purposely unknown to CompactMsgString() to exercise the default switch 68 | &dns.EDNS0_LOCAL{}, 69 | &dns.EDNS0_PADDING{}) 70 | 71 | m1.Extra = append(m1.Extra, opt) 72 | s1 = CompactMsgString(m1) 73 | if !strings.Contains(s1, allOpts) { 74 | t.Error("Expected CompactMsgString to contain", allOpts, "not", s1) 75 | } 76 | 77 | if !strings.Contains(s1, "OPT(0,0,4096") { 78 | t.Error("Expected Extended OPT output", s1) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/dnsutil/msg.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dnsutil provides helper methods to manipulate the fiddly EDNS0 Client Subnet bits, TTL 3 | reduction and RFC8467 padding in a "github.com/miekg/dns.Msg". The caller is assumed to have 4 | checked that the dns.Msg is a legitimate IN/Query prior to calling any of these functions. 5 | */ 6 | package dnsutil 7 | 8 | import ( 9 | "net" 10 | 11 | "github.com/markdingo/trustydns/internal/constants" 12 | 13 | "github.com/miekg/dns" 14 | ) 15 | 16 | var ( 17 | consts = constants.Get() 18 | ) 19 | 20 | // FindOPT searches dns.Msg.Extra for the first occurrence of an OPT RR. There should only be one. 21 | // 22 | // Return *dns.OPT if found otherwise nil 23 | func FindOPT(q *dns.Msg) *dns.OPT { 24 | for _, rr := range q.Extra { // Search Extra for OPT RRs 25 | if opt, ok := rr.(*dns.OPT); ok { 26 | return opt 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // FindECS searches dns.Msg.Extra for any occurrences of an EDNS_SUBNET sub-option in any 34 | // occurrences of a dns.OPT in the Extra list of RRs. This multi-occurrence search is more 35 | // aggressive than the standard DNS Message format intends but we really don't want an ECS to be 36 | // missed even if it is ostensibly not in exactly the right place. 37 | // 38 | // If an EDNS_SUBNET sub-option is found, return the containing OPT RR and sub-option otherwise 39 | // return nil, nil 40 | func FindECS(q *dns.Msg) (*dns.OPT, *dns.EDNS0_SUBNET) { 41 | for _, rr := range q.Extra { // Search Extra for OPT RRs 42 | if opt, ok := rr.(*dns.OPT); ok { 43 | for _, subOpt := range opt.Option { // Search OPT RR for ECS 44 | if ecs, ok := subOpt.(*dns.EDNS0_SUBNET); ok { 45 | return opt, ecs 46 | } 47 | } 48 | } 49 | } 50 | 51 | return nil, nil 52 | } 53 | 54 | // RemoveEDNS0FromOPT aggressively removes all occurrences of the specified EDNS0 sub-option in the 55 | // Extra RR list of a dns.Msg. It makes the worst-case assumption that there may be multiple options 56 | // and sub-options. 57 | // 58 | // True is returned if at least one sub-option was removed. 59 | func RemoveEDNS0FromOPT(msg *dns.Msg, edns0Code uint16) (removed bool) { 60 | outRRs := make([]dns.RR, 0) // Construct an array of surviving RRs 61 | for _, rr := range msg.Extra { 62 | inOpt, ok := rr.(*dns.OPT) 63 | if !ok { // Non OPT RRs get copied straight across 64 | outRRs = append(outRRs, rr) 65 | continue 66 | } 67 | 68 | outOpt := &dns.OPT{Hdr: inOpt.Hdr} // Create a new OPT RR to contain the option survivors 69 | for _, opt := range inOpt.Option { // Search within the OPT RR for the ECS option 70 | if opt.Option() == edns0Code { 71 | removed = true 72 | continue 73 | } 74 | outOpt.Option = append(outOpt.Option, opt) // Non-ECS options survive 75 | } 76 | if len(outOpt.Option) > 0 { // Only append new OPT RR if it's not empty 77 | outRRs = append(outRRs, outOpt) 78 | } 79 | } 80 | 81 | if removed { 82 | msg.Extra = outRRs // Return survivors to the message - if any 83 | } 84 | 85 | return 86 | } 87 | 88 | // CreateECS arbitrarily creates an EDNS0_SUBNET sub-option which is appended to the OPT in the 89 | // Extra section of the dns.Msg. If no OPT exists, one is created. This function does not check for 90 | // any pre-existing EDNS0_SUBNET sub-option. 91 | // 92 | // Return the created ecs option. 93 | func CreateECS(msg *dns.Msg, family, prefixLength int, ip net.IP) *dns.EDNS0_SUBNET { 94 | ecs := &dns.EDNS0_SUBNET{ 95 | Code: dns.EDNS0SUBNET, 96 | Family: uint16(family), 97 | SourceNetmask: uint8(prefixLength), 98 | Address: ip, // dns.OPT.pack() truncate this to SourceNetmask 99 | } 100 | 101 | optRR := FindOPT(msg) 102 | if optRR == nil { // if necessary, construct an OPT RR to contain the new ECS sub-opt 103 | optRR = NewOPT() 104 | msg.Extra = append(msg.Extra, optRR) 105 | } 106 | 107 | optRR.Option = append(optRR.Option, ecs) 108 | 109 | return ecs 110 | } 111 | 112 | // ReduceTTL reduces the TTL in all the RRs in Answer, Ns and Extra that have a TTL greater than 1. 113 | // "by" defines how much to reduce TTLs by and "minimum" is the lower limit that we'll ever let a 114 | // TTL reduce to. 115 | func ReduceTTL(msg *dns.Msg, by uint32, minimum uint32) int { 116 | changeCount := 0 117 | if len(msg.Answer) > 0 { 118 | changeCount += reduceRRSet(msg.Answer, int64(by), int64(minimum)) 119 | } 120 | if len(msg.Ns) > 0 { 121 | changeCount += reduceRRSet(msg.Ns, int64(by), int64(minimum)) 122 | } 123 | if len(msg.Extra) > 0 { 124 | changeCount += reduceRRSet(msg.Extra, int64(by), int64(minimum)) 125 | } 126 | 127 | return changeCount 128 | } 129 | 130 | // Helper that does the actual TTL Reduction work for the supplied RRSet. Even tho the "by" and 131 | // "minimum" are int64 parameters we know that they originated from a uint32 so calcs in 64bit 132 | // comfortably fit the full range of possible values without contortions. 133 | func reduceRRSet(rrset []dns.RR, by int64, minimum int64) int { 134 | changeCount := 0 135 | for _, rr := range rrset { 136 | hdr := rr.Header() 137 | ttl := int64(hdr.Ttl) // Do all calcs in 64bit signed to capture interim negatives 138 | if ttl > minimum { // Cannot reduce a ttl if it's already at the minimum 139 | ttl -= by // Could go negative here 140 | if ttl < minimum { // but this catches negatives as well as too small 141 | ttl = minimum 142 | } 143 | if uint32(ttl) != hdr.Ttl { // Only return if we actually changed the value 144 | hdr.Ttl = uint32(ttl) 145 | changeCount++ 146 | } 147 | } 148 | } 149 | 150 | return changeCount 151 | } 152 | 153 | // NewOPT creates a populated msg.OPT RR as a zero-values struct is not a valid OPT. Note that 154 | // SetUDPSize has to be set for some resolvers that are ECS aware. In particular unbound does not 155 | // seem to like a UDP size of zero. 156 | func NewOPT() *dns.OPT { 157 | optRR := &dns.OPT{} 158 | optRR.SetVersion(0) 159 | optRR.SetUDPSize(dns.DefaultMsgSize) 160 | optRR.Hdr.Name = "." 161 | optRR.Hdr.Rrtype = dns.TypeOPT 162 | 163 | return optRR 164 | } 165 | -------------------------------------------------------------------------------- /internal/dnsutil/padding.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // FindPadding searches dns.Msg.Extra for any occurrences of an EDNS0_PADDING sub-option in any 10 | // occurrences of a dns.OPT in the Extra list of RRs. The presence of padding is a signal from a DoH 11 | // client to a DoH server to pad the response. 12 | // 13 | // Return length of padding else -1 14 | func FindPadding(q *dns.Msg) int { 15 | for _, rr := range q.Extra { // Search Extra for OPT RRs 16 | if opt, ok := rr.(*dns.OPT); ok { 17 | for _, subOpt := range opt.Option { // Search OPT RR for ECS 18 | if e, ok := subOpt.(*dns.EDNS0_PADDING); ok { 19 | return len(e.Padding) 20 | } 21 | } 22 | } 23 | } 24 | 25 | return -1 26 | } 27 | 28 | // PadAndPack creates an EDNS0_PADDING sub-option which is added to the OPT in dns.Msg.Extra. If no 29 | // OPT exists, one is created. The end result is a padded, packed message that is a size modulo of 30 | // the provided size parameter. Padding is recommended by RFC8467. In particular it recommends 31 | // queries be padded "to the closest multiple of 128 octets" and responses be padded to "a multiple 32 | // of 468 octets". (Interesting that they don't say "closest multiple" for the response but I think 33 | // that's just editorial imprecision.) 34 | // 35 | // If the message has an existing padding option it is removed as padding is deemed to serve a 36 | // hop-by-hop purpose thus any pre-existing padding has already served its protective and signally 37 | // purpose when it arrived here. 38 | // 39 | // This function also calls dns.Pack() to ensure that the caller is incapable of making subsequent 40 | // modifications to the message which would obviously invalidate the carefully selected padding 41 | // sizes. 42 | // 43 | // Even if the current message is an exact modulo length (and thus apparently not requiring any 44 | // padding) we still add a padding option because that option is used to signal the remote end to 45 | // add padding in response. 46 | // 47 | // WARNING: dns.Msg.Len() and dns.Msg.Pack() only work properly with well-formed DNS messages so 48 | // this function also only works with properly formed DNS messages. In particular Len() and Pack() 49 | // can result in different lengths. 50 | // 51 | // Returns the dns.Pack() byte array or an error. 52 | func PadAndPack(msg *dns.Msg, moduloSize uint) ([]byte, error) { 53 | if moduloSize < 1 || moduloSize > consts.MaximumViableDNSMessage { 54 | return nil, fmt.Errorf("PadAndPack: Modulo size %d is not in range 1-%d", 55 | moduloSize, consts.MaximumViableDNSMessage) 56 | } 57 | var optRR *dns.OPT 58 | if len(msg.Extra) > 0 { 59 | RemoveEDNS0FromOPT(msg, dns.EDNS0PADDING) // Remove any existing PADDING 60 | if len(msg.Extra) > 0 { 61 | optRR = FindOPT(msg) // Use pre-existing OPT if present 62 | } 63 | } 64 | if optRR == nil { // If no pre-existing, create a fresh one 65 | optRR = NewOPT() 66 | msg.Extra = append(msg.Extra, optRR) 67 | } 68 | 69 | // We now have a guaranteed OPT RR with no existing padding. Add a zero length padding 70 | // option which adds the overhead of padding so we can correctly calculate the current 71 | // packed message length to deduce how much padding is needed to bring it up to the 72 | // recommended size modulo. 73 | 74 | padding := &dns.EDNS0_PADDING{Padding: make([]byte, 0)} 75 | optRR.Option = append(optRR.Option, padding) 76 | 77 | mLen := msg.Len() // This is an expensive call so cache the value 78 | 79 | extraPadding := moduloSize - (uint(mLen) % moduloSize) 80 | 81 | // It *may* be that the message is exactly the right size with a zero length padding 82 | // option. May as well avoid re-padding if we got lucky. 83 | if extraPadding > 0 { 84 | padding.Padding = make([]byte, extraPadding) 85 | optRR.Option[len(optRR.Option)-1] = padding // Replace original padding option with correct one 86 | } 87 | 88 | packed, err := msg.Pack() 89 | if err != nil { 90 | return nil, fmt.Errorf("PadAndPack dns.Pack() failed: %s", err.Error()) 91 | } 92 | 93 | // The message should have packed to the correct modulo size, but let's check just to be 94 | // sure as the msg.Len() function does not follow the same code path as the msg.Pack() 95 | // function thus there is a discrepancy risk. 96 | if uint(len(packed))%moduloSize != 0 { // Check that we did good! 97 | return nil, fmt.Errorf("PadAndPack dns.Pack() created unexpected length of %d with mod %d", 98 | len(packed), moduloSize) 99 | } 100 | 101 | return packed, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/dnsutil/padding_test.go: -------------------------------------------------------------------------------- 1 | package dnsutil 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | func TestPadding(t *testing.T) { 12 | a1, err := dns.NewRR("a.name.example.net. 300 IN A 1.2.3.4") // Create non-sensical but valid message 13 | checkFatal(t, err, "newRR a1") 14 | a2, err := dns.NewRR("a.name.example.net. 300 IN AAAA fe80::f0a2:46ff:feb5:3c98") 15 | checkFatal(t, err, "newRR a2") 16 | a3, err := dns.NewRR("compress.name.example.net. 300 IN TXT 'Some text'") 17 | checkFatal(t, err, "newRR a3") 18 | n1, err := dns.NewRR("nocompress.example.com. 300 IN NS a.ns.example.net.") 19 | checkFatal(t, err, "newRR n1") 20 | n2, err := dns.NewRR("example.net. 600 IN NS b.ns.example.net.") 21 | checkFatal(t, err, "newRR n2") 22 | e1, err := dns.NewRR("example.com. 600 IN SOA internal.e hostmaster. 1554301415 16384 2048 1048576 480") 23 | checkFatal(t, err, "newRR e1") 24 | e2, err := dns.NewRR("example.net. 600 IN MX 10 smtp.example.net.") 25 | checkFatal(t, err, "newRR e2") 26 | 27 | baseMsg := &dns.Msg{ 28 | Answer: []dns.RR{a1, a2, a3}, 29 | Ns: []dns.RR{n1, n2}, 30 | Extra: []dns.RR{e1, e2}, 31 | } 32 | 33 | tt := []struct { 34 | compress bool 35 | modulo int 36 | expectError bool 37 | canFuzz bool 38 | what string 39 | }{ 40 | {false, 17, false, true, "Small size w/o compress"}, 41 | {true, 17, false, true, "Small size with compress"}, 42 | {false, 128, false, true, "Recommended query size"}, // RFC8467 recommended client padding size 43 | {true, 128, false, true, "Recommended query size"}, 44 | {false, 468, false, true, "Recommended response size"}, // RFC8467 recommended client padding size 45 | {true, 468, false, true, "Recommended response size"}, 46 | {false, 241, false, true, "Empirically determined compressed message size"}, 47 | {true, 241, false, true, "Empirically determined compressed message size"}, 48 | {false, 344, false, true, "Empirically determined uncompressed message size"}, 49 | {true, 344, false, true, "Empirically determined uncompressed message size"}, 50 | {false, 0, true, false, "Expect error due to small modulo"}, 51 | {true, 0, true, false, "Expect error due to small modulo"}, 52 | {false, 0, true, false, "Expect error due to small modulo"}, 53 | {true, 0, true, false, "Expect error due to small modulo"}, 54 | {false, 65535 + 1, true, false, "Expect error due to oversized modulo"}, 55 | {true, 65535 + 1, true, false, "Expect error due to oversize modulo"}, 56 | } 57 | 58 | for _, tc := range tt { 59 | start := tc.modulo 60 | end := tc.modulo 61 | if tc.canFuzz { 62 | start -= 4 // Fuzz around the specified values to catch boundary conditions 63 | end += 4 64 | } 65 | for mod := start; mod < end; mod++ { 66 | m1 := baseMsg.Copy() 67 | m1.Compress = tc.compress 68 | b, err := PadAndPack(m1, uint(mod)) 69 | if (b == nil && err == nil) || (b != nil && err != nil) { 70 | t.Fatal("Both byte[] and err return cannot match", b, err) 71 | } 72 | switch { 73 | case err == nil && tc.expectError: 74 | t.Error("Expected error with", tc.what, "mod", uint(mod)) 75 | case err != nil && !tc.expectError: 76 | t.Error("Unexpected Error with", tc.what, "mod", uint(mod), err) 77 | } 78 | if uint(mod) > 0 && b != nil { 79 | if len(b)%mod != 0 { 80 | t.Error("PadAndPack returned wrong length", len(b), mod) 81 | } 82 | } 83 | } 84 | } 85 | 86 | // Check for explicit size errors 87 | 88 | m1 := baseMsg.Copy() 89 | _, err = PadAndPack(m1, 0) 90 | if err == nil { 91 | t.Fatal("Expected error return with a zero modulo") 92 | } 93 | if !strings.Contains(err.Error(), "not in range") { 94 | t.Error("Expected error message to contain 'not in range'", err) 95 | } 96 | 97 | _, err = PadAndPack(m1, 70000) 98 | if err == nil { 99 | t.Fatal("Expected error return with a huge modulo") 100 | } 101 | if !strings.Contains(err.Error(), "not in range") { 102 | t.Error("Expected error message to contain 'not in range'", err) 103 | } 104 | 105 | // Force dns.Pack() errors that in turn to cause PadAndPack to fail. Triggering this error 106 | // relies on the internals of miekg/dns which may change in the future and invalidate this 107 | // test. 108 | 109 | m1 = baseMsg.Copy() 110 | m1.Rcode = 0xFFF + 1 // dns.Pack() checks this for valid ranges 111 | _, err = PadAndPack(m1, 200) 112 | if err == nil { 113 | t.Fatal("Expected error return with a zero modulo") 114 | } 115 | if !strings.Contains(err.Error(), "dns.Pack") { 116 | t.Error("Expected error message to contain 'dns.Pack'", err) 117 | } 118 | } 119 | 120 | // Use bogus RR values to cause dns.Pack() to produce a different length than that indicated by 121 | // dns.Msg.Len() this in turn triggers an error path within PadAndPack() 122 | func TestTriggerPackError(t *testing.T) { 123 | a1 := &dns.A{Hdr: dns.RR_Header{Name: "3.to.2.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 3}} 124 | a2 := &dns.AAAA{Hdr: dns.RR_Header{Name: "300.to.290.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 300}} 125 | a3 := &dns.TXT{Hdr: dns.RR_Header{Name: "10to.2.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 10}} 126 | 127 | n1 := &dns.NS{Hdr: dns.RR_Header{Name: "11.to.2.", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 11}} 128 | n2 := &dns.NS{Hdr: dns.RR_Header{Name: "12.to.2.", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 12}} 129 | 130 | e1 := &dns.SOA{Hdr: dns.RR_Header{Name: "13.to.3.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 13}} 131 | e2 := &dns.MX{Hdr: dns.RR_Header{Name: "2.to.2.", Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 2}} 132 | 133 | m1 := &dns.Msg{ 134 | Answer: []dns.RR{a1, a2, a3}, 135 | Ns: []dns.RR{n1, n2}, 136 | Extra: []dns.RR{e1, e2}, 137 | } 138 | 139 | _, err := PadAndPack(m1, 248) 140 | if !strings.Contains(err.Error(), "unexpected length") { 141 | t.Error("Expected error message to contain 'unexpected length'", err) 142 | } 143 | } 144 | 145 | func TestFindPadding(t *testing.T) { 146 | a1, err := dns.NewRR("a.name.example.net. 300 IN A 1.2.3.4") // Create non-sensical but valid message 147 | checkFatal(t, err, "newRR a1") 148 | a2, err := dns.NewRR("a.name.example.net. 300 IN AAAA fe80::f0a2:46ff:feb5:3c98") 149 | checkFatal(t, err, "newRR a2") 150 | a3, err := dns.NewRR("compress.name.example.net. 300 IN TXT 'Some text'") 151 | checkFatal(t, err, "newRR a3") 152 | n1, err := dns.NewRR("nocompress.example.com. 300 IN NS a.ns.example.net.") 153 | checkFatal(t, err, "newRR n1") 154 | n2, err := dns.NewRR("example.net. 600 IN NS b.ns.example.net.") 155 | checkFatal(t, err, "newRR n2") 156 | e1, err := dns.NewRR("example.com. 600 IN SOA internal.e hostmaster. 1554301415 16384 2048 1048576 480") 157 | checkFatal(t, err, "newRR e1") 158 | e2, err := dns.NewRR("example.net. 600 IN MX 10 smtp.example.net.") 159 | checkFatal(t, err, "newRR e2") 160 | 161 | m1 := &dns.Msg{ 162 | Answer: []dns.RR{a1, a2, a3}, 163 | Ns: []dns.RR{n1, n2}, 164 | Extra: []dns.RR{e1, e2}, 165 | } 166 | 167 | if FindPadding(m1) >= 0 { 168 | t.Error("Did not expect to find padding with base message") 169 | } 170 | 171 | CreateECS(m1, 1, 19, net.IP{}) // Put an OPT (that lacks a padding sub-opt) in the message 172 | 173 | if FindPadding(m1) > 0 { 174 | t.Error("Did not expect to find padding with base message+ECS OPT") 175 | } 176 | 177 | // Extra contains {e1, e2, ECS}. So [2] is where we add the padding opt 178 | 179 | padding := &dns.EDNS0_PADDING{Padding: make([]byte, 0)} 180 | rr := m1.Extra[2] 181 | 182 | opt, ok := rr.(*dns.OPT) // Get our OPT back 183 | if !ok { 184 | t.Fatal("Type assertion to dns.OPT failed unexpectedly") 185 | } 186 | opt.Option = append(opt.Option, padding) // Add padding to OPT 187 | 188 | if FindPadding(m1) == -1 { // And hopefully find it now! 189 | t.Error("Did not find padding when expected") 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /internal/flagutil/stringvalue.go: -------------------------------------------------------------------------------- 1 | // Package flagutil provides additional support around the flag package. At the moment that consists 2 | // solely of the StringValue struct which conforms to the flag.Value method for multiple occurrence 3 | // flags containing string values. Conceivably an IPValue struct would be pretty useful too as well 4 | // as, e.g. a CIDRValue. 5 | // 6 | // The reason for providing StringValue is so that commands can offer a flag to set multiple values 7 | // such as: 8 | // 9 | // $command -A something -A somethingelse -A evenmore 10 | // ... 11 | // 12 | // Usage is as documented in the flags package: 13 | // 14 | // var ms flagutil.StringValue 15 | // flagSet.Var(&ms, "someopt", "Short description of opt") 16 | // args := ms.Args() // Return an array of strings 17 | // 18 | // or 19 | // 20 | // flag.Var(&ms, "someopt", "Short description of opt") 21 | // args := ms.Args() // Return an array of strings 22 | package flagutil 23 | 24 | import ( 25 | "strings" 26 | ) 27 | 28 | // StringValue is the type provided to flag.Var() 29 | type StringValue struct { 30 | strings []string 31 | } 32 | 33 | // Set appends a string to the internal array - it is called by the flag package for each occurrence 34 | // of the corresponding option on the command line. Part of the flag.Value interface. 35 | func (t *StringValue) Set(s string) error { 36 | t.strings = append(t.strings, s) 37 | 38 | return nil 39 | } 40 | 41 | // String returns a space separated string of all the arguments provided by Set. Part of the 42 | // flag.Value interface. 43 | func (t *StringValue) String() string { 44 | return strings.Join(t.strings, " ") 45 | } 46 | 47 | // Args returns a copy of the array of strings returned by Set. You can safely modify this 48 | // array without fear of changing the internal data. 49 | func (t *StringValue) Args() []string { 50 | return append([]string{}, t.strings...) 51 | } 52 | 53 | // NArg returns the number of strings created by Set 54 | func (t *StringValue) NArg() int { 55 | return len(t.strings) 56 | } 57 | -------------------------------------------------------------------------------- /internal/flagutil/stringvalue_test.go: -------------------------------------------------------------------------------- 1 | package flagutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStringValue(t *testing.T) { 8 | var ms StringValue 9 | l := ms.NArg() 10 | if l != 0 { 11 | t.Error("Expected length=0 at initial state, not", l) 12 | } 13 | s := ms.String() 14 | if s != "" { 15 | t.Error("String() at initial state should be empty, not", s) 16 | } 17 | 18 | err := ms.Set("a") 19 | if err != nil { 20 | t.Error("Unexpected an error return from Set", err) 21 | } 22 | 23 | l = ms.NArg() 24 | if l != 1 { 25 | t.Error("Expected length=1 after one set, not", l) 26 | } 27 | ms.Set("b") 28 | 29 | s = ms.String() 30 | if s != "a b" { 31 | t.Error("String should be 'a b', not", s) 32 | } 33 | 34 | ss := ms.Args() 35 | if len(ss) != 2 || ss[0] != "a" || ss[1] != "b" { 36 | t.Error("Returned array should be [a, b], not", ss) 37 | } 38 | 39 | ss[0] = "A" 40 | ss = append(ss, "c") 41 | 42 | ss = ms.Args() 43 | if len(ss) != 2 || ss[0] != "a" || ss[1] != "b" { 44 | t.Error("Second returned array should be [a, b], not", ss) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/osutil/allowed_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | // setuid/setgid don't work on Linux via Go because Linux has a nutty arrangement whereby each thread 5 | // has its own uid/gid. Perhaps because threads are processes in Linux? Anyway, it's been broken 6 | // since at least 2011 and hasn't been fixed in the intervening 8+ years. 7 | // 8 | // This is an amazing pain as Go is predominantly used for developing servers such as this one which 9 | // typically require root privileges to open network sockets. Best security practise has long been 10 | // for network daemons to subsequently setuid/setgid/chroot to minimize the capabilities of a 11 | // network break-in. At least this still works on all other Unixen I know of. Maybe that tells you 12 | // something. 13 | // 14 | // For more details see: https://github.com/golang/go/issues/1435 15 | 16 | package osutil 17 | 18 | const ( 19 | setuidAllowed = false 20 | setgidAllowed = false 21 | ) 22 | -------------------------------------------------------------------------------- /internal/osutil/allowed_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !linux || unix 2 | // +build !linux unix 3 | 4 | package osutil 5 | 6 | const ( 7 | setuidAllowed = true 8 | setgidAllowed = true 9 | ) 10 | -------------------------------------------------------------------------------- /internal/osutil/constrain.go: -------------------------------------------------------------------------------- 1 | //go:build unix || !windows 2 | // +build unix !windows 3 | 4 | // osutil is a helper package to abstract OS interactions. In particular constraining a process via 5 | // chroot, setuid and setgid. Most of this functionality has to be disabled for Linux and is a noop 6 | // for Windows. It is fully functional for Unix systems. 7 | 8 | package osutil 9 | 10 | import ( 11 | "fmt" 12 | "golang.org/x/sys/unix" 13 | "os" 14 | "os/user" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | const ( 20 | me = "osutil.Constrain: " 21 | ) 22 | 23 | // Constrain downgrades the abilities of the process by changing to a nominated uid/gid which 24 | // presumably has less power and chroots to a directory that presumably has very little in it or 25 | // below it. 26 | // 27 | // The order of operations is important. The symbolic user and group names are converted to uid and 28 | // gid first while we have access to /etc/passwd (or the moral equivalent) then chroot is performed 29 | // while we presumably have the power to access that directly. After that we eliminate supplementary 30 | // groups as part of setting the group while we have a powerful uid and then we finally issue setuid 31 | // that should make this whole sequence irreversible. 32 | // 33 | // Each step is optional if the corresponding parameter is an empty string. 34 | // 35 | // An error is returned if the downgrade could not be completed. 36 | // 37 | // Arguable we should also consider setsid and closing all un-needed file descriptors, but this is a 38 | // reasonable start for this application. It is also the case that apparently everyone re-writes 39 | // this function and most get it wrong, so I may have too... 40 | // 41 | // This function is limited on Linux and a noop on Windows. 42 | func Constrain(userName, groupName, chrootDir string) error { 43 | 44 | // Step 1: Convert symbolic names to ids 45 | 46 | uid := -1 47 | gid := -1 48 | if len(userName) > 0 { 49 | u, err := user.Lookup(userName) 50 | if err != nil { 51 | return fmt.Errorf(me+"User name lookup failed: %s", err.Error()) 52 | } 53 | uid, err = strconv.Atoi(u.Uid) 54 | if err != nil { 55 | return fmt.Errorf(me+"Could not convert UID %s to an int: %s", 56 | u.Uid, err.Error()) 57 | } 58 | } 59 | 60 | if len(groupName) > 0 { 61 | g, err := user.LookupGroup(groupName) 62 | if err != nil { 63 | return fmt.Errorf(me+"Group name lookup failed: %s", err.Error()) 64 | } 65 | gid, err = strconv.Atoi(g.Gid) 66 | if err != nil { 67 | return fmt.Errorf(me+"Could not convert GID %s to an int: %s", 68 | g.Gid, err.Error()) 69 | } 70 | } 71 | 72 | // Step 2: chdir/chroot. Must be root to do this, but let Chroot() do the checking. 73 | 74 | if len(chrootDir) > 0 { 75 | err := os.Chdir(chrootDir) 76 | if err != nil { 77 | return fmt.Errorf(me+"Could not cd to %s: %s", chrootDir, err.Error()) 78 | } 79 | 80 | err = unix.Chroot(chrootDir) 81 | if err != nil { 82 | return fmt.Errorf(me+"Could not chroot to %s: %s", chrootDir, err.Error()) 83 | } 84 | 85 | err = os.Chdir("/") 86 | if err != nil { 87 | return fmt.Errorf(me+"Could not cd to /: %s", err.Error()) 88 | } 89 | } 90 | 91 | // Step 3: setgid. This includes removing all supplementary groups. 92 | 93 | if gid != -1 { 94 | if setgidAllowed { 95 | err := unix.Setgroups([]int{}) 96 | if err != nil { 97 | return fmt.Errorf(me+"Could not clear group list: %s", err.Error()) 98 | } 99 | err = unix.Setgid(gid) 100 | if err != nil { 101 | return fmt.Errorf(me+"Could not setgid to %d/%s: %s", 102 | gid, groupName, err.Error()) 103 | } 104 | } else { 105 | fmt.Println("WARNING: Go setgid() disabled for Linux. This process remains priviledged.") 106 | } 107 | } 108 | 109 | // The final piece of the puzzle. Step 4: setuid 110 | 111 | if uid != -1 { 112 | if setuidAllowed { 113 | err := unix.Setuid(uid) 114 | if err != nil { 115 | return fmt.Errorf(me+"Could not setuid to %d/%s: %s", 116 | uid, userName, err.Error()) 117 | } 118 | } else { 119 | fmt.Println("WARNING: Go setuid() disabled for Linux. This process remains priviledged.") 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | // ConstraintReport returns a printable string showing the uid/gid/cwd of the process. Normally 127 | // called after Constrain() to "prove" that the process has been downgraded. This function is a 128 | // noop on Windows. 129 | func ConstraintReport() string { 130 | uid := os.Getuid() 131 | gid := os.Getgid() 132 | cwd, _ := os.Getwd() 133 | gList, _ := os.Getgroups() 134 | gStr := make([]string, 0, len(gList)) 135 | for _, g := range gList { 136 | gStr = append(gStr, fmt.Sprintf("%d", g)) 137 | } 138 | 139 | _ = gList 140 | s := fmt.Sprintf("uid=%d gid=%d (%s) cwd=%s", uid, gid, strings.Join(gStr, ","), cwd) 141 | 142 | return s 143 | } 144 | -------------------------------------------------------------------------------- /internal/osutil/constrain_test.go: -------------------------------------------------------------------------------- 1 | package osutil 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // This function is virtually impossible to test within the Go test framework as a single successful 10 | // test means no others can possibly run as we've thrown away all our rights. All we can do is test 11 | // a few of the error paths and you'll have to have faith that the successful code paths have been 12 | // tested... 13 | func TestConstrain(t *testing.T) { 14 | if os.Getuid() != 0 { 15 | t.Log("Warning: Cannot even partially test osutil.Constrain() as we're not running as root") 16 | } 17 | err := Constrain("bogusUser", "", "") 18 | if err == nil { 19 | t.Error("Expected Error Return with bogusUser") 20 | } else { 21 | if !strings.Contains(err.Error(), "unknown user") { 22 | t.Error("Did not get unknown user in ", err) 23 | } 24 | } 25 | 26 | err = Constrain("", "bogusGroup", "") 27 | if err == nil { 28 | t.Error("Expected Error Return with bogusGroup") 29 | } else { 30 | if !strings.Contains(err.Error(), "unknown group") { 31 | t.Error("Did not get unknown group in ", err) 32 | } 33 | } 34 | } 35 | 36 | // This is a pretty lame test 37 | func TestReport(t *testing.T) { 38 | rep := ConstraintReport() 39 | if !strings.Contains(rep, "uid=") { 40 | t.Error("ConstraintReport is really bruk", rep) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/osutil/constrain_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows || !unix 2 | // +build windows !unix 3 | 4 | package osutil 5 | 6 | const ( 7 | me = "osutil.Constrain: " 8 | ) 9 | 10 | func Constrain(userName, groupName, chrootDir string) error { 11 | return nil 12 | } 13 | 14 | func ConstraintReport() string { 15 | return "uid=windows gid=windows cwd=?" 16 | } 17 | -------------------------------------------------------------------------------- /internal/osutil/signal_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix || !windows 2 | // +build unix !windows 3 | 4 | package osutil 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | // SignalNotify sends all the main Unix signals to the supplied channel. A noop on Windows. 13 | func SignalNotify(c chan os.Signal) { 14 | signal.Notify(c, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGUSR1) 15 | } 16 | 17 | // IsSignalUSR1 returns true if the supplied signal is SIGUSR1. A noop on Windows. 18 | func IsSignalUSR1(s os.Signal) bool { 19 | return s == syscall.SIGUSR1 20 | } 21 | -------------------------------------------------------------------------------- /internal/osutil/signal_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows || !unix 2 | // +build windows !unix 3 | 4 | package osutil 5 | 6 | import ( 7 | "os" 8 | ) 9 | 10 | func SignalNotify(c chan os.Signal) { 11 | } 12 | 13 | func IsSignalUSR1(s os.Signal) bool { 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /internal/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package reporter defines a simple interface for structs to produce a printable report about 3 | themselves which are typically statistically oriented. 4 | 5 | The string returned by Report() should be one or more lines separated by newlines suitable for 6 | printing to a log file. The caller will normally split multiple lines up and prefix them with some 7 | other logging data, such as timestamps and source. Empty lines are ignored and the final trailing 8 | newline should not be present thus most single line reporters should not bother with a newline as 9 | the caller is likely to go: fmt.Println(you.Report()) or similar. 10 | */ 11 | package reporter 12 | 13 | // Reporter is the sole package interface 14 | type Reporter interface { 15 | 16 | // Name returns the name of the reportable struct. This is normally used 17 | // as a prefix for reportable output. 18 | Name() string 19 | 20 | // Report returns one or more printable set of lines separated by 21 | // newlines. If 'resetCounters' is true, then any internal values used 22 | // to produce the report should be reset to zero *after* the report is 23 | // produced. Implementation needs to manage concurrent access as 24 | // Report() may be called by multiple go-routines - albeit unlikely. 25 | Report(resetCounters bool) string 26 | } 27 | -------------------------------------------------------------------------------- /internal/resolver/doh/config.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/markdingo/trustydns/internal/bestserver" 7 | ) 8 | 9 | // Config is passed to the New() constructor. 10 | type Config struct { 11 | UseGetMethod bool // Instead of the default POST 12 | GeneratePadding bool // RFC8467 query and response padding with zeroes 13 | 14 | ECSRedactResponse bool // If server-side synthesis/set remove ECS before returning to client 15 | ECSRemove bool // If ECS options are removed from inbound queries 16 | ECSRequestIPv4PrefixLen int // Server-side synthesis if client address is IPv4 - 0=no synth 17 | ECSRequestIPv6PrefixLen int // Server-side synthesis if client address is IPv6 - 0=no synth 18 | ECSSetCIDR *net.IPNet // Set the ECS locally with this CIDR - cannot have ECSRequest* as well 19 | 20 | bestserver.LatencyConfig // Latency Config and Server URLs are passed down 21 | ServerURLs []string // to the DoH resolver. 22 | } 23 | -------------------------------------------------------------------------------- /internal/resolver/doh/reporter.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // addSuccessStats tracks successful resolutions. 9 | func (t *remote) addSuccessStats(bsIX int, total, server time.Duration, ecsRemoved, ecsSet, ecsRequest, ecsReturned bool) { 10 | t.mu.Lock() 11 | defer t.mu.Unlock() 12 | bs := t.bsList[bsIX] 13 | 14 | bs.success++ 15 | bs.totalLatency += total 16 | bs.serverLatency += server 17 | 18 | if ecsRemoved { 19 | bs.ecsRemoved++ 20 | } 21 | if ecsSet { 22 | bs.ecsSet++ 23 | } 24 | if ecsRequest { 25 | bs.ecsRequest++ 26 | } 27 | if ecsReturned { 28 | bs.ecsReturned++ 29 | } 30 | } 31 | 32 | // addGeneralFailure tracks failed resolution attempts that are not server specific. 33 | func (t *remote) addGeneralFailure(dgx dgxInt) { 34 | t.mu.Lock() 35 | defer t.mu.Unlock() 36 | 37 | t.failures[dgx]++ 38 | } 39 | 40 | // addServerFailure tracks failed resolution attempts that can be related to a specific server. 41 | func (t *remote) addServerFailure(bsIX int, dex dexInt) { 42 | t.mu.Lock() 43 | defer t.mu.Unlock() 44 | 45 | bs := t.bsList[bsIX] 46 | 47 | bs.failures[dex]++ 48 | } 49 | 50 | func (t *remote) Name() string { 51 | return "DoH Resolver" 52 | } 53 | 54 | /* 55 | Report returns a multi-line string showing stats suitable for printing to a log file. Reset counters 56 | if resetCounters is true. 57 | 58 | Output: 59 | 60 | Totals: req=305 ok=301 errs=2 (4/0) 61 | 62 | ^ ^ ^ ^ ^ 63 | | | | | | 64 | | | | | +--RFFU Error 65 | | | | +--DNSPackError 66 | | | +--Total Error Requests 67 | | +--Total Good requests 68 | +---Total Requests 69 | 70 | Server: ok=301 tl=0.254 rl=0.235 errs=5 (0/0/4/0/0/1) (ecs 0/0/305/64) URL 71 | 72 | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 73 | | | | | | | | | | | | | | | | | 74 | | | | | | | | | | | | | | | | +-- Server URL 75 | | | | | | | | | | | | | | | +--ecsReturned 76 | | | | | | | | | | | | | | +--ecsRequest 77 | | | | | | | | | | | | | +--ecsSet 78 | | | | | | | | | | | | +--ecsRemoved 79 | | | | | | | | | | | +--EDNS Client Subnet stats 80 | | | | | | | | | | +--UnpackDNSResponse 81 | | | | | | | | | +--ContentType 82 | | | | | | | | +--ResponseReadAll 83 | | | | | | | +--NonStatusOk 84 | | | | | | +--DoRequest 85 | | | | | +--CreateHTTPRequest 86 | | | | +--Per-Server Errors 87 | | | +--Remote server Latency 88 | | +--Total query Latency 89 | +--Good Requests 90 | */ 91 | func (t *remote) Report(resetCounters bool) string { 92 | if resetCounters { 93 | t.mu.Lock() 94 | defer t.mu.Unlock() 95 | } else { 96 | t.mu.RLock() 97 | defer t.mu.RUnlock() 98 | } 99 | 100 | // Create the best server reports first as that lets us calculate the summary stats for the 101 | // main report as we pass thru the individual server stats. 102 | 103 | bestReport := "" 104 | ok := 0 105 | errs := 0 106 | for _, bs := range t.bsList { 107 | bsErrs := 0 108 | ok += bs.success 109 | for _, v := range bs.failures { 110 | bsErrs += v 111 | } 112 | errs += bsErrs 113 | var tl, rl float64 114 | if bs.success > 0 { 115 | tl = bs.totalLatency.Seconds() / float64(bs.success) 116 | rl = bs.serverLatency.Seconds() / float64(bs.success) 117 | } 118 | bestReport += fmt.Sprintf("Server: ok=%d tl=%0.3f rl=%0.3f errs=%d (%s) (ecs %d/%d/%d/%d) %s\n", 119 | bs.success, tl, rl, bsErrs, formatCounters("%d", "/", bs.failures[:]), 120 | bs.ecsRemoved, bs.ecsSet, bs.ecsRequest, bs.ecsReturned, bs.name) 121 | if resetCounters { 122 | bs.resetCounters() 123 | } 124 | } 125 | for _, v := range t.failures { 126 | errs += v 127 | } 128 | mainReport := fmt.Sprintf("Totals: req=%d ok=%d errs=%d (%s)\n", 129 | ok+errs, ok, errs, 130 | formatCounters("%d", "/", t.failures[:])) 131 | 132 | if resetCounters { 133 | t.resetCounters() 134 | } 135 | 136 | return mainReport + bestReport 137 | } 138 | 139 | // formatCounters returns a nice %d/%d/%d format from an array of ints. This is less error-prone 140 | // than hard-coding one big ol' Sprintf string but obviously slower which is irrelevant here. 141 | func formatCounters(vfmt string, delim string, vals []int) string { 142 | res := "" 143 | for ix, v := range vals { 144 | if ix > 0 { 145 | res += delim 146 | } 147 | res += fmt.Sprintf(vfmt, v) 148 | } 149 | 150 | return res 151 | } 152 | -------------------------------------------------------------------------------- /internal/resolver/doh/reporter_test.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const ( 10 | expect0 = `Totals: req=0 ok=0 errs=0 (0/0) 11 | Server: ok=0 tl=0.000 rl=0.000 errs=0 (0/0/0/0/0/0) (ecs 0/0/0/0) http://localhost 12 | ` 13 | expect1 = `Totals: req=17 ok=5 errs=12 (1/0) 14 | Server: ok=5 tl=0.380 rl=0.280 errs=11 (2/3/1/1/3/1) (ecs 1/2/3/4) http://localhost 15 | ` 16 | ) 17 | 18 | func TestReporter(t *testing.T) { 19 | res, _ := New(Config{ServerURLs: []string{"http://localhost"}}, nil) 20 | nm := res.Name() 21 | if !strings.Contains(nm, "Resolver") { 22 | t.Error("reporter Name() does not contain the word 'Resolver'", nm) 23 | } 24 | 25 | st := res.Report(false) 26 | if st != expect0 { 27 | t.Error("Expected:", expect0, "Got:", st) 28 | } 29 | 30 | res.addSuccessStats(0, time.Millisecond*200, time.Millisecond*100, false, false, false, false) 31 | res.addSuccessStats(0, time.Millisecond*300, time.Millisecond*200, false, false, false, true) 32 | res.addSuccessStats(0, time.Millisecond*400, time.Millisecond*300, false, false, true, true) 33 | res.addSuccessStats(0, time.Millisecond*500, time.Millisecond*400, false, true, true, true) 34 | res.addSuccessStats(0, time.Millisecond*500, time.Millisecond*400, true, true, true, true) 35 | // 200+300+400+500+500 / 5 = 380 = Total Latency 36 | // 100+200+300+400+400 / 5 = 280 = Remote Latency (if reported by remote end) 37 | res.addGeneralFailure(dgxPackDNSQuery) // A whole bunch of distinquishible error counts 38 | res.addServerFailure(0, dexCreateHTTPRequest) 39 | res.addServerFailure(0, dexCreateHTTPRequest) 40 | res.addServerFailure(0, dexDoRequest) 41 | res.addServerFailure(0, dexDoRequest) 42 | res.addServerFailure(0, dexDoRequest) 43 | res.addServerFailure(0, dexNonStatusOk) 44 | res.addServerFailure(0, dexResponseReadAll) 45 | res.addServerFailure(0, dexContentType) 46 | res.addServerFailure(0, dexContentType) 47 | res.addServerFailure(0, dexContentType) 48 | res.addServerFailure(0, dexUnpackDNSResponse) 49 | st = res.Report(true) 50 | if st != expect1 { 51 | t.Error("Expected:", expect1, "Got:", st) 52 | } 53 | 54 | // Test that the previous resetCounters=true works 55 | st = res.Report(false) 56 | if st != expect0 { 57 | t.Error("resetCounters did not reset. Expected:", expect0, "Got:", st) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /internal/resolver/local/config.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | // Config is passed to the New() constructor. 4 | type Config struct { 5 | ResolvConfPath string 6 | LocalDomains []string // In addition to those found in the resolvConfPath 7 | 8 | // Caller can create their own Exchangers on our behalf 9 | NewDNSClientExchangerFunc func(net string) DNSClientExchanger 10 | } 11 | -------------------------------------------------------------------------------- /internal/resolver/local/reporter.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // addGeneralSuccess tracks successful resolution attempts that are not server specific. There is a 9 | // maximum of one of these calls per Resolve() call. 10 | func (t *local) addGeneralSuccess() { 11 | t.mu.Lock() 12 | defer t.mu.Unlock() 13 | 14 | t.success++ 15 | } 16 | 17 | // addGeneralFailure tracks failed resolution attempts that are not server specific. There is a 18 | // maximum of one of these calls per Resolve() call. 19 | func (t *local) addGeneralFailure(gfx gfxInt) { 20 | t.mu.Lock() 21 | defer t.mu.Unlock() 22 | 23 | t.failures[gfx]++ 24 | } 25 | 26 | // addServerSuccess tracks successful responses from servers. That simply means the server is 27 | // responding and is suited for other queries. It does not mean a particular query is 28 | // successful. There can be multiple of these call per Resolve() call. 29 | func (t *local) addServerSuccess(bsix int, tcpFallback, tcpSuperior bool, latency time.Duration) { 30 | t.mu.Lock() 31 | defer t.mu.Unlock() 32 | 33 | t.totalLatency += latency 34 | bs := t.bsList[bsix] 35 | bs.success++ 36 | if tcpFallback { 37 | bs.events[evxTCPFallback]++ 38 | } 39 | if tcpSuperior { 40 | bs.events[evxTCPSuperior]++ 41 | } 42 | bs.latency += latency 43 | } 44 | 45 | // addServerFailure tracks failed resolution attempts that are server-specific. There can be 46 | // multiple of these calls per Resolve() call since it can iterate after certain server-specific 47 | // errors. 48 | func (t *local) addServerFailure(bsix int, tcpFallback, tcpSuperior bool, sfx sfxInt) { 49 | t.mu.Lock() 50 | defer t.mu.Unlock() 51 | 52 | bs := t.bsList[bsix] 53 | bs.failures[sfx]++ 54 | if tcpFallback { 55 | bs.events[evxTCPFallback]++ 56 | } 57 | if tcpSuperior { 58 | bs.events[evxTCPSuperior]++ 59 | } 60 | } 61 | 62 | func (t *local) Name() string { 63 | return "Local Resolver" 64 | } 65 | 66 | /* 67 | Report returns a multi-line string showing stats suitable for printing to a log file. Zero counters 68 | if resetCounters is true. 69 | 70 | Totals: req=1273 ok=1273 errs=0 (0/0) 71 | 72 | ^ ^ ^ ^ ^ 73 | | | | | | 74 | | | | | +--Retry count exceeded 75 | | | | +--Timeout limit exceeded 76 | | | +--Total bad requests 77 | | +--Total good requests 78 | +--Total requests 79 | 80 | Server: req=1273 ok=1273 al=0.003 errs=0 (0/0/0/0/0/0) (ev 0/0) 127.0.0.1:53 81 | 82 | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 83 | | | | | | | | | | | | | | | 84 | | | | | | | | | | | | | | +--Server 85 | | | | | | | | | | | | | +--RFFU 86 | | | | | | | | | | | | +--TCP fallback 87 | | | | | | | | | | | +--Event counters 88 | | | | | | | | | | +--Other rcodes 89 | | | | | | | | | +--Not implemented (Rcode) 90 | | | | | | | | +--Refused (Rcode) 91 | | | | | | | +--Server fail (Rcode) 92 | | | | | | +--Format error (Rcode) 93 | | | | | +--Exchange error 94 | | | | +--Total bad requests 95 | | | +--Average latency 96 | | +--Good requests 97 | +---Total requests 98 | */ 99 | func (t *local) Report(resetCounters bool) string { 100 | if resetCounters { 101 | t.mu.Lock() 102 | defer t.mu.Unlock() 103 | } else { 104 | t.mu.RLock() 105 | defer t.mu.RUnlock() 106 | } 107 | 108 | // Create the best server reports first as that lets us calculate the summary stats for the 109 | // main report as we pass thru the individual server stats. 110 | 111 | bestReport := "" 112 | errs := 0 113 | for _, v := range t.failures { 114 | errs += v 115 | } 116 | for _, bs := range t.bsList { 117 | bsErrs := 0 118 | for _, v := range bs.failures { 119 | bsErrs += v 120 | } 121 | var al float64 122 | if bs.success > 0 { 123 | al = bs.latency.Seconds() / float64(bs.success) 124 | } 125 | bestReport += fmt.Sprintf("Server: req=%d ok=%d al=%0.3f errs=%d (%s) (ev %s) %s\n", 126 | bs.success+bsErrs, bs.success, al, bsErrs, formatCounters("%d", "/", bs.failures[:]), 127 | formatCounters("%d", "/", bs.events[:]), bs.name) 128 | if resetCounters { 129 | bs.resetCounters() 130 | } 131 | } 132 | 133 | mainReport := fmt.Sprintf("Totals: req=%d ok=%d errs=%d (%s)\n", 134 | t.success+errs, t.success, errs, formatCounters("%d", "/", t.failures[:])) 135 | 136 | if resetCounters { 137 | t.resetCounters() 138 | } 139 | 140 | return mainReport + bestReport 141 | } 142 | 143 | // formatCounters returns a nice %d/%d/%d format from an array of ints. This is less error-prone 144 | // than hard-coding one big ol' Sprintf string but obviously slower which is irrelevant here. 145 | func formatCounters(vfmt string, delim string, vals []int) string { 146 | res := "" 147 | for ix, v := range vals { 148 | if ix > 0 { 149 | res += delim 150 | } 151 | res += fmt.Sprintf(vfmt, v) 152 | } 153 | 154 | return res 155 | } 156 | -------------------------------------------------------------------------------- /internal/resolver/local/reporter_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const ( 10 | zero1 = `Totals: req=0 ok=0 errs=0 (0/0) 11 | Server: req=0 ok=0 al=0.000 errs=0 (0/0/0/0/0/0) (ev 0/0) 127.0.0.127:53 12 | Server: req=0 ok=0 al=0.000 errs=0 (0/0/0/0/0/0) (ev 0/0) [::127]:53` 13 | 14 | all1 = `Totals: req=5 ok=2 errs=3 (1/2) 15 | Server: req=8 ok=2 al=1.500 errs=6 (1/1/1/1/1/1) (ev 2/2) 127.0.0.127:53 16 | Server: req=1 ok=0 al=0.000 errs=1 (0/0/1/0/0/0) (ev 1/0) [::127]:53` 17 | ) 18 | 19 | func TestReporter(t *testing.T) { 20 | res, _ := New(Config{ResolvConfPath: "testdata/two.resolv.conf"}) 21 | nm := res.Name() 22 | if !strings.Contains(nm, "Resolver") { 23 | t.Error("Name() does not contain the word 'Resolver'", nm) 24 | } 25 | 26 | st := res.Report(false) 27 | if !strings.Contains(st, zero1) { 28 | t.Error("Report() not returning Zeroes. Want:\n", zero1, "\ngot\n", st) 29 | } 30 | 31 | res.addServerSuccess(0, true, false, time.Second) // Report successful server responses 32 | res.addGeneralSuccess() 33 | res.addServerSuccess(0, false, true, time.Second*2) // (1+2)/2 - 1.5s latency 34 | res.addGeneralSuccess() 35 | 36 | res.addServerFailure(0, true, false, sfxExchangeError) // Report all possible errors to force 37 | res.addServerFailure(0, false, true, sfxFormatError) // every counter to tick over from zero 38 | res.addServerFailure(0, false, false, sfxServerFail) 39 | res.addServerFailure(0, false, false, sfxRefused) 40 | res.addServerFailure(0, false, false, sfxNotImplemented) 41 | res.addServerFailure(0, false, false, sfxOther) 42 | 43 | res.addServerFailure(1, true, false, sfxServerFail) 44 | 45 | res.addGeneralFailure(gfxTimeout) // Report all possible general failures 46 | res.addGeneralFailure(gfxMaxAttempts) 47 | res.addGeneralFailure(gfxMaxAttempts) 48 | st = res.Report(true) 49 | if !strings.Contains(st, all1) { 50 | t.Error("Report() not returning all counters. Want:\n", all1, "\ngot\n", st) 51 | } 52 | 53 | // Test that the reset flag works 54 | 55 | st = res.Report(false) // Previous Report() reset counters so now we should be back to the zero 56 | if !strings.Contains(st, zero1) { 57 | t.Error("reporter Report(true) did not appear to reset counters. Got:", st) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/resolver/local/testdata/empty.resolv.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdingo/trustydns/766fc0e6b83bb8aaa9cf4035e3f26fc1d5141c59/internal/resolver/local/testdata/empty.resolv.conf -------------------------------------------------------------------------------- /internal/resolver/local/testdata/resolv.conf: -------------------------------------------------------------------------------- 1 | search search1.example.net search2.exAmple.net 120.0.10.in-addr.arpa 2 | nameserver 192.168.1.1 3 | nameserver 10.0.0.1 4 | nameserver 10.0.0.2 5 | nameserver 10.0.0.3 6 | options timeout:1 attempts:3 7 | -------------------------------------------------------------------------------- /internal/resolver/local/testdata/simplest.resolv.conf: -------------------------------------------------------------------------------- 1 | nameserver 127.0.0.127 2 | -------------------------------------------------------------------------------- /internal/resolver/local/testdata/timeout.resolv.conf: -------------------------------------------------------------------------------- 1 | domain example.net 2 | search example.com 3 | nameserver 127.0.0.1:65053 4 | nameserver [::1]:65053 5 | -------------------------------------------------------------------------------- /internal/resolver/local/testdata/two.resolv.conf: -------------------------------------------------------------------------------- 1 | nameserver 127.0.0.127 2 | nameserver ::127 3 | -------------------------------------------------------------------------------- /internal/resolver/resolver.go: -------------------------------------------------------------------------------- 1 | // Package resolver is the interface for resolving a dns.Msg via a local or DoH resolver 2 | package resolver 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | // DNSTransportType defines the transport type used to carry the DNS query 11 | type DNSTransportType string 12 | 13 | const ( 14 | DNSTransportUndefined DNSTransportType = "" 15 | DNSTransportHTTP = "http" 16 | DNSTransportUDP = "udp" 17 | DNSTransportTCP = "tcp" 18 | ) 19 | 20 | // QueryMetaData is a primordial struct containing metadata about the query passed to Resolve(). It 21 | // helps the function make fine-grained decisions about how to perform the resolution. For example 22 | // whether the original query originated as a TCP query or is a re-query due to a previous 23 | // truncation attempt. This structure is needed as DNS messages, unlike more recently protocols, 24 | // have almost no ability to add meta data as needed. Compare with email and nttp headers. 25 | // 26 | // Primordial because there really isn't much to it at this stage - as you can see. It's mostly a 27 | // place-holder in the event that we want to add more stuff later. 28 | type QueryMetaData struct { 29 | TransportType DNSTransportType // Of the original inbound query 30 | } 31 | 32 | // ResponseMetaData returns metadata about the qhery made by Resolve(). It mostly contains 33 | // statistical and trace meta-information. 34 | type ResponseMetaData struct { 35 | TransportType DNSTransportType // Final transport used with the resultant query 36 | 37 | TransportDuration time.Duration // Does not include ResolutionDuration 38 | ResolutionDuration time.Duration // Time taken caller actual resolver system 39 | // Total Resolution Duration = TransportDuration+ResolutionDuration 40 | 41 | PayloadSize int 42 | QueryTries int // Number of resolution attempts were made 43 | ServerTries int // Number of different servers were tried 44 | FinalServerUsed string // Name of the last server attempted 45 | } 46 | 47 | type Resolver interface { 48 | // Return true if this resolver handles this qName 49 | InBailiwick(qName string) bool 50 | 51 | // Resolve() resolved the dns.Msg query. Returns resp+respMeta or error. queryMeta can be 52 | // nil. 53 | Resolve(query *dns.Msg, queryMeta *QueryMetaData) (resp *dns.Msg, respMeta *ResponseMetaData, err error) 54 | } 55 | -------------------------------------------------------------------------------- /internal/tlsutil/client.go: -------------------------------------------------------------------------------- 1 | // Package tlsutil is a helper package to manage tls key and cert settings for clients and servers 2 | package tlsutil 3 | 4 | import ( 5 | "crypto/tls" 6 | "errors" 7 | ) 8 | 9 | // NewClientTLSConfig is a helper wrapper which creates a tls.Config for a client-side HTTPS 10 | // connection. If either root CAs are indicated or other CAs are supplied, server verification is 11 | // enabled. If client key and cert files are supplied, they are loaded as client-side certificates 12 | // to present to the server. Both key and cert must be present or both most be absent. 13 | // 14 | // Returns a tls.Config or an error. 15 | func NewClientTLSConfig(useSystemCAs bool, otherCAFiles []string, clientCertFile, clientKeyFile string) (*tls.Config, error) { 16 | verifyServer := useSystemCAs || len(otherCAFiles) > 0 // Will verify if any roots are supplied 17 | cfg := &tls.Config{InsecureSkipVerify: !verifyServer} // Ask to verify server if we have any CAs 18 | if verifyServer { // Need a cert pool if we're using system or other CAs 19 | pool, err := loadroots(useSystemCAs, otherCAFiles) 20 | if err != nil { 21 | return nil, errors.New("tlsutil:NewClientTLSConfig:" + err.Error()) 22 | } 23 | cfg.RootCAs = pool // Set server verification roots 24 | } 25 | 26 | // We must have both or neither, not one or the other. 27 | if len(clientCertFile) > 0 && len(clientKeyFile) == 0 { 28 | return nil, errors.New("tlsutil:NewClientTLSConfig Client key file missing when cert file present") 29 | } 30 | if len(clientCertFile) == 0 && len(clientKeyFile) > 0 { 31 | return nil, errors.New("tlsutil:NewClientTLSConfig Client cert file missing when key file present") 32 | } 33 | 34 | if len(clientCertFile) == 0 { 35 | return cfg, nil 36 | } 37 | 38 | var err error 39 | cfg.Certificates = make([]tls.Certificate, 1) 40 | cfg.Certificates[0], err = tls.LoadX509KeyPair(clientCertFile, clientKeyFile) 41 | if err != nil { 42 | return nil, errors.New("tlsutil:NewClientTLSConfig:tls.LoadX509KeyPair" + err.Error()) 43 | } 44 | 45 | return cfg, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/tlsutil/client_test.go: -------------------------------------------------------------------------------- 1 | package tlsutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var zeroCAs = []string{} 8 | var oneCA = []string{"testdata/rootCA.cert"} 9 | var twoCAs = []string{"testdata/rootCA.cert", "testdata/rootCA.cert2"} 10 | var emptyCA = []string{"testdata/emptyfile"} 11 | var missingCA = []string{"testdata/rootCANO"} 12 | 13 | func TestNewClient(t *testing.T) { 14 | cfg, err := NewClientTLSConfig(false, zeroCAs, "", "") 15 | if err != nil { 16 | t.Error("Unexpected error with minimalist NewClientTLSConfig", err) 17 | } 18 | if cfg == nil { 19 | t.Error("Expected a config back from NewClientTLSConfig when no error returned") 20 | } 21 | cfg, err = NewClientTLSConfig(true, zeroCAs, "", "") 22 | if err != nil { 23 | t.Error("Unexpected error with almost minimalist NewClientTLSConfig", err) 24 | } 25 | if cfg == nil { 26 | t.Error("Expected a config back from NewClientTLSConfig when no error returned") 27 | } 28 | 29 | // Good path tests 30 | cfg, err = NewClientTLSConfig(false, oneCA, "testdata/proxy.cert", "testdata/proxy.key") 31 | if err != nil { 32 | t.Error("Unexpected error with good data files", err) 33 | } 34 | cfg, err = NewClientTLSConfig(true, oneCA, "testdata/proxy.cert", "testdata/proxy.key") 35 | if err != nil { 36 | t.Error("Unexpected error with good data files and useSystemRoot", err) 37 | } 38 | 39 | // Wrong path test 40 | cfg, err = NewClientTLSConfig(false, oneCA, "testdata/proxy.key", "testdata/proxy.cert") 41 | if err == nil { 42 | t.Error("Expected error with switch key and cert files") 43 | } 44 | 45 | // Bad path tests 46 | cfg, err = NewClientTLSConfig(false, oneCA, "testdata/proxy.cert", "") 47 | if err == nil { 48 | t.Error("Expected error with missing key file") 49 | } 50 | cfg, err = NewClientTLSConfig(false, oneCA, "", "testdata/proxy.key") 51 | if err == nil { 52 | t.Error("Expected error with missing cert file") 53 | } 54 | cfg, err = NewClientTLSConfig(true, emptyCA, "testdata/proxy.cert", "testdata/proxy.key") 55 | if err == nil { 56 | t.Error("Expected an error with an empty root CA") 57 | } 58 | cfg, err = NewClientTLSConfig(true, missingCA, "testdata/proxy.cert", "testdata/proxy.key") 59 | if err == nil { 60 | t.Error("Expected an error return with a bad rootCA file") 61 | } 62 | cfg, err = NewClientTLSConfig(true, oneCA, "testdata/proxy.certNO", "testdata/proxy.key") 63 | if err == nil { 64 | t.Error("Expected an error return with a bad proxy certificate file") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/tlsutil/loadroots.go: -------------------------------------------------------------------------------- 1 | package tlsutil 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "io/ioutil" 7 | ) 8 | 9 | // loadroots loads all the indicated root CA files and returns an x509.CertPool. If neither roots or 10 | // other CAs are indicated an empty pool is returned which will tell a tls.Config *not* to try and 11 | // retrieve the roots itself. 12 | // 13 | // Returns a (possibly empty) c509.CertPool or error 14 | func loadroots(useSystemRoots bool, otherCAFiles []string) (*x509.CertPool, error) { 15 | var pool *x509.CertPool 16 | if useSystemRoots { 17 | var err error 18 | pool, err = x509.SystemCertPool() 19 | if err != nil { 20 | return nil, fmt.Errorf("tlsutil:loadroots:systemRoots failed: %s", err.Error()) 21 | } 22 | } else { 23 | pool = x509.NewCertPool() 24 | } 25 | 26 | // Load the other CA files 27 | 28 | for _, caFile := range otherCAFiles { 29 | asn1Data, err := ioutil.ReadFile(caFile) 30 | if err != nil { 31 | return nil, fmt.Errorf("tlsutil:loadroots:otherCA failed: %s", err.Error()) 32 | } 33 | 34 | if !pool.AppendCertsFromPEM(asn1Data) { 35 | return nil, fmt.Errorf("tlsutil:loadroots:appendCerts failed to add %s", caFile) 36 | } 37 | } 38 | 39 | return pool, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/tlsutil/loadroots_test.go: -------------------------------------------------------------------------------- 1 | package tlsutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLoadRoots(t *testing.T) { 8 | pool, err := loadroots(false, zeroCAs) 9 | if err != nil { 10 | t.Error("Unexpected error with minimalist loadroots", err) 11 | } 12 | if pool == nil { 13 | t.Error("Expected a pool back from loadroots when no error returned") 14 | } 15 | pool, err = loadroots(true, zeroCAs) 16 | if err != nil { 17 | t.Error("Unexpected error with almost minimalist loadroots", err) 18 | } 19 | if pool == nil { 20 | t.Error("Expected a pool back from loadroots when no error returned") 21 | } 22 | 23 | // Good path tests 24 | pool, err = loadroots(false, oneCA) 25 | if err != nil { 26 | t.Error("Unexpected error with oneCA", err) 27 | } 28 | pool, err = loadroots(true, oneCA) 29 | if err != nil { 30 | t.Error("Unexpected error with oneCA + useSystemRoot", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/tlsutil/server.go: -------------------------------------------------------------------------------- 1 | package tlsutil 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | myPrefix = "tlsutil:NewServerTLSConfig" 10 | ) 11 | 12 | // NewServerTLSConfig is a helper wrapper which creates a tls.Config for a server-side 13 | // connection. If either root CAs are indicated or other CAs are supplied, client verification is 14 | // enabled. If server keys and cert files are supplied, they are loaded as server-side certificates 15 | // to present to the client. The matching certs and keys must be in the same array position, 16 | // obviously enough. 17 | // 18 | // Returns a tls.Config or an error. 19 | func NewServerTLSConfig(useSystemCAs bool, otherCAFiles []string, certs, keys []string) (*tls.Config, error) { 20 | verifyClient := useSystemCAs || len(otherCAFiles) > 0 // Will verify if any roots are supplied 21 | cfg := &tls.Config{} 22 | if verifyClient { // Need a cert pool if we're using system or other CAs 23 | pool, err := loadroots(useSystemCAs, otherCAFiles) 24 | if err != nil { 25 | return nil, fmt.Errorf("%s:%s", myPrefix, err.Error()) 26 | } 27 | cfg.ClientCAs = pool // Set client verification roots 28 | cfg.ClientAuth = tls.RequireAndVerifyClientCert // ... and insist on legit client certs 29 | } 30 | 31 | if len(certs) != len(keys) { 32 | return nil, fmt.Errorf("%s:Certificate file count (%d) and key file count (%d) don't match", 33 | myPrefix, len(certs), len(keys)) 34 | } 35 | 36 | cfg.Certificates = make([]tls.Certificate, 0, len(certs)) 37 | for ix, certFile := range certs { 38 | keyFile := keys[ix] 39 | if len(certFile) == 0 { 40 | return nil, fmt.Errorf("%s:Empty string Certificate file @ %d not allowed", myPrefix, ix) 41 | } 42 | if len(keyFile) == 0 { 43 | return nil, fmt.Errorf("%s:Empty string Key file @ %d not allowed", myPrefix, ix) 44 | } 45 | 46 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 47 | if err != nil { 48 | return nil, fmt.Errorf("%s:tls.LoadX509KeyPair:%s for %s and %s", 49 | myPrefix, err.Error(), certFile, keyFile) 50 | } 51 | cfg.Certificates = append(cfg.Certificates, cert) 52 | } 53 | 54 | // Create the mapping between the certificate's CN and the certificate so that a single TLS 55 | // listener can accept connections for multiple domains. Callers can consult the 56 | // cfg.NameToCertificate map to determine which CNs have been mapped. 57 | 58 | cfg.BuildNameToCertificate() 59 | 60 | return cfg, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/tlsutil/server_test.go: -------------------------------------------------------------------------------- 1 | package tlsutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ( 8 | emptyAr = []string{} 9 | certAr = []string{"testdata/proxy.cert"} 10 | keyAr = []string{"testdata/proxy.key"} 11 | blankAr = []string{""} // This can come from a bogus command line caller with --tls-cert "" 12 | ) 13 | 14 | func TestNewServer(t *testing.T) { 15 | cfg, err := NewServerTLSConfig(false, zeroCAs, emptyAr, emptyAr) 16 | if err != nil { 17 | t.Error("Unexpected error with minimalist NewServerTLSConfig", err) 18 | } 19 | if cfg == nil { 20 | t.Fatal("cfg should be non-nil if no error") 21 | } 22 | cfg, err = NewServerTLSConfig(true, zeroCAs, emptyAr, emptyAr) 23 | if err != nil { 24 | t.Error("Unexpected error with almost minimalist NewServerTLSConfig", err) 25 | } 26 | if cfg == nil { 27 | t.Fatal("cfg should be non-nil if no error") 28 | } 29 | 30 | // Good path tests 31 | cfg, err = NewServerTLSConfig(false, oneCA, certAr, keyAr) 32 | if err != nil { 33 | t.Error("Unexpected error with good data files", err) 34 | } 35 | cfg, err = NewServerTLSConfig(true, oneCA, certAr, keyAr) 36 | if err != nil { 37 | t.Error("Unexpected error with good data files and useSystemRoot", err) 38 | } 39 | 40 | // Bad path tests 41 | cfg, err = NewServerTLSConfig(false, oneCA, certAr, emptyAr) 42 | if err == nil { 43 | t.Error("Expected error with missing key file") 44 | } 45 | cfg, err = NewServerTLSConfig(false, oneCA, certAr, blankAr) 46 | if err == nil { 47 | t.Error("Expected error with blank key file") 48 | } 49 | cfg, err = NewServerTLSConfig(false, oneCA, blankAr, keyAr) 50 | if err == nil { 51 | t.Error("Expected error with blank cert file") 52 | } 53 | cfg, err = NewServerTLSConfig(false, oneCA, emptyAr, keyAr) 54 | if err == nil { 55 | t.Error("Expected error with missing cert file") 56 | } 57 | cfg, err = NewServerTLSConfig(true, emptyCA, certAr, keyAr) 58 | if err == nil { 59 | t.Error("Expected an error with an empty root CA") 60 | } 61 | cfg, err = NewServerTLSConfig(true, missingCA, certAr, keyAr) 62 | if err == nil { 63 | t.Error("Expected an error return with a bad rootCA file") 64 | } 65 | cfg, err = NewServerTLSConfig(true, oneCA, []string{"testdata/proxy.certNoExit"}, keyAr) 66 | if err == nil { 67 | t.Error("Expected an error return with a bad proxy certificate file") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/tlsutil/testdata/proxy.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDWTCCAkECAQEwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCQVUxDDAKBgNV 3 | BAgMA1FMRDETMBEGA1UECgwKdHJ1c3R5ZG5zMTEUMBIGA1UEAwwLZXhhbXBsZS5u 4 | ZXQxHjAcBgkqhkiG9w0BCQEWD2RvaEBleGFtcGxlLm5ldDAeFw0xOTA2MjMwMjM0 5 | NDJaFw0yOTA2MzAwMjM0NDJaMH8xCzAJBgNVBAYTAkFVMQwwCgYDVQQIDANRTEQx 6 | FzAVBgNVBAcMDlN1bnNoaW5lIENvYXN0MRMwEQYDVQQKDAp0cnVzdHlkbnMxMRQw 7 | EgYDVQQDDAtleGFtcGxlLm5ldDEeMBwGCSqGSIb3DQEJARYPZG9oQGV4YW1wbGUu 8 | bmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyCUadIsqy+uNtXkk 9 | 8Qi++z1qS6YQu0yyHeIy41+Nx7uW9Y60Urz/iZB6pNCX51ASQRi/QWOPPBBNAGLN 10 | EuihHDobqC5DGdG+MM5OdLHCCNrxJ6bLsdlm7+lpnoDa1WOqv0DLXq5qLG3yxfdF 11 | JQ8bZ0eQwFFNWdijOA7xtDDoFp4gJKH0+jFCcUxEIlDc0l1K/SYeCYd3s9ft1N/M 12 | Xd5O/47zCDOrtKCgW9gUdCjQkYBlFU2k7GAuGfHn+OOjTMzdeey1vKUD84XhirEB 13 | sM98qjSTWbs+5yDYHkPA28fFsfQI4xJ7HERtiDrT71Elub0Gh+uQyo7DLOC3iFAg 14 | Ky6S3QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCaLAKT/bsRXWzaRcJFOL5ht6AQ 15 | alVM7wI9XAJhC3Wgwykq+0ePhj4aUNLrw0XV08F+WxHWHhyF2SiIT5C2Jm1LMvg4 16 | KX3WplUbMs87B0lxHsuKtIEEEtUVptYYFU9id9qRUFwAivGdZUytgN1j6b3Qo+La 17 | npQlLQjvsqy4ZQu9dU/sm5cL8n/TW/Xb8ey5emMPLhIlIfghJH0RliQUjsrU6ue1 18 | IzkGkuOHuAPwuvhK/kNkqF7u8+eh/AaSaMakD8GmQnYTnGmefRTedZyJtFOjbW88 19 | Z5w5ztii+pUYkauEeJCcePKzEowB5kAImL3nKsMBGiWc7chu6Ju5sCWSBc4X 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /internal/tlsutil/testdata/proxy.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDIJRp0iyrL6421 3 | eSTxCL77PWpLphC7TLId4jLjX43Hu5b1jrRSvP+JkHqk0JfnUBJBGL9BY488EE0A 4 | Ys0S6KEcOhuoLkMZ0b4wzk50scII2vEnpsux2Wbv6WmegNrVY6q/QMtermosbfLF 5 | 90UlDxtnR5DAUU1Z2KM4DvG0MOgWniAkofT6MUJxTEQiUNzSXUr9Jh4Jh3ez1+3U 6 | 38xd3k7/jvMIM6u0oKBb2BR0KNCRgGUVTaTsYC4Z8ef446NMzN157LW8pQPzheGK 7 | sQGwz3yqNJNZuz7nINgeQ8Dbx8Wx9AjjEnscRG2IOtPvUSW5vQaH65DKjsMs4LeI 8 | UCArLpLdAgMBAAECggEBAL7qfbDcK6e3e0a3V6DAugTIkcO1llJEF8ffxLEVrhXv 9 | gFGena4q8QsVEZh0DeKtg6wq4g2K3c/qsLkEhiBaXYyidU1ZS9KuO2Es+rPf+Hof 10 | 91feiIGPIt0JZyG2Qoi4+OBU+2nGsCrPenySoZd3MKm1H4QESefBefh4cOF0oX8o 11 | CMSidutzZhIAMeRTDlyhXQtBON78nLJ+7f1Bl057SNaGtE7Ld6iHWjZLh2f7RcqC 12 | xcNyhOf6xuE/XWpzgUDgrxgUfJ5bNYkHs4lKZjexErSjGPsjTWnvs4fjRj1xp8xd 13 | DRM4KocYGvZYepM4Oua2nTn/FypqaEEtEw30BIGn8LUCgYEA6+tXT9D7daHfcxGL 14 | Vwzh3WhZwLF+brt+td5pzGyZZUVcjMqiah5cy8fSWooZbmZKwQszTd+xHiDO1mG0 15 | KLYUjTZCztkMykrzR3xAJZDB09qRY4iIG2POoeYPULpV2/2NswIp14mZnnsVZ4oQ 16 | POUDsFv2domOgmoVEcLoTPBdEg8CgYEA2S4+BaTexDm35Sha4JaASkikZZDaB5Xm 17 | 18Duvj7st6Exz3Sw/JPn+/tej4deDcRsG4PJANwTyLUfMS4nFuG3ZGvDiKrc62n8 18 | z9pJd8KTQO4MQAdE6sJXzb2v0w8pQQfl0/NjeWB1sCDGoNErm1IZWDNJRVf88g98 19 | Um/5mxi6yFMCgYEA57YN22b2k2KZhPsGUElmzX9gJ9Isy7V7jkUUxKMlRkIJ1H5x 20 | ZqjLm5p3EFXzBGuToGbPzPyXiW/Ptt/fgtzS8p8InwCvf0B+EQgION0kgl95zLic 21 | dcpheMHs6O0axycRtW+6iOes6esZ6se/iw+jv+OS/nm8bnqilv9ICclKoCECgYEA 22 | ipXzhe57KIQcUOK7eu0O2Fgab6VLO+Pv9mVq84N70oHOIy+3cLWBJ050POqInghl 23 | Y/loXmART9YkHWHyF6vZNv99OsytRJvRc2E72GwVQy2kK4d39sYk+Wi9tdTK4nCD 24 | vAhnxaBD+SwxE5XmWaq9+YZgjxtikaRIFOLXSJ0zM3kCgYEAmH3nXhnwIuA1F8PI 25 | jRzPT+rxUSFzwEWQfPXwgNjvBDalPrKuPqasBWJhvEq7cflQnqr448oV6bUj6oGi 26 | wwu1koKmUj4WbO97zkVIaIBtXr9bDzUaaEoakNHy7XYnRxFvu0CesoDU53QQ4MXi 27 | ++nwqieCeDSUqN5VsO2cybL1Ins= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /internal/tlsutil/testdata/rootCA.cert: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 1 (0x1) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=AU, ST=QLD, O=trustydns1, CN=example.net/emailAddress=doh@example.net 7 | Validity 8 | Not Before: Jun 23 02:34:42 2019 GMT 9 | Not After : Jun 30 02:34:42 2029 GMT 10 | Subject: C=AU, ST=QLD, O=trustydns1, CN=example.net/emailAddress=doh@example.net 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:a7:f8:bc:d6:6a:03:4d:81:e1:a7:c4:9d:f8:d4: 16 | e4:f3:c5:6e:b4:70:40:5b:c2:66:f6:0f:6b:0b:a3: 17 | 3f:d1:61:fd:25:29:06:7a:18:1f:de:7b:66:c8:bb: 18 | 91:93:54:16:d6:02:07:02:c7:96:e5:4f:ff:74:ab: 19 | e3:ed:2b:dc:3d:fb:17:94:5b:58:7c:18:1f:f6:31: 20 | a9:23:5e:02:f5:7b:24:80:c0:af:85:2a:5d:d4:e1: 21 | ae:3a:78:e4:02:70:88:5a:1d:d9:8a:c3:e6:a6:25: 22 | 44:5d:11:7c:48:1d:b6:2c:78:03:a9:eb:88:f2:de: 23 | 17:af:92:89:75:b8:f5:c5:0f:65:da:1e:b9:a5:35: 24 | c1:e5:cb:2d:ef:72:2d:aa:b8:0e:d7:69:c6:ad:4f: 25 | 71:a8:e7:20:92:f8:89:91:0e:1e:7a:60:7d:7b:24: 26 | 0f:59:74:32:9e:ae:34:41:1e:c7:73:88:cb:18:b1: 27 | ee:e5:4c:1a:a4:41:cc:58:13:3b:7e:18:a9:d3:b8: 28 | 04:17:fc:ee:6c:cb:07:5b:48:46:5a:46:8f:4b:e6: 29 | 41:5e:c2:7c:91:4d:20:dd:dc:bb:67:be:b5:d2:5b: 30 | f6:5b:1f:d6:2a:d6:9e:77:cb:5e:6a:af:d8:61:4e: 31 | fb:82:17:da:78:a3:23:84:74:e2:6c:93:d3:da:c1: 32 | 6e:11 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Subject Key Identifier: 36 | 1D:22:53:68:86:AF:A2:3F:55:60:86:36:F5:9A:C5:4E:4D:BD:58:ED 37 | X509v3 Authority Key Identifier: 38 | keyid:1D:22:53:68:86:AF:A2:3F:55:60:86:36:F5:9A:C5:4E:4D:BD:58:ED 39 | 40 | X509v3 Basic Constraints: 41 | CA:TRUE 42 | Signature Algorithm: sha256WithRSAEncryption 43 | 19:cb:9c:77:39:fd:f3:a5:02:8d:67:3a:11:f5:54:ca:24:19: 44 | 72:2d:1a:0b:c3:b9:be:f1:74:8c:32:2e:fa:8c:93:b4:db:cb: 45 | ff:6c:2f:e7:72:e7:9f:34:0b:ae:43:70:fb:45:39:35:fa:aa: 46 | 8b:3f:42:6a:43:9a:cf:32:46:9b:6b:9d:99:b6:16:83:e6:5b: 47 | f0:ae:a8:52:81:74:ba:a0:08:ea:58:24:ae:cb:bf:21:06:30: 48 | bb:10:f1:c1:c2:60:c9:a4:4c:cf:0d:43:75:13:61:3a:f6:e4: 49 | 30:3c:69:5a:e6:71:8e:65:8e:d3:3c:cd:30:ae:c8:c3:0f:48: 50 | 66:40:97:ef:08:dd:0d:fe:26:e4:a8:f0:66:38:39:b2:32:fb: 51 | 97:1d:4c:8e:3b:9b:33:62:e9:63:45:bd:ba:6b:64:ce:7d:69: 52 | 91:bd:d5:bc:22:68:a4:60:37:68:5c:86:58:27:8b:08:d0:99: 53 | 0a:00:86:d0:53:f1:c4:53:a1:5e:f6:9e:44:00:0b:b8:36:e8: 54 | aa:1a:e4:03:ab:6b:a5:2e:1d:cb:ce:0a:db:b3:30:3b:df:63: 55 | 00:71:f6:ea:5f:81:40:03:72:65:a7:c7:fa:e3:90:73:13:d0: 56 | 2b:93:f3:36:f6:58:1d:63:41:3b:78:a5:6a:aa:f6:f1:60:27: 57 | 15:84:eb:4c 58 | -----BEGIN CERTIFICATE----- 59 | MIIDlzCCAn+gAwIBAgIBATANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJBVTEM 60 | MAoGA1UECAwDUUxEMRMwEQYDVQQKDAp0cnVzdHlkbnMxMRQwEgYDVQQDDAtleGFt 61 | cGxlLm5ldDEeMBwGCSqGSIb3DQEJARYPZG9oQGV4YW1wbGUubmV0MB4XDTE5MDYy 62 | MzAyMzQ0MloXDTI5MDYzMDAyMzQ0MlowZjELMAkGA1UEBhMCQVUxDDAKBgNVBAgM 63 | A1FMRDETMBEGA1UECgwKdHJ1c3R5ZG5zMTEUMBIGA1UEAwwLZXhhbXBsZS5uZXQx 64 | HjAcBgkqhkiG9w0BCQEWD2RvaEBleGFtcGxlLm5ldDCCASIwDQYJKoZIhvcNAQEB 65 | BQADggEPADCCAQoCggEBAKf4vNZqA02B4afEnfjU5PPFbrRwQFvCZvYPawujP9Fh 66 | /SUpBnoYH957Zsi7kZNUFtYCBwLHluVP/3Sr4+0r3D37F5RbWHwYH/YxqSNeAvV7 67 | JIDAr4UqXdThrjp45AJwiFod2YrD5qYlRF0RfEgdtix4A6nriPLeF6+SiXW49cUP 68 | ZdoeuaU1weXLLe9yLaq4Dtdpxq1PcajnIJL4iZEOHnpgfXskD1l0Mp6uNEEex3OI 69 | yxix7uVMGqRBzFgTO34YqdO4BBf87mzLB1tIRlpGj0vmQV7CfJFNIN3cu2e+tdJb 70 | 9lsf1irWnnfLXmqv2GFO+4IX2nijI4R04myT09rBbhECAwEAAaNQME4wHQYDVR0O 71 | BBYEFB0iU2iGr6I/VWCGNvWaxU5NvVjtMB8GA1UdIwQYMBaAFB0iU2iGr6I/VWCG 72 | NvWaxU5NvVjtMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABnLnHc5 73 | /fOlAo1nOhH1VMokGXItGgvDub7xdIwyLvqMk7Tby/9sL+dy5580C65DcPtFOTX6 74 | qos/QmpDms8yRptrnZm2FoPmW/CuqFKBdLqgCOpYJK7LvyEGMLsQ8cHCYMmkTM8N 75 | Q3UTYTr25DA8aVrmcY5ljtM8zTCuyMMPSGZAl+8I3Q3+JuSo8GY4ObIy+5cdTI47 76 | mzNi6WNFvbprZM59aZG91bwiaKRgN2hchlgniwjQmQoAhtBT8cRToV72nkQAC7g2 77 | 6Koa5AOra6UuHcvOCtuzMDvfYwBx9upfgUADcmWnx/rjkHMT0CuT8zb2WB1jQTt4 78 | pWqq9vFgJxWE60w= 79 | -----END CERTIFICATE----- 80 | -------------------------------------------------------------------------------- /openssl/README.md: -------------------------------------------------------------------------------- 1 | # Openssl helper scripts 2 | 3 | This directory contains `openssl` helper scripts which show how to generate a root Certificate 4 | Authority (CA) certificate and rootCA signed certificates for `trustydns-server` and 5 | `trustydns-proxy`. With such certificates you can create a DoH network in which only authorizedd 6 | proxies and servers can exchange DoH queries with each other. An alternative of course is to use 7 | firewall rules and ip filtering to achieve your access control goals but that's not very flexible 8 | and is hard to keep current if you run the proxy on mobile devices. 9 | 10 | These scripts are simplistic and are only offered as a guide. You will need to use something far 11 | more robust and secure than these helpers when setting up a production environment. 12 | 13 | ### Review site.conf and the make_* scripts 14 | 15 | `site.conf` is the openssl configuration file used by all the helper scripts. You may want to review 16 | and edit it for your environment. Be careful - it is an arcane file. You may also wish to review the 17 | helper scripts as they have various hard-coded values such as key sizes, key lifetime, email 18 | addresses and default domains. 19 | 20 | 21 | ### Generating the root CA 22 | 23 | The first step is to generate the root CA files from which all other certificates are created: 24 | 25 | ```sh 26 | ./make_rootca_cert 27 | 28 | ``` 29 | 30 | This creates `rootCA.cert` and `rootCA.key` in PEM format (it also happens to create a few other 31 | "database" files in the current directory). The `rootCA.cert` file is distributed across your 32 | deployment as it needs to be supplied to both the proxy and server via this command-line snippet: 33 | `--tls-other-roots rootCAcert.pem`. The `rootCA.key` file should be well protected. 34 | 35 | 36 | ### Generating server certificates 37 | 38 | Server certificates are encoded with a domain name so clients can verify the URL domain name against 39 | the certificate domain name. Thus `make_server_cert` is invoked with domain names on the command 40 | line. These domain names form the DoH URLs used by the proxy and server. 41 | 42 | 43 | ```sh 44 | ./make_server_cert rootCA.cert rootCA.key mydoh1.example.net mydoh2.example.net 45 | ``` 46 | 47 | You will of course need a unique server certificate for each DoH server you wish to run. A unique 48 | pair of cert/key files are generated for each domain name by this script. 49 | 50 | These key and certificate files are supplied to the server with the `--tls-key` and `--tls-cert` 51 | options respectively. 52 | 53 | 54 | ### Creating proxy certificates 55 | 56 | Unlike server certificates, proxy certificates do not have any uniquely identifying attributes such 57 | as domain names. Instead `trustydns-server` validates clients by confirming that they have been 58 | generated by the rootCA identified with the `--tls-other-roots` option. If not, the connection is 59 | rejected. 60 | 61 | ```sh 62 | ./make_proxy_cert rootCA.cert rootCA.key 63 | ``` 64 | 65 | Creates `proxy.key` and `proxy.cert` files in PEM format. While these files can be shared amongst 66 | proxy deployments, it's not a good idea to do so as that makes future revocation pretty inconvenient. 67 | 68 | The generated key and certificate files are supplied to the proxy with the `--tls-key` and 69 | `--tls-cert` options respectively. Both daemons will also need `--tls-other-roots` set to identify 70 | the rootCA certificate. You should also have `--log-tls-errors` set when first testing private 71 | certificates as the failure modes are mostly closed connections and timeouts. 72 | -------------------------------------------------------------------------------- /openssl/make_proxy_cert: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | usage="Usage: make_proxy_cert rootCACertFile rootCAKeyFile" 4 | email=doh-postmaster@example.net 5 | 6 | rootcert=$1; shift 7 | if [ -z "$rootcert" ]; then 8 | echo >&2 Error: Must supply root CA Certificate file as argument one 9 | echo >&2 $usage 10 | exit 1 11 | fi 12 | 13 | rootkey=$1; shift 14 | if [ -z "$rootkey" ]; then 15 | echo >&2 Error: Must supply root CA Key file as argument two 16 | echo >&2 $usage 17 | exit 1 18 | fi 19 | 20 | if [ ! -z "$*" ]; then 21 | echo >&2 Error: Superfluous goop on the command line: $* 22 | echo >&2 $usage 23 | exit 1 24 | fi 25 | 26 | openssl req -config site.conf -sha256 -newkey rsa:2048 -nodes \ 27 | -keyout proxy.key -out proxy.csr -days 3660 -batch \ 28 | -subj "/emailAddress=${email}" 29 | 30 | openssl x509 -req -in proxy.csr -sha256 \ 31 | -CA ${rootcert} -CAkey ${rootkey} -out proxy.cert -set_serial 01 -days 3660 32 | rm proxy.csr 33 | ls -l proxy.key proxy.cert 34 | -------------------------------------------------------------------------------- /openssl/make_rootca_cert: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | usage="Usage: make_rootca_cert" 4 | email=doh-postmaster@example.net 5 | 6 | rm -rf cadir 7 | mkdir cadir 8 | touch cadir/database cadir/database.attr 9 | echo 01 >cadir/serial 10 | openssl genrsa -out rootCA.key 2048 11 | 12 | openssl req -config site.conf -new -key rootCA.key -nodes \ 13 | -keyout rootCA.key -out rootCA.csr -days 3660 -batch -sha256 14 | 15 | openssl ca -config site.conf -extensions v3_ca -in rootCA.csr \ 16 | -out rootCA.cert -keyfile rootCA.key \ 17 | -selfsign -md sha256 -days 3660 -batch 18 | rm rootCA.csr 19 | -------------------------------------------------------------------------------- /openssl/make_server_cert: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | usage="Usage: make_server_certs rootCACertFile rootCAKeyFile domain1 ... domainn" 4 | 5 | rootcert=$1; shift 6 | if [ -z "$rootcert" ]; then 7 | echo >&2 Error: Must supply root CA Certificate file as argument one 8 | echo >&2 $usage 9 | exit 1 10 | fi 11 | 12 | rootkey=$1; shift 13 | if [ -z "$rootkey" ]; then 14 | echo >&2 Error: Must supply root CA Key file as argument two 15 | echo >&2 $usage 16 | exit 1 17 | fi 18 | 19 | domains=$* 20 | 21 | if [ -z "$domains" ]; then 22 | echo >&2 Error: Need at least one domain name on the command line after the root CA files 23 | exit 1 24 | fi 25 | 26 | for dom in $domains 27 | do 28 | email=doh-postmaster@${dom} 29 | openssl req -config site.conf -sha256 -newkey rsa:2048 -nodes \ 30 | -keyout ${dom}.key -out ${dom}.csr -days 3660 -batch \ 31 | -subj "/CN=${dom}/email=${email}" 32 | 33 | openssl x509 -req -in ${dom}.csr -sha256 \ 34 | -CA ${rootcert} -CAkey ${rootkey} -out ${dom}.cert -set_serial 01 -days 3660 35 | rm ${dom}.csr 36 | ls -l ${dom}.key ${dom}.cert 37 | done 38 | -------------------------------------------------------------------------------- /openssl/site.conf: -------------------------------------------------------------------------------- 1 | # From https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl 2 | 3 | [ req ] 4 | default_bits = 2048 5 | distinguished_name = subject 6 | req_extensions = req_ext 7 | x509_extensions = x509_ext 8 | string_mask = utf8only 9 | 10 | [ subject ] 11 | countryName_default = AU 12 | stateOrProvinceName_default = QLD 13 | localityName_default = Sunshine Coast 14 | organizationName_default = trustydns1 15 | commonName_default = example.net 16 | emailAddress_default = doh@example.net 17 | 18 | countryName = Country Name (2 letter code) 19 | stateOrProvinceName = State or Province Name (full name) 20 | localityName = Locality Name (eg, city) 21 | organizationName = Organization Name (eg, company) 22 | commonName = Common Name (e.g. server FQDN or YOUR name) 23 | emailAddress = Email Address 24 | 25 | # Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ... 26 | [ x509_ext ] 27 | 28 | subjectKeyIdentifier = hash 29 | authorityKeyIdentifier = keyid,issuer 30 | 31 | # You only need digitalSignature below. *If* you don't allow 32 | # RSA Key transport (i.e., you use ephemeral cipher suites), then 33 | # omit keyEncipherment because that's key transport. 34 | basicConstraints = CA:FALSE 35 | keyUsage = digitalSignature, keyEncipherment 36 | subjectAltName = @alternate_names 37 | nsComment = "OpenSSL Generated Certificate" 38 | 39 | # RFC 5280, Section 4.2.1.12 makes EKU optional 40 | # CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused 41 | # In either case, you probably only need serverAuth. 42 | extendedKeyUsage = serverAuth, clientAuth 43 | 44 | # Section req_ext is used when generating a certificate signing request. I.e., openssl req ... 45 | [ req_ext ] 46 | 47 | subjectKeyIdentifier = hash 48 | 49 | basicConstraints = CA:FALSE 50 | keyUsage = digitalSignature, keyEncipherment, nonRepudiation 51 | subjectAltName = @alternate_names 52 | nsComment = "OpenSSL Generated Certificate" 53 | 54 | # RFC 5280, Section 4.2.1.12 makes EKU optional 55 | # CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused 56 | # In either case, you probably only need serverAuth. 57 | # extendedKeyUsage = serverAuth, clientAuth 58 | 59 | [ alternate_names ] 60 | 61 | DNS.1 = localhost.example.com 62 | 63 | [ ca ] 64 | 65 | default_ca = CA_default 66 | 67 | [ CA_default ] 68 | 69 | dir = ./cadir 70 | 71 | certs = $dir/certs # Where the issued certs are kept 72 | crl_dir = $dir/crl # Where the issued crl are kept 73 | database = $dir/database # database index file. 74 | #unique_subject = no # Set to 'no' to allow creation of 75 | # several ctificates with same subject. 76 | new_certs_dir = $dir # default place for new certs. 77 | 78 | certificate = $dir/cacert.pem # The CA certificate 79 | serial = $dir/serial # The current serial number 80 | crlnumber = $dir/crlnumber # the current crl number 81 | # must be commented out to leave a V1 CRL 82 | crl = $dir/crl.pem # The current CRL 83 | private_key = $dir/cakey.pem # The private key 84 | RANDFILE = $dir/.rand # private random number file 85 | 86 | 87 | policy = policy_match 88 | 89 | # For the CA policy 90 | [ policy_match ] 91 | countryName = match 92 | stateOrProvinceName = match 93 | organizationName = match 94 | organizationalUnitName = optional 95 | commonName = supplied 96 | emailAddress = optional 97 | 98 | # For the 'anything' policy 99 | # At this point in time, you must list all acceptable 'object' 100 | # types. 101 | [ policy_anything ] 102 | countryName = optional 103 | stateOrProvinceName = optional 104 | localityName = optional 105 | organizationName = optional 106 | organizationalUnitName = optional 107 | commonName = supplied 108 | emailAddress = optional 109 | 110 | [ v3_ca ] 111 | 112 | 113 | # Extensions for a typical CA 114 | 115 | 116 | # PKIX recommendation. 117 | 118 | subjectKeyIdentifier = hash 119 | 120 | authorityKeyIdentifier = keyid:always,issuer 121 | 122 | # This is what PKIX recommends but some broken software chokes on critical 123 | # extensions. 124 | #basicConstraints = critical,CA:true 125 | # So we do this instead. 126 | basicConstraints = CA:true 127 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Trustydns Tools 2 | 3 | This directory contains a collection of tools which may be useful when running trustydns 4 | components. None of these tools are needed for production purposes. For those who care, the prefix 5 | 'tdt' means 'TrustyDns Tools'. At the moment, the only tools that exist are log reporting tools. 6 | 7 | # Log Reporting Tools 8 | 9 | As their names imply, `tdt-analyze-proxylog` and `tdt-analyze-serverlog` analyze the log output of 10 | `trustydns-proxy` and `trustydns-server` respectively. They produce a summary of most of the 11 | interesting statistics. The output is designed to be compact enough to send in a periodic email. 12 | 13 | To assist with the periodic email approach, `tdt-cat-yesterday-multilogs` scans a 14 | [multilog](http://cr.yp.to/daemontools/multilog.html) directory and outputs all log lines with 15 | yesterday's date. In such cases, typical usage is to run the following commands just after midnight 16 | each day: 17 | 18 | ```sh 19 | tdt-cat-yesterday-multilogs directory-of-proxy-logs | tdt-analyze-proxylog | mail -s "trustydns-proxy Status" root 20 | tdt-cat-yesterday-multilogs directory-of-server-logs | tdt-analyze-serverlog | mail -s "trustydns-proxy Status" root 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /tools/daily-proxy-stats: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # A shell script suitable for running after midnight each day. Make sure you set path to include the 4 | # trustydns/tools directory. 5 | 6 | PATH=$HOME/go/src/github.com/markdingo/trustydns/tools:$PATH 7 | 8 | who=${1:-root} 9 | logs=/var/log/trustydns-proxy 10 | 11 | tdt-cat-yesterday-multilogs $logs | tdt-analyze-proxylog | 12 | mail -s "Daily trustydns Proxy Stats from `hostname`" $who 13 | -------------------------------------------------------------------------------- /tools/daily-server-stats: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # A shell script suitable for running after midnight each day. Make sure you set path to include the 4 | # trustydns/tools directory. 5 | 6 | PATH=$HOME/go/src/github.com/markdingo/trustydns/tools:$PATH 7 | 8 | who=${1:-root} 9 | logs=/var/log/trustydns-server 10 | 11 | tdt-cat-yesterday-multilogs $logs | tdt-analyze-serverlog | 12 | mail -s "Daily trustydns server Stats from `hostname`" $who 13 | -------------------------------------------------------------------------------- /tools/tdt-cat-yesterday-multilogs: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Cat all log lines from yesterday from a multilog directory. It assumes that the dtm of 4 | # the log files are as they where when they were created and that the contents are created 5 | # by multilog and thus amenable to date decoding with tai64nlocal. 6 | # 7 | # Typically usage is to feed the output into a log analysis program such as 8 | # tdt-analyze-proxylog or tdt-analyze-serverlog. E.g.: 9 | # 10 | # tdt-cat-yesterday-multilogs /var/log/trustydns-proxy | tdt-analyze-proxylog 11 | 12 | set -e 13 | cd ${1:-.} # cd to dir on command line if given 14 | 15 | ###################################################################### 16 | # Work out when yesterday was. There's the easy way and the hard way. The hard way involves setting 17 | # the unix time value back by 1/3 of a day until the day of month changes. We can't just go back 18 | # 86400 seconds because today or yesterday may have been a short day due to daylight savings. Nor 19 | # can we just subtract a small amount as we don't know how far thru today we are. 20 | ###################################################################### 21 | 22 | yesterday="" 23 | 24 | case `uname` 25 | in 26 | Darwin|FreeBSD) 27 | yesterday=`date -v -1d +%Y-%m-%d` 28 | ;; 29 | Linux) # work our way backwards as a local day can be shorter than 86400 seconds 30 | now=`date +%s` 31 | nowDD=`date --date=@${now} +%d` 32 | try=$now 33 | for dec in 28800 43200 86400 115200 34 | do 35 | try=`expr $now - ${dec}` 36 | tryDD=`date --date=@${try} +%d` 37 | if [ $tryDD -ne $nowDD ]; 38 | then 39 | yesterday=`date --date=@${try} +%Y-%m-%d` 40 | break 41 | fi 42 | done 43 | ;; 44 | *) 45 | echo Warning: Guessing at how to determine yesterday for platform: `uname` >&2 46 | yesterday=`date -v -1d +%Y-%m-%d` # Maybe this will work, maybe it won't 47 | ;; 48 | esac 49 | 50 | if [ -z "${yesterday}" ]; then 51 | echo Warning: Could not determine the date of yesterday. Cannot continue.... >&2 52 | exit 1 53 | fi 54 | 55 | # Search for files that have been modified in the last 2 days. That will ensure we get a complete 56 | # set of log entries to greap against. 57 | 58 | find . -mtime -2 -type f | sort | xargs cat | tai64nlocal | grep ^${yesterday} 59 | 60 | --------------------------------------------------------------------------------