From 510eee69ebf5a1293fb96f8623e4e2aa9f5dd130 Mon Sep 17 00:00:00 2001 From: Ali Mosajjal Date: Sat, 13 May 2023 16:29:38 +1200 Subject: [PATCH] V2 (#48) * v2 mvp * minor changes and fixes * fixed the timer on geoip, added loglevel * added cidr acl * Update install.sh * minor yq fixes * better acl docs and var names * allow individual binds for different services * added ability to generate default config yaml * clearer parameters in geoip configs * cleaner installer wizard * added version info in build * removed extra whitespace * ACL overhaul * tweaked release parameters * converted defaultconfig to boolean * removed unused doh client * doh fixes * change override log to debug * doq connects back to the correct upstream * bugfixes in dns and ACL * moved back to zerolog and pretty logging * more fixes in logging format * fix in debug logging of DoQ * minor changes in cidr acl logic * add acl tests (#47) * add acl tests * moved yaml configs to the test file * added back previous tests and fixed the run function --------- Co-authored-by: Ali Mosajjal * acl stringifier * minor docs changes in the install.sh script --------- Co-authored-by: Andreas Groll <10852221+holygrolli@users.noreply.github.com> --- .github/workflows/release.yml | 4 +- .gitignore | 1 + Dockerfile | 6 +- acl/acl.go | 104 +++++++ acl/acl_test.go | 204 +++++++++++++ acl/cidr.go | 143 +++++++++ acl/domain.go | 179 +++++++++++ acl/geoip.go | 184 +++++++++++ acl/override.go | 130 ++++++++ cidr.csv | 2 + config.defaults.yaml | 97 ++++++ config.sample.json | 24 -- dns.go | 231 +++++--------- dns_test.go | 23 +- doc.go | 36 +-- certtools.go => dohserver/certtools.go | 2 +- dohserver/config.go | 65 ++++ dohserver/google.go | 206 +++++++++++++ dohserver/ietf.go | 218 ++++++++++++++ dohserver/main.go | 67 +++++ dohserver/parse_test.go | 119 ++++++++ dohserver/server.go | 402 +++++++++++++++++++++++++ dohserver/version.go | 29 ++ duration.go | 32 -- geoip.go | 113 ------- go.mod | 41 ++- go.sum | 368 +++++++++++++++++++++- httpproxy.go | 67 ++--- https.go | 131 +++----- install.sh | 34 ++- main.go | 313 +++++++++---------- 31 files changed, 2862 insertions(+), 713 deletions(-) create mode 100644 acl/acl.go create mode 100644 acl/acl_test.go create mode 100644 acl/cidr.go create mode 100644 acl/domain.go create mode 100644 acl/geoip.go create mode 100644 acl/override.go create mode 100644 cidr.csv create mode 100644 config.defaults.yaml delete mode 100644 config.sample.json rename certtools.go => dohserver/certtools.go (99%) create mode 100644 dohserver/config.go create mode 100644 dohserver/google.go create mode 100644 dohserver/ietf.go create mode 100644 dohserver/main.go create mode 100644 dohserver/parse_test.go create mode 100644 dohserver/server.go create mode 100644 dohserver/version.go delete mode 100644 duration.go delete mode 100644 geoip.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb5e3c7..38e299b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - goarch: "386" goos: darwin - goarch: "arm" - goos: darwin + goos: darwin steps: - uses: actions/checkout@v3 - uses: wangyoucao577/go-release-action@master @@ -27,6 +27,6 @@ jobs: goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} goversion: 1.20.3 - ldflags: "-s -w" + ldflags: "-s -w -X main.version=${{ github.event.release.tag_name }} -X main.commit=${{ github.sha }}" build_flags: -v diff --git a/.gitignore b/.gitignore index 1f68af3..5d79290 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,6 @@ _testmain.go *.test *.prof sniproxy +config.yaml .TODO.md diff --git a/Dockerfile b/Dockerfile index 9fed5ca..25cfd4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,9 @@ RUN mkdir /app ADD . /app/ WORKDIR /app ENV CGO_ENABLED=0 -RUN go build -o main . -CMD ["/app/main"] +RUN go build -ldflags "-s -w -X main.version=$(git describe --tags) -X main.commit=$(git rev-parse HEAD)" -o sniproxy . +CMD ["/app/sniproxy"] FROM scratch -COPY --from=0 /app/main /sniproxy +COPY --from=0 /app/sniproxy /sniproxy ENTRYPOINT ["/sniproxy"] diff --git a/acl/acl.go b/acl/acl.go new file mode 100644 index 0000000..d3a7a9d --- /dev/null +++ b/acl/acl.go @@ -0,0 +1,104 @@ +package acl + +import ( + "fmt" + "net" + "sort" + + "github.com/knadh/koanf" + "github.com/rs/zerolog" +) + +// Decision is the type of decision that an ACL can make for each connection info +type Decision uint8 + +func (d Decision) String() string { + switch d { + case Accept: + return "Accept" + case Reject: + return "Reject" + case ProxyIP: + return "ProxyIP" + case OriginIP: + return "OriginIP" + case Override: + return "Override" + default: + return "Unknown" + } +} + +const ( + // Accept shows the indifference of the ACL to the connection + Accept Decision = iota + // Reject shows that the ACL has rejected the connection. each ACL should check this before proceeding to check the connection against its rules + Reject + // ProxyIP shows that the ACL has decided to proxy the connection through sniproxy rather than the origin IP + ProxyIP + // OriginIP shows that the ACL has decided to proxy the connection through the origin IP rather than sniproxy + OriginIP + // Override shows that the ACL has decided to override the connection and proxy it through the specified DstIP and DstPort + Override +) + +// ConnInfo contains all the information about a connection that is available +// it also serves as an ACL enforcer in a sense that if IsRejected is set to true +// the connection is dropped +type ConnInfo struct { + SrcIP net.Addr + DstIP net.TCPAddr + Domain string + Decision +} + +type ByPriority []ACL + +func (a ByPriority) Len() int { return len(a) } +func (a ByPriority) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByPriority) Less(i, j int) bool { return a[i].Priority() < a[j].Priority() } + +type ACL interface { + Decide(*ConnInfo) error + Name() string + Priority() uint + ConfigAndStart(*zerolog.Logger, *koanf.Koanf) error +} + +// StartACLs starts all the ACLs that have been configured and registered +func StartACLs(log *zerolog.Logger, k *koanf.Koanf) ([]ACL, error) { + var a []ACL + aclK := k.Cut("acl") + for _, acl := range availableACLs { + // cut each konaf based on the name of the ACL + // only configure if the "enabled" key is set to true + if !aclK.Bool(fmt.Sprintf("%s.enabled", (acl).Name())) { + continue + } + var l = log.With().Str("acl", (acl).Name()).Logger() + // we pass the full config to each ACL so that they can cut it themselves. it's needed for some ACLs that need + // to read the config of other ACLs or the global config + if err := acl.ConfigAndStart(&l, k); err != nil { + log.Warn().Msgf("failed to start ACL %s with error %s", (acl).Name(), err) + return a, err + } + a = append(a, acl) + log.Info().Msgf("started ACL: '%s'", (acl).Name()) + + } + return a, nil +} + +// MakeDecision loops through all the ACLs and makes a decision for the connection +func MakeDecision(c *ConnInfo, a []ACL) error { + sort.Sort(ByPriority(a)) + for _, acl := range a { + if err := acl.Decide(c); err != nil { + return err + } + } + return nil +} + +// each ACL should register itself by appending itself to this list +var availableACLs []ACL diff --git a/acl/acl_test.go b/acl/acl_test.go new file mode 100644 index 0000000..6d27a53 --- /dev/null +++ b/acl/acl_test.go @@ -0,0 +1,204 @@ +package acl + +import ( + "net" + "os" + "testing" + "time" + + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/rs/zerolog" +) + +var logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339, NoColor: true}) + +var configs = map[string]string{ + "acl_domain.yaml": ` +acl: + domain: + enabled: true + priority: 20 + path: ../domains.csv + refresh_interval: 1h0m0s`, + "acl_cidr.yaml": ` +acl: + cidr: + enabled: true + priority: 30 + path: ../cidr.csv + refresh_interval: 1h0m0s`, + "acl_domain_cidr.yaml": ` +acl: + domain: + enabled: true + priority: 20 + path: ../domains.csv + refresh_interval: 1h0m0s + cidr: + enabled: true + priority: 30 + path: ../cidr.csv + refresh_interval: 1h0m0s`, + "acl_cidr_domain.yaml": ` +acl: + domain: + enabled: true + priority: 20 + path: ../domains.csv + refresh_interval: 1h0m0s + cidr: + enabled: true + priority: 19 + path: ../cidr.csv + refresh_interval: 1h0m0s`, +} + +func TestMakeDecision(t *testing.T) { + // Test cases + cases := []struct { + connInfo *ConnInfo + config string + expected Decision + }{ + { + // domain in domains.csv + connInfo: mockConnInfo("1.1.1.1", "ipinfo.io"), + config: configs["acl_domain.yaml"], + expected: ProxyIP, + }, + { + // domain NOT in domains.csv + connInfo: mockConnInfo("2.2.2.2", "google.de"), + config: configs["acl_domain.yaml"], + expected: OriginIP, + }, + { + // ip REJECT in cidr.csv + // if you want to whitelist IPs then you must include "0.0.0.0/0,reject" otherwise always accepted!! + connInfo: mockConnInfo("1.1.1.1", "google.de"), + config: configs["acl_cidr.yaml"], + expected: Reject, + }, + { + // ip ACCEPT in cidr.csv + connInfo: mockConnInfo("77.77.1.1", "google.de"), + config: configs["acl_cidr.yaml"], + expected: Accept, + }, + { + // ip ACCEPT in cidr.csv, still no ProxyIP (acl.domain not enabled) + connInfo: mockConnInfo("77.77.1.1", "ipinfo.io"), + config: configs["acl_cidr.yaml"], + expected: Accept, + }, + { + // domain in domains.csv, ip ACCEPT in cidr.csv + connInfo: mockConnInfo("77.77.1.1", "ipinfo.io"), + config: configs["acl_domain_cidr.yaml"], + expected: ProxyIP, + }, + { + // domain NOT in domains.csv, ip ACCEPT in cidr.csv + connInfo: mockConnInfo("77.77.1.1", "google.de"), + config: configs["acl_domain_cidr.yaml"], + expected: OriginIP, + }, + { + // domain in domains.csv, ip REJECT in cidr.csv + connInfo: mockConnInfo("1.1.1.1", "ipinfo.io"), + config: configs["acl_domain_cidr.yaml"], + expected: Reject, // still returns OriginIP in DNS !!! + }, + { + // domain NOT in domains.csv, ip REJECT in cidr.csv + connInfo: mockConnInfo("1.1.1.1", "google.de"), + config: configs["acl_domain_cidr.yaml"], + expected: Reject, // still returns OriginIP in DNS !!! + }, + { + // domain NOT in domains.csv, ip ACCEPT in cidr.csv + connInfo: mockConnInfo("77.77.1.1", "google.de"), + config: configs["acl_cidr_domain.yaml"], + expected: OriginIP, + }, + { + // domain in domains.csv, ip ACCEPT in cidr.csv + connInfo: mockConnInfo("77.77.1.1", "ipinfo.io"), + config: configs["acl_cidr_domain.yaml"], + expected: ProxyIP, + }, + { + // domain in domains.csv, ip REJECT in cidr.csv + connInfo: mockConnInfo("1.1.1.1", "google.de"), + config: configs["acl_cidr_domain.yaml"], + expected: Reject, + }, + { + // domain NOT in domains.csv, ip REJECT in cidr.csv + connInfo: mockConnInfo("1.1.1.1", "google.de"), + config: configs["acl_cidr_domain.yaml"], + expected: Reject, + }, + } + + // Run the test cases + for _, tc := range cases { + t.Run(tc.config, func(t *testing.T) { + MakeDecision(tc.connInfo, getAcls(&logger, tc.config)) + if tc.expected != tc.connInfo.Decision { + t.Errorf("MakeDecision (domain=%v,ip=%v,config=%v) decided %v, expected %v", tc.connInfo.Domain, tc.connInfo.SrcIP, tc.config, tc.connInfo.Decision, tc.expected) + } + }) + } +} + +// TestReverse tests the reverse function +func TestReverse(t *testing.T) { + tests := []struct { + name string + s string + want string + }{ + {name: "test1", s: "abc", want: "cba"}, + {name: "test2", s: "a", want: "a"}, + {name: "test3", s: "aab", want: "baa"}, + {name: "test4", s: "zzZ", want: "Zzz"}, + {name: "test5", s: "ab2", want: "2ba"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := reverse(tt.s); got != tt.want { + t.Errorf("reverse() = %v, want %v", got, tt.want) + } + }) + } +} + +func getAcls(log *zerolog.Logger, config string) []ACL { + var k = koanf.New(".") + if err := k.Load(rawbytes.Provider([]byte(config)), yaml.Parser()); err != nil { + log.Fatal().Msgf("error loading config file: %v", err) + } + a, err := StartACLs(&logger, k) + if err != nil { + panic(err) + } + // we need this to give acl time to (re)load + time.Sleep(1 * time.Second) + return a +} + +func mockConnInfo(srcIP string, domain string) *ConnInfo { + addr, err := net.ResolveTCPAddr("tcp", srcIP+":80") + + if err != nil { + logger.Fatal().Msgf("error parsing ip from string: %v", err) + } + + return &ConnInfo{ + SrcIP: addr, + Domain: domain, + } +} diff --git a/acl/cidr.go b/acl/cidr.go new file mode 100644 index 0000000..73a90b5 --- /dev/null +++ b/acl/cidr.go @@ -0,0 +1,143 @@ +package acl + +import ( + "bufio" + "fmt" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/knadh/koanf" + "github.com/rs/zerolog" + "github.com/yl2chen/cidranger" +) + +// CIDR acl allows sniproxy to use a list of CIDR to allow or reject connections +// The list is loaded from a file or URL and refreshed periodically +// The list is a CSV file with the CIDR in the first column and the policy in the second +// The policy can be allow or reject and defaults to reject +type cidr struct { + Path string `yaml:"path"` + RefreshInterval time.Duration `yaml:"refresh_interval"` + AllowRanger cidranger.Ranger + RejectRanger cidranger.Ranger + logger *zerolog.Logger + priority uint +} + +func (d *cidr) LoadCIDRCSV(path string) error { + d.AllowRanger = cidranger.NewPCTrieRanger() + d.RejectRanger = cidranger.NewPCTrieRanger() + + d.logger.Info().Msg("Loading the CIDR from file/url") + var scanner *bufio.Scanner + if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { + d.logger.Info().Msg("CIDR list is a URL, trying to fetch") + client := http.Client{ + CheckRedirect: func(r *http.Request, via []*http.Request) error { + r.URL.Opaque = r.URL.Path + return nil + }, + } + resp, err := client.Get(path) + if err != nil { + d.logger.Err(err) + return err + } + d.logger.Info().Msgf("(re)fetching URL: %s", path) + defer resp.Body.Close() + scanner = bufio.NewScanner(resp.Body) + + } else { + file, err := os.Open(path) + if err != nil { + return err + } + d.logger.Info().Msgf("(re)loading file: %s", path) + defer file.Close() + scanner = bufio.NewScanner(file) + } + + for scanner.Scan() { + row := scanner.Text() + // cut the line at the first comma + cidr, policy, found := strings.Cut(row, ",") + if !found { + d.logger.Info().Msg(cidr + " is not a valid csv line, assuming reject") + } + if policy == "allow" { + if _, netw, err := net.ParseCIDR(cidr); err == nil { + _ = d.AllowRanger.Insert(cidranger.NewBasicRangerEntry(*netw)) + } else { + if _, netw, err := net.ParseCIDR(cidr + "/32"); err == nil { + _ = d.AllowRanger.Insert(cidranger.NewBasicRangerEntry(*netw)) + } else { + d.logger.Err(err) + } + } + } else { + if _, netw, err := net.ParseCIDR(cidr); err == nil { + _ = d.RejectRanger.Insert(cidranger.NewBasicRangerEntry(*netw)) + } else { + if _, netw, err := net.ParseCIDR(cidr + "/32"); err == nil { + _ = d.RejectRanger.Insert(cidranger.NewBasicRangerEntry(*netw)) + } else { + d.logger.Err(err) + } + } + } + } + d.logger.Info().Msgf("%d cidr(s) loaded", d.AllowRanger.Len()) + + return nil +} + +func (d *cidr) loadCIDRCSVWorker() { + for { + _ = d.LoadCIDRCSV(d.Path) + time.Sleep(d.RefreshInterval) + } +} + +// Decide checks if the connection is allowed or rejected +func (d cidr) Decide(c *ConnInfo) error { + // get the IP from the connection + ipPort := strings.Split(c.SrcIP.String(), ":") + ip := net.ParseIP(ipPort[0]) + + prevDec := c.Decision + + if match, err := d.RejectRanger.Contains(ip); match && err == nil { + c.Decision = Reject + } + if match, err := d.AllowRanger.Contains(ip); match && err == nil { + c.Decision = prevDec + } + return nil +} + +// Name function is used to cut the YAML config file to be passed on to the ACL for config +func (d cidr) Name() string { + return "cidr" +} +func (d cidr) Priority() uint { + return d.priority +} + +// Config function is what starts the ACL +func (d *cidr) ConfigAndStart(logger *zerolog.Logger, c *koanf.Koanf) error { + c = c.Cut(fmt.Sprintf("acl.%s", d.Name())) + d.logger = logger + d.Path = c.String("path") + d.priority = uint(c.Int("priority")) + d.RefreshInterval = c.Duration("refresh_interval") + go d.loadCIDRCSVWorker() + return nil +} + +// make the acl available at import time +func init() { + availableACLs = append(availableACLs, &cidr{}) +} diff --git a/acl/domain.go b/acl/domain.go new file mode 100644 index 0000000..0a5a342 --- /dev/null +++ b/acl/domain.go @@ -0,0 +1,179 @@ +package acl + +import ( + "bufio" + "fmt" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/golang-collections/collections/tst" + "github.com/knadh/koanf" + "github.com/rs/zerolog" +) + +// domain ACL makes a decision on a connection based on the domain name derived +// from client hello's SNI. It can be used to skip the sni proxy for certain domains +type domain struct { + Path string `yaml:"domain.path"` + RefreshInterval time.Duration `yaml:"domain.refresh_interval"` + routePrefixes *tst.TernarySearchTree + routeSuffixes *tst.TernarySearchTree + routeFQDNs map[string]uint8 + logger *zerolog.Logger + priority uint +} + +const ( + matchPrefix = uint8(1) + matchSuffix = uint8(2) + matchFQDN = uint8(3) +) + +// inDomainList returns true if the domain is meant to be SKIPPED and not go through sni proxy +func (d domain) inDomainList(fqdn string) bool { + if !strings.HasSuffix(fqdn, ".") { + fqdn = fqdn + "." + } + fqdnLower := strings.ToLower(fqdn) + // check for fqdn match + if d.routeFQDNs[fqdnLower] == matchFQDN { + return false + } + // check for prefix match + if longestPrefix := d.routePrefixes.GetLongestPrefix(fqdnLower); longestPrefix != nil { + // check if the longest prefix is present in the type hashtable as a prefix + if d.routeFQDNs[longestPrefix.(string)] == matchPrefix { + return false + } + } + // check for suffix match. Note that suffix is just prefix reversed + if longestSuffix := d.routeSuffixes.GetLongestPrefix(reverse(fqdnLower)); longestSuffix != nil { + // check if the longest suffix is present in the type hashtable as a suffix + if d.routeFQDNs[longestSuffix.(string)] == matchSuffix { + return false + } + } + return true +} + +func reverse(s string) string { + r := []rune(s) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return string(r) +} + +// LoadDomainsCsv loads a domains Csv file/URL. returns 3 parameters: +// 1. a TST for all the prefixes (type 1) +// 2. a TST for all the suffixes (type 2) +// 3. a hashtable for all the full match fqdn (type 3) +func (d *domain) LoadDomainsCsv(Filename string) error { + d.logger.Info().Msg("Loading the domain from file/url") + var scanner *bufio.Scanner + if strings.HasPrefix(Filename, "http://") || strings.HasPrefix(Filename, "https://") { + d.logger.Info().Msg("domain list is a URL, trying to fetch") + client := http.Client{ + CheckRedirect: func(r *http.Request, via []*http.Request) error { + r.URL.Opaque = r.URL.Path + return nil + }, + } + resp, err := client.Get(Filename) + if err != nil { + d.logger.Err(err) + return err + } + d.logger.Info().Msgf("(re)fetching URL: %s", Filename) + defer resp.Body.Close() + scanner = bufio.NewScanner(resp.Body) + + } else { + file, err := os.Open(Filename) + if err != nil { + return err + } + d.logger.Info().Msgf("(re)loading file: %s", Filename) + defer file.Close() + scanner = bufio.NewScanner(file) + } + for scanner.Scan() { + lowerCaseLine := strings.ToLower(scanner.Text()) + // split the line by comma to understand thed.logger.c + fqdn := strings.Split(lowerCaseLine, ",") + if len(fqdn) != 2 { + d.logger.Info().Msg(lowerCaseLine + " is not a valid line, assuming FQDN") + fqdn = []string{lowerCaseLine, "fqdn"} + } + // add the fqdn to the hashtable with its type + switch entryType := fqdn[1]; entryType { + case "prefix": + d.routeFQDNs[fqdn[0]] = matchPrefix + d.routePrefixes.Insert(fqdn[0], fqdn[0]) + case "suffix": + d.routeFQDNs[fqdn[0]] = matchSuffix + // suffix match is much faster if we reverse the strings and match for prefix + d.routeSuffixes.Insert(reverse(fqdn[0]), fqdn[0]) + case "fqdn": + d.routeFQDNs[fqdn[0]] = matchFQDN + default: + //d.logger.Warnf("%s is not a valid line, assuming fqdn", lowerCaseLine) + d.logger.Info().Msg(lowerCaseLine + " is not a valid line, assuming FQDN") + d.routeFQDNs[fqdn[0]] = matchFQDN + } + } + d.logger.Info().Msgf("%s loaded with %d prefix, %d suffix and %d fqdn", Filename, d.routePrefixes.Len(), d.routeSuffixes.Len(), len(d.routeFQDNs)-d.routePrefixes.Len()-d.routeSuffixes.Len()) + + return nil +} + +func (d *domain) LoadDomainsCSVWorker() { + for { + d.LoadDomainsCsv(d.Path) + time.Sleep(d.RefreshInterval) + } +} + +// implement domain as an ACL interface +func (d domain) Decide(c *ConnInfo) error { + + if c.Decision == Reject { + c.DstIP = net.TCPAddr{IP: net.IPv4zero, Port: 0} + return nil + } + if d.inDomainList(c.Domain) { + d.logger.Debug().Msgf("domain not going through proxy: %s", c.Domain) + c.Decision = OriginIP + } else { + d.logger.Debug().Msgf("domain going through proxy: %s", c.Domain) + c.Decision = ProxyIP + } + return nil +} +func (d domain) Name() string { + return "domain" +} +func (d domain) Priority() uint { + return d.priority +} + +func (d *domain) ConfigAndStart(logger *zerolog.Logger, c *koanf.Koanf) error { + c = c.Cut(fmt.Sprintf("acl.%s", d.Name())) + d.logger = logger + d.routePrefixes = tst.New() + d.routeSuffixes = tst.New() + d.routeFQDNs = make(map[string]uint8) + d.Path = c.String("path") + d.priority = uint(c.Int("priority")) + d.RefreshInterval = c.Duration("refresh_interval") + go d.LoadDomainsCSVWorker() + return nil +} + +// make domain available to the ACL system at import time +func init() { + availableACLs = append(availableACLs, &domain{}) +} diff --git a/acl/geoip.go b/acl/geoip.go new file mode 100644 index 0000000..006b131 --- /dev/null +++ b/acl/geoip.go @@ -0,0 +1,184 @@ +package acl + +import ( + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/knadh/koanf" + "github.com/oschwald/maxminddb-golang" + "github.com/rs/zerolog" + "golang.org/x/exp/slices" +) + +// geoIP is an ACL that checks the geolocation of incoming connections and +// allows or rejects them based on the country of origin. It uses an MMDB +// database file to determine the country of origin. +// unlike ip and domain ACLs, geoIP does not load the list of countries +// from a CSV file. Instead it reads the country codes to reject or accept +// from the YAML configuration. This is due to the fact that domain and IP +// lists could be loaded from external resources and could be highly dynamic +// whereas geoip restrictions are usually static. +type geoIP struct { + Path string + AllowedCountries []string + BlockedCountries []string + Refresh time.Duration + mmdb *maxminddb.Reader + logger *zerolog.Logger + priority uint +} + +func toLowerSlice(in []string) (out []string) { + for _, v := range in { + out = append(out, strings.ToLower(v)) + } + return +} + +// getCountry returns the country code for the given IP address in ISO format +func (g geoIP) getCountry(ipAddr string) (string, error) { + ip := net.ParseIP(ipAddr) + var record struct { + Country struct { + ISOCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` + } // Or any appropriate struct + + err := g.mmdb.Lookup(ip, &record) + if err != nil { + return "", err + } + return record.Country.ISOCode, nil +} + +// initializeGeoIP loads the geolocation database from the specified g.Path. +func (g *geoIP) initializeGeoIP() error { + + g.logger.Info().Msg("Loading the domain from file/url") + var scanner []byte + if strings.HasPrefix(g.Path, "http://") || strings.HasPrefix(g.Path, "https://") { + g.logger.Info().Msg("domain list is a URL, trying to fetch") + resp, err := http.Get(g.Path) + if err != nil { + return err + } + g.logger.Info().Msgf("(re)fetching %s", g.Path) + defer resp.Body.Close() + scanner, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + + } else { + file, err := os.Open(g.Path) + if err != nil { + return err + } + g.logger.Info().Msgf("(re)loading File: %s", g.Path) + defer file.Close() + n, err := file.Read(scanner) + if err != nil { + return err + } + g.logger.Info().Msgf("geolocation database with %d bytes loaded", n) + + } + var err error + if g.mmdb, err = maxminddb.FromBytes(scanner); err != nil { + //g.logger.Warn("%d bytes read, %s", len(scanner), err) + return err + } + g.logger.Info().Msg("Loaded MMDB") + for range time.NewTicker(g.Refresh).C { + if g.mmdb, err = maxminddb.FromBytes(scanner); err != nil { + //g.logger.Warn("%d bytes read, %s", len(scanner), err) + return err + } + g.logger.Info().Msgf("Loaded MMDB %v", g.mmdb) + } + return nil +} + +// checkGeoIPSkip checks an IP address against the exclude and include lists and returns +// true if the IP address should be allowed to pass through. +// the logic is as follows: +// 1. if mmdb is not loaded or not available, it's fail-open (allow by default) +// 2. if the IP can't be resolved to a country, it's rejected +// 3. if the country is in the blocked list, it's rejected +// 4. if the country is in the allowed list, it's allowed +// note that the reject list is checked first and takes priority over the allow list +// if the IP's country doesn't match any of the above, it's allowed if the blocked list is not empty +// for example, if the blockedlist is [US] and the allowedlist is empty, a connection from +// CA will be allowed. but if blockedlist is empty and allowedlist is [US], a connection from +// CA will be rejected. +func (g geoIP) checkGeoIPSkip(addr net.Addr) bool { + if g.mmdb == nil { + return true + } + + ipPort := strings.Split(addr.String(), ":") + ip := ipPort[0] + + var country string + country, err := g.getCountry(ip) + country = strings.ToLower(country) + g.logger.Debug().Msgf("incoming tcp connection from ip %s and country %s", ip, country) + + if err != nil { + g.logger.Info().Msgf("Failed to get the geolocation of ip %s", ip) + return false + } + if slices.Contains(g.BlockedCountries, country) { + return false + } + if slices.Contains(g.AllowedCountries, country) { + return true + } + + // if exclusion is provided, the rest will be allowed + if len(g.BlockedCountries) > 0 { + return true + } + + // othewise fail + return false +} + +// implement the ACL interface +func (g geoIP) Decide(c *ConnInfo) error { + // in checkGeoIPSkip, false is reject + if !g.checkGeoIPSkip(c.SrcIP) { + g.logger.Info().Msgf("rejecting connection from ip %s", c.SrcIP) + c.Decision = Reject + } + g.logger.Debug().Msgf("GeoIP decision for ip %s is %#v", c.SrcIP, c.Decision) + return nil +} +func (g geoIP) Name() string { + return "geoip" +} +func (g geoIP) Priority() uint { + return g.priority +} + +func (g *geoIP) ConfigAndStart(logger *zerolog.Logger, c *koanf.Koanf) error { + c = c.Cut(fmt.Sprintf("acl.%s", g.Name())) + g.logger = logger + g.Path = c.String("path") + g.priority = uint(c.Int("priority")) + g.AllowedCountries = toLowerSlice(c.Strings("allowed")) + g.BlockedCountries = toLowerSlice(c.Strings("blocked")) + g.Refresh = c.Duration("refresh_interval") + go g.initializeGeoIP() + return nil +} + +// make the geoIP available at import time +func init() { + availableACLs = append(availableACLs, &geoIP{}) +} diff --git a/acl/override.go b/acl/override.go new file mode 100644 index 0000000..53f617a --- /dev/null +++ b/acl/override.go @@ -0,0 +1,130 @@ +package acl + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strings" + + "github.com/knadh/koanf" + doh "github.com/mosajjal/sniproxy/dohserver" + dohserver "github.com/mosajjal/sniproxy/dohserver" + "github.com/rs/zerolog" + "inet.af/tcpproxy" +) + +// override ACL. This ACL is used to override the destination IP to not be the one resolved by the upstream DNS or the proxy itself, rather a custom IP and port +// if the destination is HTTP, it uses tls_cert and tls_key certificate to terminate the original connection. +type override struct { + priority uint + rules map[string]string + doh *dohserver.Server + dohPort int + tcpproxy *tcpproxy.Proxy + tcpproxyport int + tlsCert string + tlsKey string + logger *zerolog.Logger +} + +// GetFreePort returns a random open port +func GetFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return GetFreePort() + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +// tcpproxy listens on a random port on localhost +func (o *override) startProxy() { + o.tcpproxy = new(tcpproxy.Proxy) + var err error + o.tcpproxyport, err = GetFreePort() + if err != nil { + o.logger.Error().Msgf("failed to get a free port for tcpproxy: %s", err) + return + } + for k, v := range o.rules { + o.logger.Info().Msgf("adding overide rule %s -> %s", k, v) + // TODO: create a regex matcher for SNIRoute + o.tcpproxy.AddSNIRoute(fmt.Sprintf("127.0.0.1:%d", o.tcpproxyport), k, tcpproxy.To(v)) + } + o.logger.Info().Msgf("starting tcpproxy on port %d", o.tcpproxyport) + o.tcpproxy.Run() +} + +func (o override) Decide(c *ConnInfo) error { + domain := strings.TrimSuffix(c.Domain, ".") + for k := range o.rules { + if strings.TrimSuffix(k, ".") == domain { + c.Decision = Override + a, _ := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", o.tcpproxyport)) + c.DstIP = *a + return nil + } + } + return nil +} + +func (o override) Name() string { + return "override" +} +func (o override) Priority() uint { + return o.priority +} + +func (o *override) ConfigAndStart(logger *zerolog.Logger, c *koanf.Koanf) error { + DNSBind := c.String("general.bind_dns_over_udp") + c = c.Cut(fmt.Sprintf("acl.%s", o.Name())) + tmpRules := c.StringMap("rules") + o.logger = logger + o.priority = uint(c.Int("priority")) + o.tlsCert = c.String("tls_cert") + o.tlsKey = c.String("tls_key") + if c.String("doh_sni") != "" { + dohSNI := c.String("doh_sni") + var err error + o.dohPort, err = GetFreePort() + if err != nil { + return err + } + dohConfig := dohserver.NewDefaultConfig() + dohConfig.Listen = []string{fmt.Sprintf("127.0.0.1:%d", o.dohPort)} + if o.tlsCert == "" || o.tlsKey == "" { + _, _, err := doh.GenerateSelfSignedCertKey(dohSNI, nil, nil, os.TempDir()) + o.logger.Info().Msg("certificate was not provided, generating a self signed cert in temp directory") + if err != nil { + o.logger.Error().Msgf("error while generating self-signed cert: %s", err) + } + o.tlsCert = filepath.Join(os.TempDir(), dohSNI+".crt") + o.tlsKey = filepath.Join(os.TempDir(), dohSNI+".key") + } + dohConfig.Cert = o.tlsCert + dohConfig.Key = o.tlsKey + dohConfig.Upstream = []string{fmt.Sprintf("udp:%s", DNSBind)} + dohS, err := dohserver.NewServer(dohConfig) + if err != nil { + return err + } + go dohS.Start() + // append a rule to the rules map to redirect the DoH SNI to DoH + tmpRules[dohSNI] = fmt.Sprintf("127.0.0.1:%d", o.dohPort) + } + o.rules = tmpRules + + go o.startProxy() + return nil +} + +// make domain available to the ACL system at import time +func init() { + availableACLs = append(availableACLs, &override{}) +} diff --git a/cidr.csv b/cidr.csv new file mode 100644 index 0000000..b125248 --- /dev/null +++ b/cidr.csv @@ -0,0 +1,2 @@ +77.77.0.0/16,allow +0.0.0.0/0,reject \ No newline at end of file diff --git a/config.defaults.yaml b/config.defaults.yaml new file mode 100644 index 0000000..c25b509 --- /dev/null +++ b/config.defaults.yaml @@ -0,0 +1,97 @@ +general: + # Upsteam DNS URI. examples: Upstream DNS URI. examples: udp://1.1.1.1:53, tcp://1.1.1.1:53, tcp-tls://1.1.1.1:853, https://dns.google/dns-query + upstream_dns: udp://8.8.8.8:53 + # Use a SOCKS proxy for upstream HTTP/HTTPS traffic. Example: socks5://admin: + upstream_socks5: + # DNS Port to listen on. Should remain 53 in most cases. MUST NOT be empty + bind_dns_over_udp: "0.0.0.0:53" + # enable DNS over TCP. empty disables it. example: "127.0.0.1:53" + bind_dns_over_tcp: + # enable DNS over TLS. empty disables it. example: "127.0.0.1:853" + bind_dns_over_tls: + # enable DNS over QUIC. empty disables it. example: "127.0.0.1:8853" + bind_dns_over_quic: + # Path to the certificate for DoH, DoT and DoQ. eg: /tmp/mycert.pem + tls_cert: + # Path to the certificate key for DoH, DoT and DoQ. eg: /tmp/mycert.key + tls_key: + # HTTP Port to listen on. Should remain 80 in most cases + bind_http: "0.0.0.0:80" + # HTTPS Port to listen on. Should remain 443 in most cases + bind_https: "0.0.0.0:443" + # Enable prometheus endpoint on IP:PORT. example: 127.0.0.1:8080. Always exposes /metrics and only supports HTTP + bind_prometheus: + # Interface used for outbound TLS connections. uses OS prefered one if empty + interface: + # Public IPv4 of the server, reply address of DNS A queries + public_ipv4: + # Public IPv6 of the server, reply address of DNS AAAA queries + public_ipv6: + # log level for the application. choices: debug, info, warn, error + # by default, the logs are colored so they are not suited for logging to a file. + # in order to disable colors, set NO_COLOR=true in the environment variables + log_level: info + +acl: + # geoip filtering + # + # the logic is as follows: + # 1. if mmdb is not loaded or not available, it's fail-open (allow by default) + # 2. if the IP can't be resolved to a country, it's rejected + # 3. if the country is in the blocked list, it's rejected + # 4. if the country is in the allowed list, it's allowed + # note that the reject list is checked first and takes priority over the allow list + # if the IP's country doesn't match any of the above, it's allowed if the blocked list is not empty + # for example, if the blockedlist is [US] and the allowedlist is empty, a connection from + # CA will be allowed. but if blockedlist is empty and allowedlist is [US], a connection from + # CA will be rejected. + geoip: + enabled: false + # priority of the geoip filter. lower priority means it's checked first, meaning it can be ovveriden by other ACLs with higehr priority number. + priority: 10 + # strictly blocked countries + blocked: + # allowed countries + allowed: + # Path to the MMDB file. eg: /tmp/Country.mmdb, https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb + path: + # Interval to re-fetch the MMDB file + refresh_interval: 24h0m0s + # domain filtering + domain: + enabled: false # false means ALL domains will be allowed to go through the proxy + # priority of the domain filter. lower priority means it's checked first. if multiple filters have the same priority, they're checked in random order + priority: 20 + # Path to the domain list. eg: /tmp/domainlist.csv. Look at the example file for the format. + path: + # Interval to re-fetch the domain list + refresh_interval: 1h0m0s + # IP/CIDR filtering + cidr: + enabled: false + # priority of the cidr filter. lower priority means it's checked first. if multiple filters have the same priority, they're checked in random order + priority: 30 + # Path to the CIDR list. eg: /tmp/cidr.csv. Look at the example file for the format. + path: + # Interval to re-fetch the domain list + refresh_interval: 1h0m0s + # FQDN override. This ACL is used to override the destination IP to not be the one resolved by the upstream DNS or the proxy itself, rather a custom IP and port + # if the destination is HTTP, it uses tls_cert and tls_key certificate to terminate the original connection. + override: + enabled: false + # priority of the override filter. lower priority means it's checked first. if multiple filters have the same priority, they're checked in random order + priority: 40 + # override rules. unlike others, this one does not require a path to a file. it's a map of FQDNs wildcards to IPs and ports. only HTTPS is supported + # currently, these rules are checked with a simple for loop and string matching, + # so it's not suited for a large number of rules. if you have a big list of rules + # use a reverse proxy in front of sniproxy rather than using sniproxy as a reverse proxy + rules: + "one.one.one.one": "1.1.1.1:443" + "google.com": "8.8.8.8:443" + # enable listening on DoH on a specific SNI. example: "myawesomedoh.example.com". empty disables it. If you need DoH to be enabled and don't want + # any other overrides, enable this ACL with empty rules. DoH SNI will add a default rule and start. + doh_sni: "myawesomedoh.example.com" + # Path to the certificate for handling tls decryption. eg: /tmp/mycert.pem + tls_cert: + # Path to the certificate key handling tls decryption. eg: /tmp/mycert.key + tls_key: diff --git a/config.sample.json b/config.sample.json deleted file mode 100644 index f4d86ef..0000000 --- a/config.sample.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "bindIP": "0.0.0.0", - "upstreamDNS": "udp://8.8.8.8:53", - "domainListPath": "domains.csv", - "domainListRefreshInterval": "60s", - "bindDnsOverTcp": false, - "bindDnsOverTls": false, - "bindDnsOverQuic": false, - "tlsCert": "", - "tlsKey": "", - "allDomains": false, - "dnsPort": 53, - "geoipExclude": "", - "geoipInclude": "", - "geoipPath": "", - "geoipRefreshInterval": "", - "httpPort": 80, - "httpsPort": 443, - "interface": "", - "publicIPv4": "", - "reverseProxy": "", - "reverseProxyCert": "", - "reverseProxyKey": "" -} diff --git a/dns.go b/dns.go index c0819e3..1baf3d7 100644 --- a/dns.go +++ b/dns.go @@ -1,136 +1,30 @@ package main import ( - "bufio" "context" "crypto/tls" "fmt" "net" - "net/http" - "os" "strings" "sync" "time" - "github.com/golang-collections/collections/tst" "github.com/mosajjal/dnsclient" doqserver "github.com/mosajjal/doqd/pkg/server" - slog "golang.org/x/exp/slog" + "github.com/mosajjal/sniproxy/acl" + "github.com/rs/zerolog" "github.com/miekg/dns" ) +// DNSClient is a wrapper around the DNS client type DNSClient struct { - C dnsclient.Client + dnsclient.Client } -var ( - matchPrefix = uint8(1) - matchSuffix = uint8(2) - matchFQDN = uint8(3) -) var dnsLock sync.RWMutex -var dnslog = slog.New(log.Handler().WithAttrs([]slog.Attr{{Key: "service", Value: slog.StringValue("dns")}})) - -// inDomainList returns true if the domain is meant to be SKIPPED and not go through sni proxy -func inDomainList(fqdn string) bool { - fqdnLower := strings.ToLower(fqdn) - // check for fqdn match - if c.routeFQDNs[fqdnLower] == matchFQDN { - return false - } - // check for prefix match - if longestPrefix := c.routePrefixes.GetLongestPrefix(fqdnLower); longestPrefix != nil { - // check if the longest prefix is present in the type hashtable as a prefix - if c.routeFQDNs[longestPrefix.(string)] == matchPrefix { - return false - } - } - // check for suffix match. Note that suffix is just prefix reversed - if longestSuffix := c.routeSuffixes.GetLongestPrefix(reverse(fqdnLower)); longestSuffix != nil { - // check if the longest suffix is present in the type hashtable as a suffix - if c.routeFQDNs[longestSuffix.(string)] == matchSuffix { - return false - } - } - return true -} - -func reverse(s string) string { - r := []rune(s) - for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { - r[i], r[j] = r[j], r[i] - } - return string(r) -} - -// LoadDomainsCsv loads a domains Csv file/URL. returns 3 parameters: -// 1. a TST for all the prefixes (type 1) -// 2. a TST for all the suffixes (type 2) -// 3. a hashtable for all the full match fqdn (type 3) -func LoadDomainsCsv(Filename string) (*tst.TernarySearchTree, *tst.TernarySearchTree, map[string]uint8, error) { - prefix := tst.New() - suffix := tst.New() - all := make(map[string]uint8) - dnslog.Info("Loading the domain from file/url") - var scanner *bufio.Scanner - if strings.HasPrefix(Filename, "http://") || strings.HasPrefix(Filename, "https://") { - dnslog.Info("domain list is a URL, trying to fetch") - client := http.Client{ - CheckRedirect: func(r *http.Request, via []*http.Request) error { - r.URL.Opaque = r.URL.Path - return nil - }, - } - resp, err := client.Get(Filename) - if err != nil { - dnslog.Error(err.Error()) - return prefix, suffix, all, err - } - dnslog.Info("(re)fetching URL", "url", Filename) - defer resp.Body.Close() - scanner = bufio.NewScanner(resp.Body) - - } else { - file, err := os.Open(Filename) - if err != nil { - return prefix, suffix, all, err - } - dnslog.Info("(re)loading File", "file", Filename) - defer file.Close() - scanner = bufio.NewScanner(file) - } - - for scanner.Scan() { - lowerCaseLine := strings.ToLower(scanner.Text()) - // split the line by comma to understand thednslog.c - fqdn := strings.Split(lowerCaseLine, ",") - if len(fqdn) != 2 { - dnslog.Info(lowerCaseLine + " is not a valid line, assuming FQDN") - fqdn = []string{lowerCaseLine, "fqdn"} - } - // add the fqdn to the hashtable with its type - switch entryType := fqdn[1]; entryType { - case "prefix": - all[fqdn[0]] = matchPrefix - prefix.Insert(fqdn[0], fqdn[0]) - case "suffix": - all[fqdn[0]] = matchSuffix - // suffix match is much faster if we reverse the strings and match for prefix - suffix.Insert(reverse(fqdn[0]), fqdn[0]) - case "fqdn": - all[fqdn[0]] = matchFQDN - default: - //dnslog.Warnf("%s is not a valid line, assuming fqdn", lowerCaseLine) - dnslog.Info(lowerCaseLine + " is not a valid line, assuming FQDN") - all[fqdn[0]] = matchFQDN - } - } - dnslog.Info(fmt.Sprintf("%s loaded with %d prefix, %d suffix and %d fqdn", Filename, prefix.Len(), suffix.Len(), len(all)-prefix.Len()-suffix.Len())) - - return prefix, suffix, all, nil -} +var dnslog = logger.With().Str("service", "dns").Logger() func (dnsc *DNSClient) performExternalAQuery(fqdn string, QType uint16) ([]dns.RR, time.Duration, error) { if !strings.HasSuffix(fqdn, ".") { @@ -142,29 +36,31 @@ func (dnsc *DNSClient) performExternalAQuery(fqdn string, QType uint16) ([]dns.R msg.SetQuestion(fqdn, QType) msg.SetEdns0(1232, true) dnsLock.Lock() - if dnsc.C == nil { - return nil, 0, fmt.Errorf("DNS client is not initialised") + if dnsc == nil { + return nil, 0, fmt.Errorf("dns client is not initialised") } - res, trr, err := dnsc.C.Query(context.Background(), &msg) + res, trr, err := dnsc.Query(context.Background(), &msg) if err != nil { if err.Error() == "EOF" { - dnslog.Info("reconnecting DNS...") + dnslog.Info().Msg("reconnecting DNS...") // dnsc.C.Close() // dnsc.C, err = dnsclient.New(c.UpstreamDNS, true) - err = c.dnsClient.C.Reconnect() + err = c.dnsClient.Reconnect() } } dnsLock.Unlock() return res, trr, err } -func processQuestion(q dns.Question) ([]dns.RR, error) { +func processQuestion(q dns.Question, decision acl.Decision) ([]dns.RR, error) { c.recievedDNS.Inc(1) // Check to see if we should respond with our own IP - if c.AllDomains || !inDomainList(q.Name) { - // Return the public IP. + switch decision { + + // Return the public IP. + case acl.ProxyIP, acl.Override: c.proxiedDNS.Inc(1) - dnslog.Info("returned sniproxy address for domain", "fqdn", q.Name) + dnslog.Info().Msgf("returned sniproxy address for domain %s", q.Name) if q.Qtype == dns.TypeA { rr, err := dns.NewRR(fmt.Sprintf("%s A %s", q.Name, c.PublicIPv4)) @@ -178,17 +74,23 @@ func processQuestion(q dns.Question) ([]dns.RR, error) { // return an empty response if we don't have an IPv6 address return []dns.RR{}, nil } - } + + // return empty response for rejected ACL + case acl.Reject: + // drop the request + dnslog.Debug().Msgf("rejected request for domain %s", q.Name) + return []dns.RR{}, nil // Otherwise do an upstream query and use that answer. - resp, rtt, err := c.dnsClient.performExternalAQuery(q.Name, q.Qtype) - if err != nil { - return nil, err + default: + resp, rtt, err := c.dnsClient.performExternalAQuery(q.Name, q.Qtype) + if err != nil { + return nil, err + } + dnslog.Info().Msgf("returned origin address for fqdn %s and rtt %s", q.Name, rtt) + return resp, nil } - - dnslog.Info("returned origin address", "fqdn", q.Name, "rtt", rtt) - - return resp, nil + return []dns.RR{}, nil } func (dnsc DNSClient) lookupDomain4(domain string) (net.IP, error) { @@ -224,9 +126,13 @@ func handleDNS(w dns.ResponseWriter, r *dns.Msg) { } for _, q := range m.Question { - answers, err := processQuestion(q) + connInfo := acl.ConnInfo{ + SrcIP: w.RemoteAddr(), + Domain: q.Name, + } + acl.MakeDecision(&connInfo, c.acl) + answers, err := processQuestion(q, connInfo.Decision) if err != nil { - dnslog.Error(err.Error()) continue } m.Answer = append(m.Answer, answers...) @@ -235,74 +141,83 @@ func handleDNS(w dns.ResponseWriter, r *dns.Msg) { w.WriteMsg(m) } -func runDNS() { +func runDNS(l zerolog.Logger) { + dnslog = l.With().Str("service", "dns").Logger() dns.HandleFunc(".", handleDNS) // start DNS UDP serverUdp - go func() { - serverUDP := &dns.Server{Addr: fmt.Sprintf(":%d", c.DNSPort), Net: "udp"} - dnslog.Info("Started UDP DNS", "host", "0.0.0.0", "port", c.DNSPort) - err := serverUDP.ListenAndServe() - defer serverUDP.Shutdown() - if err != nil { - dnslog.Error("Error starting UDP DNS server", err) - dnslog.Info(fmt.Sprintf("Failed to start server: %s\nYou can run the following command to pinpoint which process is listening on port %d\nsudo ss -pltun -at '( dport = :%d or sport = :%d )'", err.Error(), c.DNSPort, c.DNSPort, c.DNSPort)) - panic(2) - } - }() - + if c.BindDNSOverUDP != "" { + go func() { + serverUDP := &dns.Server{Addr: c.BindDNSOverUDP, Net: "udp"} + dnslog.Info().Msgf("started udp dns on %s", c.BindDNSOverUDP) + err := serverUDP.ListenAndServe() + defer serverUDP.Shutdown() + if err != nil { + dnslog.Error().Msgf("error starting udp dns server: %s", err) + dnslog.Info().Msgf("failed to start server: %s\nyou can run the following command to pinpoint which process is listening on your bind\nsudo ss -pltun", c.BindDNSOverUDP) + panic(2) + } + }() + } // start DNS UDP serverTcp - if c.BindDNSOverTCP { + if c.BindDNSOverTCP != "" { go func() { - serverTCP := &dns.Server{Addr: fmt.Sprintf(":%d", c.DNSPort), Net: "tcp"} - dnslog.Info("Started TCP DNS", "host", "0.0.0.0", "port", c.DNSPort) + serverTCP := &dns.Server{Addr: c.BindDNSOverTCP, Net: "tcp"} + dnslog.Info().Msgf("started tcp dns on %s", c.BindDNSOverTCP) err := serverTCP.ListenAndServe() defer serverTCP.Shutdown() if err != nil { - dnslog.Error("Failed to start server", err) - dnslog.Info(fmt.Sprintf("You can run the following command to pinpoint which process is listening on port %d\nsudo ss -pltun -at '( dport = :%d or sport = :%d )'", c.DNSPort, c.DNSPort, c.DNSPort)) + dnslog.Error().Msgf("failed to start server %s", err) + dnslog.Info().Msgf("failed to start server: %s\nyou can run the following command to pinpoint which process is listening on your bind\nsudo ss -pltun", c.BindDNSOverUDP) } }() } // start DNS UDP serverTls - if c.BindDNSOverTLS { + if c.BindDNSOverTLS != "" { go func() { crt, err := tls.LoadX509KeyPair(c.TLSCert, c.TLSKey) if err != nil { - dnslog.Error(err.Error()) + dnslog.Error().Msg(err.Error()) panic(2) } tlsConfig := &tls.Config{} tlsConfig.Certificates = []tls.Certificate{crt} - serverTLS := &dns.Server{Addr: ":853", Net: "tcp-tls", TLSConfig: tlsConfig} - dnslog.Info("Started DoT DNS", "host", "0.0.0.0", "port", 853) + serverTLS := &dns.Server{Addr: c.BindDNSOverTLS, Net: "tcp-tls", TLSConfig: tlsConfig} + dnslog.Info().Msgf("started dot dns on %s", c.BindDNSOverTLS) err = serverTLS.ListenAndServe() defer serverTLS.Shutdown() if err != nil { - dnslog.Error(err.Error()) + dnslog.Error().Msg(err.Error()) } }() } - if c.BindDNSOverQuic { + if c.BindDNSOverQuic != "" { crt, err := tls.LoadX509KeyPair(c.TLSCert, c.TLSKey) if err != nil { - dnslog.Error(err.Error()) + dnslog.Error().Msg(err.Error()) } tlsConfig := &tls.Config{} tlsConfig.Certificates = []tls.Certificate{crt} // Create the QUIC listener - doqServer, err := doqserver.New(":8853", crt, "127.0.0.1:53", true) + doqConf := doqserver.Config{ + ListenAddr: c.BindDNSOverQuic, + Cert: crt, + Upstream: c.BindDNSOverUDP, + TLSCompat: true, + Debug: httpslog.GetLevel() == zerolog.DebugLevel, + } + doqServer, err := doqserver.New(doqConf) if err != nil { - dnslog.Error(err.Error()) + dnslog.Error().Msg(err.Error()) } // Accept QUIC connections - dnslog.Info("Starting QUIC listener on :8853") + dnslog.Info().Msgf("starting quic listener %s", c.BindDNSOverQuic) go doqServer.Listen() } diff --git a/dns_test.go b/dns_test.go index 4126788..4adee23 100644 --- a/dns_test.go +++ b/dns_test.go @@ -7,33 +7,12 @@ import ( "github.com/mosajjal/dnsclient" ) -func Test_reverse(t *testing.T) { - tests := []struct { - name string - s string - want string - }{ - {name: "test1", s: "abc", want: "cba"}, - {name: "test2", s: "a", want: "a"}, - {name: "test3", s: "aab", want: "baa"}, - {name: "test4", s: "zzZ", want: "Zzz"}, - {name: "test5", s: "ab2", want: "2ba"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := reverse(tt.s); got != tt.want { - t.Errorf("reverse() = %v, want %v", got, tt.want) - } - }) - } -} - func TestDNSClient_lookupDomain4(t *testing.T) { tmp, err := dnsclient.New("udp://1.1.1.1:53", true, "") if err != nil { t.Errorf("failed to set up DNS client") } - dnsc := DNSClient{C: tmp} + dnsc := DNSClient{tmp} tests := []struct { client DNSClient name string diff --git a/doc.go b/doc.go index 6f28090..2ee7891 100644 --- a/doc.go +++ b/doc.go @@ -23,7 +23,7 @@ Using "go install" command: Using Docker or Podman: - docker run -d --pull always -p 80:80 -p 443:443 -p 53:53/udp -v "$(pwd):/tmp/" ghcr.io/mosajjal/sniproxy:latest --domainListPath https://raw.githubusercontent.com/mosajjal/sniproxy/master/domains.csv + docker run -d --pull always -p 80:80 -p 443:443 -p 53:53/udp -v "$(pwd)/config.defaults.yaml:/tmp/config.yaml" ghcr.io/mosajjal/sniproxy:latest --config /tmp/config.yaml Using the installer script: @@ -34,36 +34,14 @@ Using the installer script: sniproxy can be configured using a configuration file or command line flags. The configuration file is a JSON file, and an example configuration file can be found under config.sample.json. +Usage: + + sniproxy [flags] + Flags: - --allDomains Route all HTTP(s) traffic through the SNI proxy - --bindDnsOverQuic enable DNS over QUIC as well as UDP - --bindDnsOverTcp enable DNS over TCP as well as UDP - --bindDnsOverTls enable DNS over TLS as well as UDP - --bindIP string Bind 443 and 80 to a Specific IP Address. Doesn't apply to DNS Server. DNS Server always listens on 0.0.0.0 (default "0.0.0.0") - -c, --config string path to JSON configuration file - --dnsPort uint DNS Port to listen on. Should remain 53 in most cases (default 53) - --domainListPath string Path to the domain list. eg: /tmp/domainlist.csv. Look at the example file for the format. - --domainListRefreshInterval duration Interval to re-fetch the domain list (default 1h0m0s) - --geoipExclude strings Exclude countries to be allowed to connect. example: US,CA - --geoipInclude strings Include countries to be allowed to connect. example: US,CA - --geoipPath string path to MMDB URL/path - Example: https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb - --geoipRefreshInterval duration MMDB refresh interval (default 1h0m0s) - -h, --help help for sniproxy - --httpPort uint HTTP Port to listen on. Should remain 80 in most cases (default 80) - --httpsPort uint HTTPS Port to listen on. Should remain 443 in most cases (default 443) - --interface string Interface used for outbound TLS connections. uses OS prefered one if empty - --prometheus string Enable prometheus endpoint on IP:PORT. example: 127.0.0.1:8080. Always exposes /metrics and only supports HTTP - --publicIPv4 string Public IP of the server, reply address of DNS A queries (default "YOUR_IPv4") - --publicIPv6 string Public IPv6 of the server, reply address of DNS AAAA queries (default "YOUR_IPv6") - --reverseProxy string enable reverse proxy for a specific FQDN and upstream URL. example: www.example.com::http://127.0.0.1:4001 - --reverseProxyCert string Path to the certificate for reverse proxy. eg: /tmp/mycert.pem - --reverseProxyKey string Path to the certificate key for reverse proxy. eg: /tmp/mycert.key - --tlsCert string Path to the certificate for DoH, DoT and DoQ. eg: /tmp/mycert.pem - --tlsKey string Path to the certificate key for DoH, DoT and DoQ. eg: /tmp/mycert.key - --upstreamDNS string Upstream DNS URI. examples: udp://1.1.1.1:53, tcp://1.1.1.1:53, tcp-tls://1.1.1.1:853, https://dns.google/dns-query (default "udp://8.8.8.8:53") - --upstreamSOCKS5 string Use a SOCKS proxy for upstream HTTP/HTTPS traffic. Example: socks5://admin:admin@127.0.0.1:1080 + -c, --config string path to YAML configuration file (default "./config.defaults.yaml") + -h, --help help for sniproxy # Setting Up an SNI Proxy Using Vultr diff --git a/certtools.go b/dohserver/certtools.go similarity index 99% rename from certtools.go rename to dohserver/certtools.go index bd4acc1..8506312 100644 --- a/certtools.go +++ b/dohserver/certtools.go @@ -1,4 +1,4 @@ -package main +package doh import ( "bytes" diff --git a/dohserver/config.go b/dohserver/config.go new file mode 100644 index 0000000..446e95c --- /dev/null +++ b/dohserver/config.go @@ -0,0 +1,65 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +import ( + "regexp" +) + +type config struct { + Listen []string `toml:"listen"` + LocalAddr string `toml:"local_addr"` + Cert string `toml:"cert"` + Key string `toml:"key"` + Path string `toml:"path"` + Upstream []string `toml:"upstream"` + Timeout uint `toml:"timeout"` + Tries uint `toml:"tries"` + Verbose bool `toml:"verbose"` + DebugHTTPHeaders []string `toml:"debug_http_headers"` + LogGuessedIP bool `toml:"log_guessed_client_ip"` + ECSAllowNonGlobalIP bool `toml:"ecs_allow_non_global_ip"` + ECSUsePreciseIP bool `toml:"ecs_use_precise_ip"` + TLSClientAuth bool `toml:"tls_client_auth"` + TLSClientAuthCA string `toml:"tls_client_auth_ca"` +} + +var rxUpstreamWithTypePrefix = regexp.MustCompile("^[a-z-]+(:)") + +func addressAndType(us string) (string, string) { + p := rxUpstreamWithTypePrefix.FindStringSubmatchIndex(us) + if len(p) != 4 { + return "", "" + } + + return us[p[2]+1:], us[:p[2]] +} + +type configError struct { + err string +} + +func (e *configError) Error() string { + return e.err +} diff --git a/dohserver/google.go b/dohserver/google.go new file mode 100644 index 0000000..0059cb1 --- /dev/null +++ b/dohserver/google.go @@ -0,0 +1,206 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "strconv" + "strings" + "time" + + jsondns "github.com/m13253/dns-over-https/v2/json-dns" + "github.com/miekg/dns" + "golang.org/x/net/idna" +) + +func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest { + name := r.FormValue("name") + if name == "" { + return &DNSRequest{ + errcode: 400, + errtext: "Invalid argument value: \"name\"", + } + } + if punycode, err := idna.ToASCII(name); err == nil { + name = punycode + } else { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error()), + } + } + + rrTypeStr := r.FormValue("type") + rrType := uint16(1) + if rrTypeStr == "" { + } else if v, err := strconv.ParseUint(rrTypeStr, 10, 16); err == nil { + rrType = uint16(v) + } else if v, ok := dns.StringToType[strings.ToUpper(rrTypeStr)]; ok { + rrType = v + } else { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"type\" = %q", rrTypeStr), + } + } + + cdStr := r.FormValue("cd") + cd := false + if cdStr == "1" || strings.EqualFold(cdStr, "true") { + cd = true + } else if cdStr == "0" || strings.EqualFold(cdStr, "false") || cdStr == "" { + } else { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"cd\" = %q", cdStr), + } + } + + ednsClientSubnet := r.FormValue("edns_client_subnet") + ednsClientFamily := uint16(0) + ednsClientAddress := net.IP(nil) + ednsClientNetmask := uint8(255) + if ednsClientSubnet != "" { + if ednsClientSubnet == "0/0" { + ednsClientSubnet = "0.0.0.0/0" + } + + var err error + ednsClientFamily, ednsClientAddress, ednsClientNetmask, err = parseSubnet(ednsClientSubnet) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: err.Error(), + } + } + } else { + ednsClientAddress = s.findClientIP(r) + if ednsClientAddress == nil { + ednsClientNetmask = 0 + } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 56 + } + } + + msg := new(dns.Msg) + msg.SetQuestion(dns.Fqdn(name), rrType) + msg.CheckingDisabled = cd + opt := new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.SetUDPSize(dns.DefaultMsgSize) + opt.SetDo(true) + if ednsClientAddress != nil { + edns0Subnet := new(dns.EDNS0_SUBNET) + edns0Subnet.Code = dns.EDNS0SUBNET + edns0Subnet.Family = ednsClientFamily + edns0Subnet.SourceNetmask = ednsClientNetmask + edns0Subnet.SourceScope = 0 + edns0Subnet.Address = ednsClientAddress + opt.Option = append(opt.Option, edns0Subnet) + } + msg.Extra = append(msg.Extra, opt) + + return &DNSRequest{ + request: msg, + isTailored: ednsClientSubnet == "", + } +} + +func parseSubnet(ednsClientSubnet string) (ednsClientFamily uint16, ednsClientAddress net.IP, ednsClientNetmask uint8, err error) { + slash := strings.IndexByte(ednsClientSubnet, '/') + if slash < 0 { + ednsClientAddress = net.ParseIP(ednsClientSubnet) + if ednsClientAddress == nil { + err = fmt.Errorf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet) + return + } + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 56 + } + } else { + ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) + if ednsClientAddress == nil { + err = fmt.Errorf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet) + return + } + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + } else { + ednsClientFamily = 2 + } + netmask, err1 := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8) + if err1 != nil { + err = fmt.Errorf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet) + return + } + ednsClientNetmask = uint8(netmask) + } + + return +} + +func (s *Server) generateResponseGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) { + respJSON := jsondns.Marshal(req.response) + respStr, err := json.Marshal(respJSON) + if err != nil { + log.Println(err) + jsondns.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500) + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + now := time.Now().UTC().Format(http.TimeFormat) + w.Header().Set("Date", now) + w.Header().Set("Last-Modified", now) + w.Header().Set("Vary", "Accept") + if respJSON.HaveTTL { + if req.isTailored { + w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) + } else { + w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) + } + w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) + } + if respJSON.Status == dns.RcodeServerFailure { + w.WriteHeader(503) + } + w.Write(respStr) +} diff --git a/dohserver/ietf.go b/dohserver/ietf.go new file mode 100644 index 0000000..91fa4e3 --- /dev/null +++ b/dohserver/ietf.go @@ -0,0 +1,218 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "log" + "net" + "net/http" + "strconv" + "strings" + "time" + + jsondns "github.com/m13253/dns-over-https/v2/json-dns" + "github.com/miekg/dns" +) + +func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest { + requestBase64 := r.FormValue("dns") + requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"dns\" = %q", requestBase64), + } + } + if len(requestBinary) == 0 && (r.Header.Get("Content-Type") == "application/dns-message" || r.Header.Get("Content-Type") == "application/dns-udpwireformat") { + requestBinary, err = io.ReadAll(r.Body) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Failed to read request body (%s)", err.Error()), + } + } + } + if len(requestBinary) == 0 { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"dns\""), + } + } + + if s.patchDNSCryptProxyReqID(w, r, requestBinary) { + return &DNSRequest{ + errcode: 444, + } + } + + msg := new(dns.Msg) + err = msg.Unpack(requestBinary) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), + } + } + + if s.conf.Verbose && len(msg.Question) > 0 { + question := &msg.Question[0] + questionName := question.Name + questionClass := "" + if qclass, ok := dns.ClassToString[question.Qclass]; ok { + questionClass = qclass + } else { + questionClass = strconv.FormatUint(uint64(question.Qclass), 10) + } + questionType := "" + if qtype, ok := dns.TypeToString[question.Qtype]; ok { + questionType = qtype + } else { + questionType = strconv.FormatUint(uint64(question.Qtype), 10) + } + var clientip net.IP = nil + if s.conf.LogGuessedIP { + clientip = s.findClientIP(r) + } + if clientip != nil { + fmt.Printf("%s - - [%s] \"%s %s %s\"\n", clientip, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType) + } else { + fmt.Printf("%s - - [%s] \"%s %s %s\"\n", r.RemoteAddr, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType) + } + } + + transactionID := msg.Id + msg.Id = dns.Id() + opt := msg.IsEdns0() + if opt == nil { + opt = new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.SetUDPSize(dns.DefaultMsgSize) + opt.SetDo(false) + msg.Extra = append([]dns.RR{opt}, msg.Extra...) + } + var edns0Subnet *dns.EDNS0_SUBNET + for _, option := range opt.Option { + if option.Option() == dns.EDNS0SUBNET { + edns0Subnet = option.(*dns.EDNS0_SUBNET) + break + } + } + isTailored := edns0Subnet == nil + + if edns0Subnet == nil { + ednsClientFamily := uint16(0) + ednsClientAddress := s.findClientIP(r) + ednsClientNetmask := uint8(255) + if ednsClientAddress != nil { + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + if s.conf.ECSUsePreciseIP { + ednsClientNetmask = 32 + } else { + ednsClientNetmask = 24 + ednsClientAddress = ednsClientAddress.Mask(net.CIDRMask(24, 32)) + } + } else { + ednsClientFamily = 2 + if s.conf.ECSUsePreciseIP { + ednsClientNetmask = 128 + } else { + ednsClientNetmask = 56 + ednsClientAddress = ednsClientAddress.Mask(net.CIDRMask(56, 128)) + } + } + edns0Subnet = new(dns.EDNS0_SUBNET) + edns0Subnet.Code = dns.EDNS0SUBNET + edns0Subnet.Family = ednsClientFamily + edns0Subnet.SourceNetmask = ednsClientNetmask + edns0Subnet.SourceScope = 0 + edns0Subnet.Address = ednsClientAddress + opt.Option = append(opt.Option, edns0Subnet) + } + } + + return &DNSRequest{ + request: msg, + transactionID: transactionID, + isTailored: isTailored, + } +} + +func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) { + respJSON := jsondns.Marshal(req.response) + req.response.Id = req.transactionID + respBytes, err := req.response.Pack() + if err != nil { + log.Printf("DNS packet construct failure with upstream %s: %v\n", req.currentUpstream, err) + jsondns.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500) + return + } + + w.Header().Set("Content-Type", "application/dns-message") + now := time.Now().UTC().Format(http.TimeFormat) + w.Header().Set("Date", now) + w.Header().Set("Last-Modified", now) + w.Header().Set("Vary", "Accept") + + if respJSON.HaveTTL { + if req.isTailored { + w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) + } else { + w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) + } + w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) + } + + if respJSON.Status == dns.RcodeServerFailure { + log.Printf("received server failure from upstream %s: %v\n", req.currentUpstream, req.response) + w.WriteHeader(503) + } + _, err = w.Write(respBytes) + if err != nil { + log.Printf("failed to write to client: %v\n", err) + } +} + +// Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe +func (s *Server) patchDNSCryptProxyReqID(w http.ResponseWriter, r *http.Request, requestBinary []byte) bool { + if strings.Contains(r.UserAgent(), "dnscrypt-proxy") && bytes.Equal(requestBinary, []byte("\xca\xfe\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\x00\x02\x00\x01\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00")) { + if s.conf.Verbose { + log.Println("DNSCrypt-Proxy detected. Patching response.") + } + w.Header().Set("Content-Type", "application/dns-message") + w.Header().Set("Vary", "Accept, User-Agent") + now := time.Now().UTC().Format(http.TimeFormat) + w.Header().Set("Date", now) + w.Write([]byte("\xca\xfe\x81\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\xa8\xa7\r\nWorkaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe\r\nRefer to https://github.com/jedisct1/dnscrypt-proxy/issues/526 for details.")) + return true + } + return false +} diff --git a/dohserver/main.go b/dohserver/main.go new file mode 100644 index 0000000..2bd2d0d --- /dev/null +++ b/dohserver/main.go @@ -0,0 +1,67 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +import ( + "fmt" + "io" + "log" + "os" + "strconv" +) + +func checkPIDFile(pidFile string) (bool, error) { +retry: + f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) + if os.IsExist(err) { + pidStr, err := os.ReadFile(pidFile) + if err != nil { + return false, err + } + pid, err := strconv.ParseUint(string(pidStr), 10, 0) + if err != nil { + return false, err + } + _, err = os.Stat(fmt.Sprintf("/proc/%d", pid)) + if os.IsNotExist(err) { + err = os.Remove(pidFile) + if err != nil { + return false, err + } + goto retry + } else if err != nil { + return false, err + } + log.Printf("Already running on PID %d, exiting.\n", pid) + return false, nil + } else if err != nil { + return false, err + } + defer f.Close() + _, err = io.WriteString(f, strconv.FormatInt(int64(os.Getpid()), 10)) + if err != nil { + return false, err + } + return true, nil +} diff --git a/dohserver/parse_test.go b/dohserver/parse_test.go new file mode 100644 index 0000000..b14b0d5 --- /dev/null +++ b/dohserver/parse_test.go @@ -0,0 +1,119 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestParseCIDR(t *testing.T) { + t.Parallel() + for _, ednsClientSubnet := range []string{ + "2001:db8::/0", + "2001:db8::/56", + "2001:db8::/129", + "2001:db8::", + + "127.0.0.1/0", + "127.0.0.1/24", + "127.0.0.1/33", + "127.0.0.1", + + "::ffff:7f00:1/0", + "::ffff:7f00:1/120", + "::ffff:7f00:1", + "127.0.0.1/0", + "127.0.0.1/24", + "127.0.0.1", + } { + _, ip, ipNet, err := parseSubnet(ednsClientSubnet) + if err != nil { + t.Errorf("ecs:%s ip:[%v] ipNet:[%v] err:[%v]", ednsClientSubnet, ip, ipNet, err) + } + } +} + +func TestParseInvalidCIDR(t *testing.T) { + t.Parallel() + + for _, ip := range []string{ + "test", + "test/0", + "test/24", + "test/34", + "test/56", + "test/129", + } { + _, _, _, err := parseSubnet(ip) + if err == nil { + t.Errorf("expected error for %q", ip) + } + } +} + +func TestEdns0SubnetParseCIDR(t *testing.T) { + t.Parallel() + // init dns Msg + msg := new(dns.Msg) + msg.Id = dns.Id() + msg.SetQuestion(dns.Fqdn("example.com"), 1) + + // init edns0Subnet + edns0Subnet := new(dns.EDNS0_SUBNET) + edns0Subnet.Code = dns.EDNS0SUBNET + edns0Subnet.SourceScope = 0 + + // init opt + opt := new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.SetUDPSize(dns.DefaultMsgSize) + + opt.Option = append(opt.Option, edns0Subnet) + msg.Extra = append(msg.Extra, opt) + + for _, subnet := range []string{"::ffff:7f00:1/120", "127.0.0.1/24"} { + var err error + edns0Subnet.Family, edns0Subnet.Address, edns0Subnet.SourceNetmask, err = parseSubnet(subnet) + if err != nil { + t.Error(err) + continue + } + t.Log(msg.Pack()) + } + + // ------127.0.0.1/24----- + // [143 29 1 0 0 1 0 0 0 0 0 1 7 101 120 97 109 112 108 101 3 99 111 109 0 0 1 0 1 0 + // opt start 0 41 16 0 0 0 0 0 0 11 + // subnet start 0 8 0 7 0 1 24 0 + // client subnet start 127 0 0] + + // -----::ffff:7f00:1/120---- + // [111 113 1 0 0 1 0 0 0 0 0 1 7 101 120 97 109 112 108 101 3 99 111 109 0 0 1 0 1 0 + // opt start 0 41 16 0 0 0 0 0 0 23 + // subnet start 0 8 0 19 0 2 120 0 + // client subnet start 0 0 0 0 0 0 0 0 0 0 255 255 127 0 0] +} diff --git a/dohserver/server.go b/dohserver/server.go new file mode 100644 index 0000000..9814a3b --- /dev/null +++ b/dohserver/server.go @@ -0,0 +1,402 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "log" + "math/rand" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/gorilla/handlers" + jsondns "github.com/m13253/dns-over-https/v2/json-dns" + "github.com/miekg/dns" +) + +type Server struct { + conf *config + udpClient *dns.Client + tcpClient *dns.Client + tcpClientTLS *dns.Client + servemux *http.ServeMux +} + +type DNSRequest struct { + request *dns.Msg + response *dns.Msg + transactionID uint16 + currentUpstream string + isTailored bool + errcode int + errtext string +} + +func NewDefaultConfig() *config { + conf := &config{} + if len(conf.Listen) == 0 { + conf.Listen = []string{"127.0.0.1:8053", "[::1]:8053"} + } + + if conf.Path == "" { + conf.Path = "/dns-query" + } + if len(conf.Upstream) == 0 { + conf.Upstream = []string{"udp:8.8.8.8:53", "udp:8.8.4.4:53"} + } + if conf.Timeout == 0 { + conf.Timeout = 10 + } + if conf.Tries == 0 { + conf.Tries = 1 + } + return conf +} + +func NewServer(conf *config) (*Server, error) { + timeout := time.Duration(conf.Timeout) * time.Second + s := &Server{ + conf: conf, + udpClient: &dns.Client{ + Net: "udp", + UDPSize: dns.DefaultMsgSize, + Timeout: timeout, + }, + tcpClient: &dns.Client{ + Net: "tcp", + Timeout: timeout, + }, + tcpClientTLS: &dns.Client{ + Net: "tcp-tls", + Timeout: timeout, + }, + servemux: http.NewServeMux(), + } + if conf.LocalAddr != "" { + udpLocalAddr, err := net.ResolveUDPAddr("udp", conf.LocalAddr) + if err != nil { + return nil, err + } + tcpLocalAddr, err := net.ResolveTCPAddr("tcp", conf.LocalAddr) + if err != nil { + return nil, err + } + s.udpClient.Dialer = &net.Dialer{ + Timeout: timeout, + LocalAddr: udpLocalAddr, + } + s.tcpClient.Dialer = &net.Dialer{ + Timeout: timeout, + LocalAddr: tcpLocalAddr, + } + s.tcpClientTLS.Dialer = &net.Dialer{ + Timeout: timeout, + LocalAddr: tcpLocalAddr, + } + } + s.servemux.HandleFunc(conf.Path, s.handlerFunc) + return s, nil +} + +func (s *Server) Start() error { + servemux := http.Handler(s.servemux) + if s.conf.Verbose { + servemux = handlers.CombinedLoggingHandler(os.Stdout, servemux) + } + + var clientCAPool *x509.CertPool + if s.conf.TLSClientAuth { + if s.conf.TLSClientAuthCA != "" { + clientCA, err := os.ReadFile(s.conf.TLSClientAuthCA) + if err != nil { + log.Fatalf("Reading certificate for client authentication has failed: %v", err) + } + clientCAPool = x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(clientCA) + log.Println("Certificate loaded for client TLS authentication") + } else { + log.Fatalln("TLS client authentication requires both tls_client_auth and tls_client_auth_ca, exiting.") + } + } + + results := make(chan error, len(s.conf.Listen)) + for _, addr := range s.conf.Listen { + go func(addr string) { + var err error + if s.conf.Cert != "" || s.conf.Key != "" { + if clientCAPool != nil { + srvtls := &http.Server{ + Handler: servemux, + Addr: addr, + TLSConfig: &tls.Config{ + ClientCAs: clientCAPool, + ClientAuth: tls.RequireAndVerifyClientCert, + GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, e error) { + c, err := tls.LoadX509KeyPair(s.conf.Cert, s.conf.Key) + if err != nil { + fmt.Printf("Error loading server certificate key pair: %v\n", err) + return nil, err + } + return &c, nil + }, + }, + } + err = srvtls.ListenAndServeTLS("", "") + } else { + err = http.ListenAndServeTLS(addr, s.conf.Cert, s.conf.Key, servemux) + } + } else { + err = http.ListenAndServe(addr, servemux) + } + if err != nil { + log.Println(err) + } + results <- err + }(addr) + } + // wait for all handlers + for i := 0; i < cap(results); i++ { + err := <-results + if err != nil { + return err + } + } + close(results) + return nil +} + +func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if realIP := r.Header.Get("X-Real-IP"); realIP != "" { + if strings.ContainsRune(realIP, ':') { + r.RemoteAddr = "[" + realIP + "]:0" + } else { + r.RemoteAddr = realIP + ":0" + } + _, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + r.RemoteAddr = realIP + } + } + + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Max-Age", "3600") + w.Header().Set("Server", USER_AGENT) + w.Header().Set("X-Powered-By", USER_AGENT) + + if r.Method == "OPTIONS" { + w.Header().Set("Content-Length", "0") + return + } + + if r.Form == nil { + const maxMemory = 32 << 20 // 32 MB + r.ParseMultipartForm(maxMemory) + } + + for _, header := range s.conf.DebugHTTPHeaders { + if value := r.Header.Get(header); value != "" { + log.Printf("%s: %s\n", header, value) + } + } + + contentType := r.Header.Get("Content-Type") + if ct := r.FormValue("ct"); ct != "" { + contentType = ct + } + if contentType == "" { + // Guess request Content-Type based on other parameters + if r.FormValue("name") != "" { + contentType = "application/dns-json" + } else if r.FormValue("dns") != "" { + contentType = "application/dns-message" + } + } + var responseType string + for _, responseCandidate := range strings.Split(r.Header.Get("Accept"), ",") { + responseCandidate = strings.SplitN(responseCandidate, ";", 2)[0] + if responseCandidate == "application/json" { + responseType = "application/json" + break + } else if responseCandidate == "application/dns-udpwireformat" { + responseType = "application/dns-message" + break + } else if responseCandidate == "application/dns-message" { + responseType = "application/dns-message" + break + } + } + if responseType == "" { + // Guess response Content-Type based on request Content-Type + if contentType == "application/dns-json" { + responseType = "application/json" + } else if contentType == "application/dns-message" { + responseType = "application/dns-message" + } else if contentType == "application/dns-udpwireformat" { + responseType = "application/dns-message" + } + } + + var req *DNSRequest + if contentType == "application/dns-json" { + req = s.parseRequestGoogle(ctx, w, r) + } else if contentType == "application/dns-message" { + req = s.parseRequestIETF(ctx, w, r) + } else if contentType == "application/dns-udpwireformat" { + req = s.parseRequestIETF(ctx, w, r) + } else { + jsondns.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415) + return + } + if req.errcode == 444 { + return + } + if req.errcode != 0 { + jsondns.FormatError(w, req.errtext, req.errcode) + return + } + + req = s.patchRootRD(req) + + err := s.doDNSQuery(ctx, req) + if err != nil { + jsondns.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) + return + } + + if responseType == "application/json" { + s.generateResponseGoogle(ctx, w, r, req) + } else if responseType == "application/dns-message" { + s.generateResponseIETF(ctx, w, r, req) + } else { + panic("Unknown response Content-Type") + } +} + +func (s *Server) findClientIP(r *http.Request) net.IP { + noEcs := r.URL.Query().Get("no_ecs") + if strings.ToLower(noEcs) == "true" { + return nil + } + + XForwardedFor := r.Header.Get("X-Forwarded-For") + if XForwardedFor != "" { + for _, addr := range strings.Split(XForwardedFor, ",") { + addr = strings.TrimSpace(addr) + ip := net.ParseIP(addr) + if jsondns.IsGlobalIP(ip) { + return ip + } + } + } + XRealIP := r.Header.Get("X-Real-IP") + if XRealIP != "" { + addr := strings.TrimSpace(XRealIP) + ip := net.ParseIP(addr) + if s.conf.ECSAllowNonGlobalIP || jsondns.IsGlobalIP(ip) { + return ip + } + } + + remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if err != nil { + return nil + } + ip := remoteAddr.IP + if s.conf.ECSAllowNonGlobalIP || jsondns.IsGlobalIP(ip) { + return ip + } + return nil +} + +// Workaround a bug causing Unbound to refuse returning anything about the root +func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest { + for _, question := range req.request.Question { + if question.Name == "." { + req.request.RecursionDesired = true + } + } + return req +} + +// Return the position index for the question of qtype from a DNS msg, otherwise return -1 +func (s *Server) indexQuestionType(msg *dns.Msg, qtype uint16) int { + for i, question := range msg.Question { + if question.Qtype == qtype { + return i + } + } + return -1 +} + +func (s *Server) doDNSQuery(ctx context.Context, req *DNSRequest) (err error) { + numServers := len(s.conf.Upstream) + for i := uint(0); i < s.conf.Tries; i++ { + req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)] + + upstream, t := addressAndType(req.currentUpstream) + + switch t { + default: + log.Printf("invalid DNS type %q in upstream %q", t, upstream) + return &configError{"invalid DNS type"} + // Use DNS-over-TLS (DoT) if configured to do so + case "tcp-tls": + req.response, _, err = s.tcpClientTLS.ExchangeContext(ctx, req.request, upstream) + case "tcp", "udp": + // Use TCP if always configured to or if the Query type dictates it (AXFR) + if t == "tcp" || (s.indexQuestionType(req.request, dns.TypeAXFR) > -1) { + req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream) + } else { + req.response, _, err = s.udpClient.ExchangeContext(ctx, req.request, upstream) + if err == nil && req.response != nil && req.response.Truncated { + log.Println(err) + req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream) + } + + // Retry with TCP if this was an IXFR request and we only received an SOA + if (s.indexQuestionType(req.request, dns.TypeIXFR) > -1) && + (len(req.response.Answer) == 1) && + (req.response.Answer[0].Header().Rrtype == dns.TypeSOA) { + req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream) + } + } + } + + if err == nil { + return nil + } + log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error()) + } + return err +} diff --git a/dohserver/version.go b/dohserver/version.go new file mode 100644 index 0000000..ec98356 --- /dev/null +++ b/dohserver/version.go @@ -0,0 +1,29 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package doh + +const ( + VERSION = "2.3.4" + USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)" +) diff --git a/duration.go b/duration.go deleted file mode 100644 index 3863f0a..0000000 --- a/duration.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "time" -) - -type duration struct { - time.Duration -} - -func (d *duration) UnmarshalJSON(b []byte) error { - var v interface{} - if err := json.Unmarshal(b, &v); err != nil { - return err - } - switch value := v.(type) { - case float64: - d.Duration = time.Duration(value) - return nil - case string: - var err error - d.Duration, err = time.ParseDuration(value) - if err != nil { - return err - } - return nil - default: - return errors.New("invalid duration") - } -} diff --git a/geoip.go b/geoip.go deleted file mode 100644 index daeb522..0000000 --- a/geoip.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "io" - "net" - "net/http" - "os" - "strings" - "time" - - "github.com/oschwald/maxminddb-golang" - "golang.org/x/exp/slices" - slog "golang.org/x/exp/slog" -) - -var geolog = slog.New(log.Handler().WithAttrs([]slog.Attr{{Key: "service", Value: slog.StringValue("geoip")}})) - -// getCountry returns the country code for the given IP address. -func getCountry(ipAddr string) (string, error) { - ip := net.ParseIP(ipAddr) - var record struct { - Country struct { - ISOCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` - } // Or any appropriate struct - - err := c.mmdb.Lookup(ip, &record) - if err != nil { - return "", err - } - return record.Country.ISOCode, nil -} - -// initializeGeoIP loads the geolocation database from the specified path. -func initializeGeoIP(path string) error { - - geolog.Info("Loading the domain from file/url") - var scanner []byte - if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { - geolog.Info("domain list is a URL, trying to fetch") - resp, err := http.Get(path) - if err != nil { - return err - } - geolog.Info("(re)fetching", "path", path) - defer resp.Body.Close() - scanner, err = io.ReadAll(resp.Body) - if err != nil { - return err - } - - } else { - file, err := os.Open(path) - if err != nil { - return err - } - geolog.Info("(re)loading File: ", path) - defer file.Close() - n, err := file.Read(scanner) - if err != nil { - return err - } - geolog.Info("geolocation database loaded", n) - - } - var err error - if c.mmdb, err = maxminddb.FromBytes(scanner); err != nil { - //geolog.Warn("%d bytes read, %s", len(scanner), err) - return err - } - geolog.Info("Loaded MMDB") - for range time.NewTicker(c.GeoIPRefreshInterval).C { - if c.mmdb, err = maxminddb.FromBytes(scanner); err != nil { - //geolog.Warn("%d bytes read, %s", len(scanner), err) - return err - } - geolog.Info("Loaded MMDB %v", c.mmdb) - } - return nil -} - -// checkGeoIPSkip checks an IP address against the exclude and include lists and returns -// true if the IP address should be allowed to pass through. -func checkGeoIPSkip(ipport string) bool { - if c.mmdb == nil { - return true - } - - ipPort := strings.Split(ipport, ":") - ip := ipPort[0] - - var country string - country, err := getCountry(ip) - country = strings.ToLower(country) - if err != nil { - geolog.Info("Failed to get the geolocation of", "ip", ip, "country", country) - return false - } - if slices.Contains(c.GeoIPExclude, country) { - return false - } - if slices.Contains(c.GeoIPInclude, country) { - return true - } - - // if exclusion is provided, the rest will be allowed - if len(c.GeoIPExclude) > 0 { - return true - } - - // othewise fail - return false -} diff --git a/go.mod b/go.mod index 650a012..a200ad0 100644 --- a/go.mod +++ b/go.mod @@ -5,39 +5,58 @@ go 1.20 require ( github.com/deathowl/go-metrics-prometheus v0.0.0-20221009205350-f2a1482ba35b github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 + github.com/gorilla/handlers v1.5.1 + github.com/knadh/koanf v1.5.0 + github.com/m13253/dns-over-https/v2 v2.3.3 github.com/miekg/dns v1.1.54 github.com/mosajjal/dnsclient v0.1.1-0.20230206011533-99fa18d84393 - github.com/mosajjal/doqd v0.0.0-20230201205103-19d9a309dc6a + github.com/mosajjal/doqd v0.0.0-20230511083851-8f2810faeb07 github.com/oschwald/maxminddb-golang v1.10.0 - github.com/prometheus/client_golang v1.15.0 + github.com/prometheus/client_golang v1.15.1 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 + github.com/rs/zerolog v1.29.1 github.com/spf13/cobra v1.7.0 - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 - golang.org/x/net v0.9.0 + github.com/yl2chen/cidranger v1.0.2 + golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea + golang.org/x/net v0.10.0 + inet.af/tcpproxy v0.0.0-20221017015627-91f861402626 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/pprof v0.0.0-20230426061923-93006964c1fc // indirect + github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/onsi/ginkgo/v2 v2.9.2 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.42.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/onsi/ginkgo/v2 v2.9.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.43.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/quic-go/quic-go v0.34.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.1 // indirect - golang.org/x/crypto v0.8.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/tools v0.8.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.9.1 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 05fb889..cdb0377 100644 --- a/go.sum +++ b/go.sum @@ -1,54 +1,233 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deathowl/go-metrics-prometheus v0.0.0-20221009205350-f2a1482ba35b h1:M0of+jR+/C7vMb5eoHmMtIRVvv9CSXqAJVqOvhIYTic= github.com/deathowl/go-metrics-prometheus v0.0.0-20221009205350-f2a1482ba35b/go.mod h1:kZ9Xvhj+PTMJ415unU/sutrnWDVqG0PDS/Sl4Rt3xkE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20230426061923-93006964c1fc h1:AGDHt781oIcL4EFk7cPnvBUYTwU8BEU6GDTO3ZMn1sE= -github.com/google/pprof v0.0.0-20230426061923-93006964c1fc/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/pprof v0.0.0-20230429030804-905365eefe3e h1:yuPVjc55Y343adYwQLA29DR8KrA0yuLJLlN5LxqlsgQ= +github.com/google/pprof v0.0.0-20230429030804-905365eefe3e/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3 h1:2XF1Vzq06X+inNqgJ9tRnGuw+ZVCB3FazXODD6JE1R8= +github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= +github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc h1:RhT2pjLo3EVRmldbEcBdeRA7CGPWsNEJC+Y/N1aXQbg= +github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/m13253/dns-over-https/v2 v2.3.3 h1:yb8fH7Js402X89Nsd9KWbb+odNbhhXzv6uU42vwnrz8= +github.com/m13253/dns-over-https/v2 v2.3.3/go.mod h1:9X63wUDr5eHQUfL7H+fX5VC3B2yKgbBoVHlQMrEqdPo= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI= github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -57,31 +236,63 @@ github.com/mosajjal/dnsclient v0.1.1-0.20230206011533-99fa18d84393 h1:0z8akaPI28 github.com/mosajjal/dnsclient v0.1.1-0.20230206011533-99fa18d84393/go.mod h1:a7Dm03DazQMCGIZx37JLQBgjzSR29oGWAQmA67MGI1w= github.com/mosajjal/doqd v0.0.0-20230201205103-19d9a309dc6a h1:SY+kMKt0eMsPSdCXVYvHDeObqwK0Fnm6n63ayAe8x/4= github.com/mosajjal/doqd v0.0.0-20230201205103-19d9a309dc6a/go.mod h1:zplAd0brmTfRUAH00w+gBfx+IFIbiY4QXSU09qL7H20= +github.com/mosajjal/doqd v0.0.0-20230511083851-8f2810faeb07 h1:4RJKiEODvpwIK0rPbm5vmcHFq5T1WIeVqhhNgKfwx8I= +github.com/mosajjal/doqd v0.0.0-20230511083851-8f2810faeb07/go.mod h1:518PeGj2I3EFD70w/qKSbm9siriLbZiA8v02Dw2yM/w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= +github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us= +github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= @@ -93,8 +304,21 @@ github.com/quic-go/quic-go v0.34.0/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= @@ -107,66 +331,202 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +inet.af/tcpproxy v0.0.0-20221017015627-91f861402626 h1:2dMP3Ox/Wh5BiItwOt4jxRsfzkgyBrHzx2nW28Yg6nc= +inet.af/tcpproxy v0.0.0-20221017015627-91f861402626/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/httpproxy.go b/httpproxy.go index df8da6f..1765025 100644 --- a/httpproxy.go +++ b/httpproxy.go @@ -1,17 +1,17 @@ package main import ( - "fmt" "io" + "net" "net/http" - "net/url" "strings" "time" - slog "golang.org/x/exp/slog" + "github.com/mosajjal/sniproxy/acl" + "github.com/rs/zerolog" ) -var httplog = slog.New(log.Handler().WithAttrs([]slog.Attr{{Key: "service", Value: slog.StringValue("http")}})) +var httplog = logger.With().Str("service", "http").Logger() var passthruRequestHeaderKeys = [...]string{ "Accept", @@ -37,13 +37,14 @@ var passthruResponseHeaderKeys = [...]string{ "Vary", } -func runHTTP() { +func runHTTP(l zerolog.Logger) { + httplog = l.With().Str("service", "http").Logger() handler := http.DefaultServeMux handler.HandleFunc("/", handle80) s := &http.Server{ - Addr: fmt.Sprintf(":%d", c.HTTPPort), + Addr: c.BindHTTP, Handler: handler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, @@ -51,27 +52,34 @@ func runHTTP() { } if err := s.ListenAndServe(); err != nil { - httplog.Error(err.Error()) + httplog.Error().Msg(err.Error()) panic(-1) } } func handle80(w http.ResponseWriter, r *http.Request) { c.recievedHTTP.Inc(1) - if !checkGeoIPSkip(r.RemoteAddr) { + + addr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) + + connInfo := acl.ConnInfo{ + SrcIP: addr, + Domain: r.Host, + } + acl.MakeDecision(&connInfo, c.acl) + if connInfo.Decision == acl.Reject || connInfo.Decision == acl.ProxyIP || err != nil { + httplog.Info().Msgf("rejected request from ip: %s", r.RemoteAddr) http.Error(w, "Could not reach origin server", 403) return } - httplog.Info("rejected request", "ip", r.RemoteAddr) - // if the URL starts with the public IP, it needs to be skipped to avoid loops if strings.HasPrefix(r.Host, c.PublicIPv4) { - httplog.Warn("someone is requesting HTTP to sniproxy itself, ignoring...") + httplog.Warn().Msg("someone is requesting HTTP to sniproxy itself, ignoring...") http.Error(w, "Could not reach origin server", 404) return } - httplog.Info("REQ", "method", r.Method, "host", r.Host, "url", r.URL) + httplog.Info().Msgf("REQ method %s, host: %s, url: %s", r.Method, r.Host, r.URL) // Construct filtered header to send to origin server hh := http.Header{} @@ -96,31 +104,11 @@ func handle80(w http.ResponseWriter, r *http.Request) { rr.URL.Host = r.Host // check to see if this host is listed to be processed, otherwise RESET - if !c.AllDomains && inDomainList(r.Host+".") { - http.Error(w, "Could not reach origin server", 403) - httplog.Warn("a client requested connection to " + r.Host + ", but it's not allowed as per configuration.. sending 403") - return - } - - // if host is the reverse proxy, this request needs to be handled by the upstream address - if r.Host == c.reverseProxySNI { - reverseProxyURI, err := url.Parse(c.reverseProxyAddr) - if err != nil { - httplog.Error("failed to parse reverseproxy url", err) - - } - // TODO: maybe this won't work and I need to be more specific - // rr.URL = reverseProxyURI - hostPort := "" - if reverseProxyURI.Port() != "80" { - hostPort = fmt.Sprintf("%s:%s", reverseProxyURI.Host, reverseProxyURI.Port()) - } else { - hostPort = reverseProxyURI.Host - } - rr.URL.Host = reverseProxyURI.Host - // add the port to the host header - rr.Header.Set("Host", hostPort) - } + // if !c.AllDomains && inDomainList(r.Host+".") { + // http.Error(w, "Could not reach origin server", 403) + // httplog.Warn().Msg("a client requested connection to " + r.Host + ", but it's not allowed as per configuration.. sending 403") + // return + // } transport := http.Transport{ Dial: c.dialer.Dial, @@ -130,13 +118,12 @@ func handle80(w http.ResponseWriter, r *http.Request) { resp, err := transport.RoundTrip(&rr) if err != nil { // TODO: Passthru more error information - http.Error(w, "Could not reach origin server", 500) - httplog.Error(err.Error()) + httplog.Error().Msg(err.Error()) return } defer resp.Body.Close() - httplog.Info("http response", "status_code", resp.Status) + httplog.Info().Msgf("http response with status_code %s", resp.Status) // Transfer filtered header from origin server -> client respH := w.Header() diff --git a/https.go b/https.go index d8914a2..3d2dda7 100644 --- a/https.go +++ b/https.go @@ -1,83 +1,68 @@ package main import ( - "crypto/rand" - "crypto/tls" - "crypto/x509" "fmt" "net" - slog "golang.org/x/exp/slog" + "github.com/mosajjal/sniproxy/acl" + "github.com/rs/zerolog" "golang.org/x/net/proxy" ) -var httpslog = slog.New(log.Handler().WithAttrs([]slog.Attr{{Key: "service", Value: slog.StringValue("https")}})) - -// handle HTTPS connections coming to the reverse proxy. this will get a connction from the handle443 function -// need to grab the HTTP request from this, and pass it on to the HTTP handler. -func handleReverse(conn net.Conn) error { - httpslog.Info("connecting to HTTP") - // send the reverse conn to local HTTP listner - srcAddr := net.TCPAddr{ - IP: c.sourceAddr, - Port: 0, - } - target, err := net.DialTCP("tcp", &srcAddr, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(c.HTTPPort)}) - if err != nil { - return err - } - pipe(conn, target) - return nil -} +var httpslog = logger.With().Str("service", "https").Logger() func handle443(conn net.Conn) error { c.recievedHTTPS.Inc(1) - if !checkGeoIPSkip(conn.RemoteAddr().String()) { - httpslog.Warn("Rejected request due to GEOIP restriction", "ip", conn.RemoteAddr().String()) - conn.Close() - return nil - } defer conn.Close() incoming := make([]byte, 2048) n, err := conn.Read(incoming) if err != nil { - httpslog.Error(err.Error()) + httpslog.Err(err) return err } sni, err := GetHostname(incoming) if err != nil { - httpslog.Error(err.Error()) + httpslog.Err(err) return err } - // check SNI against domainlist for an extra layer of security - if !c.AllDomains && inDomainList(sni+".") { - httpslog.Warn("a client requested connection to " + sni + ", but it's not allowed as per configuration.. resetting TCP") + connInfo := acl.ConnInfo{ + SrcIP: conn.RemoteAddr(), + Domain: sni, + } + acl.MakeDecision(&connInfo, c.acl) + + if connInfo.Decision == acl.Reject { + httpslog.Warn().Msgf("ACL rejection for ip %s", conn.RemoteAddr().String()) conn.Close() return nil } - rAddr, err := c.dnsClient.lookupDomain4(sni) - rPort := 443 - if err != nil || rAddr == nil { - httpslog.Warn(err.Error()) - return err - } - // TODO: handle timeout and context here - if rAddr.IsLoopback() || rAddr.IsPrivate() || rAddr.Equal(net.IPv4(0, 0, 0, 0)) || rAddr.Equal(net.IP(c.PublicIPv4)) { - httpslog.Info("connection to private IP or self ignored") + // check SNI against domainlist for an extra layer of security + if connInfo.Decision == acl.OriginIP { + httpslog.Warn().Msg("a client requested connection to " + sni + ", but it's not allowed as per configuration.. resetting TCP") + conn.Close() return nil } - // if SNI is the reverse proxy, this request needs to be handled by a HTTPS handler - if sni == c.reverseProxySNI { - rAddr = net.IPv4(127, 0, 0, 1) - // TODO: maybe 65000 as a static port is not a good idea and this needs to be random OR unix socket - rPort = 65000 + rPort := 443 + var rAddr net.IP + if connInfo.Decision == acl.Override { + httpslog.Debug().Msgf("overriding destination IP %s with %s", rAddr.String(), connInfo.DstIP.String()) + rAddr = connInfo.DstIP.IP + rPort = connInfo.DstIP.Port + } else { + rAddr, err = c.dnsClient.lookupDomain4(sni) + if err != nil || rAddr == nil { + httpslog.Warn().Msg(err.Error()) + return err + } + // TODO: handle timeout and context here + if rAddr.IsLoopback() || rAddr.IsPrivate() || rAddr.Equal(net.IPv4(0, 0, 0, 0)) || rAddr.Equal(net.IP(c.PublicIPv4)) || rAddr.Equal(net.IP(c.sourceAddr)) || rAddr.Equal(net.IP(c.PublicIPv6)) { + httpslog.Info().Msg("connection to private IP or self ignored") + return nil + } } - httpslog.Info("establishing connection", - "remote_ip", rAddr, - "source_ip", conn.RemoteAddr().String(), - "host", sni, - ) + + httpslog.Info().Msgf("establishing connection to %s:%d from %s with SNI %s", rAddr.String(), rPort, conn.RemoteAddr().String(), sni) var target *net.TCPConn if c.dialer == proxy.Direct { // with the manipulation of the soruce address, we can set the outbound interface @@ -87,14 +72,14 @@ func handle443(conn net.Conn) error { } target, err = net.DialTCP("tcp", &srcAddr, &net.TCPAddr{IP: rAddr, Port: rPort}) if err != nil { - httpslog.Error("could not connect to target", err) + httpslog.Info().Msgf("could not connect to target with error: %s", err) conn.Close() return err } } else { tmp, err := c.dialer.Dial("tcp", fmt.Sprintf("%s:%d", rAddr, rPort)) if err != nil { - httpslog.Error("could not connect to target", err) + httpslog.Info().Msgf("could not connect to target with error: %s", err) conn.Close() return err } @@ -107,48 +92,18 @@ func handle443(conn net.Conn) error { return nil } -func runReverse() { - // reverse https can't run on 443. we'll pick a random port and pipe the 443 traffic back to it. - cert, err := tls.LoadX509KeyPair(c.ReverseProxyCert, c.ReverseProxyKey) - if err != nil { - httpslog.Error(err.Error()) - } - config := tls.Config{Certificates: []tls.Certificate{cert}} - config.Rand = rand.Reader - listener, err := tls.Listen("tcp", ":65000", &config) - if err != nil { - httpslog.Error(err.Error()) - } - for { - conn, err := listener.Accept() - if err != nil { - httpslog.Error(err.Error()) - break - } - defer conn.Close() - tlscon, ok := conn.(*tls.Conn) - if ok { - state := tlscon.ConnectionState() - for _, v := range state.PeerCertificates { - fmt.Println(x509.MarshalPKIXPublicKey(v.PublicKey)) - } - } - go handleReverse(conn) - } -} - -func runHTTPS() { - - l, err := net.Listen("tcp", c.BindIP+fmt.Sprintf(":%d", c.HTTPSPort)) +func runHTTPS(log zerolog.Logger) { + httpslog = log.With().Str("service", "https").Logger() + l, err := net.Listen("tcp", c.BindHTTPS) if err != nil { - httpslog.Error(err.Error()) + httpslog.Err(err) panic(-1) } defer l.Close() for { c, err := l.Accept() if err != nil { - httpslog.Error(err.Error()) + httpslog.Err(err) } go func() { go handle443(c) diff --git a/install.sh b/install.sh index e1721a5..912515c 100644 --- a/install.sh +++ b/install.sh @@ -61,20 +61,30 @@ fi # create a folder under /opt for sniproxy mkdir -p /opt/sniproxy +execCommand="/opt/sniproxy/sniproxy" +configPath="/opt/sniproxy/sniproxy.yaml" +yqPath="/opt/sniproxy/yq" + # download sniproxy -wget -O /opt/sniproxy/sniproxy http://bin.n0p.me/sniproxy +wget -O $execCommand http://bin.n0p.me/sniproxy +# make it executable +chmod +x $execCommand + +# download yq +wget -O $yqPath http://bin.n0p.me/yq # make it executable -chmod +x /opt/sniproxy/sniproxy +chmod +x $yqPath + +# generate the default config +$execCommand --defaultconfig $configPath # ask which domains to proxy echo "sniproxy can proxy all HTTPS traffic or only specific domains, if you have a domain list URL, enter it below, otherwise press Enter to proxy all HTTPS traffic" read domainlist -execCommand="/opt/sniproxy/sniproxy" - # if domainslist is not empty, there should be a --domainListPath argument added to sniproxy execute command if [ -n "$domainlist" ]; then - execCommand="$execCommand --domainListPath $domainlist" + $yqPath -i '.acl.domain.enabled = true, .acl.domain.path = '"$domainlist" $configPath fi # ask if DNS over TCP should be enabled @@ -82,7 +92,7 @@ echo "Do you want to enable DNS over TCP? (y/n)" read dnsOverTCP # if yes, add --bindDnsOverTcp argument to sniproxy execute command if [ "$dnsOverTCP" = "y" ]; then - execCommand="$execCommand --bindDnsOverTcp" + $yqPath -i '.general.bind_dns_over_tcp = true' $configPath fi # ask if DNS over TLS should be enabled @@ -90,7 +100,7 @@ echo "Do you want to enable DNS over TLS? (y/n)" read dnsOverTLS # if yes, add --bindDnsOverTls argument to sniproxy execute command if [ "$dnsOverTLS" = "y" ]; then - execCommand="$execCommand --bindDnsOverTls" + $yqPath -i '.general.bind_dns_over_tls = true' $configPath fi # ask for DNS over QUIC @@ -98,7 +108,7 @@ echo "Do you want to enable DNS over QUIC? (y/n)" read dnsOverQUIC # if yes, add --bindDnsOverQuic argument to sniproxy execute command if [ "$dnsOverQUIC" = "y" ]; then - execCommand="$execCommand --bindDnsOverQuic" + $yqPath -i '.general.bind_dns_over_quic = true' $configPath fi # if any of DNS over TLS or DNS over QUIC is enabled, ask for the certificate path and key path @@ -112,7 +122,7 @@ if [ "$dnsOverTLS" = "y" ] || [ "$dnsOverQUIC" = "y" ]; then if [ -z "$certPath" ] || [ -z "$keyPath" ]; then echo "WARNING: Using self-signed certificates" else - execCommand="$execCommand --tlsCert $certPath --tlsKey $keyPath" + $yqPath -i '.general.tls_cert = '"$certPath"', .general.tls_key = '"$keyPath" $configPath fi fi @@ -124,7 +134,7 @@ After=network.target [Service] Type=simple -ExecStart=$execCommand +ExecStart=$execCommand --config $configPath Restart=on-failure [Install] @@ -147,8 +157,8 @@ publicIP=$(curl -s 4.ident.me) # print some instructions for setting up DNS in clients to this echo "sniproxy is now running, you can set up DNS in your clients to $publicIP" -echo "you can check the status of sniproxy by running: systemctl status sniproxy" -echo "you can check the logs of sniproxy by running: journalctl -u sniproxy" +echo "you can check the status of sniproxy by running: sudo systemctl status sniproxy" +echo "you can check the logs of sniproxy by running: sudo journalctl -u sniproxy" echo "some of the features of sniproxy are not covered by this script, please refer to the GitHub page for more information: github.com/moasjjal/sniproxy" echo "if journal shows empty, you might need to reboot the server, sniproxy is set up as a service so it should start automatically after reboot" diff --git a/main.go b/main.go index 40befe6..b89dd63 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "io" "net" @@ -12,62 +11,52 @@ import ( "strings" "time" + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/rs/zerolog" + prometheusmetrics "github.com/deathowl/go-metrics-prometheus" - "github.com/golang-collections/collections/tst" "github.com/mosajjal/dnsclient" - "github.com/oschwald/maxminddb-golang" "github.com/prometheus/client_golang/prometheus" "github.com/rcrowley/go-metrics" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" + _ "embed" + stdlog "log" + "github.com/miekg/dns" - slog "golang.org/x/exp/slog" "golang.org/x/net/proxy" + + "github.com/mosajjal/sniproxy/acl" + doh "github.com/mosajjal/sniproxy/dohserver" ) type runConfig struct { - BindIP string `json:"bindIP"` - PublicIPv4 string `json:"publicIPv4"` - PublicIPv6 string `json:"publicIPv6"` - UpstreamDNS string `json:"upstreamDNS"` - UpstreamSOCKS5 string `json:"upstreamSOCKS5"` - DomainListPath string `json:"domainListPath"` - DomainListRefreshInterval duration `json:"domainListRefreshInterval"` - BindDNSOverTCP bool `json:"bindDnsOverTcp"` - BindDNSOverTLS bool `json:"bindDnsOverTls"` - BindDNSOverQuic bool `json:"bindDnsOverQuic"` - AllDomains bool `json:"allDomains"` - TLSCert string `json:"tlsCert"` - TLSKey string `json:"tlsKey"` - ReverseProxy string `json:"reverseProxy"` - ReverseProxyCert string `json:"reverseProxyCert"` - ReverseProxyKey string `json:"reverseProxyKey"` - HTTPPort uint `json:"httpPort"` - HTTPSPort uint `json:"httpsPort"` - DNSPort uint `json:"dnsPort"` - Interface string `json:"interface"` - Prometheus string `json:"prometheus"` - - routePrefixes *tst.TernarySearchTree - routeSuffixes *tst.TernarySearchTree - routeFQDNs map[string]uint8 - - GeoIPPath string `json:"geoipPath"` - GeoIPRefreshInterval time.Duration `json:"geoipRefreshInterval"` - GeoIPInclude []string `json:"geoipInclude"` - GeoIPExclude []string `json:"geoipExculde"` - - mmdb *maxminddb.Reader + PublicIPv4 string `yaml:"public_ipv4"` + PublicIPv6 string `yaml:"public_ipv6"` + UpstreamDNS string `yaml:"upstream_dns"` + UpstreamSOCKS5 string `yaml:"upstream_socks5"` + BindDNSOverUDP string `yaml:"bind_dns_over_udp"` + BindDNSOverTCP string `yaml:"bind_dns_over_tcp"` + BindDNSOverTLS string `yaml:"bind_dns_over_tls"` + BindDNSOverQuic string `yaml:"bind_dns_over_quic"` + TLSCert string `yaml:"tls_cert"` + TLSKey string `yaml:"tls_key"` + BindHTTP string `yaml:"bind_http"` + BindHTTPS string `yaml:"bind_https"` + Interface string `yaml:"interface"` + BindPrometheus string `yaml:"bind_prometheus"` + + acl []acl.ACL dnsClient DNSClient dialer proxy.Dialer sourceAddr net.IP - reverseProxySNI string - reverseProxyAddr string - // metrics recievedHTTP metrics.Counter proxiedHTTP metrics.Counter @@ -79,7 +68,17 @@ type runConfig struct { var c runConfig -var log = slog.New(slog.NewTextHandler(os.Stderr)) +var ( + version string = "v2-UNKNOWN" + commit string = "NOT PROVIDED" +) + +//go:embed config.defaults.yaml +var defaultConfig []byte + +// disable colors in logging if NO_COLOR is set +var nocolorLog = strings.ToLower(os.Getenv("NO_COLOR")) == "true" +var logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339, NoColor: nocolorLog}) func pipe(conn1 net.Conn, conn2 net.Conn) { chan1 := getChannel(conn1) @@ -120,12 +119,7 @@ func getChannel(conn net.Conn) chan []byte { return c } -func getPublicIPv4() string { - pub, _ := getPublicIPv4Inner() - return pub -} - -func getPublicIPv4Inner() (string, error) { +func getPublicIPv4() (string, error) { conn, err := net.Dial("udp", "8.8.8.8:53") if err != nil { return "", err @@ -161,24 +155,19 @@ func getPublicIPv4Inner() (string, error) { return externalIP, nil } - log.Error("Could not automatically find the public IPv4 address. Please specify it in the configuration.", nil) + logger.Error().Msg("Could not automatically find the public IPv4 address. Please specify it in the configuration.") } return "", nil } -func getPublicIPv6() string { - pub, _ := getPublicIPv6Inner() - return pub -} - func cleanIPv6(ip string) string { ip = strings.TrimPrefix(ip, "[") ip = strings.TrimSuffix(ip, "]") return ip } -func getPublicIPv6Inner() (string, error) { +func getPublicIPv6() (string, error) { conn, err := net.Dial("udp6", "[2001:4860:4860::8888]:53") if err != nil { return "", err @@ -211,10 +200,9 @@ func getPublicIPv6Inner() (string, error) { } if externalIP != "" { - return cleanIPv6(externalIP), nil } - log.Error("Could not automatically find the public IPv6 address. Please specify it in the configuration.", nil) + logger.Error().Msg("Could not automatically find the public IPv6 address. Please specify it in the configuration.") } return "", nil @@ -230,65 +218,85 @@ func main() { }, } flags := cmd.Flags() - flags.StringVar(&c.BindIP, "bindIP", "0.0.0.0", "Bind 443 and 80 to a Specific IP Address. Doesn't apply to DNS Server. DNS Server always listens on 0.0.0.0") - flags.StringVar(&c.UpstreamDNS, "upstreamDNS", "udp://8.8.8.8:53", "Upstream DNS URI. examples: udp://1.1.1.1:53, tcp://1.1.1.1:53, tcp-tls://1.1.1.1:853, https://dns.google/dns-query") - flags.StringVar(&c.UpstreamSOCKS5, "upstreamSOCKS5", "", "Use a SOCKS proxy for upstream HTTP/HTTPS traffic. Example: socks5://admin:admin@127.0.0.1:1080") - flags.StringVar(&c.DomainListPath, "domainListPath", "", "Path to the domain list. eg: /tmp/domainlist.csv. Look at the example file for the format. ") - flags.DurationVar(&c.DomainListRefreshInterval.Duration, "domainListRefreshInterval", 60*time.Minute, "Interval to re-fetch the domain list") - flags.BoolVar(&c.AllDomains, "allDomains", false, "Route all HTTP(s) traffic through the SNI proxy") - flags.StringVar(&c.PublicIPv4, "publicIPv4", getPublicIPv4(), "Public IP of the server, reply address of DNS A queries") - flags.StringVar(&c.PublicIPv6, "publicIPv6", getPublicIPv6(), "Public IPv6 of the server, reply address of DNS AAAA queries") - flags.BoolVar(&c.BindDNSOverTCP, "bindDnsOverTcp", false, "enable DNS over TCP as well as UDP") - flags.BoolVar(&c.BindDNSOverTLS, "bindDnsOverTls", false, "enable DNS over TLS as well as UDP") - flags.BoolVar(&c.BindDNSOverQuic, "bindDnsOverQuic", false, "enable DNS over QUIC as well as UDP") - flags.StringVar(&c.TLSCert, "tlsCert", "", "Path to the certificate for DoH, DoT and DoQ. eg: /tmp/mycert.pem") - flags.StringVar(&c.TLSKey, "tlsKey", "", "Path to the certificate key for DoH, DoT and DoQ. eg: /tmp/mycert.key") - - // set an domain to be redirected to a real webserver. essentially adding a simple reverse proxy to sniproxy - flags.StringVar(&c.ReverseProxy, "reverseProxy", "", "enable reverse proxy for a specific FQDN and upstream URL. example: www.example.com::http://127.0.0.1:4001") - flags.StringVar(&c.ReverseProxyCert, "reverseProxyCert", "", "Path to the certificate for reverse proxy. eg: /tmp/mycert.pem") - flags.StringVar(&c.ReverseProxyKey, "reverseProxyKey", "", "Path to the certificate key for reverse proxy. eg: /tmp/mycert.key") - - // geoip helper to limit client countries - flags.StringVar(&c.GeoIPPath, "geoipPath", "", "path to MMDB URL/path\nExample: https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb") - flags.DurationVar(&c.GeoIPRefreshInterval, "geoipRefreshInterval", time.Hour, "MMDB refresh interval") - flags.StringSliceVar(&c.GeoIPExclude, "geoipExclude", []string{""}, "Exclude countries to be allowed to connect. example: US,CA") - flags.StringSliceVar(&c.GeoIPInclude, "geoipInclude", []string{""}, "Include countries to be allowed to connect. example: US,CA") - - flags.UintVar(&c.HTTPPort, "httpPort", 80, "HTTP Port to listen on. Should remain 80 in most cases") - flags.UintVar(&c.HTTPSPort, "httpsPort", 443, "HTTPS Port to listen on. Should remain 443 in most cases") - flags.UintVar(&c.DNSPort, "dnsPort", 53, "DNS Port to listen on. Should remain 53 in most cases") - - flags.StringVar(&c.Interface, "interface", "", "Interface used for outbound TLS connections. uses OS prefered one if empty") - - flags.StringVar(&c.Prometheus, "prometheus", "", "Enable prometheus endpoint on IP:PORT. example: 127.0.0.1:8080. Always exposes /metrics and only supports HTTP") - - config := flags.StringP("config", "c", "", "path to JSON configuration file") + config := flags.StringP("config", "c", "", "path to YAML configuration file") + _ = flags.Bool("defaultconfig", false, "write the default config yaml file to path") + _ = flags.BoolP("version", "v", false, "show version info and exit") if err := cmd.Execute(); err != nil { - log.Error("failed to execute command", err) + logger.Error().Msgf("failed to execute command: %s", err) return } if flags.Changed("help") { return } + if flags.Changed("version") { + fmt.Printf("sniproxy version %s, commit %s\n", version, commit) + return + } + if flags.Changed("defaultconfig") { + fmt.Fprintf(os.Stdout, string(defaultConfig)) + return + } + k := koanf.New(".") + // load the defaults + if err := k.Load(rawbytes.Provider(defaultConfig), yaml.Parser()); err != nil { + panic(err) + } if *config != "" { - configFile, err := os.Open(*config) - if err != nil { - log.Error("failed to open config file", err) - } - defer configFile.Close() - fileStat, _ := configFile.Stat() - configBytes := make([]byte, fileStat.Size()) - _, err = configFile.Read(configBytes) - if err != nil { - log.Error("Could not read the config file", err) + if err := k.Load(file.Provider(*config), yaml.Parser()); err != nil { + panic(err) } + } - err = json.Unmarshal(configBytes, &c) - if err != nil { - log.Error("failed to parse config file", err) - } + logger.Info().Msgf("starting sniproxy. version %s, commit %s", version, commit) + + // verify and load config + generalConfig := k.Cut("general") + + stdlog.SetFlags(0) + stdlog.SetOutput(logger) + + switch l := generalConfig.String("log_level"); l { + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + logger = logger.With().Caller().Logger() + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + logger = logger.With().Caller().Logger() + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + + c.UpstreamDNS = generalConfig.String("upstream_dns") + c.UpstreamSOCKS5 = generalConfig.String("upstream_socks5") + c.BindDNSOverUDP = generalConfig.String("bind_dns_over_udp") + c.BindDNSOverTCP = generalConfig.String("bind_dns_over_tcp") + c.BindDNSOverTLS = generalConfig.String("bind_dns_over_tls") + c.BindDNSOverQuic = generalConfig.String("bind_dns_over_quic") + c.TLSCert = generalConfig.String("tls_cert") + c.TLSKey = generalConfig.String("tls_key") + c.BindHTTP = generalConfig.String("bind_http") + c.BindHTTPS = generalConfig.String("bind_https") + c.Interface = generalConfig.String("interface") + c.PublicIPv4 = generalConfig.String("public_ipv4") + if c.PublicIPv4 == "" { + c.PublicIPv4, _ = getPublicIPv4() + } + c.PublicIPv6 = generalConfig.String("public_ipv6") + if c.PublicIPv6 == "" { + c.PublicIPv6, _ = getPublicIPv6() + } + c.BindPrometheus = generalConfig.String("prometheus") + + var err error + c.acl, err = acl.StartACLs(&logger, k) + if err != nil { + logger.Error().Msgf("failed to start ACLs: %s", err) + return } // set up metrics @@ -299,81 +307,53 @@ func main() { c.recievedHTTPS = metrics.GetOrRegisterCounter("https.requests.recieved", metrics.DefaultRegistry) c.proxiedHTTPS = metrics.GetOrRegisterCounter("https.requests.proxied", metrics.DefaultRegistry) - if c.Prometheus != "" { + if c.BindPrometheus != "" { p := prometheusmetrics.NewPrometheusProvider(metrics.DefaultRegistry, "sniproxy", c.PublicIPv4, prometheus.DefaultRegisterer, 1*time.Second) go p.UpdatePrometheusMetrics() go func() { http.Handle("/metrics", promhttp.Handler()) - log.Info("starting metrics server", - "address", c.Prometheus, - ) - if err := http.ListenAndServe(c.Prometheus, promhttp.Handler()); err != nil { - log.Error(err.Error()) + logger.Info().Str( + "address", c.BindPrometheus, + ).Msg("starting metrics server") + if err := http.ListenAndServe(c.BindPrometheus, promhttp.Handler()); err != nil { + logger.Error().Msgf("%s", err) } }() } - if c.DomainListPath == "" { - log.Warn("Domain list (--domainListPath) is not specified, routing ALL domains through the SNI proxy") - c.AllDomains = true - } if c.PublicIPv4 != "" { - log.Info("server info", "public_ip", c.PublicIPv4) + logger.Info().Str("public_ip", c.PublicIPv4).Msg("server info") } else { - log.Error("Could not automatically determine public IPv4. you should provide it manually using --publicIPv4") + logger.Error().Msg("Could not automatically determine public IPv4. you should provide it manually using --publicIPv4") } if c.PublicIPv6 != "" { - log.Info("server info", "public_ip", c.PublicIPv6) + logger.Info().Str("public_ip", c.PublicIPv6).Msg("server info") } else { - log.Error("Could not automatically determine public IPv6. you should provide it manually using --publicIPv6") + logger.Error().Msg("Could not automatically determine public IPv6. you should provide it manually using --publicIPv6") } // generate self-signed certificate if not provided if c.TLSCert == "" && c.TLSKey == "" { - _, _, err := GenerateSelfSignedCertKey(c.PublicIPv4, nil, nil, os.TempDir()) - log.Info("Certificate was not provided, using a self signed cert") + _, _, err := doh.GenerateSelfSignedCertKey(c.PublicIPv4, nil, nil, os.TempDir()) + logger.Info().Msg("certificate was not provided, generating a self signed cert in temp directory") if err != nil { - log.Error("Error while generating self-signed cert: ", err) + logger.Error().Msgf("error while generating self-signed cert: %s", err) } c.TLSCert = filepath.Join(os.TempDir(), c.PublicIPv4+".crt") c.TLSKey = filepath.Join(os.TempDir(), c.PublicIPv4+".key") } - // parse reverseproxy and split it into url and sni - if c.ReverseProxy != "" { - log.Info("enablibng a reverse proxy") - - tmp := strings.Split(c.ReverseProxy, "::") - c.reverseProxySNI, c.reverseProxyAddr = tmp[0], tmp[1] - go runReverse() - } - - // throw an error if geoip include or exclude is present, but geoippath is not - if c.GeoIPPath == "" && (len(c.GeoIPInclude)+len(c.GeoIPExclude) >= 1) { - log.Error("GeoIP include or exclude is present, but GeoIPPath is not") - } - - // load mmdb if provided - if c.GeoIPPath != "" { - go initializeGeoIP(c.GeoIPPath) - c.GeoIPExclude = toLowerSlice(c.GeoIPExclude) - log.Info("GeoIP", "exclude", c.GeoIPExclude) - c.GeoIPInclude = toLowerSlice(c.GeoIPInclude) - log.Info("GeoIP", "include", c.GeoIPInclude) - } - // Finds source addr for outbound connections if interface is not empty if c.Interface != "" { - log.Info("Using", "interface", c.Interface) + logger.Info().Msgf("Using interface %s", c.Interface) ief, err := net.InterfaceByName(c.Interface) if err != nil { - log.Error(err.Error()) + logger.Err(err) } addrs, err := ief.Addrs() if err != nil { - log.Error(err.Error()) - + logger.Err(err) } c.sourceAddr = net.ParseIP(addrs[0].String()) @@ -382,20 +362,20 @@ func main() { if c.UpstreamSOCKS5 != "" { uri, err := url.Parse(c.UpstreamSOCKS5) if err != nil { - log.Error(err.Error()) + logger.Err(err) } if uri.Scheme != "socks5" { - log.Error("only SOCKS5 is supported", nil) + logger.Error().Msg("only SOCKS5 is supported") return } - log.Info("Using an upstream SOCKS5 proxy", "address", uri.Host) + logger.Info().Msgf("Using an upstream SOCKS5 proxy: %s", uri.Host) u := uri.User.Username() p, _ := uri.User.Password() socksAuth := proxy.Auth{User: u, Password: p} c.dialer, err = proxy.SOCKS5("tcp", uri.Host, &socksAuth, proxy.Direct) if err != nil { - fmt.Fprintln(os.Stderr, "can't connect to the proxy:", err) + logger.Error().Msgf("can't connect to proxy: %s", err.Error()) os.Exit(1) } } else { @@ -404,28 +384,13 @@ func main() { tmp, err := dnsclient.New(c.UpstreamDNS, true, c.UpstreamSOCKS5) if err != nil { - log.Error(err.Error()) + logger.Err(err) } - c.dnsClient = DNSClient{C: tmp} - defer c.dnsClient.C.Close() - go runHTTP() - go runHTTPS() - go runDNS() - - // fetch domain list and refresh them periodically - if !c.AllDomains { - c.routePrefixes, c.routeSuffixes, c.routeFQDNs, _ = LoadDomainsCsv(c.DomainListPath) - for range time.NewTicker(c.DomainListRefreshInterval.Duration).C { - c.routePrefixes, c.routeSuffixes, c.routeFQDNs, _ = LoadDomainsCsv(c.DomainListPath) - } - } else { - select {} - } -} + c.dnsClient = DNSClient{tmp} + defer c.dnsClient.Close() + go runHTTP(logger) + go runHTTPS(logger) + go runDNS(logger) -func toLowerSlice(in []string) (out []string) { - for _, v := range in { - out = append(out, strings.ToLower(v)) - } - return + select {} }