From 91d60d2a64d8a56d43c6ac22aaa8d908a78bdaa6 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 12 Dec 2022 21:24:20 +0700 Subject: [PATCH] Import code, preparing for release --- .github/workflows/ci.yml | 26 ++ README.md | 132 ++++++++++- cmd/ctrld/cli.go | 116 +++++++++ cmd/ctrld/dns_proxy.go | 215 +++++++++++++++++ cmd/ctrld/dns_proxy_test.go | 117 +++++++++ cmd/ctrld/main.go | 65 +++++ cmd/ctrld/os_linux.go | 25 ++ cmd/ctrld/os_mac.go | 28 +++ cmd/ctrld/os_windows.go | 14 ++ cmd/ctrld/prog.go | 159 +++++++++++++ config.go | 140 +++++++++++ config_test.go | 269 +++++++++++++++++++++ docs/config.md | 303 ++++++++++++++++++++++++ docs/contributing.md | 5 + doh.go | 99 ++++++++ doq.go | 75 ++++++ dot.go | 35 +++ go.mod | 53 +++++ go.sum | 457 ++++++++++++++++++++++++++++++++++++ log.go | 29 +++ resolver.go | 112 +++++++++ testhelper/config.go | 72 ++++++ 22 files changed, 2545 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 cmd/ctrld/cli.go create mode 100644 cmd/ctrld/dns_proxy.go create mode 100644 cmd/ctrld/dns_proxy_test.go create mode 100644 cmd/ctrld/main.go create mode 100644 cmd/ctrld/os_linux.go create mode 100644 cmd/ctrld/os_mac.go create mode 100644 cmd/ctrld/os_windows.go create mode 100644 cmd/ctrld/prog.go create mode 100644 config.go create mode 100644 config_test.go create mode 100644 docs/config.md create mode 100644 docs/contributing.md create mode 100644 doh.go create mode 100644 doq.go create mode 100644 dot.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 log.go create mode 100644 resolver.go create mode 100644 testhelper/config.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8eaaab8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +on: + push: + branches: [main] + pull_request: + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: ["windows-latest", "ubuntu-latest", "macOS-latest"] + go: ["1.19.x"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - uses: WillAbides/setup-go-faster@v1.7.0 + with: + go-version: ${{ matrix.go }} + - run: "go test -race ./..." + - uses: dominikh/staticcheck-action@v1.2.0 + with: + version: "2022.1.1" + install-go: false + cache-key: ${{ matrix.go }} diff --git a/README.md b/README.md index 92fea08..7e53170 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,132 @@ # ctrld -A highly configurable, multi-protocol DNS forwarding proxy +A highly configurable DNS forwarding proxy with support for: +- Multiple listeners for incoming queries +- Multiple upstreams with fallbacks +- Multiple network policy driven DNS query steering +- Policy driven domain based "split horizon" DNS with wildcard support + +All DNS protocols are supported, including: +- `UDP 53` +- `DNS-over-HTTPS` +- `DNS-over-TLS` +- `DNS-over-HTTP/3` (DOH3) +- `DNS-over-QUIC` + +## Use Cases +1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters). +2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B. +3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D. +4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream. + + +## OS Support +- Windows (386, amd64, arm) +- Mac (amd64, arm) +- Linux (386, amd64, arm, mips) + +## Download +Download pre-compiled binaries from the [Releases](#) section. + +## Build +`ctrld` requires `go1.19+`: + +```shell +$ go build +``` + +or + +```shell +$ go install +``` + +## Arguments +``` +Usage: + ctrld [command] + +Available Commands: + help Help about any command + interfaces Manage Interface DNS settings + run Run the DNS proxy server + +Flags: + -h, --help help for ctrld + -j, --json json output + -v, --verbose verbose log output + --version version for ctrld + +Use "ctrld [command] --help" for more information about a command. +``` +## Usage +To start the server with default configuration, simply run: `ctrld run`. This will create a generic `config.toml` file in the working directory and start the service. +1. Start the server + ``` + $ sudo ./ctrld run + ``` + +2. Run a test query using a DNS client, for example, `dig`: + ``` + $ dig verify.controld.com @127.0.0.1 +short + api.controld.com. + 147.185.34.1 + ``` + +If `verify.controld.com` resolves, you're successfully using the default Control D upstream. + + +## Configuration +### Example +- Start `listener.0` on 127.0.0.1:53 +- Accept queries from any source address +- Send all queries to `upstream.0` via DoH protocol + +### Default Config +```toml +[listener] + + [listener.0] + ip = "127.0.0.1" + port = 53 + restricted = false + +[network] + + [network.0] + cidrs = ["0.0.0.0/0"] + name = "Network 0" + +[service] + log_level = "info" + log_path = "" + +[upstream] + + [upstream.0] + bootstrap_ip = "76.76.2.11" + endpoint = "https://freedns.controld.com/p1" + name = "Control D - Anti-Malware" + timeout = 5000 + type = "doh" + + [upstream.1] + bootstrap_ip = "76.76.2.11" + endpoint = "p2.freedns.controld.com" + name = "Control D - No Ads" + timeout = 3000 + type = "doq" + +``` + +### Advanced +The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file. + +## Contributing + +See [Contribution Guideline](./docs/contributing.md) + +## Roadmap +The following functionality is on the roadmap and will be available in future releases. +- Prometheus metrics exporter +- Local caching +- Service self-installation diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go new file mode 100644 index 0000000..f431500 --- /dev/null +++ b/cmd/ctrld/cli.go @@ -0,0 +1,116 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "runtime" + + "github.com/kardianos/service" + "github.com/pelletier/go-toml" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + v = viper.NewWithOptions(viper.KeyDelimiter("::")) + defaultConfigWritten = false +) + +func initCLI() { + // Enable opening via explorer.exe on Windows. + // See: https://github.com/spf13/cobra/issues/844. + cobra.MousetrapHelpText = "" + + rootCmd := &cobra.Command{ + Use: "ctrld", + Short: "Running Control-D DNS proxy server", + Version: "1.0.0", + } + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose log output") + + runCmd := &cobra.Command{ + Use: "run", + Short: "Run the DNS proxy server", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if daemon && runtime.GOOS == "windows" { + log.Fatal("Cannot run in daemon mode. Please install a Windows service.") + } + if configPath != "" { + v.SetConfigFile(configPath) + } + if err := v.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + writeConfigFile() + defaultConfigWritten = true + } else { + log.Fatalf("failed to decode config file: %v", err) + } + } + if err := v.Unmarshal(&cfg); err != nil { + log.Fatalf("failed to unmarshal config: %v", err) + } + initLogging() + if daemon { + exe, err := os.Executable() + if err != nil { + mainLog.Error().Err(err).Msg("failed to find the binary") + os.Exit(1) + } + curDir, err := os.Getwd() + if err != nil { + mainLog.Error().Err(err).Msg("failed to get current working directory") + os.Exit(1) + } + // If running as daemon, re-run the command in background, with daemon off. + cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...) + cmd.Dir = curDir + if err := cmd.Start(); err != nil { + mainLog.Error().Err(err).Msg("failed to start process as daemon") + os.Exit(1) + } + mainLog.Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started") + os.Exit(0) + } + + s, err := service.New(&prog{}, svcConfig) + if err != nil { + mainLog.Fatal().Err(err).Msg("failed create new service") + } + serviceLogger, err := s.Logger(nil) + if err != nil { + mainLog.Error().Err(err).Msg("failed to get service logger") + return + } + + if err := s.Run(); err != nil { + if sErr := serviceLogger.Error(err); sErr != nil { + mainLog.Error().Err(sErr).Msg("failed to write service log") + } + mainLog.Error().Err(err).Msg("failed to start service") + } + }, + } + runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon") + runCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file") + + rootCmd.AddCommand(runCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func writeConfigFile() { + c := v.AllSettings() + bs, err := toml.Marshal(c) + if err != nil { + log.Fatalf("unable to marshal config to toml: %v", err) + } + if err := os.WriteFile("config.toml", bs, 0600); err != nil { + log.Printf("failed to write config file: %v\n", err) + } +} diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go new file mode 100644 index 0000000..06378bb --- /dev/null +++ b/cmd/ctrld/dns_proxy.go @@ -0,0 +1,215 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/miekg/dns" + + "github.com/Control-D-Inc/ctrld" +) + +func (p *prog) serveUDP(listenerNum string) error { + listenerConfig := p.cfg.Listener[listenerNum] + // make sure ip is allocated + if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil { + mainLog.Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip") + return allocErr + } + + handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { + domain := canonicalName(m.Question[0].Name) + reqId := requestID() + fmtSrcToDest := fmtRemoteToLocal(listenerNum, w.RemoteAddr().String(), w.LocalAddr().String()) + t := time.Now() + ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) + ctrld.Log(ctx, proxyLog.Debug(), "%s received query: %s", fmtSrcToDest, domain) + upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, w.RemoteAddr(), domain) + var answer *dns.Msg + if !matched && listenerConfig.Restricted { + answer = new(dns.Msg) + answer.SetRcode(m, dns.RcodeRefused) + + } else { + answer = p.proxy(ctx, upstreams, m) + rtt := time.Since(t) + ctrld.Log(ctx, proxyLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt) + } + if err := w.WriteMsg(answer); err != nil { + ctrld.Log(ctx, mainLog.Error().Err(err), "serveUDP: failed to send DNS response to client") + } + }) + s := &dns.Server{ + Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)), + Net: "udp", + Handler: handler, + } + return s.ListenAndServe() +} + +func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) { + upstreams := []string{"upstream." + defaultUpstreamNum} + matchedPolicy := "no policy" + matchedNetwork := "no network" + matchedRule := "no rule" + matched := false + + defer func() { + if !matched && lc.Restricted { + ctrld.Log(ctx, proxyLog.Info(), "query refused, %s does not match any network policy", addr.String()) + return + } + ctrld.Log(ctx, proxyLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams) + }() + + if lc.Policy == nil { + return upstreams, false + } + + do := func(policyUpstreams []string) { + upstreams = append([]string(nil), policyUpstreams...) + } + + for _, rule := range lc.Policy.Rules { + // There's only one entry per rule, config validation ensures this. + for source, targets := range rule { + if source == domain || wildcardMatches(source, domain) { + matchedPolicy = lc.Policy.Name + matchedRule = source + do(targets) + matched = true + return upstreams, matched + } + } + } + + var sourceIP net.IP + switch addr := addr.(type) { + case *net.UDPAddr: + sourceIP = addr.IP + case *net.TCPAddr: + sourceIP = addr.IP + } + for _, rule := range lc.Policy.Networks { + for source, targets := range rule { + networkNum := strings.TrimPrefix(source, "network.") + nc := p.cfg.Network[networkNum] + if nc == nil { + continue + } + + for _, ipNet := range nc.IPNets { + if ipNet.Contains(sourceIP) { + matchedPolicy = lc.Policy.Name + matchedNetwork = source + do(targets) + matched = true + return upstreams, matched + } + } + } + } + + return upstreams, matched +} + +func (p *prog) proxy(ctx context.Context, upstreams []string, msg *dns.Msg) *dns.Msg { + upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams) + resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg { + ctrld.Log(ctx, proxyLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name) + dnsResolver, err := ctrld.NewResolver(upstreamConfig) + if err != nil { + ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to create resolver") + return nil + } + if upstreamConfig.Timeout > 0 { + timeoutCtx, cancel := context.WithTimeout(ctx, time.Millisecond*time.Duration(upstreamConfig.Timeout)) + defer cancel() + ctx = timeoutCtx + } + answer, err := dnsResolver.Resolve(ctx, msg) + if err != nil { + ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to resolve query") + return nil + } + return answer + } + for n, upstreamConfig := range upstreamConfigs { + if answer := resolve(n, upstreamConfig, msg); answer != nil { + return answer + } + } + ctrld.Log(ctx, proxyLog.Error(), "all upstreams failed") + answer := new(dns.Msg) + answer.SetRcode(msg, dns.RcodeServerFailure) + return answer +} + +// canonicalName returns canonical name from FQDN with "." trimmed. +func canonicalName(fqdn string) string { + q := strings.TrimSpace(fqdn) + q = strings.TrimSuffix(q, ".") + // https://datatracker.ietf.org/doc/html/rfc4343 + q = strings.ToLower(q) + + return q +} + +func wildcardMatches(wildcard, domain string) bool { + // Wildcard match. + wildCardParts := strings.Split(wildcard, "*") + if len(wildCardParts) != 2 { + return false + } + + switch { + case len(wildCardParts[0]) > 0 && len(wildCardParts[1]) > 0: + // Domain must match both prefix and suffix. + return strings.HasPrefix(domain, wildCardParts[0]) && strings.HasSuffix(domain, wildCardParts[1]) + + case len(wildCardParts[1]) > 0: + // Only suffix must match. + return strings.HasSuffix(domain, wildCardParts[1]) + + case len(wildCardParts[0]) > 0: + // Only prefix must match. + return strings.HasPrefix(domain, wildCardParts[0]) + } + + return false +} + +func fmtRemoteToLocal(listenerNum, remote, local string) string { + return fmt.Sprintf("%s -> listener.%s: %s:", remote, listenerNum, local) +} + +func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig { + upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams)) + for _, upstream := range upstreams { + upstreamNum := strings.TrimPrefix(upstream, "upstream.") + upstreamConfigs = append(upstreamConfigs, p.cfg.Upstream[upstreamNum]) + } + if len(upstreamConfigs) == 0 { + upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} + } + return upstreamConfigs +} + +func requestID() string { + b := make([]byte, 3) // 6 chars + if _, err := rand.Read(b); err != nil { + panic(err) + } + return hex.EncodeToString(b) +} + +var osUpstreamConfig = &ctrld.UpstreamConfig{ + Name: "OS resolver", + Type: "os", +} diff --git a/cmd/ctrld/dns_proxy_test.go b/cmd/ctrld/dns_proxy_test.go new file mode 100644 index 0000000..8435f7d --- /dev/null +++ b/cmd/ctrld/dns_proxy_test.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/testhelper" +) + +func Test_wildcardMatches(t *testing.T) { + tests := []struct { + name string + wildcard string + domain string + match bool + }{ + {"prefix parent should not match", "*.windscribe.com", "windscribe.com", false}, + {"prefix", "*.windscribe.com", "anything.windscribe.com", true}, + {"prefix not match other domain", "*.windscribe.com", "example.com", false}, + {"prefix not match domain in name", "*.windscribe.com", "wwindscribe.com", false}, + {"suffix", "suffix.*", "suffix.windscribe.com", true}, + {"suffix not match other", "suffix.*", "suffix1.windscribe.com", false}, + {"both", "suffix.*.windscribe.com", "suffix.anything.windscribe.com", true}, + {"both not match", "suffix.*.windscribe.com", "suffix1.suffix.windscribe.com", false}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := wildcardMatches(tc.wildcard, tc.domain); got != tc.match { + t.Errorf("unexpected result, wildcard: %s, domain: %s, want: %v, got: %v", tc.wildcard, tc.domain, tc.match, got) + } + }) + } +} + +func Test_canonicalName(t *testing.T) { + tests := []struct { + name string + domain string + canonical string + }{ + {"fqdn to canonical", "windscribe.com.", "windscribe.com"}, + {"already canonical", "windscribe.com", "windscribe.com"}, + {"case insensitive", "Windscribe.Com.", "windscribe.com"}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := canonicalName(tc.domain); got != tc.canonical { + t.Errorf("unexpected result, want: %s, got: %s", tc.canonical, got) + } + }) + } +} + +func Test_prog_upstreamFor(t *testing.T) { + cfg := testhelper.SampleConfig(t) + prog := &prog{cfg: cfg} + for _, nc := range prog.cfg.Network { + for _, cidr := range nc.Cidrs { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + t.Fatal(err) + } + nc.IPNets = append(nc.IPNets, ipNet) + } + } + + tests := []struct { + name string + ip string + defaultUpstreamNum string + lc *ctrld.ListenerConfig + domain string + upstreams []string + matched bool + }{ + {"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true}, + {"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true}, + {"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true}, + {"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + for _, network := range []string{"udp", "tcp"} { + var ( + addr net.Addr + err error + ) + switch network { + case "udp": + addr, err = net.ResolveUDPAddr(network, tc.ip) + case "tcp": + addr, err = net.ResolveTCPAddr(network, tc.ip) + } + require.NoError(t, err) + require.NotNil(t, addr) + ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, requestID()) + upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain) + assert.Equal(t, tc.matched, matched) + assert.Equal(t, tc.upstreams, upstreams) + } + }) + } +} diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go new file mode 100644 index 0000000..7b539eb --- /dev/null +++ b/cmd/ctrld/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/rs/zerolog" + + "github.com/Control-D-Inc/ctrld" +) + +var ( + configPath string + daemon bool + cfg ctrld.Config + verbose bool + + bootstrapDNS = "76.76.2.0" + + rootLogger = zerolog.New(io.Discard) + mainLog = rootLogger + proxyLog = rootLogger +) + +func main() { + ctrld.InitConfig(v, "config") + initCLI() +} + +func initLogging() { + writers := []io.Writer{io.Discard} + isLog := cfg.Service.LogLevel != "" + if logPath := cfg.Service.LogPath; logPath != "" { + logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to creating log file: %v", err) + os.Exit(1) + } + isLog = true + writers = append(writers, logFile) + } + zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs + if verbose || isLog { + consoleWriter := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { + w.TimeFormat = time.StampMilli + }) + writers = append(writers, consoleWriter) + multi := zerolog.MultiLevelWriter(writers...) + mainLog = mainLog.Output(multi).With().Timestamp().Str("prefix", "main").Logger() + proxyLog = proxyLog.Output(multi).With().Timestamp().Logger() + // TODO: find a better way. + ctrld.ProxyLog = proxyLog + } + if cfg.Service.LogLevel == "" { + return + } + level, err := zerolog.ParseLevel(cfg.Service.LogLevel) + if err != nil { + mainLog.Warn().Err(err).Msg("could not set log level") + return + } + zerolog.SetGlobalLevel(level) +} diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go new file mode 100644 index 0000000..6d952c9 --- /dev/null +++ b/cmd/ctrld/os_linux.go @@ -0,0 +1,25 @@ +package main + +import ( + "os/exec" +) + +// allocate loopback ip +// sudo ip a add 127.0.0.2/24 dev lo +func allocateIP(ip string) error { + cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo") + if err := cmd.Run(); err != nil { + mainLog.Error().Err(err).Msg("allocateIP failed") + return err + } + return nil +} + +func deAllocateIP(ip string) error { + cmd := exec.Command("ip", "a", "del", ip+"/24", "dev", "lo") + if err := cmd.Run(); err != nil { + mainLog.Error().Err(err).Msg("deAllocateIP failed") + return err + } + return nil +} diff --git a/cmd/ctrld/os_mac.go b/cmd/ctrld/os_mac.go new file mode 100644 index 0000000..b635ea5 --- /dev/null +++ b/cmd/ctrld/os_mac.go @@ -0,0 +1,28 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "os/exec" +) + +// allocate loopback ip +// sudo ifconfig lo0 alias 127.0.0.2 up +func allocateIP(ip string) error { + cmd := exec.Command("ifconfig", "lo0", "alias", ip, "up") + if err := cmd.Run(); err != nil { + mainLog.Error().Err(err).Msg("allocateIP failed") + return err + } + return nil +} + +func deAllocateIP(ip string) error { + cmd := exec.Command("ifconfig", "lo0", "-alias", ip) + if err := cmd.Run(); err != nil { + mainLog.Error().Err(err).Msg("deAllocateIP failed") + return err + } + return nil +} diff --git a/cmd/ctrld/os_windows.go b/cmd/ctrld/os_windows.go new file mode 100644 index 0000000..e7c2c56 --- /dev/null +++ b/cmd/ctrld/os_windows.go @@ -0,0 +1,14 @@ +//go:build windows +// +build windows + +package main + +// TODO(cuonglm): implement. +func allocateIP(ip string) error { + return nil +} + +// TODO(cuonglm): implement. +func deAllocateIP(ip string) error { + return nil +} diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go new file mode 100644 index 0000000..a016ea5 --- /dev/null +++ b/cmd/ctrld/prog.go @@ -0,0 +1,159 @@ +package main + +import ( + "errors" + "net" + "os" + "strconv" + "sync" + "syscall" + + "github.com/kardianos/service" + "github.com/miekg/dns" + + "github.com/Control-D-Inc/ctrld" +) + +var errWindowsAddrInUse = syscall.Errno(0x2740) + +var svcConfig = &service.Config{ + Name: "ctrld", + DisplayName: "Control-D Helper Service", +} + +type prog struct { + cfg *ctrld.Config +} + +func (p *prog) Start(s service.Service) error { + p.cfg = &cfg + go p.run() + return nil +} + +func (p *prog) run() { + var wg sync.WaitGroup + wg.Add(len(p.cfg.Listener)) + + for _, nc := range p.cfg.Network { + for _, cidr := range nc.Cidrs { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + proxyLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr") + continue + } + nc.IPNets = append(nc.IPNets, ipNet) + } + } + for n := range p.cfg.Upstream { + uc := p.cfg.Upstream[n] + uc.Init() + + if uc.BootstrapIP == "" { + // resolve it manually and set the bootstrap ip + c := new(dns.Client) + m := new(dns.Msg) + m.SetQuestion(uc.Domain+".", dns.TypeA) + m.RecursionDesired = true + r, _, err := c.Exchange(m, net.JoinHostPort(bootstrapDNS, "53")) + if err != nil { + proxyLog.Error().Err(err).Msgf("could not resolve domain %s for upstream.%s", uc.Domain, n) + } else { + if r.Rcode != dns.RcodeSuccess { + proxyLog.Error().Msgf("could not resolve domain return code: %d, upstream.%s", r.Rcode, n) + } else { + for _, a := range r.Answer { + if ar, ok := a.(*dns.A); ok { + uc.BootstrapIP = ar.A.String() + proxyLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Setting bootstrap IP for upstream.%s", n) + } + } + } + } + } + } + + for listenerNum := range p.cfg.Listener { + go func(listenerNum string) { + defer wg.Done() + listenerConfig := p.cfg.Listener[listenerNum] + upstreamConfig := p.cfg.Upstream[listenerNum] + if upstreamConfig == nil { + proxyLog.Error().Msgf("missing upstream config for: [listener.%s]", listenerNum) + return + } + addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) + proxyLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr) + err := p.serveUDP(listenerNum) + if err != nil && !defaultConfigWritten { + proxyLog.Error().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) + return + } + + if opErr, ok := err.(*net.OpError); ok { + if sErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(sErr.Err, errWindowsAddrInUse) { + proxyLog.Warn().Msgf("Address %s already in used, pick a random one", addr) + pc, err := net.ListenPacket("udp", net.JoinHostPort(listenerConfig.IP, "0")) + if err != nil { + proxyLog.Error().Err(err).Msg("failed to listen packet") + return + } + _, portStr, _ := net.SplitHostPort(pc.LocalAddr().String()) + port, err := strconv.Atoi(portStr) + if err != nil { + proxyLog.Error().Err(err).Msg("malformed port") + return + } + listenerConfig.Port = port + v.Set("listener", map[string]*ctrld.ListenerConfig{ + "0": { + IP: "127.0.0.1", + Port: port, + }, + }) + writeConfigFile() + proxyLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, pc.LocalAddr()) + // There can be a race between closing the listener and start our own UDP server, but it's + // rare, and we only do this once, so let conservative here. + if err := pc.Close(); err != nil { + proxyLog.Error().Err(err).Msg("failed to close packet conn") + return + } + if err := p.serveUDP(listenerNum); err != nil { + proxyLog.Error().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) + return + } + } + } + }(listenerNum) + } + + wg.Wait() +} + +func (p *prog) Stop(s service.Service) error { + if err := p.deAllocateIP(); err != nil { + mainLog.Error().Err(err).Msg("de-allocate ip failed") + return err + } + return nil +} + +func (p *prog) allocateIP(ip string) error { + if !p.cfg.Service.AllocateIP { + return nil + } + return allocateIP(ip) +} + +func (p *prog) deAllocateIP() error { + if !p.cfg.Service.AllocateIP { + return nil + } + for _, lc := range p.cfg.Listener { + if err := deAllocateIP(lc.IP); err != nil { + return err + } + } + return nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..2ec92e8 --- /dev/null +++ b/config.go @@ -0,0 +1,140 @@ +package ctrld + +import ( + "net" + "net/url" + "strings" + + "github.com/go-playground/validator/v10" + "github.com/spf13/viper" +) + +// InitConfig initializes default config values for given *viper.Viper instance. +func InitConfig(v *viper.Viper, name string) { + v.SetConfigName(name) + v.SetConfigType("toml") + v.AddConfigPath("$HOME/.ctrld") + v.AddConfigPath(".") + + v.SetDefault("service", ServiceConfig{ + LogLevel: "info", + }) + v.SetDefault("listener", map[string]*ListenerConfig{ + "0": { + IP: "127.0.0.1", + Port: 53, + }, + }) + v.SetDefault("network", map[string]*NetworkConfig{ + "0": { + Name: "Network 0", + Cidrs: []string{"0.0.0.0/0"}, + }, + }) + v.SetDefault("upstream", map[string]*UpstreamConfig{ + "0": { + BootstrapIP: "76.76.2.11", + Name: "Control D - Anti-Malware", + Type: "doh", + Endpoint: "https://freedns.controld.com/p1", + Timeout: 5000, + }, + "1": { + BootstrapIP: "76.76.2.11", + Name: "Control D - No Ads", + Type: "doq", + Endpoint: "p2.freedns.controld.com", + Timeout: 3000, + }, + }) +} + +// Config represents ctrld supported configuration. +type Config struct { + Service ServiceConfig `mapstructure:"service"` + Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"` + Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"` + Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"` +} + +// ServiceConfig specifies the general ctrld config. +type ServiceConfig struct { + LogLevel string `mapstructure:"log_level" toml:"log_level"` + LogPath string `mapstructure:"log_path" toml:"log_path"` + Daemon bool `mapstructure:"-" toml:"-"` + AllocateIP bool `mapstructure:"-" toml:"-"` +} + +// NetworkConfig specifies configuration for networks where ctrld will handle requests. +type NetworkConfig struct { + Name string `mapstructure:"name" toml:"name"` + Cidrs []string `mapstructure:"cidrs" toml:"cidrs" validate:"dive,cidr"` + IPNets []*net.IPNet `mapstructure:"-" toml:"-"` +} + +// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to. +type UpstreamConfig struct { + Name string `mapstructure:"name" toml:"name"` + Type string `mapstructure:"type" toml:"type" validate:"oneof=doh doh3 dot doq os legacy"` + Endpoint string `mapstructure:"endpoint" toml:"endpoint" validate:"required_unless=Type os"` + BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip"` + Domain string `mapstructure:"-" toml:"-"` + Timeout int `mapstructure:"timeout" toml:"timeout" validate:"gte=0"` +} + +// ListenerConfig specifies the networks configuration that ctrld will run on. +type ListenerConfig struct { + IP string `mapstructure:"ip" toml:"ip" validate:"ip"` + Port int `mapstructure:"port" toml:"port" validate:"gt=0"` + Restricted bool `mapstructure:"restricted" toml:"restricted"` + Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy"` +} + +// ListenerPolicyConfig specifies the policy rules for ctrld to filter incoming requests. +type ListenerPolicyConfig struct { + Name string `mapstructure:"name" toml:"name"` + Networks []Rule `mapstructure:"networks" toml:"networks" validate:"dive,len=1"` + Rules []Rule `mapstructure:"rules" toml:"rules" validate:"dive,len=1"` +} + +// Rule is a map from source to list of upstreams. +// ctrld uses rule to perform requests matching and forward +// the request to corresponding upstreams if it's matched. +type Rule map[string][]string + +// Init initialized necessary values for an UpstreamConfig. +func (uc *UpstreamConfig) Init() { + if u, err := url.Parse(uc.Endpoint); err == nil { + uc.Domain = u.Host + } + if uc.Domain != "" { + return + } + + if !strings.Contains(uc.Endpoint, ":") { + uc.Domain = uc.Endpoint + uc.Endpoint = net.JoinHostPort(uc.Endpoint, defaultPortFor(uc.Type)) + } + host, _, _ := net.SplitHostPort(uc.Endpoint) + uc.Domain = host + if net.ParseIP(uc.Domain) != nil { + uc.BootstrapIP = uc.Domain + } +} + +// ValidateConfig validates the given config. +func ValidateConfig(validate *validator.Validate, cfg *Config) error { + return validate.Struct(cfg) +} + +func defaultPortFor(typ string) string { + switch typ { + case resolverTypeDOH, resolverTypeDOH3: + return "443" + case resolverTypeDOQ, resolverTypeDOT: + return "853" + case resolverTypeLegacy: + return "53" + } + return "53" +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..ef038c3 --- /dev/null +++ b/config_test.go @@ -0,0 +1,269 @@ +package ctrld_test + +import ( + "testing" + + "github.com/go-playground/validator/v10" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/testhelper" +) + +func TestLoadConfig(t *testing.T) { + cfg := testhelper.SampleConfig(t) + validate := validator.New() + require.NoError(t, ctrld.ValidateConfig(validate, cfg)) + + assert.Equal(t, "info", cfg.Service.LogLevel) + assert.Equal(t, "/path/to/log.log", cfg.Service.LogPath) + + assert.Len(t, cfg.Network, 2) + assert.Contains(t, cfg.Network, "0") + assert.Contains(t, cfg.Network, "1") + + assert.Len(t, cfg.Upstream, 3) + assert.Contains(t, cfg.Upstream, "0") + assert.Contains(t, cfg.Upstream, "1") + assert.Contains(t, cfg.Upstream, "2") + + assert.Len(t, cfg.Listener, 2) + assert.Contains(t, cfg.Listener, "0") + assert.Contains(t, cfg.Listener, "1") + + require.NotNil(t, cfg.Listener["0"].Policy) + assert.Equal(t, "My Policy", cfg.Listener["0"].Policy.Name) + require.NotNil(t, cfg.Listener["0"].Policy.Networks) + assert.Len(t, cfg.Listener["0"].Policy.Networks, 3) + + require.NotNil(t, cfg.Listener["0"].Policy.Rules) + assert.Len(t, cfg.Listener["0"].Policy.Rules, 2) + assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru") + assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host") +} + +func TestLoadDefaultConfig(t *testing.T) { + cfg := defaultConfig(t) + validate := validator.New() + require.NoError(t, ctrld.ValidateConfig(validate, cfg)) + assert.Len(t, cfg.Listener, 1) + assert.Len(t, cfg.Upstream, 2) +} + +func TestConfigValidation(t *testing.T) { + tests := []struct { + name string + cfg *ctrld.Config + wantErr bool + }{ + {"invalid Config", &ctrld.Config{}, true}, + {"default Config", defaultConfig(t), false}, + {"sample Config", testhelper.SampleConfig(t), false}, + {"invalid cidr", invalidNetworkConfig(t), true}, + {"invalid upstream type", invalidUpstreamType(t), true}, + {"invalid upstream timeout", invalidUpstreamTimeout(t), true}, + {"invalid upstream missing endpoint", invalidUpstreamMissingEndpoind(t), true}, + {"invalid listener ip", invalidListenerIP(t), true}, + {"invalid listener port", invalidListenerPort(t), true}, + {"os upstream", configWithOsUpstream(t), false}, + {"invalid rules", configWithInvalidRules(t), true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + validate := validator.New() + err := ctrld.ValidateConfig(validate, tc.cfg) + if tc.wantErr && err == nil { + t.Fatalf("expected error, but got nil: %+v", tc.cfg) + } + if !tc.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err != nil { + t.Logf("%v", err) + } + }) + } +} + +func defaultConfig(t *testing.T) *ctrld.Config { + v := viper.New() + ctrld.InitConfig(v, "test_load_default_config") + _, ok := v.ReadInConfig().(viper.ConfigFileNotFoundError) + require.True(t, ok) + + var cfg ctrld.Config + require.NoError(t, v.Unmarshal(&cfg)) + return &cfg +} + +func invalidNetworkConfig(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Network["0"].Cidrs = []string{"172.16.256.255/16", "2001:cdba:0000:0000:0000:0000:3257:9652/256"} + return cfg +} + +func invalidUpstreamType(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["0"].Type = "DOH" + return cfg +} + +func invalidUpstreamTimeout(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["0"].Timeout = -1 + return cfg +} + +func invalidUpstreamMissingEndpoind(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["0"].Endpoint = "" + return cfg +} + +func invalidListenerIP(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Listener["0"].IP = "invalid ip" + return cfg +} + +func invalidListenerPort(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Listener["0"].Port = 0 + return cfg +} + +func configWithOsUpstream(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["os"] = &ctrld.UpstreamConfig{ + Name: "OS", + Type: "os", + Endpoint: "", + } + return cfg +} + +func configWithInvalidRules(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Listener["0"].Policy = &ctrld.ListenerPolicyConfig{ + Name: "Invalid Policy", + Networks: []ctrld.Rule{{"*.com": []string{"upstream.1"}, "*.net": []string{"upstream.0"}}}, + Rules: nil, + } + return cfg +} + +func TestUpstreamConfig_Init(t *testing.T) { + tests := []struct { + name string + uc *ctrld.UpstreamConfig + expected *ctrld.UpstreamConfig + }{ + { + "doh+doh3", + &ctrld.UpstreamConfig{ + Name: "doh", + Type: "doh", + Endpoint: "https://example.com", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &ctrld.UpstreamConfig{ + Name: "doh", + Type: "doh", + Endpoint: "https://example.com", + BootstrapIP: "", + Domain: "example.com", + Timeout: 0, + }, + }, + { + "dot+doq", + &ctrld.UpstreamConfig{ + Name: "dot", + Type: "dot", + Endpoint: "freedns.controld.com:8853", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &ctrld.UpstreamConfig{ + Name: "dot", + Type: "dot", + Endpoint: "freedns.controld.com:8853", + BootstrapIP: "", + Domain: "freedns.controld.com", + Timeout: 0, + }, + }, + { + "dot+doq without port", + &ctrld.UpstreamConfig{ + Name: "dot", + Type: "dot", + Endpoint: "freedns.controld.com", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &ctrld.UpstreamConfig{ + Name: "dot", + Type: "dot", + Endpoint: "freedns.controld.com:853", + BootstrapIP: "", + Domain: "freedns.controld.com", + Timeout: 0, + }, + }, + { + "legacy", + &ctrld.UpstreamConfig{ + Name: "legacy", + Type: "legacy", + Endpoint: "1.2.3.4:53", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &ctrld.UpstreamConfig{ + Name: "legacy", + Type: "legacy", + Endpoint: "1.2.3.4:53", + BootstrapIP: "1.2.3.4", + Domain: "1.2.3.4", + Timeout: 0, + }, + }, + { + "legacy without port", + &ctrld.UpstreamConfig{ + Name: "legacy", + Type: "legacy", + Endpoint: "1.2.3.4", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &ctrld.UpstreamConfig{ + Name: "legacy", + Type: "legacy", + Endpoint: "1.2.3.4:53", + BootstrapIP: "1.2.3.4", + Domain: "1.2.3.4", + Timeout: 0, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.uc.Init() + assert.Equal(t, tc.expected, tc.uc) + }) + } +} diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..3b24144 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,303 @@ +# Configuration File +The config file allows for advanced configuration of the `ctrld` utility to cover a vast array of use cases. +1. Source IP based DNS routing policies +2. Destination IP based DNS routing policies +3. Split horizon DNS + +- [Config Location](#config-location) +- [Example Config](#example-config) + - [Service](#service) - general configurations + - [Upstreams](#upstream) - where to send DNS queries + - [Networks](#network) - where did the DNS queries come from + - [Listeners](#listener) - what receives DNS queries and defines policies + - [Policies](#policy) - what receives DNS queries and defines policies + + +## Config Location +`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `config.toml` found in following order: + + - `$HOME/.ctrld` + - Current directory + +The user can choose to override default value using command line `--config` or `-c`: + +```shell +ctrld run --config /path/to/myconfig.toml +``` + +If no configuration files found, a default `config.toml` file will be created in the current directory. + +# Example Config + +```toml +[service] + log_level = "info" + log_path = "" + +[network.0] + cidrs = ["0.0.0.0/0"] + name = "Everyone" + +[network.1] + cidrs = ["10.10.10.0/24"] + name = "Admins" + +[upstream.0] + bootstrap_ip = "76.76.2.11" + endpoint = "https://freedns.controld.com/p1" + name = "Control D - Anti-Malware" + timeout = 5000 + type = "doh" + +[upstream.1] + bootstrap_ip = "76.76.2.11" + endpoint = "p2.freedns.controld.com" + name = "Control D - No Ads" + timeout = 5000 + type = "doq" + +[upstream.2] + bootstrap_ip = "76.76.2.22" + endpoint = "private.dns.controld.com" + name = "Control D - Private" + timeout = 5000 + type = "dot" + +[listener.0] + ip = "127.0.0.1" + port = 53 + +[listener.0.policy] + name = "My Policy" + networks = [ + {"network.0" = ["upstream.1"]}, + ] + rules = [ + {"*.local" = ["upstream.1"]}, + {"test.com" = ["upstream.2", "upstream.1"]}, + ] + +[listener.1] + ip = "127.0.0.69" + port = 53 + restricted = true +``` + +See below for details on each configuration block. + +## Service +The `[service]` section controls general behaviors. + +```toml +[service] + log_level = "debug" + log_path = "log.txt" +``` + +### log_level +Logging level you wish to enable. + + - Type: string + - Required: no + - Valid values: `debug`, `info`, `warn`, `error`, `fatal`, `panic` + - Default: `info` + + +### log_path +Relative or absolute path of the log file. + +- Type: string +- Required: no + +The above config will look like this at query time. + +``` +2022-11-14T22:18:53.808 INF Setting bootstrap IP for upstream.0 bootstrap_ip=76.76.2.11 +2022-11-14T22:18:53.808 INF Starting DNS server on listener.0: 127.0.0.1:53 +2022-11-14T22:18:56.381 DBG [9fd5d3] 127.0.0.1:53978 -> listener.0: 127.0.0.1:53: received query: verify.controld.com +2022-11-14T22:18:56.381 INF [9fd5d3] no policy, no network, no rule -> [upstream.0] +2022-11-14T22:18:56.381 DBG [9fd5d3] sending query to upstream.0: Control D - DOH Free +2022-11-14T22:18:56.381 DBG [9fd5d3] debug dial context freedns.controld.com:443 - tcp - 76.76.2.0 +2022-11-14T22:18:56.381 DBG [9fd5d3] sending doh request to: 76.76.2.11:443 +2022-11-14T22:18:56.420 DBG [9fd5d3] received response of 118 bytes in 39.662597ms +``` + +## Upstream +The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. + +```toml +[upstream.0] + bootstrap_ip = "" + endpoint = "https://freedns.controld.com/p1" + name = "Control D - DOH" + timeout = 5000 + type = "doh" + +[upstream.1] + bootstrap_ip = "" + endpoint = "https://freedns.controld.com/p1" + name = "Control D - DOH3" + timeout = 5000 + type = "doh3" + +[upstream.2] + bootstrap_ip = "" + endpoint = "p1.freedns.controld.com" + name = "Controld D - DOT" + timeout = 5000 + type = "dot" + +[upstream.3] + bootstrap_ip = "" + endpoint = "p1.freedns.controld.com" + name = "Controld D - DOT" + timeout = 5000 + type = "doq" + +[upstream.4] + bootstrap_ip = "" + endpoint = "76.76.2.2" + name = "Control D - Ad Blocking" + timeout = 5000 + type = "legacy" +``` + +### bootstrap_ip +IP address of upstream DNS server when hostname or URL is used. This exists to prevent the bootstrapping cycle problem. +For example, if the `Endpoint` is set to `https://freedns.controld.com/p1`, `ctrld` needs to know the ip address of `freedns.controld.com` to be able to do communication. To do that, `ctrld` may need to use OS resolver, which may or may not be set. + +If `bootstrap_ip` is empty, `ctrld` will resolve this itself using its own bootstrap DNS, normal users should not care about `bootstrap_ip` and just leave it empty. + + - type: ip address string + - required: no + +### endpoint +IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint. + + - Type: string + - Required: yes + + Default ports are implied for each protocol, but can be overriden. ie. `p1.freedns.controld.com:1024` + +### name +Human-readable name of the upstream. + +- Type: string +- Required: no + +### timeout +Timeout in milliseconds before request failsover to the next upstream (if defined). + +Value `0` means no timeout. + + - Type: number + - required: no + +### type +The protocol that `ctrld` will use to send DNS requests to upstream. + + - Type: string + - required: yes + - Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`, `os` + +## Network +The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs. + +```toml +[network.0] + cidrs = ["0.0.0.0/0"] + name = "Any Network" + +[network.1] + cidrs = ["192.168.1.0/24"] + name = "Home Wifi " +``` + +### name +Name of the network. + + - Type: string + - Required: no + +### cidrs +Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section. + + - Type: array of network CIDR string + - Required: no + + +## listener +The `[listener]` section specifies the ip and port of the local DNS server. You can have multiple listeners, and attached policies. + +```toml +[listener.0] + ip = "127.0.0.1" + port = 53 + +[listener.1] + ip = "10.10.10.1" + port = 53 + restricted = true +``` + +### ip +IP address that serves the incoming requests. + +- Type: string +- Required: yes + +### port +Port number that the listener will listen on for incoming requests. + +- Type: number +- Required: yes + +### restricted +If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`. + +- Type: bool +- Required: no + +### policy +Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to. +If no `policy` is defined or the requests do not match any policy rules, it will be forwarded to corresponding upstream of the listener. For example, the request to `listener.0` will be forwarded to `upstream.0`. + +The policy `rule` syntax is a simple `toml` inline table with exactly one key/value pair per rule. `key` is either the `network` or a domain. Value is the list of the upstreams. For example: + +```toml +[listener.0.policy] +name = "My Policy" + +networks = [ + {"network.0" = ["upstream.1"]}, +] + +rules = [ + {"*.local" = ["upstream.1"]}, + {"test.com" = ["upstream.2", "upstream.1"]}, +] +``` + +Above policy will: +- Forward requests on `listener.0` from `network.0` to `upstream.1`. +- Forward requests on `listener.0` for `.local` suffixed domains to `upstream.1`. +- Forward requests on `listener.0` for `test.com` to `upstream.2`. If timeout is reached, retry on `upstream.1`. +- All other requests on `listener.0` that do not match above conditions will be forwarded to `upstream.0`. + +#### name +`name` is the name for the policy. + +- Type: string +- Required: no + +### networks: +`networks` is the list of network rules of the policy. + +- type: array of networks + +### rules: +`rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain. + +- type: array of rule + +[toml_link]: https://toml.io/en diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..bd077df --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,5 @@ +# Contribution Guideline + +Pull requests are welcome! + +Please filling [an issue](https://github.com/Control-D-Inc/ctrld/issues/new/choose) before submitting the PR. \ No newline at end of file diff --git a/doh.go b/doh.go new file mode 100644 index 0000000..f3e3810 --- /dev/null +++ b/doh.go @@ -0,0 +1,99 @@ +package ctrld + +import ( + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "net" + "net/http" + "time" + + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" + "github.com/miekg/dns" +) + +func newDohResolver(uc *UpstreamConfig) *dohResolver { + http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + dialer := &net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 10 * time.Second, + } + Log(ctx, ProxyLog.Debug(), "debug dial context %s - %s - %s", addr, network, bootstrapDNS) + // if we have a bootstrap ip set, use it to avoid DNS lookup + if uc.BootstrapIP != "" && addr == fmt.Sprintf("%s:443", uc.Domain) { + addr = fmt.Sprintf("%s:443", uc.BootstrapIP) + Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr) + } + return dialer.DialContext(ctx, network, addr) + } + r := &dohResolver{endpoint: uc.Endpoint, isDoH3: uc.Type == resolverTypeDOH3} + if r.isDoH3 { + r.doh3DialFunc = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + host := addr + Log(ctx, ProxyLog.Debug(), "debug dial context D0H3 %s - %s", addr, bootstrapDNS) + // if we have a bootstrap ip set, use it to avoid DNS lookup + if uc.BootstrapIP != "" && addr == fmt.Sprintf("%s:443", uc.Domain) { + addr = fmt.Sprintf("%s:443", uc.BootstrapIP) + Log(ctx, ProxyLog.Debug(), "sending doh3 request to: %s", addr) + } + remoteAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + return nil, err + } + return quic.DialEarlyContext(ctx, udpConn, remoteAddr, host, tlsCfg, cfg) + } + } + return r +} + +type dohResolver struct { + endpoint string + isDoH3 bool + doh3DialFunc func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) +} + +func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + data, err := msg.Pack() + if err != nil { + return nil, err + } + enc := base64.RawURLEncoding.EncodeToString(data) + url := fmt.Sprintf("%s?dns=%s", r.endpoint, enc) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("could not create request: %w", err) + } + req.Header.Set("Content-Type", "application/dns-message") + req.Header.Set("Accept", "application/dns-message") + + c := http.Client{} + if r.isDoH3 { + c.Transport = &http3.RoundTripper{} + c.Transport.(*http3.RoundTripper).Dial = r.doh3DialFunc + defer c.Transport.(*http3.RoundTripper).Close() + } + resp, err := c.Do(req) + if err != nil { + return nil, fmt.Errorf("could not perform request: %w", err) + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("could not read message from response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("wrong response from DOH server, got: %s, status: %d", string(buf), resp.StatusCode) + } + + answer := new(dns.Msg) + return answer, answer.Unpack(buf) +} diff --git a/doq.go b/doq.go new file mode 100644 index 0000000..4ac3f43 --- /dev/null +++ b/doq.go @@ -0,0 +1,75 @@ +package ctrld + +import ( + "context" + "crypto/tls" + "io" + "net" + "time" + + "github.com/lucas-clemente/quic-go" + "github.com/miekg/dns" +) + +type doqResolver struct { + uc *UpstreamConfig +} + +func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + endpoint := r.uc.Endpoint + tlsConfig := &tls.Config{NextProtos: []string{"doq"}} + if r.uc.BootstrapIP != "" { + tlsConfig.ServerName = r.uc.Domain + _, port, _ := net.SplitHostPort(endpoint) + endpoint = net.JoinHostPort(r.uc.BootstrapIP, port) + } + return resolve(ctx, msg, endpoint, tlsConfig) +} + +func resolve(ctx context.Context, msg *dns.Msg, endpoint string, tlsConfig *tls.Config) (*dns.Msg, error) { + session, err := quic.DialAddr(endpoint, tlsConfig, nil) + if err != nil { + return nil, err + } + defer session.CloseWithError(quic.ApplicationErrorCode(quic.NoError), "") + + msgBytes, err := msg.Pack() + if err != nil { + return nil, err + } + + stream, err := session.OpenStream() + if err != nil { + return nil, err + } + + deadline, ok := ctx.Deadline() + if !ok { + deadline = time.Now().Add(5 * time.Second) + } + _ = stream.SetDeadline(deadline) + + var msgLen = uint16(len(msgBytes)) + var msgLenBytes = []byte{byte(msgLen >> 8), byte(msgLen & 0xFF)} + if _, err := stream.Write(msgLenBytes); err != nil { + return nil, err + } + + if _, err := stream.Write(msgBytes); err != nil { + return nil, err + } + + buf, err := io.ReadAll(stream) + if err != nil { + return nil, err + } + + _ = stream.Close() + + answer := new(dns.Msg) + if err := answer.Unpack(buf[2:]); err != nil { + return nil, err + } + answer.SetReply(msg) + return answer, nil +} diff --git a/dot.go b/dot.go new file mode 100644 index 0000000..4107467 --- /dev/null +++ b/dot.go @@ -0,0 +1,35 @@ +package ctrld + +import ( + "context" + "crypto/tls" + "net" + + "github.com/miekg/dns" +) + +type dotResolver struct { + uc *UpstreamConfig +} + +func (r *dotResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + // The dialer is used to prevent bootstrapping cycle. + // If r.endpoing is set to dns.controld.dev, we need to resolve + // dns.controld.dev first. By using a dialer with custom resolver, + // we ensure that we can always resolve the bootstrap domain + // regardless of the machine DNS status. + dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53")) + dnsClient := &dns.Client{ + Net: "tcp-tls", + Dialer: dialer, + } + endpoint := r.uc.Endpoint + if r.uc.BootstrapIP != "" { + dnsClient.TLSConfig = &tls.Config{ServerName: r.uc.Domain} + _, port, _ := net.SplitHostPort(endpoint) + endpoint = net.JoinHostPort(r.uc.BootstrapIP, port) + } + + answer, _, err := dnsClient.ExchangeContext(ctx, msg, endpoint) + return answer, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..67ec79a --- /dev/null +++ b/go.mod @@ -0,0 +1,53 @@ +module github.com/Control-D-Inc/ctrld + +go 1.19 + +require ( + github.com/go-playground/validator/v10 v10.11.1 + github.com/kardianos/service v1.2.1 + github.com/lucas-clemente/quic-go v0.29.1 + github.com/miekg/dns v1.1.50 + github.com/pelletier/go-toml v1.9.5 + github.com/rs/zerolog v1.28.0 + github.com/spf13/cobra v1.1.1 + github.com/spf13/viper v1.7.0 + github.com/stretchr/testify v1.7.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.1 // indirect + github.com/marten-seemann/qpack v0.2.1 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect + golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect + golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.12 // indirect + gopkg.in/ini.v1 v1.51.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d971225 --- /dev/null +++ b/go.sum @@ -0,0 +1,457 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk= +github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0= +github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= +github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= +github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= +golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/log.go b/log.go new file mode 100644 index 0000000..a4689b3 --- /dev/null +++ b/log.go @@ -0,0 +1,29 @@ +package ctrld + +import ( + "context" + "fmt" + "io" + + "github.com/rs/zerolog" +) + +// ProxyLog emits the log record for proxy operations. +// The caller should set it only once. +var ProxyLog = zerolog.New(io.Discard) + +// ReqIdCtxKey is the context.Context key for a request id. +type ReqIdCtxKey struct{} + +// Log emits the logs for a particular zerolog event. +// The request id associated with the context will be included if presents. +func Log(ctx context.Context, e *zerolog.Event, format string, v ...any) { + id, ok := ctx.Value(ReqIdCtxKey{}).(string) + if !ok { + e.Msgf(format, v...) + return + } + e.MsgFunc(func() string { + return fmt.Sprintf("[%s] %s", id, fmt.Sprintf(format, v...)) + }) +} diff --git a/resolver.go b/resolver.go new file mode 100644 index 0000000..ac065ce --- /dev/null +++ b/resolver.go @@ -0,0 +1,112 @@ +package ctrld + +import ( + "context" + "errors" + "fmt" + "net" + "strings" + + "github.com/miekg/dns" +) + +const ( + resolverTypeDOH = "doh" + resolverTypeDOH3 = "doh3" + resolverTypeDOT = "dot" + resolverTypeDOQ = "doq" + resolverTypeOS = "os" + resolverTypeLegacy = "legacy" +) + +var bootstrapDNS = "76.76.2.0" + +// Resolver is the interface that wraps the basic DNS operations. +// +// Resolve resolves the DNS query, return the result and the corresponding error. +type Resolver interface { + Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) +} + +var errUnknownResolver = errors.New("unknown resolver") + +// NewResolver creates a Resolver based on the given upstream config. +func NewResolver(uc *UpstreamConfig) (Resolver, error) { + typ, endpoint := uc.Type, uc.Endpoint + switch typ { + case resolverTypeDOH, resolverTypeDOH3: + return newDohResolver(uc), nil + case resolverTypeDOT: + return &dotResolver{uc: uc}, nil + case resolverTypeDOQ: + return &doqResolver{uc: uc}, nil + case resolverTypeOS: + return &osResolver{}, nil + case resolverTypeLegacy: + return &legacyResolver{endpoint: endpoint}, nil + } + return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ) +} + +type osResolver struct{} + +func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + domain := canonicalName(msg.Question[0].Name) + addrs, err := net.DefaultResolver.LookupHost(ctx, domain) + if err != nil { + return nil, err + } + if len(addrs) == 0 { + return nil, errors.New("no answer") + } + answer := new(dns.Msg) + answer.SetReply(msg) + ip := net.ParseIP(addrs[0]) + a := &dns.A{ + A: ip, + Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, + } + if ip.To4() != nil { + a.Hdr.Rrtype = dns.TypeA + } + + msg.Answer = append(msg.Answer, a) + return msg, nil +} + +func newDialer(dnsAddress string) *net.Dialer { + return &net.Dialer{ + Resolver: &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{} + return d.DialContext(ctx, network, dnsAddress) + }, + }, + } +} + +type legacyResolver struct { + endpoint string +} + +func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + // See comment in (*dotResolver).resolve method. + dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53")) + dnsClient := &dns.Client{ + Net: "udp", + Dialer: dialer, + } + answer, _, err := dnsClient.ExchangeContext(ctx, msg, r.endpoint) + return answer, err +} + +// canonicalName returns canonical name from FQDN with "." trimmed. +func canonicalName(fqdn string) string { + q := strings.TrimSpace(fqdn) + q = strings.TrimSuffix(q, ".") + // https://datatracker.ietf.org/doc/html/rfc4343 + q = strings.ToLower(q) + + return q +} diff --git a/testhelper/config.go b/testhelper/config.go new file mode 100644 index 0000000..fe548fc --- /dev/null +++ b/testhelper/config.go @@ -0,0 +1,72 @@ +package testhelper + +import ( + "strings" + "testing" + + "github.com/Control-D-Inc/ctrld" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func SampleConfig(t *testing.T) *ctrld.Config { + v := viper.NewWithOptions(viper.KeyDelimiter("::")) + ctrld.InitConfig(v, "test_load_config") + require.NoError(t, v.ReadConfig(strings.NewReader(sampleConfigContent))) + var cfg ctrld.Config + require.NoError(t, v.Unmarshal(&cfg)) + return &cfg +} + +var sampleConfigContent = ` +[service] +log_level = "info" +log_path = "/path/to/log.log" + +[network.0] +name = "Home Wifi" +cidrs = ["192.168.0.0/24"] + +[network.1] +name = "Kids Wifi" +cidrs = ["192.168.1.0/24"] + +[upstream.0] +name = "Control D - Standard Devices" +type = "doh" +endpoint = "https://dns.controld.com/12345abcd/main-device" +timeout = 5 + +[upstream.1] +name = "Control D - Kids Devices" +type = "dot" +endpoint = "12345abcd-kids-devices.dns.controld.com" +timeout = 5 + +[upstream.2] +name = "Google" +type = "legacy" +endpoint = "8.8.8.8" +timeout = 5 + +[listener.0] +ip = "127.0.0.1" +port = 53 + +[listener.1] +ip = "10.10.42.69" +port = 1337 + +[listener.0.policy] +name = "My Policy" +networks = [ + {"network.0" = ["upstream.1", "upstream.0"]}, + {"network.1" = ["upstream.0"]}, + {"network.2" = ["upstream.1"]}, +] + +rules = [ + {"*.ru" = ["upstream.1"]}, + {"*.local.host" = ["upstream.2", "upstream.0"]}, +] +`