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"]}, +] +`