├── www ├── static │ ├── doggo.png │ └── help.png └── api │ └── api.md ├── Dockerfile-cli ├── Dockerfile-api ├── config-api-sample.toml ├── pkg ├── config │ ├── config.go │ ├── config_unix.go │ └── config_windows.go ├── utils │ └── logger.go ├── models │ └── models.go └── resolvers │ ├── dnscrypt.go │ ├── classic.go │ ├── utils.go │ ├── doh.go │ ├── resolver.go │ └── doq.go ├── .gitignore ├── completions ├── doggo.zsh └── doggo.fish ├── Makefile ├── internal └── app │ ├── app.go │ ├── questions.go │ ├── output.go │ └── nameservers.go ├── .github └── workflows │ └── release.yml ├── cmd ├── doggo │ ├── parse.go │ ├── help.go │ └── cli.go └── api │ ├── config.go │ ├── api.go │ ├── assets │ ├── main.js │ └── style.css │ ├── index.html │ └── handlers.go ├── go.mod ├── TODO.md ├── .goreleaser.yml ├── README.md ├── LICENSE └── go.sum /www/static/doggo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Windscribe/doggo/HEAD/www/static/doggo.png -------------------------------------------------------------------------------- /www/static/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Windscribe/doggo/HEAD/www/static/help.png -------------------------------------------------------------------------------- /Dockerfile-cli: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | ARG ARCH 3 | FROM ${ARCH}/alpine 4 | COPY doggo /usr/bin/doggo 5 | ENTRYPOINT ["/usr/bin/doggo"] -------------------------------------------------------------------------------- /Dockerfile-api: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | ARG ARCH 3 | FROM ${ARCH}/alpine 4 | WORKDIR /app 5 | COPY doggo-api.bin . 6 | COPY config-api-sample.toml config.toml 7 | CMD ["./doggo-api.bin"] 8 | -------------------------------------------------------------------------------- /config-api-sample.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | address = ":8080" 3 | name = "doggo-api" 4 | # WARNING If these timeouts are less than 1s, 5 | # the server connection breaks. 6 | read_timeout=7000 7 | write_timeout=7000 8 | keepalive_timeout=5000 9 | max_body_size=10000 -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "net" 4 | 5 | // the whole `FEC0::/10` prefix is deprecated. 6 | // [RFC 3879]: https://tools.ietf.org/html/rfc3879 7 | func isUnicastLinkLocal(ip net.IP) bool { 8 | return len(ip) == net.IPv6len && ip[0] == 0xfe && ip[1] == 0xc0 9 | } 10 | -------------------------------------------------------------------------------- /www/api/api.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ``` 4 | curl --request POST \ 5 | --url http://localhost:8080/lookup/ \ 6 | --header 'Content-Type: application/json' \ 7 | --data '{ 8 | "query": ["mrkaran.dev"], 9 | "type": ["A"], 10 | "class": ["IN"], 11 | "nameservers": ["9.9.9.9"] 12 | }' 13 | ``` 14 | -------------------------------------------------------------------------------- /pkg/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | // InitLogger initializes logger. 6 | func InitLogger() *logrus.Logger { 7 | logger := logrus.New() 8 | logger.SetFormatter(&logrus.TextFormatter{ 9 | FullTimestamp: true, 10 | DisableLevelTruncation: true, 11 | }) 12 | return logger 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | dist/ 8 | .idea/ 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | vendor/ 18 | 19 | # App Specific 20 | .env 21 | config.yml 22 | .DS_Store 23 | bin/* 24 | node_modules -------------------------------------------------------------------------------- /pkg/config/config_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package config 4 | 5 | import ( 6 | "net" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // DefaultResolvConfPath specifies path to default resolv config file on UNIX. 12 | const DefaultResolvConfPath = "/etc/resolv.conf" 13 | 14 | // GetDefaultServers get system default nameserver 15 | func GetDefaultServers() ([]string, int, []string, error) { 16 | // if no nameserver is provided, take it from `resolv.conf` 17 | cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath) 18 | if err != nil { 19 | return nil, 0, nil, err 20 | } 21 | servers := make([]string, 0) 22 | for _, server := range cfg.Servers { 23 | ip := net.ParseIP(server) 24 | if isUnicastLinkLocal(ip) { 25 | continue 26 | } 27 | servers = append(servers, server) 28 | } 29 | return servers, cfg.Ndots, cfg.Search, nil 30 | } 31 | -------------------------------------------------------------------------------- /completions/doggo.zsh: -------------------------------------------------------------------------------- 1 | #compdef doggo 2 | 3 | __doggo() { 4 | _arguments \ 5 | "(- 1 *)"{-v,--version}"[Show version of doggo]" \ 6 | "(- 1 *)"{-\?,--help}"[Show list of command-line options]" \ 7 | {-q,--query}"[Hostname to query the DNS records for]::_hosts" \ 8 | {-t,--type}"[Type of the DNS Record]:(record type):(A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT)" \ 9 | {-n,--nameserver}"[Address of a specific nameserver to send queries to]::_hosts;" \ 10 | {-c,--class}"[Network class of the DNS record being queried]:(network class):(IN CH HS)" \ 11 | {-J,--json}"[Format the output as JSON]" \ 12 | {--color}"[Defaults to true. Set --color=false to disable colored output]:(setting):(true false)" \ 13 | {--debug}"[Enable debug logging]:(setting):(true false)" \ 14 | --time"[Shows how long the response took from the server"] \ 15 | '*:filename:_hosts' 16 | } 17 | 18 | __doggo 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CLI_BIN := ./bin/doggo.bin 2 | API_BIN := ./bin/doggo-api.bin 3 | 4 | HASH := $(shell git rev-parse --short HEAD) 5 | BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S') 6 | VERSION := ${HASH} 7 | 8 | .PHONY: build-cli 9 | build-cli: 10 | go build -o ${CLI_BIN} -ldflags="-X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}'" ./cmd/doggo/ 11 | 12 | .PHONY: build-api 13 | build-api: 14 | go build -o ${API_BIN} -ldflags="-X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}'" ./cmd/api/ 15 | 16 | .PHONY: build 17 | build: build-api build-cli 18 | 19 | .PHONY: run-cli 20 | run-cli: build-cli ## Build and Execute the CLI binary after the build step. 21 | ${CLI_BIN} 22 | 23 | .PHONY: run-api 24 | run-api: build-api ## Build and Execute the API binary after the build step. 25 | ${API_BIN} --config config-api-sample.toml 26 | 27 | .PHONY: clean 28 | clean: 29 | go clean 30 | - rm -rf ./bin/ 31 | 32 | .PHONY: lint 33 | lint: 34 | golangci-lint run 35 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "github.com/mr-karan/doggo/pkg/models" 6 | "github.com/mr-karan/doggo/pkg/resolvers" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // App represents the structure for all app wide configuration. 11 | type App struct { 12 | Logger *logrus.Logger 13 | Version string 14 | QueryFlags models.QueryFlags 15 | Questions []dns.Question 16 | Resolvers []resolvers.Resolver 17 | ResolverOpts resolvers.Options 18 | Nameservers []models.Nameserver 19 | } 20 | 21 | // NewApp initializes an instance of App which holds app wide configuration. 22 | func New(logger *logrus.Logger, buildVersion string) App { 23 | app := App{ 24 | Logger: logger, 25 | Version: buildVersion, 26 | QueryFlags: models.QueryFlags{ 27 | QNames: []string{}, 28 | QTypes: []string{}, 29 | QClasses: []string{}, 30 | Nameservers: []string{}, 31 | }, 32 | Nameservers: []models.Nameserver{}, 33 | } 34 | return app 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.18 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.repository_owner }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v2 32 | with: 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | DOCKER_CLI_EXPERIMENTAL: enabled 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /cmd/doggo/parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // loadUnparsedArgs tries to parse all the arguments 10 | // which are unparsed by `flag` library. These arguments don't have any specific 11 | // order so we have to deduce based on the pattern of argument. 12 | // For eg, a nameserver must always begin with `@`. In this 13 | // pattern we deduce the arguments and append it to the 14 | // list of internal query flags. 15 | // In case an argument isn't able to fit in any of the existing 16 | // pattern it is considered to be a "hostname". 17 | // Eg of unparsed argument: `dig mrkaran.dev @1.1.1.1 AAAA` 18 | // where `@1.1.1.1` and `AAAA` are "unparsed" args. 19 | // Returns a list of nameserver, queryTypes, queryClasses, queryNames. 20 | func loadUnparsedArgs(args []string) ([]string, []string, []string, []string) { 21 | var ns, qt, qc, qn []string 22 | for _, arg := range args { 23 | if strings.HasPrefix(arg, "@") { 24 | ns = append(ns, strings.Trim(arg, "@")) 25 | } else if _, ok := dns.StringToType[strings.ToUpper(arg)]; ok { 26 | qt = append(qt, arg) 27 | } else if _, ok := dns.StringToClass[strings.ToUpper(arg)]; ok { 28 | qc = append(qc, arg) 29 | } else { 30 | // if nothing matches, consider it's a query name. 31 | qn = append(qn, arg) 32 | } 33 | } 34 | return ns, qt, qc, qn 35 | } 36 | -------------------------------------------------------------------------------- /cmd/api/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/knadh/koanf/parsers/toml" 9 | "github.com/knadh/koanf/providers/env" 10 | "github.com/knadh/koanf/providers/file" 11 | "github.com/knadh/koanf/providers/posflag" 12 | "github.com/sirupsen/logrus" 13 | flag "github.com/spf13/pflag" 14 | ) 15 | 16 | // Config is the config given by the user 17 | type Config struct { 18 | HTTPAddr string `koanf:"listen_addr"` 19 | } 20 | 21 | func initConfig() { 22 | f := flag.NewFlagSet("api", flag.ContinueOnError) 23 | f.Usage = func() { 24 | fmt.Println(f.FlagUsages()) 25 | os.Exit(0) 26 | } 27 | 28 | // Register --config flag. 29 | f.StringSlice("config", []string{"config.toml"}, 30 | "Path to one or more TOML config files to load in order") 31 | 32 | // Register --version flag. 33 | f.Bool("version", false, "Show build version") 34 | f.Parse(os.Args[1:]) 35 | // Display version. 36 | if ok, _ := f.GetBool("version"); ok { 37 | fmt.Println(buildVersion, buildDate) 38 | os.Exit(0) 39 | } 40 | 41 | // Read the config files. 42 | cFiles, _ := f.GetStringSlice("config") 43 | for _, f := range cFiles { 44 | logger.WithFields(logrus.Fields{ 45 | "file": f, 46 | }).Info("reading config") 47 | if err := ko.Load(file.Provider(f), toml.Parser()); err != nil { 48 | logger.Fatalf("error reading config: %v", err) 49 | } 50 | } 51 | // Load environment variables and merge into the loaded config. 52 | if err := ko.Load(env.Provider("DOGGO_API_", ".", func(s string) string { 53 | return strings.Replace(strings.ToLower( 54 | strings.TrimPrefix(s, "DOGGO_API_")), "__", ".", -1) 55 | }), nil); err != nil { 56 | logger.Fatalf("error loading env config: %v", err) 57 | } 58 | 59 | ko.Load(posflag.Provider(f, ".", ko), nil) 60 | } 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mr-karan/doggo 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/ameshkov/dnscrypt/v2 v2.3.0 7 | github.com/ameshkov/dnsstamps v1.0.3 8 | github.com/fatih/color v1.18.0 9 | github.com/go-chi/chi v1.5.5 10 | github.com/knadh/koanf v1.5.0 11 | github.com/miekg/dns v1.1.62 12 | github.com/olekukonko/tablewriter v0.0.5 13 | github.com/quic-go/quic-go v0.48.1 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/spf13/pflag v1.0.5 16 | golang.org/x/sys v0.26.0 17 | ) 18 | 19 | require ( 20 | github.com/AdguardTeam/golibs v0.29.0 // indirect 21 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 22 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect 23 | github.com/fsnotify/fsnotify v1.7.0 // indirect 24 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 25 | github.com/google/pprof v0.0.0-20241023014458-598669927662 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | github.com/mattn/go-runewidth v0.0.16 // indirect 29 | github.com/mitchellh/copystructure v1.2.0 // indirect 30 | github.com/mitchellh/mapstructure v1.5.0 // indirect 31 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 32 | github.com/onsi/ginkgo/v2 v2.20.2 // indirect 33 | github.com/pelletier/go-toml v1.9.5 // indirect 34 | github.com/quic-go/qpack v0.5.1 // indirect 35 | github.com/rivo/uniseg v0.4.7 // indirect 36 | go.uber.org/mock v0.5.0 // indirect 37 | golang.org/x/crypto v0.28.0 // indirect 38 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 39 | golang.org/x/mod v0.21.0 // indirect 40 | golang.org/x/net v0.30.0 // indirect 41 | golang.org/x/sync v0.8.0 // indirect 42 | golang.org/x/text v0.19.0 // indirect 43 | golang.org/x/tools v0.26.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /internal/app/questions.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // LoadFallbacks sets fallbacks for options 10 | // that are not specified by the user but necessary 11 | // for the resolver. 12 | func (app *App) LoadFallbacks() { 13 | if len(app.QueryFlags.QTypes) == 0 { 14 | app.QueryFlags.QTypes = append(app.QueryFlags.QTypes, "A") 15 | } 16 | if len(app.QueryFlags.QClasses) == 0 { 17 | app.QueryFlags.QClasses = append(app.QueryFlags.QClasses, "IN") 18 | } 19 | } 20 | 21 | // PrepareQuestions takes a list of query names, query types and query classes 22 | // and prepare a question for each combination of the above. 23 | func (app *App) PrepareQuestions() { 24 | for _, n := range app.QueryFlags.QNames { 25 | for _, t := range app.QueryFlags.QTypes { 26 | for _, c := range app.QueryFlags.QClasses { 27 | app.Questions = append(app.Questions, dns.Question{ 28 | Name: n, 29 | Qtype: dns.StringToType[strings.ToUpper(t)], 30 | Qclass: dns.StringToClass[strings.ToUpper(c)], 31 | }) 32 | } 33 | } 34 | } 35 | } 36 | 37 | // ReverseLookup is used to perform a reverse DNS Lookup 38 | // using an IPv4 or IPv6 address. 39 | // Query Type is set to PTR, Query Class is set to IN. 40 | // Query Names must be formatted in in-addr.arpa. or ip6.arpa format. 41 | func (app *App) ReverseLookup() { 42 | app.QueryFlags.QTypes = []string{"PTR"} 43 | app.QueryFlags.QClasses = []string{"IN"} 44 | formattedNames := make([]string, 0, len(app.QueryFlags.QNames)) 45 | 46 | for _, n := range app.QueryFlags.QNames { 47 | addr, err := dns.ReverseAddr(n) 48 | if err != nil { 49 | app.Logger.WithError(err).Error("error formatting address") 50 | app.Logger.Exit(2) 51 | } 52 | formattedNames = append(formattedNames, addr) 53 | } 54 | app.QueryFlags.QNames = formattedNames 55 | } 56 | -------------------------------------------------------------------------------- /completions/doggo.fish: -------------------------------------------------------------------------------- 1 | # Meta options 2 | complete -c doggo -l 'version' -d "Show version of doggo" 3 | complete -c doggo -l 'help' -d "Show list of command-line options" 4 | 5 | # Single line all options 6 | complete -c doggo -x -a "(__fish_print_hostnames) A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT IN CH HS" 7 | 8 | # Query options 9 | complete -c doggo -s 'q' -l 'query' -d "Hostname to query the DNS records for" -x -a "(__fish_print_hostnames)" 10 | complete -c doggo -s 't' -l 'type' -d "Type of the DNS Record" -x -a "A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT" 11 | complete -c doggo -s 'n' -l 'nameserver' -d "Address of a specific nameserver to send queries to" -x -a "1.1.1.1 8.8.8.8 9.9.9.9 (__fish_print_hostnames)" 12 | complete -c doggo -s 'c' -l 'class' -d "Network class of the DNS record being queried" -x -a "IN CH HS" 13 | 14 | # Transport options 15 | complete -c doggo -x -a "@udp:// @tcp:// @https:// @tls:// @sdns://" -d "Select the protocol for resolving queries" 16 | 17 | # Resolver options 18 | complete -c doggo -l 'ndots' -d "Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise" 19 | complete -c doggo -l 'search' -d "Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list" 20 | complete -c doggo -l 'timeout' -d "Specify timeout (in seconds) for the resolver to return a response" 21 | complete -c doggo -s '-4' -l 'ipv4' -d "Use IPv4 only" 22 | complete -c doggo -s '-6' -l 'ipv6' -d "Use IPv6 only" 23 | 24 | # Output options 25 | complete -c doggo -s 'J' -l 'json' -d "Format the output as JSON" 26 | complete -c doggo -l 'color' -d "Defaults to true. Set --color=false to disable colored output" 27 | complete -c doggo -l 'debug' -d "Enable debug logging" 28 | complete -c doggo -l 'time' -d "Shows how long the response took from the server" -------------------------------------------------------------------------------- /cmd/api/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/mr-karan/doggo/internal/app" 10 | "github.com/mr-karan/doggo/pkg/utils" 11 | "github.com/sirupsen/logrus" 12 | 13 | "github.com/go-chi/chi" 14 | "github.com/go-chi/chi/middleware" 15 | "github.com/knadh/koanf" 16 | ) 17 | 18 | var ( 19 | logger = utils.InitLogger() 20 | ko = koanf.New(".") 21 | // Version and date of the build. This is injected at build-time. 22 | buildVersion = "unknown" 23 | buildDate = "unknown" 24 | //go:embed assets/* 25 | assetsDir embed.FS 26 | //go:embed index.html 27 | html []byte 28 | ) 29 | 30 | func main() { 31 | initConfig() 32 | 33 | // Initialize app. 34 | app := app.New(logger, buildVersion) 35 | 36 | // Register router instance. 37 | r := chi.NewRouter() 38 | 39 | // Register middlewares 40 | r.Use(middleware.RequestID) 41 | r.Use(middleware.RealIP) 42 | r.Use(middleware.Logger) 43 | r.Use(middleware.Recoverer) 44 | 45 | // Frontend Handlers. 46 | assets, _ := fs.Sub(assetsDir, "assets") 47 | r.Get("/assets/*", func(w http.ResponseWriter, r *http.Request) { 48 | fs := http.StripPrefix("/assets/", http.FileServer(http.FS(assets))) 49 | fs.ServeHTTP(w, r) 50 | }) 51 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 52 | w.Header().Add("Content-Type", "text/html") 53 | w.Write(html) 54 | }) 55 | 56 | // API Handlers. 57 | r.Get("/api/", wrap(app, handleIndexAPI)) 58 | r.Get("/api/ping/", wrap(app, handleHealthCheck)) 59 | r.Post("/api/lookup/", wrap(app, handleLookup)) 60 | 61 | // HTTP Server. 62 | srv := &http.Server{ 63 | Addr: ko.String("server.address"), 64 | Handler: r, 65 | ReadTimeout: ko.Duration("server.read_timeout") * time.Millisecond, 66 | WriteTimeout: ko.Duration("server.write_timeout") * time.Millisecond, 67 | IdleTimeout: ko.Duration("server.keepalive_timeout") * time.Millisecond, 68 | } 69 | 70 | logger.WithFields(logrus.Fields{ 71 | "address": srv.Addr, 72 | }).Info("starting server") 73 | 74 | if err := srv.ListenAndServe(); err != nil { 75 | logger.Fatalf("couldn't start server: %v", err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # doggo - Initial Release Milestone 2 | 3 | ## Resolver 4 | - [x] Create a DNS Resolver struct 5 | - [x] Add methods to initialise the config, set defaults 6 | - [x] Add a resolve method 7 | - [x] Make it separate from Hub 8 | - [x] Parse output into separate fields 9 | - [x] Test IPv6/IPv4 only options 10 | - [x] Add DOH support 11 | - [x] Add DOT support 12 | - [x] Add DNS protocol on TCP mode support. 13 | - [x] Change lookup method. 14 | - [x] Major records supported 15 | - [x] Support multiple resolvers 16 | - [x] Take multiple transport options and initialise resolvers accordingly. 17 | - [x] Add timeout support 18 | - [x] Support SOA/NXDOMAIN 19 | 20 | ## CLI Features 21 | - [x] `ndots` support 22 | - [x] `search list` support 23 | - [x] JSON output 24 | - [x] Colorized output 25 | - [x] Table output 26 | - [x] Parsing options free-form 27 | 28 | ## CLI Grunt 29 | - [x] Query args 30 | - [x] Neatly package them to load args in different functions 31 | - [x] Upper case is not mandatory for query type/classes 32 | - [x] Output 33 | - [x] Custom Help Text 34 | - [x] Add examples 35 | - [x] Colorize 36 | - [x] Add different commands 37 | - [x] Add client transport options 38 | - [x] Fix an issue while loading free form args, where the same records are being added twice 39 | - [x] Remove urfave/cli in favour of `pflag + koanf` 40 | - [x] Flags - Remove unneeded ones 41 | 42 | ## Documentation 43 | - [x] README 44 | - [x] Usage 45 | - [x] Installation 46 | - [x] Features 47 | 48 | 49 | ## Release Checklist 50 | - [x] Goreleaser 51 | - [x] Snap 52 | - [x] Docker 53 | --- 54 | # Future Release 55 | 56 | - [ ] Support obscure protocol tweaks in `dig` 57 | - [x] Support more DNS Record Types 58 | - [x] Shell completions 59 | - [x] zsh 60 | - [x] fish 61 | - [ ] Add tests for Resolvers. 62 | - [ ] Add tests for CLI Output. 63 | - [ ] Homebrew - Goreleaser 64 | - [ ] Add support for `dig +trace` like functionality. 65 | - [ ] Add `dig +short` short output 66 | - [x] Add `--strategy` for picking nameservers. 67 | - [ ] Explore `dig.rc` kinda file 68 | - [x] Separate Authority/Answer in JSON output. 69 | - [x] Error on NXDomain (Related upstream [bug](https://github.com/miekg/dns/issues/1198)) 70 | -------------------------------------------------------------------------------- /pkg/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | const ( 6 | // DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS. 7 | DefaultTLSPort = "853" 8 | // DefaultUDPPort specifies the default port for a DNS server connecting over UDP. 9 | DefaultUDPPort = "53" 10 | // DefaultTCPPort specifies the default port for a DNS server connecting over TCP. 11 | DefaultTCPPort = "53" 12 | // DefaultDOQPort specifies the default port for a DNS server connecting over DNS over QUIC. 13 | DefaultDOQPort = "853" 14 | UDPResolver = "udp" 15 | DOHResolver = "doh" 16 | DOH3Resolver = "doh3" 17 | TCPResolver = "tcp" 18 | DOTResolver = "dot" 19 | DNSCryptResolver = "dnscrypt" 20 | DOQResolver = "doq" 21 | ) 22 | 23 | // QueryFlags is used store the query params 24 | // supplied by the user. 25 | type QueryFlags struct { 26 | QNames []string `koanf:"query" json:"query"` 27 | QTypes []string `koanf:"type" json:"type"` 28 | QClasses []string `koanf:"class" json:"class"` 29 | Nameservers []string `koanf:"nameservers" json:"nameservers"` 30 | UseIPv4 bool `koanf:"ipv4" json:"ipv4"` 31 | UseIPv6 bool `koanf:"ipv6" json:"ipv6"` 32 | Ndots int `koanf:"ndots" json:"ndots"` 33 | Timeout time.Duration `koanf:"timeout" json:"timeout"` 34 | Color bool `koanf:"color" json:"-"` 35 | DisplayTimeTaken bool `koanf:"time" json:"-"` 36 | ShowJSON bool `koanf:"json" json:"-"` 37 | ShortOutput bool `koanf:"short" short:"-"` 38 | UseSearchList bool `koanf:"search" json:"-"` 39 | ReverseLookup bool `koanf:"reverse" reverse:"-"` 40 | Strategy string `koanf:"strategy" strategy:"-"` 41 | InsecureSkipVerify bool `koanf:"skip-hostname-verification" skip-hostname-verification:"-"` 42 | TLSHostname string `koanf:"tls-hostname" tls-hostname:"-"` 43 | RootCAs string `koanf:"root-cas"` 44 | } 45 | 46 | // Nameserver represents the type of Nameserver 47 | // along with the server address. 48 | type Nameserver struct { 49 | Address string 50 | Type string 51 | } 52 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | 5 | builds: 6 | - binary: doggo 7 | id: cli 8 | goos: 9 | - windows 10 | - darwin 11 | - linux 12 | goarch: 13 | - amd64 14 | - arm64 15 | goarm: 16 | - 6 17 | - 7 18 | ldflags: 19 | - -s -w -X "main.buildVersion={{ .Tag }} ({{ .ShortCommit }} {{ .Date }})" 20 | dir: ./cmd/doggo/ 21 | 22 | - binary: doggo-api.bin 23 | id: api 24 | goos: 25 | - windows 26 | - darwin 27 | - linux 28 | goarch: 29 | - amd64 30 | - arm64 31 | goarm: 32 | - 6 33 | - 7 34 | ldflags: 35 | - -s -w -X "main.buildVersion={{ .Tag }} ({{ .ShortCommit }} {{ .Date }})" 36 | dir: ./cmd/api/ 37 | 38 | archives: 39 | - format: tar.gz 40 | files: 41 | - README.md 42 | - LICENSE 43 | - completions/ 44 | 45 | dockers: 46 | - image_templates: 47 | - "ghcr.io/mr-karan/doggo:{{ .Tag }}" 48 | - "ghcr.io/mr-karan/doggo:latest" 49 | id: doggo 50 | # IDs to filter the binaries/packages. 51 | ids: 52 | - cli 53 | dockerfile: Dockerfile-cli 54 | build_flag_templates: 55 | - "--build-arg" 56 | - "ARCH=amd64" 57 | - image_templates: 58 | - "ghcr.io/mr-karan/doggo:{{ .Tag }}-arm64v8" 59 | - "ghcr.io/mr-karan/doggo:latest-arm64v8" 60 | id: doggo-arm 61 | # IDs to filter the binaries/packages. 62 | ids: 63 | - cli 64 | goarch: arm64 65 | dockerfile: Dockerfile-cli 66 | build_flag_templates: 67 | - "--build-arg" 68 | - "ARCH=arm64v8" 69 | 70 | # - image_templates: 71 | # - "ghcr.io/mr-karan/doggo-api:{{ .Tag }}" 72 | # - "ghcr.io/mr-karan/doggo-api:latest" 73 | # id: doggo-api 74 | # ids: 75 | # - api 76 | # dockerfile: Dockerfile-api 77 | # build_flag_templates: 78 | # - "--build-arg" 79 | # - "ARCH=amd64" 80 | # extra_files: 81 | # - config-api-sample.toml 82 | # - image_templates: 83 | # - "ghcr.io/mr-karan/doggo-api:{{ .Tag }}-arm64v8" 84 | # - "ghcr.io/mr-karan/doggo-api:latest-arm64v8" 85 | # id: doggo-api-arm 86 | # ids: 87 | # - api 88 | # goarch: arm64 89 | # dockerfile: Dockerfile-api 90 | # build_flag_templates: 91 | # - "--build-arg" 92 | # - "ARCH=arm64v8" 93 | # extra_files: 94 | # - config-api-sample.toml 95 | -------------------------------------------------------------------------------- /pkg/resolvers/dnscrypt.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ameshkov/dnscrypt/v2" 7 | "github.com/miekg/dns" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // DNSCryptResolver represents the config options for setting up a Resolver. 12 | type DNSCryptResolver struct { 13 | client *dnscrypt.Client 14 | server string 15 | resolverInfo *dnscrypt.ResolverInfo 16 | resolverOptions Options 17 | } 18 | 19 | // DNSCryptResolverOpts holds options for setting up a DNSCrypt resolver. 20 | type DNSCryptResolverOpts struct { 21 | UseTCP bool 22 | } 23 | 24 | // NewDNSCryptResolver accepts a list of nameservers and configures a DNS resolver. 25 | func NewDNSCryptResolver(server string, dnscryptOpts DNSCryptResolverOpts, resolverOpts Options) (Resolver, error) { 26 | net := "udp" 27 | if dnscryptOpts.UseTCP { 28 | net = "tcp" 29 | } 30 | 31 | client := &dnscrypt.Client{Net: net, Timeout: resolverOpts.Timeout, UDPSize: 4096} 32 | resolverInfo, err := client.Dial(server) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &DNSCryptResolver{ 37 | client: client, 38 | resolverInfo: resolverInfo, 39 | server: resolverInfo.ServerAddress, 40 | resolverOptions: resolverOpts, 41 | }, nil 42 | } 43 | 44 | // Lookup takes a dns.Question and sends them to DNS Server. 45 | // It parses the Response from the server in a custom output format. 46 | func (r *DNSCryptResolver) Lookup(question dns.Question) (Response, error) { 47 | var ( 48 | rsp Response 49 | messages = prepareMessages(question, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 50 | ) 51 | for _, msg := range messages { 52 | r.resolverOptions.Logger.WithFields(logrus.Fields{ 53 | "domain": msg.Question[0].Name, 54 | "ndots": r.resolverOptions.Ndots, 55 | "nameserver": r.server, 56 | }).Debug("Attempting to resolve") 57 | now := time.Now() 58 | in, err := r.client.Exchange(&msg, r.resolverInfo) 59 | if err != nil { 60 | return rsp, err 61 | } 62 | rtt := time.Since(now) 63 | // pack questions in output. 64 | for _, q := range msg.Question { 65 | ques := Question{ 66 | Name: q.Name, 67 | Class: dns.ClassToString[q.Qclass], 68 | Type: dns.TypeToString[q.Qtype], 69 | } 70 | rsp.Questions = append(rsp.Questions, ques) 71 | } 72 | // get the authorities and answers. 73 | output := parseMessage(in, rtt, r.server) 74 | rsp.Authorities = output.Authorities 75 | rsp.Answers = output.Answers 76 | 77 | if len(output.Answers) > 0 { 78 | // stop iterating the searchlist. 79 | break 80 | } 81 | } 82 | return rsp, nil 83 | } 84 | -------------------------------------------------------------------------------- /cmd/api/assets/main.js: -------------------------------------------------------------------------------- 1 | const $ = document.querySelector.bind(document); 2 | const $new = document.createElement.bind(document); 3 | const $show = (el) => { 4 | el.classList.remove('hidden'); 5 | }; 6 | const $hide = (el) => { 7 | el.classList.add('hidden'); 8 | }; 9 | 10 | const apiURL = '/api/lookup/'; 11 | 12 | (function () { 13 | const fields = ['name', 'address', 'type', 'ttl', 'rtt']; 14 | 15 | // createRow creates a table row with the given cell values. 16 | function createRow(item) { 17 | const tr = $new('tr'); 18 | fields.forEach((f) => { 19 | const td = $new('td'); 20 | td.innerText = item[f]; 21 | td.classList.add(f); 22 | tr.appendChild(td); 23 | }); 24 | return tr; 25 | } 26 | 27 | const handleSubmit = async () => { 28 | const tbody = $('#table tbody'), 29 | tbl = $('#table'); 30 | tbody.innerHTML = ''; 31 | $hide(tbl); 32 | 33 | const q = $('input[name=q]').value.trim(), 34 | typ = $('select[name=type]').value, 35 | addr = $('input[name=address]').value.trim(); 36 | 37 | // Post to the API. 38 | const req = await fetch(apiURL, { 39 | method: 'POST', 40 | headers: { 'Content-Type': 'application/json' }, 41 | body: JSON.stringify({ query: [q,], type: [typ,], nameservers: [addr,] }) 42 | }); 43 | 44 | const res = await req.json(); 45 | 46 | if (res.status != 'success') { 47 | const error = (res && res.message) || response.statusText; 48 | throw(error); 49 | return; 50 | } 51 | 52 | if (res.data[0].answers == null) { 53 | throw('No records found.'); 54 | return; 55 | } 56 | 57 | res.data[0].answers.forEach((item) => { 58 | tbody.appendChild(createRow(item)); 59 | }); 60 | 61 | $show(tbl); 62 | }; 63 | 64 | // Capture the form submit. 65 | $('#form').onsubmit = async (e) => { 66 | e.preventDefault(); 67 | 68 | const msg = $('#message'); 69 | $hide(msg); 70 | 71 | try { 72 | await handleSubmit(); 73 | } catch(e) { 74 | msg.innerText = e.toString(); 75 | $show(msg); 76 | throw e; 77 | } 78 | }; 79 | 80 | // Change the address on ns change. 81 | const ns = $("#ns"), addr = $("#address"); 82 | addr.value = ns.value; 83 | 84 | ns.onchange = (e) => { 85 | addr.value = e.target.value; 86 | if(addr.value === "") { 87 | addr.focus(); 88 | } 89 | }; 90 | })(); -------------------------------------------------------------------------------- /pkg/resolvers/classic.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | 7 | "github.com/miekg/dns" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // ClassicResolver represents the config options for setting up a Resolver. 12 | type ClassicResolver struct { 13 | client *dns.Client 14 | server string 15 | resolverOptions Options 16 | } 17 | 18 | // ClassicResolverOpts holds options for setting up a Classic resolver. 19 | type ClassicResolverOpts struct { 20 | UseTLS bool 21 | UseTCP bool 22 | } 23 | 24 | // NewClassicResolver accepts a list of nameservers and configures a DNS resolver. 25 | func NewClassicResolver(server string, classicOpts ClassicResolverOpts, resolverOpts Options) (Resolver, error) { 26 | net := "udp" 27 | client := &dns.Client{ 28 | Timeout: resolverOpts.Timeout, 29 | Net: "udp", 30 | } 31 | 32 | if classicOpts.UseTCP { 33 | net = "tcp" 34 | } 35 | 36 | if resolverOpts.UseIPv4 { 37 | net = net + "4" 38 | } 39 | if resolverOpts.UseIPv6 { 40 | net = net + "6" 41 | } 42 | 43 | if classicOpts.UseTLS { 44 | net = net + "-tls" 45 | // Provide extra TLS config for doing/skipping hostname verification. 46 | client.TLSConfig = &tls.Config{ 47 | ServerName: resolverOpts.TLSHostname, 48 | InsecureSkipVerify: resolverOpts.InsecureSkipVerify, 49 | RootCAs: resolverOpts.RootCAs, 50 | } 51 | } 52 | 53 | client.Net = net 54 | 55 | return &ClassicResolver{ 56 | client: client, 57 | server: server, 58 | resolverOptions: resolverOpts, 59 | }, nil 60 | } 61 | 62 | // Lookup takes a dns.Question and sends them to DNS Server. 63 | // It parses the Response from the server in a custom output format. 64 | func (r *ClassicResolver) Lookup(question dns.Question) (Response, error) { 65 | var ( 66 | rsp Response 67 | messages = prepareMessages(question, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 68 | ) 69 | for _, msg := range messages { 70 | r.resolverOptions.Logger.WithFields(logrus.Fields{ 71 | "domain": msg.Question[0].Name, 72 | "ndots": r.resolverOptions.Ndots, 73 | "nameserver": r.server, 74 | }).Debug("Attempting to resolve") 75 | 76 | // Since the library doesn't include tcp.Dial time, 77 | // it's better to not rely on `rtt` provided here and calculate it ourselves. 78 | now := time.Now() 79 | in, _, err := r.client.Exchange(&msg, r.server) 80 | if err != nil { 81 | return rsp, err 82 | } 83 | 84 | // In case the response size exceeds 512 bytes (can happen with lot of TXT records), 85 | // fallback to TCP as with UDP the response is truncated. Fallback mechanism is in-line with `dig`. 86 | if in.Truncated { 87 | switch r.client.Net { 88 | case "udp": 89 | r.client.Net = "tcp" 90 | case "udp4": 91 | r.client.Net = "tcp4" 92 | case "udp6": 93 | r.client.Net = "tcp6" 94 | default: 95 | r.client.Net = "tcp" 96 | } 97 | r.resolverOptions.Logger.WithField("protocol", r.client.Net).Debug("Response truncated; retrying now") 98 | return r.Lookup(question) 99 | } 100 | 101 | // Pack questions in output. 102 | for _, q := range msg.Question { 103 | ques := Question{ 104 | Name: q.Name, 105 | Class: dns.ClassToString[q.Qclass], 106 | Type: dns.TypeToString[q.Qtype], 107 | } 108 | rsp.Questions = append(rsp.Questions, ques) 109 | } 110 | rtt := time.Since(now) 111 | 112 | // Get the authorities and answers. 113 | output := parseMessage(in, rtt, r.server) 114 | rsp.Authorities = output.Authorities 115 | rsp.Answers = output.Answers 116 | 117 | if len(output.Answers) > 0 { 118 | // Stop iterating the searchlist. 119 | break 120 | } 121 | } 122 | return rsp, nil 123 | } 124 | -------------------------------------------------------------------------------- /cmd/api/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #4338ca; 3 | --secondary: #333; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | :focus { 11 | outline: 0; 12 | } 13 | 14 | body { 15 | font-family: "Segoe UI", "Helvetica Neue", Inter, sans-serif; 16 | font-size: 16px; 17 | line-height: 24px; 18 | color: #111; 19 | margin: 0 auto; 20 | } 21 | 22 | h1, h2, h3, h4 { 23 | line-height: 1.3em; 24 | } 25 | 26 | a { 27 | color: var(--primary); 28 | } 29 | a:hover { 30 | color: #111; 31 | text-decoration: none; 32 | } 33 | 34 | input, select, button { 35 | border-radius: 5px; 36 | border: 1px solid #ddd; 37 | font-size: 1.3rem; 38 | padding: 10px 15px; 39 | width: 100%; 40 | } 41 | input:focus, select:focus { 42 | border-color: var(--primary); 43 | } 44 | button { 45 | border-color: var(--primary); 46 | background: var(--primary); 47 | color: #fff; 48 | cursor: pointer; 49 | width: auto; 50 | padding: 10px 30px; 51 | } 52 | button:focus, 53 | button:hover { 54 | border-color: var(--secondary); 55 | background: var(--secondary); 56 | } 57 | 58 | label { 59 | display: block; 60 | padding-bottom: 0.5rem; 61 | } 62 | 63 | .box { 64 | box-shadow: 1px 1px 4px #eee; 65 | border: 1px solid #eee; 66 | padding: 30px; 67 | border-radius: 3px; 68 | } 69 | 70 | .hidden { 71 | display: none !important; 72 | } 73 | 74 | .main { 75 | margin: 60px auto 30px auto; 76 | max-width: 900px; 77 | } 78 | 79 | header { 80 | text-align: center; 81 | font-size: 1.5em; 82 | margin-bottom: 60px; 83 | } 84 | .logo span { 85 | color: var(--primary); 86 | font-weight: 900; 87 | } 88 | 89 | form { 90 | margin-bottom: 45px; 91 | } 92 | 93 | .row { 94 | display: flex; 95 | margin-bottom: 15px; 96 | } 97 | .row .field { 98 | flex: 50%; 99 | } 100 | .row .field:last-child { 101 | margin-left: 30px; 102 | } 103 | 104 | .submit { 105 | text-align: right; 106 | } 107 | .help { 108 | color: #666; 109 | font-size: 0.875em; 110 | } 111 | 112 | #message { 113 | color: #ff3300; 114 | } 115 | 116 | table.box { 117 | width: 100%; 118 | max-width: 100%; 119 | padding: 0; 120 | } 121 | table th { 122 | background: #f9fafb; 123 | color: #666; 124 | font-size: 0.875em; 125 | border-bottom: 1px solid #ddd; 126 | } 127 | table th, tbody td { 128 | padding: 10px 15px; 129 | text-align: left; 130 | } 131 | td.name { 132 | font-weight: bold; 133 | } 134 | th.type, td.type { 135 | text-align: center; 136 | } 137 | td.type { 138 | background: #d1fae5; 139 | color: #065f46; 140 | font-weight: bold; 141 | } 142 | 143 | footer { 144 | margin: 60px 0 0 0; 145 | text-align: center; 146 | } 147 | footer a { 148 | text-decoration: none; 149 | } 150 | 151 | 152 | @media (max-width: 650px) { 153 | .main { 154 | margin: 60px 30px 30px 30px; 155 | } 156 | .box { 157 | box-shadow: none; 158 | border: 0; 159 | padding: 0; 160 | } 161 | 162 | .row { 163 | display: block; 164 | } 165 | .field { 166 | margin: 0 0 20px 0; 167 | } 168 | .row .field:last-child { 169 | margin: 0; 170 | } 171 | .submit button { 172 | width: 100%; 173 | } 174 | 175 | table { 176 | table-layout: fixed; 177 | } 178 | table th { 179 | width: 100%; 180 | } 181 | table tr { 182 | border-bottom: 0; 183 | display: flex; 184 | flex-direction: row; 185 | flex-wrap: wrap; 186 | margin-bottom: 30px; 187 | } 188 | table td { 189 | border: 1px solid #eee; 190 | margin: 0 -1px -1px 0; 191 | position: relative; 192 | width: 100%; 193 | word-wrap:break-word; 194 | } 195 | table th.type, table td.type { 196 | text-align: left; 197 | } 198 | table td span { 199 | display: block; 200 | } 201 | } -------------------------------------------------------------------------------- /cmd/api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Doggo DNS 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Doggo DNS

13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 36 |
37 |
38 |
39 |
40 | 41 | 48 |
49 |
50 | 51 | 53 |

54 | To use different protocols like DOH, DOT etc. refer to the instructions 55 | here. 56 |

57 |
58 |
59 |
60 |

61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 90 | 91 | 92 | 98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /pkg/config/config_windows.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "unsafe" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | // GAA_FLAG_INCLUDE_GATEWAYS Return the addresses of default gateways. 12 | // This flag is supported on Windows Vista and later. 13 | const GAA_FLAG_INCLUDE_GATEWAYS = 0x00000080 14 | 15 | // IpAdapterWinsServerAddress structure in a linked list of Windows Internet Name Service (WINS) server addresses for the adapter. 16 | type IpAdapterWinsServerAddress struct { 17 | Length uint32 18 | _ uint32 19 | Next *IpAdapterWinsServerAddress 20 | Address windows.SocketAddress 21 | } 22 | 23 | // IpAdapterGatewayAddress structure in a linked list of gateways for the adapter. 24 | type IpAdapterGatewayAddress struct { 25 | Length uint32 26 | _ uint32 27 | Next *IpAdapterGatewayAddress 28 | Address windows.SocketAddress 29 | } 30 | 31 | // IpAdapterAddresses structure is the header node for a linked list of addresses for a particular adapter. 32 | // This structure can simultaneously be used as part of a linked list of IP_ADAPTER_ADDRESSES structures. 33 | type IpAdapterAddresses struct { 34 | Length uint32 35 | IfIndex uint32 36 | Next *IpAdapterAddresses 37 | AdapterName *byte 38 | FirstUnicastAddress *windows.IpAdapterUnicastAddress 39 | FirstAnycastAddress *windows.IpAdapterAnycastAddress 40 | FirstMulticastAddress *windows.IpAdapterMulticastAddress 41 | FirstDnsServerAddress *windows.IpAdapterDnsServerAdapter 42 | DnsSuffix *uint16 43 | Description *uint16 44 | FriendlyName *uint16 45 | PhysicalAddress [syscall.MAX_ADAPTER_ADDRESS_LENGTH]byte 46 | PhysicalAddressLength uint32 47 | Flags uint32 48 | Mtu uint32 49 | IfType uint32 50 | OperStatus uint32 51 | Ipv6IfIndex uint32 52 | ZoneIndices [16]uint32 53 | FirstPrefix *windows.IpAdapterPrefix 54 | /* more fields might be present here. */ 55 | TransmitLinkSpeed uint64 56 | ReceiveLinkSpeed uint64 57 | FirstWinsServerAddress *IpAdapterWinsServerAddress 58 | FirstGatewayAddress *IpAdapterGatewayAddress 59 | } 60 | 61 | func adapterAddresses() ([]*IpAdapterAddresses, error) { 62 | var b []byte 63 | // https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses 64 | // #define WORKING_BUFFER_SIZE 15000 65 | l := uint32(15000) 66 | for { 67 | b = make([]byte, l) 68 | err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, GAA_FLAG_INCLUDE_GATEWAYS|windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) 69 | if err == nil { 70 | if l == 0 { 71 | return nil, nil 72 | } 73 | break 74 | } 75 | if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { 76 | return nil, os.NewSyscallError("getadaptersaddresses", err) 77 | } 78 | if l <= uint32(len(b)) { 79 | return nil, os.NewSyscallError("getadaptersaddresses", err) 80 | } 81 | } 82 | aas := make([]*IpAdapterAddresses, 0, uintptr(l)/unsafe.Sizeof(IpAdapterAddresses{})) 83 | for aa := (*IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next { 84 | aas = append(aas, aa) 85 | } 86 | return aas, nil 87 | } 88 | 89 | func getDefaultDNSServers() ([]string, error) { 90 | ifs, err := adapterAddresses() 91 | if err != nil { 92 | return nil, err 93 | } 94 | dnsServers := make([]string, 0) 95 | for _, ifi := range ifs { 96 | if ifi.OperStatus != windows.IfOperStatusUp { 97 | continue 98 | } 99 | 100 | if ifi.FirstGatewayAddress == nil { 101 | continue 102 | } 103 | 104 | for dnsServer := ifi.FirstDnsServerAddress; dnsServer != nil; dnsServer = dnsServer.Next { 105 | ip := dnsServer.Address.IP() 106 | if isUnicastLinkLocal(ip) { 107 | continue 108 | } 109 | dnsServers = append(dnsServers, ip.String()) 110 | } 111 | } 112 | return dnsServers, nil 113 | } 114 | 115 | // GetDefaultServers get system default nameserver 116 | func GetDefaultServers() ([]string, int, []string, error) { 117 | // TODO: DNS Suffix 118 | servers, err := getDefaultDNSServers() 119 | return servers, 0, nil, err 120 | } 121 | -------------------------------------------------------------------------------- /pkg/resolvers/utils.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | // prepareMessages takes a DNS Question and returns the 13 | // corresponding DNS messages for the same. 14 | func prepareMessages(q dns.Question, ndots int, searchList []string) []dns.Msg { 15 | var ( 16 | possibleQNames = constructPossibleQuestions(q.Name, ndots, searchList) 17 | messages = make([]dns.Msg, 0, len(possibleQNames)) 18 | ) 19 | 20 | for _, qName := range possibleQNames { 21 | msg := dns.Msg{} 22 | // generate a random id for the transaction. 23 | msg.Id = dns.Id() 24 | msg.RecursionDesired = true 25 | // It's recommended to only send 1 question for 1 DNS message. 26 | msg.Question = []dns.Question{{ 27 | Name: qName, 28 | Qtype: q.Qtype, 29 | Qclass: q.Qclass, 30 | }} 31 | messages = append(messages, msg) 32 | } 33 | 34 | return messages 35 | } 36 | 37 | // NameList returns all of the names that should be queried based on the 38 | // config. It is based off of go's net/dns name building, but it does not 39 | // check the length of the resulting names. 40 | // NOTE: It is taken from `miekg/dns/clientconfig.go: func (c *ClientConfig) NameList` 41 | // and slightly modified. 42 | func constructPossibleQuestions(name string, ndots int, searchList []string) []string { 43 | // if this domain is already fully qualified, no append needed. 44 | if dns.IsFqdn(name) { 45 | return []string{name} 46 | } 47 | 48 | // Check to see if the name has more labels than Ndots. Do this before making 49 | // the domain fully qualified. 50 | hasNdots := dns.CountLabel(name) > ndots 51 | // Make the domain fully qualified. 52 | name = dns.Fqdn(name) 53 | 54 | // Make a list of names based off search. 55 | names := []string{} 56 | 57 | // If name has enough dots, try that first. 58 | if hasNdots { 59 | names = append(names, name) 60 | } 61 | for _, s := range searchList { 62 | names = append(names, dns.Fqdn(name+s)) 63 | } 64 | // If we didn't have enough dots, try after suffixes. 65 | if !hasNdots { 66 | names = append(names, name) 67 | } 68 | return names 69 | } 70 | 71 | // parseMessage takes a `dns.Message` and returns a custom 72 | // Response data struct. 73 | func parseMessage(msg *dns.Msg, rtt time.Duration, server string) Response { 74 | var resp Response 75 | timeTaken := fmt.Sprintf("%dms", rtt.Milliseconds()) 76 | 77 | // Parse Authorities section. 78 | for _, ns := range msg.Ns { 79 | // check for SOA record 80 | soa, ok := ns.(*dns.SOA) 81 | if !ok { 82 | // Currently we only check for SOA in Authority. 83 | // If it's not SOA, skip this message. 84 | continue 85 | } 86 | mname := soa.Ns + " " + soa.Mbox + 87 | " " + strconv.FormatInt(int64(soa.Serial), 10) + 88 | " " + strconv.FormatInt(int64(soa.Refresh), 10) + 89 | " " + strconv.FormatInt(int64(soa.Retry), 10) + 90 | " " + strconv.FormatInt(int64(soa.Expire), 10) + 91 | " " + strconv.FormatInt(int64(soa.Minttl), 10) 92 | h := ns.Header() 93 | name := h.Name 94 | qclass := dns.Class(h.Class).String() 95 | ttl := strconv.FormatInt(int64(h.Ttl), 10) + "s" 96 | qtype := dns.Type(h.Rrtype).String() 97 | auth := Authority{ 98 | Name: name, 99 | Type: qtype, 100 | TTL: ttl, 101 | Class: qclass, 102 | MName: mname, 103 | Nameserver: server, 104 | RTT: timeTaken, 105 | Status: dns.RcodeToString[msg.Rcode], 106 | } 107 | resp.Authorities = append(resp.Authorities, auth) 108 | } 109 | // Parse Answers section. 110 | for _, a := range msg.Answer { 111 | var ( 112 | h = a.Header() 113 | // Source https://github.com/jvns/dns-lookup/blob/main/dns.go#L121. 114 | parts = strings.Split(a.String(), "\t") 115 | ans = Answer{ 116 | Name: h.Name, 117 | Type: dns.Type(h.Rrtype).String(), 118 | TTL: strconv.FormatInt(int64(h.Ttl), 10) + "s", 119 | Class: dns.Class(h.Class).String(), 120 | Address: parts[len(parts)-1], 121 | RTT: timeTaken, 122 | Nameserver: server, 123 | } 124 | ) 125 | 126 | resp.Answers = append(resp.Answers, ans) 127 | } 128 | return resp 129 | } 130 | -------------------------------------------------------------------------------- /internal/app/output.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/fatih/color" 8 | "github.com/miekg/dns" 9 | "github.com/mr-karan/doggo/pkg/resolvers" 10 | "github.com/olekukonko/tablewriter" 11 | ) 12 | 13 | func (app *App) outputJSON(rsp []resolvers.Response) { 14 | // Pretty print with 4 spaces. 15 | res, err := json.MarshalIndent(rsp, "", " ") 16 | if err != nil { 17 | app.Logger.WithError(err).Error("unable to output data in JSON") 18 | app.Logger.Exit(-1) 19 | } 20 | fmt.Printf("%s", res) 21 | } 22 | 23 | func (app *App) outputShort(rsp []resolvers.Response) { 24 | for _, r := range rsp { 25 | for _, a := range r.Answers { 26 | fmt.Printf("%s\n", a.Address) 27 | } 28 | } 29 | } 30 | 31 | func (app *App) outputTerminal(rsp []resolvers.Response) { 32 | var ( 33 | green = color.New(color.FgGreen, color.Bold).SprintFunc() 34 | blue = color.New(color.FgBlue, color.Bold).SprintFunc() 35 | yellow = color.New(color.FgYellow, color.Bold).SprintFunc() 36 | cyan = color.New(color.FgCyan, color.Bold).SprintFunc() 37 | red = color.New(color.FgRed, color.Bold).SprintFunc() 38 | magenta = color.New(color.FgMagenta, color.Bold).SprintFunc() 39 | ) 40 | 41 | // Disables colorized output if user specified. 42 | if !app.QueryFlags.Color { 43 | color.NoColor = true 44 | } 45 | 46 | // Conditional Time column. 47 | table := tablewriter.NewWriter(color.Output) 48 | header := []string{"Name", "Type", "Class", "TTL", "Address", "Nameserver"} 49 | if app.QueryFlags.DisplayTimeTaken { 50 | header = append(header, "Time Taken") 51 | } 52 | 53 | // Show output in case if it's not 54 | // a NOERROR. 55 | outputStatus := false 56 | for _, r := range rsp { 57 | for _, a := range r.Authorities { 58 | if dns.StringToRcode[a.Status] != dns.RcodeSuccess { 59 | outputStatus = true 60 | } 61 | } 62 | for _, a := range r.Answers { 63 | if dns.StringToRcode[a.Status] != dns.RcodeSuccess { 64 | outputStatus = true 65 | } 66 | } 67 | } 68 | if outputStatus { 69 | header = append(header, "Status") 70 | } 71 | 72 | // Formatting options for the table. 73 | table.SetHeader(header) 74 | table.SetAutoWrapText(true) 75 | table.SetAutoFormatHeaders(true) 76 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 77 | table.SetAlignment(tablewriter.ALIGN_LEFT) 78 | table.SetCenterSeparator("") 79 | table.SetColumnSeparator("") 80 | table.SetRowSeparator("") 81 | table.SetHeaderLine(false) 82 | table.SetBorder(false) 83 | table.SetTablePadding("\t") // pad with tabs 84 | table.SetNoWhiteSpace(true) 85 | 86 | for _, r := range rsp { 87 | for _, ans := range r.Answers { 88 | var typOut string 89 | switch typ := ans.Type; typ { 90 | case "A": 91 | typOut = blue(ans.Type) 92 | case "AAAA": 93 | typOut = blue(ans.Type) 94 | case "MX": 95 | typOut = magenta(ans.Type) 96 | case "NS": 97 | typOut = cyan(ans.Type) 98 | case "CNAME": 99 | typOut = yellow(ans.Type) 100 | case "TXT": 101 | typOut = yellow(ans.Type) 102 | case "SOA": 103 | typOut = red(ans.Type) 104 | default: 105 | typOut = blue(ans.Type) 106 | } 107 | output := []string{green(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver} 108 | // Print how long it took 109 | if app.QueryFlags.DisplayTimeTaken { 110 | output = append(output, ans.RTT) 111 | } 112 | if outputStatus { 113 | output = append(output, red(ans.Status)) 114 | } 115 | table.Append(output) 116 | } 117 | for _, auth := range r.Authorities { 118 | var typOut string 119 | switch typ := auth.Type; typ { 120 | case "SOA": 121 | typOut = red(auth.Type) 122 | default: 123 | typOut = blue(auth.Type) 124 | } 125 | output := []string{green(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver} 126 | // Print how long it took 127 | if app.QueryFlags.DisplayTimeTaken { 128 | output = append(output, auth.RTT) 129 | } 130 | if outputStatus { 131 | output = append(output, red(auth.Status)) 132 | } 133 | table.Append(output) 134 | } 135 | } 136 | table.Render() 137 | } 138 | 139 | // Output takes a list of `dns.Answers` and based 140 | // on the output format specified displays the information. 141 | func (app *App) Output(responses []resolvers.Response) { 142 | if app.QueryFlags.ShowJSON { 143 | app.outputJSON(responses) 144 | } else if app.QueryFlags.ShortOutput { 145 | app.outputShort(responses) 146 | } else { 147 | app.outputTerminal(responses) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /cmd/api/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/mr-karan/doggo/internal/app" 12 | "github.com/mr-karan/doggo/pkg/models" 13 | "github.com/mr-karan/doggo/pkg/resolvers" 14 | ) 15 | 16 | type httpResp struct { 17 | Status string `json:"status"` 18 | Message string `json:"message,omitempty"` 19 | Data interface{} `json:"data,omitempty"` 20 | } 21 | 22 | func handleIndexAPI(w http.ResponseWriter, r *http.Request) { 23 | var ( 24 | app = r.Context().Value("app").(app.App) 25 | ) 26 | 27 | sendResponse(w, http.StatusOK, fmt.Sprintf("Welcome to Doggo API. Version: %s", app.Version)) 28 | return 29 | } 30 | 31 | func handleHealthCheck(w http.ResponseWriter, r *http.Request) { 32 | sendResponse(w, http.StatusOK, "PONG") 33 | return 34 | } 35 | 36 | func handleLookup(w http.ResponseWriter, r *http.Request) { 37 | var ( 38 | app = r.Context().Value("app").(app.App) 39 | ) 40 | 41 | // Read body. 42 | b, err := ioutil.ReadAll(r.Body) 43 | defer r.Body.Close() 44 | if err != nil { 45 | app.Logger.WithError(err).Error("error reading request body") 46 | sendErrorResponse(w, fmt.Sprintf("Invalid JSON payload"), http.StatusBadRequest, nil) 47 | return 48 | } 49 | // Prepare query flags. 50 | var qFlags models.QueryFlags 51 | if err := json.Unmarshal(b, &qFlags); err != nil { 52 | app.Logger.WithError(err).Error("error unmarshalling payload") 53 | sendErrorResponse(w, fmt.Sprintf("Invalid JSON payload"), http.StatusBadRequest, nil) 54 | return 55 | } 56 | 57 | app.QueryFlags = qFlags 58 | // Load fallbacks. 59 | app.LoadFallbacks() 60 | 61 | // Load Questions. 62 | app.PrepareQuestions() 63 | 64 | if len(app.Questions) == 0 { 65 | sendErrorResponse(w, fmt.Sprintf("Missing field `query`."), http.StatusBadRequest, nil) 66 | return 67 | } 68 | 69 | // Load Nameservers. 70 | err = app.LoadNameservers() 71 | if err != nil { 72 | app.Logger.WithError(err).Error("error loading nameservers") 73 | sendErrorResponse(w, fmt.Sprintf("Error looking up for records."), http.StatusInternalServerError, nil) 74 | return 75 | } 76 | 77 | // Load Resolvers. 78 | rslvrs, err := resolvers.LoadResolvers(resolvers.Options{ 79 | Nameservers: app.Nameservers, 80 | UseIPv4: app.QueryFlags.UseIPv4, 81 | UseIPv6: app.QueryFlags.UseIPv6, 82 | SearchList: app.ResolverOpts.SearchList, 83 | Ndots: app.ResolverOpts.Ndots, 84 | Timeout: app.QueryFlags.Timeout * time.Second, 85 | Logger: app.Logger, 86 | }) 87 | if err != nil { 88 | app.Logger.WithError(err).Error("error loading resolver") 89 | sendErrorResponse(w, fmt.Sprintf("Error looking up for records."), http.StatusInternalServerError, nil) 90 | return 91 | } 92 | app.Resolvers = rslvrs 93 | 94 | var responses []resolvers.Response 95 | for _, q := range app.Questions { 96 | for _, rslv := range app.Resolvers { 97 | resp, err := rslv.Lookup(q) 98 | if err != nil { 99 | app.Logger.WithError(err).Error("error looking up DNS records") 100 | sendErrorResponse(w, fmt.Sprintf("Error looking up for records."), http.StatusInternalServerError, nil) 101 | return 102 | } 103 | responses = append(responses, resp) 104 | } 105 | } 106 | sendResponse(w, http.StatusOK, responses) 107 | return 108 | } 109 | 110 | // wrap is a middleware that wraps HTTP handlers and injects the "app" context. 111 | func wrap(app app.App, next http.HandlerFunc) http.HandlerFunc { 112 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 113 | ctx := context.WithValue(r.Context(), "app", app) 114 | next.ServeHTTP(w, r.WithContext(ctx)) 115 | }) 116 | } 117 | 118 | // sendResponse sends a JSON envelope to the HTTP response. 119 | func sendResponse(w http.ResponseWriter, code int, data interface{}) { 120 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 121 | w.WriteHeader(code) 122 | 123 | out, err := json.Marshal(httpResp{Status: "success", Data: data}) 124 | if err != nil { 125 | sendErrorResponse(w, "Internal Server Error", http.StatusInternalServerError, nil) 126 | return 127 | } 128 | 129 | w.Write(out) 130 | } 131 | 132 | // sendErrorResponse sends a JSON error envelope to the HTTP response. 133 | func sendErrorResponse(w http.ResponseWriter, message string, code int, data interface{}) { 134 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 135 | w.WriteHeader(code) 136 | 137 | resp := httpResp{Status: "error", 138 | Message: message, 139 | Data: data} 140 | out, _ := json.Marshal(resp) 141 | w.Write(out) 142 | } 143 | -------------------------------------------------------------------------------- /pkg/resolvers/doh.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "github.com/miekg/dns" 15 | "github.com/quic-go/quic-go/http3" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // DOHResolver represents the config options for setting up a DOH based resolver. 20 | type DOHResolver struct { 21 | client *http.Client 22 | server string 23 | resolverOptions Options 24 | } 25 | 26 | // NewDOHResolver accepts a nameserver address and configures a DOH based resolver. 27 | func NewDOHResolver(server string, resolverOpts Options, doh3 bool) (Resolver, error) { 28 | // do basic validation 29 | if doh3 { 30 | server = strings.Replace(server, "https3", "https", 1) 31 | } 32 | u, err := url.ParseRequestURI(server) 33 | if err != nil { 34 | return nil, fmt.Errorf("%s is not a valid HTTPS nameserver", server) 35 | } 36 | if u.Scheme != "https" { 37 | return nil, fmt.Errorf("missing https in %s", server) 38 | } 39 | httpClient := &http.Client{ 40 | Timeout: resolverOpts.Timeout, 41 | Transport: &http.Transport{ 42 | TLSClientConfig: &tls.Config{ 43 | InsecureSkipVerify: resolverOpts.InsecureSkipVerify, 44 | RootCAs: resolverOpts.RootCAs, 45 | }, 46 | }, 47 | } 48 | 49 | if doh3 { 50 | httpClient.Transport = &http3.RoundTripper{ 51 | TLSClientConfig: &tls.Config{ 52 | InsecureSkipVerify: resolverOpts.InsecureSkipVerify, 53 | RootCAs: resolverOpts.RootCAs, 54 | }, 55 | } 56 | } 57 | 58 | return &DOHResolver{ 59 | client: httpClient, 60 | server: server, 61 | resolverOptions: resolverOpts, 62 | }, nil 63 | } 64 | 65 | // Lookup takes a dns.Question and sends them to DNS Server. 66 | // It parses the Response from the server in a custom output format. 67 | func (r *DOHResolver) Lookup(question dns.Question) (Response, error) { 68 | var ( 69 | rsp Response 70 | messages = prepareMessages(question, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 71 | ) 72 | 73 | for _, msg := range messages { 74 | r.resolverOptions.Logger.WithFields(logrus.Fields{ 75 | "domain": msg.Question[0].Name, 76 | "ndots": r.resolverOptions.Ndots, 77 | "nameserver": r.server, 78 | }).Debug("Attempting to resolve") 79 | // get the DNS Message in wire format. 80 | b, err := msg.Pack() 81 | if err != nil { 82 | return rsp, err 83 | } 84 | now := time.Now() 85 | 86 | // Make an HTTP POST request to the DNS server with the DNS message as wire format bytes in the body. 87 | req, err := http.NewRequest("POST", r.server, bytes.NewBuffer(b)) 88 | if err != nil { 89 | return rsp, err 90 | } 91 | if len(r.resolverOptions.Headers) > 0 { 92 | req.Header = r.resolverOptions.Headers 93 | } 94 | req.Header.Set("Content-Type", "application/dns-message") 95 | 96 | resp, err := r.client.Do(req) 97 | 98 | if err != nil { 99 | return rsp, err 100 | } 101 | if resp.StatusCode == http.StatusMethodNotAllowed { 102 | url, err := url.Parse(r.server) 103 | if err != nil { 104 | return rsp, err 105 | } 106 | url.RawQuery = fmt.Sprintf("dns=%v", base64.RawURLEncoding.EncodeToString(b)) 107 | resp, err = r.client.Get(url.String()) 108 | if err != nil { 109 | return rsp, err 110 | } 111 | } 112 | if resp.StatusCode != http.StatusOK { 113 | return rsp, fmt.Errorf("error from nameserver %s", resp.Status) 114 | } 115 | rtt := time.Since(now) 116 | // if debug, extract the response headers 117 | if r.resolverOptions.Logger.IsLevelEnabled(logrus.DebugLevel) { 118 | for header, value := range resp.Header { 119 | r.resolverOptions.Logger.WithFields(logrus.Fields{ 120 | header: value, 121 | }).Debug("DOH response header") 122 | } 123 | } 124 | // extract the binary response in DNS Message. 125 | body, err := ioutil.ReadAll(resp.Body) 126 | if err != nil { 127 | return rsp, err 128 | } 129 | 130 | err = msg.Unpack(body) 131 | if err != nil { 132 | return rsp, err 133 | } 134 | // pack questions in output. 135 | for _, q := range msg.Question { 136 | ques := Question{ 137 | Name: q.Name, 138 | Class: dns.ClassToString[q.Qclass], 139 | Type: dns.TypeToString[q.Qtype], 140 | } 141 | rsp.Questions = append(rsp.Questions, ques) 142 | } 143 | // get the authorities and answers. 144 | output := parseMessage(&msg, rtt, r.server) 145 | rsp.Authorities = output.Authorities 146 | rsp.Answers = output.Answers 147 | 148 | if len(output.Answers) > 0 { 149 | // stop iterating the searchlist. 150 | break 151 | } 152 | } 153 | return rsp, nil 154 | } 155 | -------------------------------------------------------------------------------- /pkg/resolvers/resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "crypto/x509" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | "github.com/mr-karan/doggo/pkg/models" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Options represent a set of common options 14 | // to configure a Resolver. 15 | type Options struct { 16 | Logger *logrus.Logger 17 | 18 | Nameservers []models.Nameserver 19 | UseIPv4 bool 20 | UseIPv6 bool 21 | SearchList []string 22 | Ndots int 23 | Timeout time.Duration 24 | Strategy string 25 | InsecureSkipVerify bool 26 | TLSHostname string 27 | Headers http.Header 28 | RootCAs *x509.CertPool 29 | } 30 | 31 | // Resolver implements the configuration for a DNS 32 | // Client. Different types of providers can load 33 | // a DNS Resolver satisfying this interface. 34 | type Resolver interface { 35 | Lookup(dns.Question) (Response, error) 36 | } 37 | 38 | // Response represents a custom output format 39 | // for DNS queries. It wraps metadata about the DNS query 40 | // and the DNS Answer as well. 41 | type Response struct { 42 | Answers []Answer `json:"answers"` 43 | Authorities []Authority `json:"authorities"` 44 | Questions []Question `json:"questions"` 45 | } 46 | 47 | type Question struct { 48 | Name string `json:"name"` 49 | Type string `json:"type"` 50 | Class string `json:"class"` 51 | } 52 | 53 | type Answer struct { 54 | Name string `json:"name"` 55 | Type string `json:"type"` 56 | Class string `json:"class"` 57 | TTL string `json:"ttl"` 58 | Address string `json:"address"` 59 | Status string `json:"status"` 60 | RTT string `json:"rtt"` 61 | Nameserver string `json:"nameserver"` 62 | } 63 | 64 | type Authority struct { 65 | Name string `json:"name"` 66 | Type string `json:"type"` 67 | Class string `json:"class"` 68 | TTL string `json:"ttl"` 69 | MName string `json:"mname"` 70 | Status string `json:"status"` 71 | RTT string `json:"rtt"` 72 | Nameserver string `json:"nameserver"` 73 | } 74 | 75 | // LoadResolvers loads differently configured 76 | // resolvers based on a list of nameserver. 77 | func LoadResolvers(opts Options) ([]Resolver, error) { 78 | // For each nameserver, initialise the correct resolver. 79 | rslvrs := make([]Resolver, 0, len(opts.Nameservers)) 80 | 81 | for _, ns := range opts.Nameservers { 82 | if ns.Type == models.DOHResolver { 83 | opts.Logger.Debug("initiating DOH resolver") 84 | rslvr, err := NewDOHResolver(ns.Address, opts, false) 85 | if err != nil { 86 | return rslvrs, err 87 | } 88 | rslvrs = append(rslvrs, rslvr) 89 | } 90 | if ns.Type == models.DOH3Resolver { 91 | opts.Logger.Debug("initiating DOH3 resolver") 92 | rslvr, err := NewDOHResolver(ns.Address, opts, true) 93 | if err != nil { 94 | return rslvrs, err 95 | } 96 | rslvrs = append(rslvrs, rslvr) 97 | } 98 | if ns.Type == models.DOTResolver { 99 | opts.Logger.Debug("initiating DOT resolver") 100 | rslvr, err := NewClassicResolver(ns.Address, 101 | ClassicResolverOpts{ 102 | UseTLS: true, 103 | UseTCP: true, 104 | }, opts) 105 | 106 | if err != nil { 107 | return rslvrs, err 108 | } 109 | rslvrs = append(rslvrs, rslvr) 110 | } 111 | if ns.Type == models.TCPResolver { 112 | opts.Logger.Debug("initiating TCP resolver") 113 | rslvr, err := NewClassicResolver(ns.Address, 114 | ClassicResolverOpts{ 115 | UseTLS: false, 116 | UseTCP: true, 117 | }, opts) 118 | if err != nil { 119 | return rslvrs, err 120 | } 121 | rslvrs = append(rslvrs, rslvr) 122 | } 123 | if ns.Type == models.UDPResolver { 124 | opts.Logger.Debug("initiating UDP resolver") 125 | rslvr, err := NewClassicResolver(ns.Address, 126 | ClassicResolverOpts{ 127 | UseTLS: false, 128 | UseTCP: false, 129 | }, opts) 130 | if err != nil { 131 | return rslvrs, err 132 | } 133 | rslvrs = append(rslvrs, rslvr) 134 | } 135 | if ns.Type == models.DNSCryptResolver { 136 | opts.Logger.Debug("initiating DNSCrypt resolver") 137 | rslvr, err := NewDNSCryptResolver(ns.Address, 138 | DNSCryptResolverOpts{ 139 | UseTCP: false, 140 | }, opts) 141 | if err != nil { 142 | return rslvrs, err 143 | } 144 | rslvrs = append(rslvrs, rslvr) 145 | } 146 | if ns.Type == models.DOQResolver { 147 | opts.Logger.Debug("initiating DOQ resolver") 148 | rslvr, err := NewDOQResolver(ns.Address, opts) 149 | if err != nil { 150 | return rslvrs, err 151 | } 152 | rslvrs = append(rslvrs, rslvr) 153 | } 154 | } 155 | return rslvrs, nil 156 | } 157 | -------------------------------------------------------------------------------- /internal/app/nameservers.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "net/url" 8 | "time" 9 | 10 | "github.com/ameshkov/dnsstamps" 11 | "github.com/mr-karan/doggo/pkg/config" 12 | "github.com/mr-karan/doggo/pkg/models" 13 | ) 14 | 15 | // LoadNameservers reads all the user given 16 | // nameservers and loads to App. 17 | func (app *App) LoadNameservers() error { 18 | for _, srv := range app.QueryFlags.Nameservers { 19 | ns, err := initNameserver(srv) 20 | if err != nil { 21 | return fmt.Errorf("error parsing nameserver: %s", srv) 22 | } 23 | // check if properly initialised. 24 | if ns.Address != "" && ns.Type != "" { 25 | app.Nameservers = append(app.Nameservers, ns) 26 | } 27 | } 28 | 29 | // Set `ndots` to the user specified value. 30 | app.ResolverOpts.Ndots = app.QueryFlags.Ndots 31 | // fallback to system nameserver 32 | // in case no nameserver is specified by user. 33 | if len(app.Nameservers) == 0 { 34 | ns, ndots, search, err := getDefaultServers(app.QueryFlags.Strategy) 35 | if err != nil { 36 | return fmt.Errorf("error fetching system default nameserver") 37 | } 38 | // `-1` indicates the flag is not set. 39 | // use from config if user hasn't specified any value. 40 | if app.ResolverOpts.Ndots == -1 { 41 | app.ResolverOpts.Ndots = ndots 42 | } 43 | if len(search) > 0 && app.QueryFlags.UseSearchList { 44 | app.ResolverOpts.SearchList = search 45 | } 46 | app.Nameservers = append(app.Nameservers, ns...) 47 | } 48 | // if the user hasn't given any override of `ndots` AND has 49 | // given a custom nameserver. Set `ndots` to 1 as the fallback value 50 | if app.ResolverOpts.Ndots == -1 { 51 | app.ResolverOpts.Ndots = 0 52 | } 53 | return nil 54 | } 55 | 56 | func initNameserver(n string) (models.Nameserver, error) { 57 | // Instantiate a UDP resolver with default port as a fallback. 58 | ns := models.Nameserver{ 59 | Type: models.UDPResolver, 60 | Address: net.JoinHostPort(n, models.DefaultUDPPort), 61 | } 62 | u, err := url.Parse(n) 63 | if err != nil { 64 | return ns, err 65 | } 66 | switch u.Scheme { 67 | case "sdns": 68 | stamp, err := dnsstamps.NewServerStampFromString(n) 69 | if err != nil { 70 | return ns, err 71 | } 72 | switch stamp.Proto { 73 | case dnsstamps.StampProtoTypeDoH: 74 | ns.Type = models.DOHResolver 75 | address := url.URL{Scheme: "https", Host: stamp.ProviderName, Path: stamp.Path} 76 | ns.Address = address.String() 77 | case dnsstamps.StampProtoTypeDNSCrypt: 78 | ns.Type = models.DNSCryptResolver 79 | ns.Address = n 80 | default: 81 | return ns, fmt.Errorf("unsupported protocol: %v", stamp.Proto.String()) 82 | } 83 | 84 | case "https": 85 | ns.Type = models.DOHResolver 86 | ns.Address = u.String() 87 | 88 | case "https3": 89 | ns.Type = models.DOH3Resolver 90 | ns.Address = u.String() 91 | 92 | case "tls": 93 | ns.Type = models.DOTResolver 94 | if u.Port() == "" { 95 | ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultTLSPort) 96 | } else { 97 | ns.Address = net.JoinHostPort(u.Hostname(), u.Port()) 98 | } 99 | 100 | case "tcp": 101 | ns.Type = models.TCPResolver 102 | if u.Port() == "" { 103 | ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultTCPPort) 104 | } else { 105 | ns.Address = net.JoinHostPort(u.Hostname(), u.Port()) 106 | } 107 | 108 | case "udp": 109 | ns.Type = models.UDPResolver 110 | if u.Port() == "" { 111 | ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultUDPPort) 112 | } else { 113 | ns.Address = net.JoinHostPort(u.Hostname(), u.Port()) 114 | } 115 | case "quic": 116 | ns.Type = models.DOQResolver 117 | if u.Port() == "" { 118 | ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultDOQPort) 119 | } else { 120 | ns.Address = net.JoinHostPort(u.Hostname(), u.Port()) 121 | } 122 | } 123 | 124 | return ns, nil 125 | } 126 | 127 | func getDefaultServers(strategy string) ([]models.Nameserver, int, []string, error) { 128 | // Load nameservers from `/etc/resolv.conf`. 129 | dnsServers, ndots, search, err := config.GetDefaultServers() 130 | if err != nil { 131 | return nil, 0, nil, err 132 | } 133 | servers := make([]models.Nameserver, 0, len(dnsServers)) 134 | 135 | switch strategy { 136 | case "random": 137 | // Choose a random server from the list. 138 | rand.Seed(time.Now().Unix()) 139 | srv := dnsServers[rand.Intn(len(dnsServers))] 140 | ns := models.Nameserver{ 141 | Type: models.UDPResolver, 142 | Address: net.JoinHostPort(srv, models.DefaultUDPPort), 143 | } 144 | servers = append(servers, ns) 145 | 146 | case "first": 147 | // Choose the first from the list, always. 148 | srv := dnsServers[0] 149 | ns := models.Nameserver{ 150 | Type: models.UDPResolver, 151 | Address: net.JoinHostPort(srv, models.DefaultUDPPort), 152 | } 153 | servers = append(servers, ns) 154 | 155 | default: 156 | // Default behaviour is to load all nameservers. 157 | for _, s := range dnsServers { 158 | ns := models.Nameserver{ 159 | Type: models.UDPResolver, 160 | Address: net.JoinHostPort(s, models.DefaultUDPPort), 161 | } 162 | servers = append(servers, ns) 163 | } 164 | } 165 | 166 | return servers, ndots, search, nil 167 | } 168 | -------------------------------------------------------------------------------- /cmd/doggo/help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | // appHelpTextTemplate is the text/template to customise the Help output. 11 | // Uses text/template to render templates. 12 | var appHelpTextTemplate = `{{ "NAME" | color "" "heading" }}: 13 | {{ .Name | color "green" "bold" }} 🐶 {{.Description}} 14 | 15 | {{ "USAGE" | color "" "heading" }}: 16 | {{ .Name | color "green" "bold" }} [--] {{ "[query options]" | color "yellow" "" }} {{ "[arguments...]" | color "cyan" "" }} 17 | 18 | {{ "VERSION" | color "" "heading" }}: 19 | {{.Version | color "red" "" }} - {{.Date | color "red" ""}} 20 | 21 | {{ "EXAMPLES" | color "" "heading" }}: 22 | {{ .Name | color "green" "bold" }} {{ "mrkaran.dev" | color "cyan" "" }} Query a domain using defaults. 23 | {{ .Name | color "green" "bold" }} {{ "mrkaran.dev CNAME" | color "cyan" "" }} Query for a CNAME record. 24 | {{ .Name | color "green" "bold" }} {{ "mrkaran.dev MX @9.9.9.9" | color "cyan" "" }} Uses a custom DNS resolver. 25 | {{ .Name | color "green" "bold" }} {{"-q mrkaran.dev -t MX -n 1.1.1.1" | color "yellow" ""}} Using named arguments. 26 | 27 | {{ "Free Form Arguments" | color "" "heading" }}: 28 | Supply hostnames, query types, and classes without flags. Example: 29 | {{ .Name | color "green" "bold" }} {{"mrkaran.dev A @1.1.1.1" | color "cyan" "" }} 30 | 31 | {{ "Transport Options" | color "" "heading" }}: 32 | Specify the protocol with a URL-type scheme. 33 | UDP is used if no scheme is specified. 34 | 35 | {{"@udp://" | color "yellow" ""}} eg: @1.1.1.1 initiates a {{"UDP" | color "cyan" ""}} query to 1.1.1.1:53. 36 | {{"@tcp://" | color "yellow" ""}} eg: @tcp://1.1.1.1 initiates a {{"TCP" | color "cyan" ""}} query to 1.1.1.1:53. 37 | {{"@https://" | color "yellow" ""}} eg: @https://cloudflare-dns.com/dns-query initiates a {{"DOH" | color "cyan" ""}} query to Cloudflare via DoH. 38 | {{"@tls://" | color "yellow" ""}} eg: @tls://1.1.1.1 initiates a {{"DoT" | color "cyan" ""}} query to 1.1.1.1:853. 39 | {{"@sdns://" | color "yellow" ""}} initiates a {{"DNSCrypt" | color "cyan" ""}} or {{"DoH" | color "cyan" ""}} query using a DNS stamp. 40 | {{"@quic://" | color "yellow" ""}} initiates a {{"DOQ" | color "cyan" ""}} query. 41 | 42 | {{ "Query Options" | color "" "heading" }}: 43 | {{"-q, --query=HOSTNAME" | color "yellow" ""}} Hostname to query the DNS records for (eg {{"mrkaran.dev" | color "cyan" ""}}). 44 | {{"-t, --type=TYPE" | color "yellow" ""}} Type of the DNS Record ({{"A, MX, NS" | color "cyan" ""}} etc). 45 | {{"-n, --nameserver=ADDR" | color "yellow" ""}} Address of a specific nameserver to send queries to ({{"9.9.9.9, 8.8.8.8" | color "cyan" ""}} etc). 46 | {{"-c, --class=CLASS" | color "yellow" ""}} Network class of the DNS record ({{"IN, CH, HS" | color "cyan" ""}} etc). 47 | {{"-x, --reverse" | color "yellow" ""}} Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively. 48 | 49 | {{ "Resolver Options" | color "" "heading" }}: 50 | {{"--strategy=STRATEGY" | color "yellow" ""}} Specify strategy to query nameserver listed in etc/resolv.conf. ({{"all, random, first" | color "cyan" ""}}). 51 | {{"--ndots=INT" | color "yellow" ""}} Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise. 52 | {{"--search" | color "yellow" ""}} Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list. 53 | {{"--timeout" | color "yellow" ""}} Specify timeout (in seconds) for the resolver to return a response. 54 | {{"-4 --ipv4" | color "yellow" ""}} Use IPv4 only. 55 | {{"-6 --ipv6" | color "yellow" ""}} Use IPv6 only. 56 | {{"--ndots=INT" | color "yellow" ""}} Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise. 57 | {{"--tls-hostname=HOSTNAME" | color "yellow" ""}} Provide a hostname for doing verification of the certificate if the provided DoT nameserver is an IP. 58 | {{"--skip-hostname-verification" | color "yellow" ""}} Skip TLS Hostname Verification in case of DOT Lookups. 59 | {{"--header" | color "yellow" ""}} HTTP headers to supply to the resolver. Can supply multiple times. 60 | {{"--root-cas=FILE" | color "yellow" ""}} Root CAs file to use for TLS verification. 61 | 62 | {{ "Output Options" | color "" "heading" }}: 63 | {{"-J, --json " | color "yellow" ""}} Format the output as JSON. 64 | {{"--short" | color "yellow" ""}} Short output format. Shows only the response section. 65 | {{"--color " | color "yellow" ""}} Defaults to true. Set --color=false to disable colored output. 66 | {{"--debug " | color "yellow" ""}} Enable debug logging. 67 | {{"--time" | color "yellow" ""}} Shows how long the response took from the server. 68 | ` 69 | 70 | func renderCustomHelp() { 71 | helpTmplVars := map[string]string{ 72 | "Name": "doggo", 73 | "Description": "DNS Client for Humans", 74 | "Version": buildVersion, 75 | "Date": buildDate, 76 | } 77 | tmpl, err := template.New("test").Funcs(template.FuncMap{ 78 | "color": func(clr string, format string, str string) string { 79 | formatter := color.New() 80 | switch c := clr; c { 81 | case "yellow": 82 | formatter = formatter.Add(color.FgYellow) 83 | case "red": 84 | formatter = formatter.Add(color.FgRed) 85 | case "cyan": 86 | formatter = formatter.Add(color.FgCyan) 87 | case "green": 88 | formatter = formatter.Add(color.FgGreen) 89 | } 90 | switch f := format; f { 91 | case "bold": 92 | formatter = formatter.Add(color.Bold) 93 | case "underline": 94 | formatter = formatter.Add(color.Underline) 95 | case "heading": 96 | formatter = formatter.Add(color.Bold, color.Underline) 97 | } 98 | return formatter.SprintFunc()(str) 99 | }, 100 | }).Parse(appHelpTextTemplate) 101 | if err != nil { 102 | // should ideally never happen. 103 | panic(err) 104 | } 105 | err = tmpl.Execute(color.Output, helpTmplVars) 106 | if err != nil { 107 | // should ideally never happen. 108 | panic(err) 109 | } 110 | os.Exit(0) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/resolvers/doq.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "os" 12 | "time" 13 | 14 | "github.com/miekg/dns" 15 | "github.com/quic-go/quic-go" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // DOQResolver represents the config options for setting up a DOQ based resolver. 20 | type DOQResolver struct { 21 | tls *tls.Config 22 | server string 23 | resolverOptions Options 24 | } 25 | 26 | // NewDOQResolver accepts a nameserver address and configures a DOQ based resolver. 27 | func NewDOQResolver(server string, resolverOpts Options) (Resolver, error) { 28 | return &DOQResolver{ 29 | tls: &tls.Config{ 30 | NextProtos: []string{"doq"}, 31 | RootCAs: resolverOpts.RootCAs, 32 | }, 33 | server: server, 34 | resolverOptions: resolverOpts, 35 | }, nil 36 | } 37 | 38 | // Lookup takes a dns.Question and sends them to DNS Server. 39 | // It parses the Response from the server in a custom output format. 40 | func (r *DOQResolver) Lookup(question dns.Question) (Response, error) { 41 | var ( 42 | rsp Response 43 | messages = prepareMessages(question, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 44 | ) 45 | 46 | session, err := quic.DialAddr(context.TODO(), r.server, r.tls, nil) 47 | if err != nil { 48 | return rsp, err 49 | } 50 | defer session.CloseWithError(quic.ApplicationErrorCode(quic.NoError), "") 51 | 52 | for _, msg := range messages { 53 | r.resolverOptions.Logger.WithFields(logrus.Fields{ 54 | "domain": msg.Question[0].Name, 55 | "ndots": r.resolverOptions.Ndots, 56 | "nameserver": r.server, 57 | }).Debug("Attempting to resolve") 58 | 59 | // ref: https://www.rfc-editor.org/rfc/rfc9250.html#name-dns-message-ids 60 | msg.Id = 0 61 | 62 | // get the DNS Message in wire format. 63 | var b []byte 64 | b, err = msg.Pack() 65 | if err != nil { 66 | return rsp, err 67 | } 68 | now := time.Now() 69 | 70 | var stream quic.Stream 71 | stream, err = session.OpenStream() 72 | if err != nil { 73 | return rsp, err 74 | } 75 | 76 | var msgLen = uint16(len(b)) 77 | var msgLenBytes = []byte{byte(msgLen >> 8), byte(msgLen & 0xFF)} 78 | _, err = stream.Write(msgLenBytes) 79 | if err != nil { 80 | return rsp, err 81 | } 82 | // Make a QUIC request to the DNS server with the DNS message as wire format bytes in the body. 83 | _, err = stream.Write(b) 84 | if err != nil { 85 | return rsp, err 86 | } 87 | 88 | err = stream.SetDeadline(time.Now().Add(r.resolverOptions.Timeout)) 89 | if err != nil { 90 | return rsp, err 91 | } 92 | 93 | var buf []byte 94 | buf, err = io.ReadAll(stream) 95 | if err != nil { 96 | if errors.Is(err, os.ErrDeadlineExceeded) { 97 | return rsp, fmt.Errorf("timeout") 98 | } 99 | return rsp, err 100 | } 101 | rtt := time.Since(now) 102 | 103 | _ = stream.Close() 104 | 105 | packetLen := binary.BigEndian.Uint16(buf[:2]) 106 | if packetLen != uint16(len(buf[2:])) { 107 | return rsp, fmt.Errorf("packet length mismatch") 108 | } 109 | err = msg.Unpack(buf[2:]) 110 | if err != nil { 111 | return rsp, err 112 | } 113 | // pack questions in output. 114 | for _, q := range msg.Question { 115 | ques := Question{ 116 | Name: q.Name, 117 | Class: dns.ClassToString[q.Qclass], 118 | Type: dns.TypeToString[q.Qtype], 119 | } 120 | rsp.Questions = append(rsp.Questions, ques) 121 | } 122 | // get the authorities and answers. 123 | output := parseMessage(&msg, rtt, r.server) 124 | rsp.Authorities = output.Authorities 125 | rsp.Answers = output.Answers 126 | 127 | if len(output.Answers) > 0 { 128 | // stop iterating the searchlist. 129 | break 130 | } 131 | } 132 | return rsp, nil 133 | } 134 | 135 | func (r *DOQResolver) SetTLSConfig(tlsCfg *tls.Config) { 136 | r.tls = tlsCfg 137 | } 138 | 139 | // Lookup1 likes Lookup, but return a list of dns.Msg instead. 140 | func (r *DOQResolver) Lookup1(question dns.Question) ([]dns.Msg, error) { 141 | messages := prepareMessages(question, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 142 | resp := make([]dns.Msg, 0, len(messages)) 143 | 144 | network := "udp" 145 | if isIPv6(r.server) || question.Qtype == dns.TypeAAAA { 146 | network = "udp6" 147 | } 148 | udpAddr, err := net.ResolveUDPAddr(network, r.server) 149 | if err != nil { 150 | return nil, err 151 | } 152 | udpConn, err := net.ListenPacket(network, "") 153 | if err != nil { 154 | return nil, err 155 | } 156 | defer udpConn.Close() 157 | 158 | session, err := quic.Dial(context.TODO(), udpConn, udpAddr, r.tls, nil) 159 | if err != nil { 160 | return nil, err 161 | } 162 | defer session.CloseWithError(quic.ApplicationErrorCode(quic.NoError), "") 163 | 164 | for _, msg := range messages { 165 | // ref: https://www.rfc-editor.org/rfc/rfc9250.html#name-dns-message-ids 166 | msg.Id = 0 167 | 168 | // get the DNS Message in wire format. 169 | var b []byte 170 | b, err = msg.Pack() 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | var stream quic.Stream 176 | stream, err = session.OpenStreamSync(context.Background()) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | var msgLen = uint16(len(b)) 182 | var msgLenBytes = []byte{byte(msgLen >> 8), byte(msgLen & 0xFF)} 183 | _, err = stream.Write(msgLenBytes) 184 | if err != nil { 185 | return nil, err 186 | } 187 | // Make a QUIC request to the DNS server with the DNS message as wire format bytes in the body. 188 | _, err = stream.Write(b) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | err = stream.SetDeadline(time.Now().Add(r.resolverOptions.Timeout)) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | buf, err := io.ReadAll(stream) 199 | if err != nil { 200 | if errors.Is(err, os.ErrDeadlineExceeded) { 201 | return nil, fmt.Errorf("timeout") 202 | } 203 | return nil, err 204 | } 205 | // io.ReadAll hide the io.EOF error returned by quic-go server. 206 | // Once we figure out why quic-go server sends io.EOF after running 207 | // for a long time, we can have a better way to handle this. For now, 208 | // make sure io.EOF error returned, so the caller can handle it cleanly. 209 | if len(buf) == 0 { 210 | return nil, io.EOF 211 | } 212 | _ = stream.Close() 213 | 214 | packetLen := binary.BigEndian.Uint16(buf[:2]) 215 | if packetLen != uint16(len(buf[2:])) { 216 | return nil, fmt.Errorf("packet length mismatch,got: %d, want: %d", packetLen, len(buf[2:])) 217 | } 218 | if err := msg.Unpack(buf[2:]); err != nil { 219 | return nil, err 220 | } 221 | resp = append(resp, msg) 222 | } 223 | return resp, nil 224 | } 225 | 226 | func isIPv6(ip string) bool { 227 | justIP, _, err := net.SplitHostPort(ip) 228 | if err != nil { 229 | justIP = ip 230 | } 231 | parsedIP := net.ParseIP(justIP) 232 | return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil 233 | } 234 | -------------------------------------------------------------------------------- /cmd/doggo/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/knadh/koanf" 12 | "github.com/knadh/koanf/providers/posflag" 13 | "github.com/mr-karan/doggo/internal/app" 14 | "github.com/mr-karan/doggo/pkg/resolvers" 15 | "github.com/mr-karan/doggo/pkg/utils" 16 | "github.com/sirupsen/logrus" 17 | flag "github.com/spf13/pflag" 18 | ) 19 | 20 | var ( 21 | // Version and date of the build. This is injected at build-time. 22 | buildVersion = "unknown" 23 | buildDate = "unknown" 24 | logger = utils.InitLogger() 25 | k = koanf.New(".") 26 | headerFlags arrayFlags 27 | ) 28 | 29 | type arrayFlags []string 30 | 31 | func (i *arrayFlags) Type() string { 32 | return "slice" 33 | } 34 | 35 | func (i *arrayFlags) String() string { 36 | return "" 37 | } 38 | 39 | func (i *arrayFlags) Set(value string) error { 40 | *i = append(*i, value) 41 | return nil 42 | } 43 | 44 | func main() { 45 | // Initialize app. 46 | app := app.New(logger, buildVersion) 47 | 48 | // Configure Flags. 49 | f := flag.NewFlagSet("config", flag.ContinueOnError) 50 | 51 | // Custom Help Text. 52 | f.Usage = renderCustomHelp 53 | 54 | // Query Options. 55 | f.StringSliceP("query", "q", []string{}, "Domain name to query") 56 | f.StringSliceP("type", "t", []string{}, "Type of DNS record to be queried (A, AAAA, MX etc)") 57 | f.StringSliceP("class", "c", []string{}, "Network class of the DNS record to be queried (IN, CH, HS etc)") 58 | f.StringSliceP("nameservers", "n", []string{}, "Address of the nameserver to send packets to") 59 | f.BoolP("reverse", "x", false, "Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively.") 60 | 61 | // Resolver Options 62 | f.Int("timeout", 5, "Sets the timeout for a query to T seconds. The default timeout is 5 seconds.") 63 | f.Bool("search", true, "Use the search list provided in resolv.conf. It sets the `ndots` parameter as well unless overridden by `ndots` flag.") 64 | f.Int("ndots", -1, "Specify the ndots parameter. Default value is taken from resolv.conf and fallbacks to 1 if ndots statement is missing in resolv.conf") 65 | f.BoolP("ipv4", "4", false, "Use IPv4 only") 66 | f.BoolP("ipv6", "6", false, "Use IPv6 only") 67 | f.String("strategy", "all", "Strategy to query nameservers in resolv.conf file (`all`, `random`, `first`)") 68 | f.String("tls-hostname", "", "Provide a hostname for doing verification of the certificate if the provided DoT nameserver is an IP") 69 | f.Bool("skip-hostname-verification", false, "Skip TLS Hostname Verification") 70 | f.Var(&headerFlags, "header", "Headers to supply to DoH resolvers") 71 | f.String("root-cas", "", "Root CAs file to use for TLS verification") 72 | 73 | // Output Options 74 | f.BoolP("json", "J", false, "Set the output format as JSON") 75 | f.Bool("short", false, "Short output format") 76 | f.Bool("time", false, "Display how long it took for the response to arrive") 77 | f.Bool("color", true, "Show colored output") 78 | f.Bool("debug", false, "Enable debug mode") 79 | 80 | f.Bool("version", false, "Show version of doggo") 81 | 82 | // Parse and Load Flags. 83 | err := f.Parse(os.Args[1:]) 84 | if err != nil { 85 | app.Logger.WithError(err).Error("error parsing flags") 86 | app.Logger.Exit(2) 87 | } 88 | if err = k.Load(posflag.Provider(f, ".", k), nil); err != nil { 89 | app.Logger.WithError(err).Error("error loading flags") 90 | f.Usage() 91 | app.Logger.Exit(2) 92 | } 93 | 94 | // If version flag is set, output version and quit. 95 | if k.Bool("version") { 96 | fmt.Printf("%s - %s\n", buildVersion, buildDate) 97 | app.Logger.Exit(0) 98 | } 99 | 100 | // Set log level. 101 | if k.Bool("debug") { 102 | // Set logger level 103 | app.Logger.SetLevel(logrus.DebugLevel) 104 | } else { 105 | app.Logger.SetLevel(logrus.InfoLevel) 106 | } 107 | 108 | // Unmarshall flags to the app. 109 | err = k.Unmarshal("", &app.QueryFlags) 110 | if err != nil { 111 | app.Logger.WithError(err).Error("error loading args") 112 | app.Logger.Exit(2) 113 | } 114 | 115 | // Load all `non-flag` arguments 116 | // which will be parsed separately. 117 | nsvrs, qt, qc, qn := loadUnparsedArgs(f.Args()) 118 | app.QueryFlags.Nameservers = append(app.QueryFlags.Nameservers, nsvrs...) 119 | app.QueryFlags.QTypes = append(app.QueryFlags.QTypes, qt...) 120 | app.QueryFlags.QClasses = append(app.QueryFlags.QClasses, qc...) 121 | app.QueryFlags.QNames = append(app.QueryFlags.QNames, qn...) 122 | 123 | app.Logger.WithField("query_flags", fmt.Sprintf("%+v", app.QueryFlags)).Debug("Loaded config") 124 | 125 | // Check if reverse flag is passed. If it is, then set 126 | // query type as PTR and query class as IN. 127 | // Modify query name like 94.2.0.192.in-addr.arpa if it's an IPv4 address. 128 | // Use IP6.ARPA nibble format otherwise. 129 | 130 | if app.QueryFlags.ReverseLookup { 131 | app.ReverseLookup() 132 | } 133 | 134 | // Load fallbacks. 135 | app.LoadFallbacks() 136 | 137 | // Load Questions. 138 | app.PrepareQuestions() 139 | 140 | // Load Nameservers. 141 | err = app.LoadNameservers() 142 | if err != nil { 143 | app.Logger.WithError(err).Error("error loading nameservers") 144 | app.Logger.Exit(2) 145 | } 146 | 147 | // load doh headers 148 | resolverHeaders := http.Header{} 149 | if len(headerFlags) > 0 { 150 | for _, header := range headerFlags { 151 | header, value, _ := strings.Cut(header, ":") 152 | if len(value) > 0 { 153 | resolverHeaders.Add(header, strings.TrimSpace(value)) 154 | } 155 | } 156 | } 157 | 158 | var certPool *x509.CertPool 159 | 160 | if app.QueryFlags.RootCAs != "" { 161 | certPool = x509.NewCertPool() 162 | 163 | rootCAs, err := os.ReadFile(app.QueryFlags.RootCAs) 164 | if err != nil { 165 | app.Logger.WithError(err).Error("error reading root CAs") 166 | app.Logger.Exit(2) 167 | } 168 | 169 | ok := certPool.AppendCertsFromPEM(rootCAs) 170 | if !ok { 171 | app.Logger.Error("error loading root CAs") 172 | app.Logger.Exit(2) 173 | } 174 | } 175 | 176 | // Load Resolvers. 177 | rslvrs, err := resolvers.LoadResolvers(resolvers.Options{ 178 | Nameservers: app.Nameservers, 179 | UseIPv4: app.QueryFlags.UseIPv4, 180 | UseIPv6: app.QueryFlags.UseIPv6, 181 | SearchList: app.ResolverOpts.SearchList, 182 | Ndots: app.ResolverOpts.Ndots, 183 | Timeout: app.QueryFlags.Timeout * time.Second, 184 | Logger: app.Logger, 185 | Strategy: app.QueryFlags.Strategy, 186 | InsecureSkipVerify: app.QueryFlags.InsecureSkipVerify, 187 | TLSHostname: app.QueryFlags.TLSHostname, 188 | Headers: resolverHeaders, 189 | RootCAs: certPool, 190 | }) 191 | if err != nil { 192 | app.Logger.WithError(err).Error("error loading resolver") 193 | app.Logger.Exit(2) 194 | } 195 | app.Resolvers = rslvrs 196 | 197 | // Run the app. 198 | app.Logger.Debug("Starting doggo 🐶") 199 | if len(app.QueryFlags.QNames) == 0 { 200 | f.Usage() 201 | app.Logger.Exit(0) 202 | } 203 | 204 | // Resolve Queries. 205 | var responses []resolvers.Response 206 | for _, q := range app.Questions { 207 | for _, rslv := range app.Resolvers { 208 | resp, err := rslv.Lookup(q) 209 | if err != nil { 210 | app.Logger.WithError(err).Error("error looking up DNS records") 211 | } 212 | responses = append(responses, resp) 213 | } 214 | } 215 | app.Output(responses) 216 | 217 | // Quitting. 218 | app.Logger.Exit(0) 219 | } 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 |

doggo

5 |

6 | 🐶 Command-line DNS client for humans 7 |
8 | doggo.mrkaran.dev 9 |

10 | doggo CLI usage 11 |

12 | 13 | --- 14 | 15 | **doggo** is a modern command-line DNS client (like _dig_) written in Golang. It outputs information in a neat concise manner and supports protocols like DoH, DoT, DoQ, and DNSCrypt as well. 16 | 17 | It's totally inspired from [dog](https://github.com/ogham/dog/) which is written in Rust. I wanted to add some features to it but since I don't know Rust, I found it as a nice opportunity 18 | to experiment with writing a DNS Client from scratch in `Go` myself. Hence the name `dog` +`go` => **doggo**. 19 | 20 | ## Features 21 | 22 | - Human readable output - supports **colors** and **tabular** format. 23 | - Supports **JSON** format - can be useful while writing scripts. 24 | - Has support for multiple transport protocols: 25 | - DNS over **HTTPS** (DoH) 26 | - DNS over **TLS** (DoT) 27 | - DNS over **QUIC** (DoQ) 28 | - DNS over **TCP** 29 | - DNS over **UDP** 30 | - DNS over **DNSCrypt** 31 | - Supports **ndots** and **search** configurations from `resolv.conf` or command-line arguments. 32 | - Supports multiple resolvers at once. 33 | - Supports IPv4 **and** IPv6 _both_. 34 | - Available as a web tool as well: [https://doggo.mrkaran.dev](https://doggo.mrkaran.dev). 35 | - Shell completions for `zsh` and `fish`. 36 | - Reverse DNS Lookups. 37 | 38 | ## Installation 39 | 40 | ### Binary 41 | 42 | You can grab the latest binaries for Linux, MacOS and Windows from the [Releases](https://github.com/mr-karan/doggo/releases) section. 43 | 44 | For eg, to pull the latest `linux-amd64` binary: 45 | 46 | ```shell 47 | $ cd "$(mktemp -d)" 48 | $ curl -sL "https://github.com/mr-karan/doggo/releases/download/v0.3.7/doggo_0.3.7_linux_amd64.tar.gz" | tar xz 49 | $ mv doggo /usr/local/bin 50 | # doggo should be available now in your $PATH 51 | $ doggo 52 | ``` 53 | ### Docker 54 | 55 | Images are hosted on Github Container Registry (ghcr.io). 56 | You can view all the tags [here](https://github.com/users/mr-karan/packages/container/package/doggo). It even supports **ARM** so you can spin up a container on your RPi to do DNS lookups, cause why not. 57 | 58 | **Pull** 59 | 60 | `docker pull ghcr.io/mr-karan/doggo:latest` 61 | 62 | **Running** 63 | 64 | You can supply all arguments to the CLI directly to `docker run` command. Eg: 65 | 66 | `docker run ghcr.io/mr-karan/doggo:latest mrkaran.dev @1.1.1.1 MX` 67 | 68 | ### Package Managers 69 | 70 | #### Homebrew 71 | 72 | Install via [Homebrew](https://brew.sh/) 73 | 74 | ```bash 75 | $ brew install doggo 76 | ``` 77 | 78 | #### Arch 79 | 80 | ```bash 81 | yay -S doggo-bin 82 | ``` 83 | 84 | ### From Source 85 | 86 | You need to have `go` installed in your system. 87 | 88 | ```bash 89 | $ go install github.com/mr-karan/doggo/cmd/doggo@latest 90 | ``` 91 | 92 | The binary will be available at `$GOPATH/bin/doggo`. 93 | 94 | ## Usage Examples 95 | 96 | **Do a simple DNS Lookup for `mrkaran.dev`** 97 | 98 | ```bash 99 | $ doggo mrkaran.dev 100 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 101 | mrkaran.dev. A IN 20s 13.250.205.9 127.0.0.1:53 102 | mrkaran.dev. A IN 20s 206.189.89.118 127.0.0.1:53 103 | ``` 104 | 105 | **Query MX records for `github.com` using `9.9.9.9` resolver** 106 | 107 | ``` 108 | doggo MX github.com @9.9.9.9 109 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 110 | github.com. MX IN 3600s 10 alt3.aspmx.l.google.com. 9.9.9.9:53 111 | github.com. MX IN 3600s 5 alt1.aspmx.l.google.com. 9.9.9.9:53 112 | github.com. MX IN 3600s 10 alt4.aspmx.l.google.com. 9.9.9.9:53 113 | github.com. MX IN 3600s 5 alt2.aspmx.l.google.com. 9.9.9.9:53 114 | github.com. MX IN 3600s 1 aspmx.l.google.com. 9.9.9.9:53 115 | ``` 116 | 117 | or using _named parameters_: 118 | 119 | ```bash 120 | $ doggo -t MX -n 9.9.9.9 github.com 121 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 122 | github.com. MX IN 3600s 10 alt3.aspmx.l.google.com. 9.9.9.9:53 123 | github.com. MX IN 3600s 5 alt1.aspmx.l.google.com. 9.9.9.9:53 124 | github.com. MX IN 3600s 10 alt4.aspmx.l.google.com. 9.9.9.9:53 125 | github.com. MX IN 3600s 5 alt2.aspmx.l.google.com. 9.9.9.9:53 126 | github.com. MX IN 3600s 1 aspmx.l.google.com. 9.9.9.9:53 127 | ``` 128 | 129 | **Query DNS records for `archive.org` using Cloudflare DoH resolver** 130 | 131 | ```bash 132 | $ doggo archive.org @https://cloudflare-dns.com/dns-query 133 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 134 | archive.org. A IN 41s 207.241.224.2 https://cloudflare-dns.com/dns-query 135 | ``` 136 | 137 | **Query DNS records for `internetfreedom.in` with JSON output** 138 | 139 | ```bash 140 | $ doggo internetfreedom.in --json | jq 141 | { 142 | "responses": { 143 | "answers": [ 144 | { 145 | "name": "internetfreedom.in.", 146 | "type": "A", 147 | "class": "IN", 148 | "ttl": "22s", 149 | "address": "104.27.158.96", 150 | "rtt": "37ms", 151 | "nameserver": "127.0.0.1:53" 152 | }, 153 | { 154 | "name": "internetfreedom.in.", 155 | "type": "A", 156 | "class": "IN", 157 | "ttl": "22s", 158 | "address": "104.27.159.96", 159 | "rtt": "37ms", 160 | "nameserver": "127.0.0.1:53" 161 | }, 162 | { 163 | "name": "internetfreedom.in.", 164 | "type": "A", 165 | "class": "IN", 166 | "ttl": "22s", 167 | "address": "172.67.202.77", 168 | "rtt": "37ms", 169 | "nameserver": "127.0.0.1:53" 170 | } 171 | ], 172 | "queries": [ 173 | { 174 | "name": "internetfreedom.in.", 175 | "type": "A", 176 | "class": "IN" 177 | } 178 | ] 179 | } 180 | } 181 | ``` 182 | 183 | **Query DNS records for `duckduckgo.com` and show RTT (Round Trip Time)** 184 | 185 | ```bash 186 | $ doggo duckduckgo.com --time 187 | NAME TYPE CLASS TTL ADDRESS NAMESERVER TIME TAKEN 188 | duckduckgo.com. A IN 30s 40.81.94.43 127.0.0.1:53 45ms 189 | ``` 190 | 191 | ## Command-line Arguments 192 | 193 | ![](www/static/help.png) 194 | 195 | ### Transport Options 196 | 197 | URL scheme of the server is used to identify which resolver to use for lookups. If no scheme is specified, defaults to `udp`. 198 | 199 | ``` 200 | @udp:// eg: @1.1.1.1 initiates a UDP resolver for 1.1.1.1:53. 201 | @tcp:// eg: @1.1.1.1 initiates a TCP resolver for 1.1.1.1:53. 202 | @https:// eg: @https://cloudflare-dns.com/dns-query initiates a DOH resolver for Cloudflare DoH server. 203 | @tls:// eg: @1.1.1.1 initiates a DoT resolver for 1.1.1.1:853. 204 | @sdns:// eg: @sdns://AgcAAAAAAAAABzEuMC4wLjEAEmRucy5jbG91ZGZsYXJlLmNvbQovZG5zLXF1ZXJ5 205 | initiates a DNSCrypt or DoH resolver using its DNS stamp. 206 | @quic:// eg: @quic://dns.adguard.com 207 | initiates a DNS over QUIC resolver for Adguard DNS Resolver. 208 | ``` 209 | 210 | ### Query Options 211 | 212 | ``` 213 | -q, --query=HOSTNAME Hostname to query the DNS records for (eg mrkaran.dev). 214 | -t, --type=TYPE Type of the DNS Record (A, MX, NS etc). 215 | -n, --nameserver=ADDR Address of a specific nameserver to send queries to (9.9.9.9, 8.8.8.8 etc). 216 | -c, --class=CLASS Network class of the DNS record (IN, CH, HS etc). 217 | ``` 218 | 219 | ### Resolver Options 220 | 221 | ``` 222 | --strategy=STRATEGY Specify strategy to query nameserver listed in etc/resolv.conf. Defaults to `all` (`random`, `first`, `all`). 223 | --ndots=INT Specify ndots parameter. Takes value from /etc/resolv.conf if using the system nameserver or 1 otherwise. 224 | --search Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list. 225 | --timeout Specify timeout (in seconds) for the resolver to return a response. 226 | -4 --ipv4 Use IPv4 only. 227 | -6 --ipv6 Use IPv6 only. 228 | --tls-hostname=HOSTNAME Provide a hostname for doing verification of the certificate if the provided DoT nameserver is an IP. 229 | --skip-hostname-verification Skip TLS Hostname Verification in case of DOT Lookups. 230 | ``` 231 | 232 | 233 | ### Output Options 234 | 235 | ``` 236 | -J, --json Format the output as JSON. 237 | --color Defaults to true. Set --color=false to disable colored output. 238 | --debug Enable debug logging. 239 | --time Shows how long the response took from the server. 240 | --short Short output format. Shows only the response section. 241 | ``` 242 | 243 | --- 244 | 245 | ## Contributing 246 | 247 | I'm open to accept feature requests and/or issues. I understand `doggo` is a new DNS Client in the town and there might be some edge cases I am not handling. 248 | Please feel free to open issues if you ever come across such a case. 249 | For now I am focussing more on [planned features](TODO.md) for a **stable** v1.0 release _soon_. 250 | 251 | ## License 252 | 253 | [LICENSE](./LICENSE) 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/AdguardTeam/golibs v0.29.0 h1:NG3eUXaUwRTgKssblolh4XHME8MQCCdogyIZxxv4bOU= 4 | github.com/AdguardTeam/golibs v0.29.0/go.mod h1:vjw1OVZG6BYyoqGRY88U4LCJLOMfhBFhU0UJBdaSAuQ= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 7 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 8 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= 9 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 15 | github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs= 16 | github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE= 17 | github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= 18 | github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= 19 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 23 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 24 | github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= 25 | github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= 26 | github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= 27 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= 28 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= 29 | github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= 33 | github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 34 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 35 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 36 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 37 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 38 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 39 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 40 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 41 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 42 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 43 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 44 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 45 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 47 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 49 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 50 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 51 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 52 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 53 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 54 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 55 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 56 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 57 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 58 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 59 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 60 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 61 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 62 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 63 | github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= 64 | github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= 65 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 66 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 67 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 68 | github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= 69 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 70 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 71 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 72 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 73 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 74 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 75 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 76 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 77 | github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 78 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 79 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 80 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 81 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 82 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 83 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 84 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 85 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 86 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 87 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 88 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 89 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 90 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 91 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 92 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 93 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 94 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 95 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 96 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 97 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 98 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 99 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 100 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 101 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 102 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 103 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 104 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 108 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 109 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 110 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 111 | github.com/google/pprof v0.0.0-20241023014458-598669927662 h1:SKMkD83p7FwUqKmBsPdLHF5dNyxq3jOWwu9w9UyH5vA= 112 | github.com/google/pprof v0.0.0-20241023014458-598669927662/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 113 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 114 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 115 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 116 | github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= 117 | github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= 118 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 119 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 120 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 121 | github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= 122 | github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 123 | github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 124 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 125 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 126 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 127 | github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 128 | github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= 129 | github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 130 | github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 131 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 132 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 133 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 134 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 135 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 136 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 137 | github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 138 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 139 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 140 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 141 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 142 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 143 | github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= 144 | github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= 145 | github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= 146 | github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= 147 | github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= 148 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 149 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 150 | github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= 151 | github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= 152 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 153 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 154 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 155 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 156 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 157 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 158 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 159 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 160 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 161 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 162 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 163 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 164 | github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= 165 | github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= 166 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 167 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 168 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 169 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 170 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 171 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 172 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 173 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 174 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 175 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 176 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 177 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 178 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 179 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 180 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 181 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 182 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 183 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 184 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 185 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 186 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 187 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 188 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 189 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 190 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 191 | github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 192 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 193 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 194 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 195 | github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= 196 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 197 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 198 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 199 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 200 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 201 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 202 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 203 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 204 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 205 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 206 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 207 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 208 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 209 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 210 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 211 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 212 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 213 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 214 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 215 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 216 | github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= 217 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 218 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 219 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 220 | github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= 221 | github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= 222 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 223 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 224 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 225 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 226 | github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 227 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 228 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 229 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 230 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 231 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 232 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 233 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 234 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 235 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 236 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 237 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 238 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 239 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 240 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 241 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 242 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 243 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 244 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 245 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 246 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 247 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 248 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 249 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 250 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 251 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 252 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 253 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 254 | github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA= 255 | github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 256 | github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= 257 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 258 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 259 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 260 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 261 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 262 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 263 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 264 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 265 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 266 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 267 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 268 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 269 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 270 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 271 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 272 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 273 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 274 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 275 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 276 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 277 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 278 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 279 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 280 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 281 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 282 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 283 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 284 | go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= 285 | go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 286 | go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= 287 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 288 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 289 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 290 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 291 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 292 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 293 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 294 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 295 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 296 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 297 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 298 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 299 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 300 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 301 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 302 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 303 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 304 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 305 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 306 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 307 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 308 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 309 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 310 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 311 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 312 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 313 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 314 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 315 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 316 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 317 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 318 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 319 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 320 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 321 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 322 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 323 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 324 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 325 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 326 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 327 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 328 | golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 329 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 330 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 331 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 332 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 333 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 334 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 335 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 336 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 337 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 338 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 339 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 340 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 341 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 342 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 343 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 344 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 345 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 346 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 347 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 348 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 349 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 350 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 351 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 352 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 374 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 375 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 376 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 377 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 378 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 379 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 380 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 381 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 382 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 383 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 384 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 385 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 386 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 387 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 388 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 389 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 390 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 391 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 392 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 393 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 394 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 395 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 396 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 397 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 398 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 399 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 400 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 401 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 402 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 403 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 404 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 405 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 406 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 407 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 408 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 409 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 410 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 411 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 412 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 413 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 414 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 415 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 416 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 417 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 418 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 419 | google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 420 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 421 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 422 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 423 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 424 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 425 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 426 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 427 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 428 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 429 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 430 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 431 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 432 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 433 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 434 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 435 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 436 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 437 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 438 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 439 | gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= 440 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 441 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 442 | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 443 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 444 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 445 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 446 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 447 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 448 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 449 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 450 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 451 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 452 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 453 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 454 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 455 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 456 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 457 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 458 | --------------------------------------------------------------------------------