├── .gitignore ├── go.mod ├── doc.go ├── .github └── workflows │ ├── go-test.yml │ └── go-check.yml ├── LICENSE ├── utils.go ├── README.md ├── connection.go ├── service.go ├── service_test.go ├── client.go ├── server.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/viamrobotics/zeroconf 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/cenkalti/backoff v2.2.1+incompatible 7 | github.com/edaniels/golog v0.0.0-20220930140416-6e52e83a97fc 8 | github.com/miekg/dns v1.1.41 9 | github.com/pkg/errors v0.9.1 10 | golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 11 | ) 12 | 13 | require go.uber.org/multierr v1.6.0 14 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package zeroconf is a pure Golang library that employs Multicast DNS-SD for 2 | // browsing and resolving services in your network and registering own services 3 | // in the local network. 4 | // 5 | // It basically implements aspects of the standards 6 | // RFC 6762 (mDNS) and 7 | // RFC 6763 (DNS-SD). 8 | // Though it does not support all requirements yet, the aim is to provide a 9 | // complient solution in the long-term with the community. 10 | // 11 | // By now, it should be compatible to [Avahi](http://avahi.org/) (tested) and 12 | // Apple's Bonjour (untested). Should work in the most office, home and private 13 | // environments. 14 | package zeroconf 15 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: unittest 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | unit: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ "ubuntu", "windows", "macos" ] 15 | go: [ "1.15.x", "1.16.x" ] 16 | runs-on: ${{ matrix.os }}-latest 17 | name: Unit tests (${{ matrix.os}}, Go ${{ matrix.go }}) 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go }} 23 | - name: Go information 24 | run: | 25 | go version 26 | go env 27 | - name: Run tests 28 | run: go test -v ./... 29 | - name: Run tests (32 bit) 30 | if: ${{ matrix.os != 'macos' }} # can't run 32 bit tests on OSX. 31 | env: 32 | GOARCH: 386 33 | run: go test -v ./... 34 | - name: Run tests with race detector 35 | if: ${{ matrix.os == 'ubuntu' }} # speed things up. Windows and OSX VMs are slow 36 | run: go test -v -race ./... 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stefan Smarzly 4 | Copyright (c) 2014 Oleksandr Lobunets 5 | 6 | Note: Copyright for portions of project zeroconf.sd are held by Oleksandr 7 | Lobunets, 2014, as part of project bonjour. All other copyright for 8 | project zeroconf.sd are held by Stefan Smarzly, 2016. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/go-check.yml: -------------------------------------------------------------------------------- 1 | name: linting 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | unit: 11 | runs-on: ubuntu-latest 12 | name: All 13 | strategy: 14 | fail-fast: false 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | submodules: recursive 19 | - uses: actions/setup-go@v2 20 | with: 21 | go-version: "1.16.x" 22 | - name: Install staticcheck 23 | run: go install honnef.co/go/tools/cmd/staticcheck@434f5f3816b358fe468fa83dcba62d794e7fe04b # 2021.1 (v0.2.0) 24 | - name: Check that go.mod is tidy 25 | uses: protocol/multiple-go-modules@v1.0 26 | with: 27 | run: | 28 | go mod tidy 29 | if [[ -n $(git ls-files --other --exclude-standard --directory -- go.sum) ]]; then 30 | echo "go.sum was added by go mod tidy" 31 | exit 1 32 | fi 33 | git diff --exit-code -- go.sum go.mod 34 | - name: gofmt 35 | run: | 36 | out=$(gofmt -s -l .) 37 | if [[ -n "$out" ]]; then 38 | echo $out | awk '{print "::error file=" $0 ",line=0,col=0::File is not gofmt-ed."}' 39 | exit 1 40 | fi 41 | - name: go vet 42 | uses: protocol/multiple-go-modules@v1.0 43 | with: 44 | run: go vet ./... 45 | - name: staticcheck 46 | uses: protocol/multiple-go-modules@v1.0 47 | with: 48 | run: | 49 | set -o pipefail 50 | staticcheck ./... | sed -e 's@\(.*\)\.go@./\1.go@g' 51 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package zeroconf 2 | 3 | import ( 4 | "context" 5 | "runtime/debug" 6 | "strings" 7 | "time" 8 | 9 | "github.com/edaniels/golog" 10 | ) 11 | 12 | func parseSubtypes(service string) (string, []string) { 13 | subtypes := strings.Split(service, ",") 14 | return subtypes[0], subtypes[1:] 15 | } 16 | 17 | // trimDot is used to trim the dots from the start or end of a string 18 | func trimDot(s string) string { 19 | return strings.Trim(s, ".") 20 | } 21 | 22 | // panicCapturingGo spawns a goroutine to run the given function and captures 23 | // any panic that occurs and logs it. 24 | func panicCapturingGo(f func()) { 25 | panicCapturingGoWithCallback(f, nil) 26 | } 27 | 28 | const waitDur = 3 * time.Second 29 | 30 | // panicCapturingGoWithCallback spawns a goroutine to run the given function and captures 31 | // any panic that occurs, logs it, and calls the given callback. The callback can be 32 | // used for restart functionality. 33 | func panicCapturingGoWithCallback(f func(), callback func(err interface{})) { 34 | go func() { 35 | defer func() { 36 | if err := recover(); err != nil { 37 | debug.PrintStack() 38 | golog.Global().Errorw("panic while running function", "error", err) 39 | if callback == nil { 40 | return 41 | } 42 | golog.Global().Infow("waiting a bit to call callback", "wait", waitDur.String()) 43 | time.Sleep(waitDur) 44 | callback(err) 45 | } 46 | }() 47 | f() 48 | }() 49 | } 50 | 51 | // managedGo keeps the given function alive in the background until 52 | // it terminates normally. 53 | func managedGo(f func(), onComplete func()) { 54 | panicCapturingGoWithCallback(func() { 55 | defer func() { 56 | if err := recover(); err == nil && onComplete != nil { 57 | onComplete() 58 | } else if err != nil { 59 | // re-panic 60 | panic(err) 61 | } 62 | }() 63 | f() 64 | }, func(_ interface{}) { 65 | managedGo(f, onComplete) 66 | }) 67 | } 68 | 69 | // selectContextOrWait either terminates because the given context is done 70 | // or the given duration elapses. It returns true if the duration elapsed. 71 | func selectContextOrWait(ctx context.Context, dur time.Duration) bool { 72 | timer := time.NewTimer(dur) 73 | defer timer.Stop() 74 | return selectContextOrWaitChan(ctx, timer.C) 75 | } 76 | 77 | // selectContextOrWaitChan either terminates because the given context is done 78 | // or the given time channel is received on. It returns true if the channel 79 | // was received on. 80 | func selectContextOrWaitChan(ctx context.Context, c <-chan time.Time) bool { 81 | select { 82 | case <-ctx.Done(): 83 | return false 84 | default: 85 | } 86 | select { 87 | case <-ctx.Done(): 88 | return false 89 | case <-c: 90 | } 91 | return true 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ZeroConf: Service Discovery with mDNS 2 | ===================================== 3 | ZeroConf is a pure Golang library that employs Multicast DNS-SD for 4 | 5 | * browsing and resolving services in your network 6 | * registering own services 7 | 8 | in the local network. 9 | 10 | It basically implements aspects of the standards 11 | [RFC 6762](https://tools.ietf.org/html/rfc6762) (mDNS) and 12 | [RFC 6763](https://tools.ietf.org/html/rfc6763) (DNS-SD). 13 | Though it does not support all requirements yet, the aim is to provide a compliant solution in the long-term with the community. 14 | 15 | By now, it should be compatible to [Avahi](http://avahi.org/) (tested) and Apple's Bonjour (untested). 16 | Target environments: private LAN/Wifi, small or isolated networks. 17 | 18 | [![GoDoc](https://godoc.org/github.com/viamrobotics/zeroconf?status.svg)](https://godoc.org/github.com/viamrobotics/zeroconf) 19 | [![Go Report Card](https://goreportcard.com/badge/github.com/viamrobotics/zeroconf)](https://goreportcard.com/report/github.com/viamrobotics/zeroconf) 20 | [![Tests](https://github.com/viamrobotics/zeroconf/actions/workflows/go-test.yml/badge.svg)](https://github.com/viamrobotics/zeroconf/actions/workflows/go-test.yml) 21 | 22 | ## Install 23 | Nothing is as easy as that: 24 | ```bash 25 | $ go get -u github.com/viamrobotics/zeroconf 26 | ``` 27 | This package requires **Go 1.7** (context in std lib) or later. 28 | 29 | ## Browse for services in your local network 30 | 31 | ```go 32 | // Discover all services on the network (e.g. _workstation._tcp) 33 | resolver, err := zeroconf.NewResolver(nil) 34 | if err != nil { 35 | log.Fatalln("Failed to initialize resolver:", err.Error()) 36 | } 37 | 38 | entries := make(chan *zeroconf.ServiceEntry) 39 | go func(results <-chan *zeroconf.ServiceEntry) { 40 | for entry := range results { 41 | log.Println(entry) 42 | } 43 | log.Println("No more entries.") 44 | }(entries) 45 | 46 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) 47 | defer cancel() 48 | err = resolver.Browse(ctx, "_workstation._tcp", "local.", entries) 49 | if err != nil { 50 | log.Fatalln("Failed to browse:", err.Error()) 51 | } 52 | 53 | <-ctx.Done() 54 | ``` 55 | 56 | ## Lookup a specific service instance 57 | 58 | ```go 59 | // Example filled soon. 60 | ``` 61 | 62 | ## Register a service 63 | 64 | ```go 65 | server, err := zeroconf.Register("GoZeroconf", "_workstation._tcp", "local.", 42424, []string{"txtv=0", "lo=1", "la=2"}, nil) 66 | if err != nil { 67 | panic(err) 68 | } 69 | defer server.Shutdown() 70 | 71 | // Clean exit. 72 | sig := make(chan os.Signal, 1) 73 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 74 | select { 75 | case <-sig: 76 | // Exit by user 77 | case <-time.After(time.Second * 120): 78 | // Exit by timeout 79 | } 80 | 81 | log.Println("Shutting down.") 82 | ``` 83 | 84 | ## Credits 85 | Great thanks to [hashicorp](https://github.com/hashicorp/mdns) and to [oleksandr](https://github.com/oleksandr/bonjour) and all contributing authors for the code this projects bases upon. 86 | Large parts of the code are still the same. 87 | 88 | However, there are several reasons why I decided to create a fork of the original project: 89 | The previous project seems to be unmaintained. There are several useful pull requests waiting. I merged most of them in this project. 90 | Still, the implementation has some bugs and lacks some other features that make it quite unreliable in real LAN environments when running continously. 91 | Last but not least, the aim for this project is to build a solution that targets standard conformance in the long term with the support of the community. 92 | Though, resiliency should remain a top goal. 93 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package zeroconf 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "golang.org/x/net/ipv4" 8 | "golang.org/x/net/ipv6" 9 | ) 10 | 11 | var ( 12 | // Multicast groups used by mDNS 13 | mdnsGroupIPv4 = net.IPv4(224, 0, 0, 251) 14 | mdnsGroupIPv6 = net.ParseIP("ff02::fb") 15 | 16 | // mDNS wildcard addresses 17 | mdnsWildcardAddrIPv4 = &net.UDPAddr{ 18 | IP: net.ParseIP("224.0.0.0"), 19 | Port: 5353, 20 | } 21 | mdnsWildcardAddrIPv6 = &net.UDPAddr{ 22 | IP: net.ParseIP("ff02::"), 23 | // IP: net.ParseIP("fd00::12d3:26e7:48db:e7d"), 24 | Port: 5353, 25 | } 26 | 27 | // mDNS endpoint addresses 28 | ipv4Addr = &net.UDPAddr{ 29 | IP: mdnsGroupIPv4, 30 | Port: 5353, 31 | } 32 | ipv6Addr = &net.UDPAddr{ 33 | IP: mdnsGroupIPv6, 34 | Port: 5353, 35 | } 36 | ) 37 | 38 | func joinUdp6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, []net.Interface, error) { 39 | udpConn, err := net.ListenUDP("udp6", mdnsWildcardAddrIPv6) 40 | if err != nil { 41 | return nil, nil, err 42 | } 43 | 44 | // Join multicast groups to receive announcements 45 | pkConn := ipv6.NewPacketConn(udpConn) 46 | pkConn.SetControlMessage(ipv6.FlagInterface, true) 47 | 48 | if len(interfaces) == 0 { 49 | interfaces = listMulticastInterfaces() 50 | } 51 | // log.Println("Using multicast interfaces: ", interfaces) 52 | 53 | var failedJoins int 54 | joinedIfcs := make([]net.Interface, 0, len(interfaces)) 55 | for _, iface := range interfaces { 56 | if err := pkConn.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { 57 | // log.Println("Udp6 JoinGroup failed for iface ", iface) 58 | failedJoins++ 59 | continue 60 | } 61 | ifaceCopy := iface 62 | joinedIfcs = append(joinedIfcs, ifaceCopy) 63 | } 64 | if failedJoins == len(interfaces) { 65 | pkConn.Close() 66 | return nil, nil, fmt.Errorf("udp6: failed to join any of these interfaces: %v", interfaces) 67 | } 68 | 69 | return pkConn, joinedIfcs, nil 70 | } 71 | 72 | func joinUdp4Multicast(interfaces []net.Interface) (*ipv4.PacketConn, []net.Interface, error) { 73 | udpConn, err := net.ListenUDP("udp4", mdnsWildcardAddrIPv4) 74 | if err != nil { 75 | // log.Printf("[ERR] bonjour: Failed to bind to udp4 mutlicast: %v", err) 76 | return nil, nil, err 77 | } 78 | 79 | // Join multicast groups to receive announcements 80 | pkConn := ipv4.NewPacketConn(udpConn) 81 | pkConn.SetControlMessage(ipv4.FlagInterface, true) 82 | 83 | if len(interfaces) == 0 { 84 | interfaces = listMulticastInterfaces() 85 | } 86 | // log.Println("Using multicast interfaces: ", interfaces) 87 | 88 | var failedJoins int 89 | joinedIfcs := make([]net.Interface, 0, len(interfaces)) 90 | for _, iface := range interfaces { 91 | if err := pkConn.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { 92 | // log.Println("Udp4 JoinGroup failed for iface ", iface) 93 | failedJoins++ 94 | continue 95 | } 96 | ifaceCopy := iface 97 | joinedIfcs = append(joinedIfcs, ifaceCopy) 98 | } 99 | if failedJoins == len(interfaces) { 100 | pkConn.Close() 101 | return nil, nil, fmt.Errorf("udp4: failed to join any of these interfaces: %v", interfaces) 102 | } 103 | 104 | return pkConn, joinedIfcs, nil 105 | } 106 | 107 | func listMulticastInterfaces() []net.Interface { 108 | var interfaces []net.Interface 109 | ifaces, err := net.Interfaces() 110 | if err != nil { 111 | return nil 112 | } 113 | for _, ifi := range ifaces { 114 | if (ifi.Flags & net.FlagUp) == 0 { 115 | continue 116 | } 117 | if (ifi.Flags & net.FlagMulticast) > 0 { 118 | interfaces = append(interfaces, ifi) 119 | } 120 | } 121 | 122 | return interfaces 123 | } 124 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package zeroconf 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | // ServiceRecord contains the basic description of a service, which contains instance name, service type & domain 10 | type ServiceRecord struct { 11 | Instance string `json:"name"` // Instance name (e.g. "My web page") 12 | Service string `json:"type"` // Service name (e.g. _http._tcp.) 13 | Subtypes []string `json:"subtypes"` // Service subtypes 14 | Domain string `json:"domain"` // If blank, assumes "local" 15 | 16 | // private variable populated on ServiceRecord creation 17 | serviceName string 18 | serviceInstanceName string 19 | serviceTypeName string 20 | } 21 | 22 | // ServiceName returns a complete service name (e.g. _foobar._tcp.local.), which is composed 23 | // of a service name (also referred as service type) and a domain. 24 | func (s *ServiceRecord) ServiceName() string { 25 | return s.serviceName 26 | } 27 | 28 | // ServiceInstanceName returns a complete service instance name (e.g. MyDemo\ Service._foobar._tcp.local.), 29 | // which is composed from service instance name, service name and a domain. 30 | func (s *ServiceRecord) ServiceInstanceName() string { 31 | return s.serviceInstanceName 32 | } 33 | 34 | // ServiceTypeName returns the complete identifier for a DNS-SD query. 35 | func (s *ServiceRecord) ServiceTypeName() string { 36 | return s.serviceTypeName 37 | } 38 | 39 | // NewServiceRecord constructs a ServiceRecord. 40 | func NewServiceRecord(instance, service string, domain string) *ServiceRecord { 41 | service, subtypes := parseSubtypes(service) 42 | s := &ServiceRecord{ 43 | Instance: instance, 44 | Service: service, 45 | Domain: domain, 46 | serviceName: fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)), 47 | } 48 | 49 | for _, subtype := range subtypes { 50 | s.Subtypes = append(s.Subtypes, fmt.Sprintf("%s._sub.%s", trimDot(subtype), s.serviceName)) 51 | } 52 | 53 | // Cache service instance name 54 | if instance != "" { 55 | s.serviceInstanceName = fmt.Sprintf("%s.%s", trimDot(s.Instance), s.ServiceName()) 56 | } 57 | 58 | // Cache service type name domain 59 | typeNameDomain := "local" 60 | if len(s.Domain) > 0 { 61 | typeNameDomain = trimDot(s.Domain) 62 | } 63 | s.serviceTypeName = fmt.Sprintf("_services._dns-sd._udp.%s.", typeNameDomain) 64 | 65 | return s 66 | } 67 | 68 | // lookupParams contains configurable properties to create a service discovery request 69 | type lookupParams struct { 70 | ServiceRecord 71 | Entries chan<- *ServiceEntry // Entries Channel 72 | 73 | isBrowsing bool 74 | stopProbing chan struct{} 75 | once sync.Once 76 | } 77 | 78 | // newLookupParams constructs a lookupParams. 79 | func newLookupParams(instance, service, domain string, isBrowsing bool, entries chan<- *ServiceEntry) *lookupParams { 80 | p := &lookupParams{ 81 | ServiceRecord: *NewServiceRecord(instance, service, domain), 82 | Entries: entries, 83 | isBrowsing: isBrowsing, 84 | } 85 | if !isBrowsing { 86 | p.stopProbing = make(chan struct{}) 87 | } 88 | return p 89 | } 90 | 91 | // Notify subscriber that no more entries will arrive. Mostly caused 92 | // by an expired context. 93 | func (l *lookupParams) done() { 94 | close(l.Entries) 95 | } 96 | 97 | func (l *lookupParams) disableProbing() { 98 | l.once.Do(func() { close(l.stopProbing) }) 99 | } 100 | 101 | // ServiceEntry represents a browse/lookup result for client API. 102 | // It is also used to configure service registration (server API), which is 103 | // used to answer multicast queries. 104 | type ServiceEntry struct { 105 | ServiceRecord 106 | HostName string `json:"hostname"` // Host machine DNS name 107 | Port int `json:"port"` // Service Port 108 | Text []string `json:"text"` // Service info served as a TXT record 109 | TTL uint32 `json:"ttl"` // TTL of the service record 110 | AddrIPv4 []net.IP `json:"-"` // Host machine IPv4 address 111 | AddrIPv6 []net.IP `json:"-"` // Host machine IPv6 address 112 | } 113 | 114 | // NewServiceEntry constructs a ServiceEntry. 115 | func NewServiceEntry(instance, service string, domain string) *ServiceEntry { 116 | return &ServiceEntry{ 117 | ServiceRecord: *NewServiceRecord(instance, service, domain), 118 | } 119 | } 120 | 121 | // NonServiceHostName is used for matching questions against simple `machine-name.local` 122 | // requests. Not for service discovery requests. 123 | func (s *ServiceEntry) NonServiceHostName() string { 124 | return s.HostName 125 | } 126 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | package zeroconf 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/edaniels/golog" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | var ( 14 | mdnsName = "test--xxxxxxxxxxxx" 15 | mdnsService = "_test--xxxx._tcp" 16 | mdnsSubtype = "_test--xxxx._tcp,_fancy" 17 | mdnsDomain = "local." 18 | mdnsPort = 8888 19 | ) 20 | 21 | func startMDNS(t *testing.T, ctx context.Context, port int, name, service, domain string) { 22 | logger := golog.NewTestLogger(t) 23 | // 5353 is default mdns port 24 | server, err := Register(name, service, domain, port, []string{"txtv=0", "lo=1", "la=2"}, nil, logger) 25 | if err != nil { 26 | panic(errors.Wrap(err, "while registering mdns service")) 27 | } 28 | defer server.Shutdown() 29 | log.Printf("Published service: %s, type: %s, domain: %s", name, service, domain) 30 | 31 | <-ctx.Done() 32 | 33 | log.Print("Shutting down.") 34 | } 35 | 36 | func TestBasic(t *testing.T) { 37 | logger := golog.NewTestLogger(t) 38 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 39 | defer cancel() 40 | 41 | go startMDNS(t, ctx, mdnsPort, mdnsName, mdnsService, mdnsDomain) 42 | 43 | time.Sleep(time.Second) 44 | 45 | resolver, err := NewResolver(logger) 46 | if err != nil { 47 | t.Fatalf("Expected create resolver success, but got %v", err) 48 | } 49 | entries := make(chan *ServiceEntry, 100) 50 | if err := resolver.Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { 51 | t.Fatalf("Expected browse success, but got %v", err) 52 | } 53 | <-ctx.Done() 54 | 55 | if len(entries) != 1 { 56 | t.Fatalf("Expected number of service entries is 1, but got %d", len(entries)) 57 | } 58 | result := <-entries 59 | if result.Domain != mdnsDomain { 60 | t.Fatalf("Expected domain is %s, but got %s", mdnsDomain, result.Domain) 61 | } 62 | if result.Service != mdnsService { 63 | t.Fatalf("Expected service is %s, but got %s", mdnsService, result.Service) 64 | } 65 | if result.Instance != mdnsName { 66 | t.Fatalf("Expected instance is %s, but got %s", mdnsName, result.Instance) 67 | } 68 | if result.Port != mdnsPort { 69 | t.Fatalf("Expected port is %d, but got %d", mdnsPort, result.Port) 70 | } 71 | } 72 | 73 | func TestNoRegister(t *testing.T) { 74 | logger := golog.NewTestLogger(t) 75 | resolver, err := NewResolver(logger) 76 | if err != nil { 77 | t.Fatalf("Expected create resolver success, but got %v", err) 78 | } 79 | 80 | // before register, mdns resolve shuold not have any entry 81 | entries := make(chan *ServiceEntry) 82 | go func(results <-chan *ServiceEntry) { 83 | s := <-results 84 | if s != nil { 85 | t.Errorf("Expected empty service entries but got %v", *s) 86 | } 87 | }(entries) 88 | 89 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 90 | if err := resolver.Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { 91 | t.Fatalf("Expected browse success, but got %v", err) 92 | } 93 | <-ctx.Done() 94 | cancel() 95 | } 96 | 97 | func TestSubtype(t *testing.T) { 98 | t.Run("browse with subtype", func(t *testing.T) { 99 | logger := golog.NewTestLogger(t) 100 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 101 | defer cancel() 102 | 103 | go startMDNS(t, ctx, mdnsPort, mdnsName, mdnsSubtype, mdnsDomain) 104 | 105 | time.Sleep(time.Second) 106 | 107 | resolver, err := NewResolver(logger) 108 | if err != nil { 109 | t.Fatalf("Expected create resolver success, but got %v", err) 110 | } 111 | entries := make(chan *ServiceEntry, 100) 112 | if err := resolver.Browse(ctx, mdnsSubtype, mdnsDomain, entries); err != nil { 113 | t.Fatalf("Expected browse success, but got %v", err) 114 | } 115 | <-ctx.Done() 116 | 117 | if len(entries) != 1 { 118 | t.Fatalf("Expected number of service entries is 1, but got %d", len(entries)) 119 | } 120 | result := <-entries 121 | if result.Domain != mdnsDomain { 122 | t.Fatalf("Expected domain is %s, but got %s", mdnsDomain, result.Domain) 123 | } 124 | if result.Service != mdnsService { 125 | t.Fatalf("Expected service is %s, but got %s", mdnsService, result.Service) 126 | } 127 | if result.Instance != mdnsName { 128 | t.Fatalf("Expected instance is %s, but got %s", mdnsName, result.Instance) 129 | } 130 | if result.Port != mdnsPort { 131 | t.Fatalf("Expected port is %d, but got %d", mdnsPort, result.Port) 132 | } 133 | }) 134 | 135 | t.Run("browse without subtype", func(t *testing.T) { 136 | logger := golog.NewTestLogger(t) 137 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 138 | defer cancel() 139 | 140 | go startMDNS(t, ctx, mdnsPort, mdnsName, mdnsSubtype, mdnsDomain) 141 | 142 | time.Sleep(time.Second) 143 | 144 | resolver, err := NewResolver(logger) 145 | if err != nil { 146 | t.Fatalf("Expected create resolver success, but got %v", err) 147 | } 148 | entries := make(chan *ServiceEntry, 100) 149 | if err := resolver.Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { 150 | t.Fatalf("Expected browse success, but got %v", err) 151 | } 152 | <-ctx.Done() 153 | 154 | if len(entries) != 1 { 155 | t.Fatalf("Expected number of service entries is 1, but got %d", len(entries)) 156 | } 157 | result := <-entries 158 | if result.Domain != mdnsDomain { 159 | t.Fatalf("Expected domain is %s, but got %s", mdnsDomain, result.Domain) 160 | } 161 | if result.Service != mdnsService { 162 | t.Fatalf("Expected service is %s, but got %s", mdnsService, result.Service) 163 | } 164 | if result.Instance != mdnsName { 165 | t.Fatalf("Expected instance is %s, but got %s", mdnsName, result.Instance) 166 | } 167 | if result.Port != mdnsPort { 168 | t.Fatalf("Expected port is %d, but got %d", mdnsPort, result.Port) 169 | } 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package zeroconf 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/cenkalti/backoff" 13 | "github.com/edaniels/golog" 14 | "github.com/miekg/dns" 15 | "go.uber.org/multierr" 16 | "golang.org/x/net/ipv4" 17 | "golang.org/x/net/ipv6" 18 | ) 19 | 20 | // IPType specifies the IP traffic the client listens for. 21 | // This does not guarantee that only mDNS entries of this sepcific 22 | // type passes. E.g. typical mDNS packets distributed via IPv4, often contain 23 | // both DNS A and AAAA entries. 24 | type IPType uint8 25 | 26 | // Options for IPType. 27 | const ( 28 | IPv4 = 0x01 29 | IPv6 = 0x02 30 | IPv4AndIPv6 = (IPv4 | IPv6) //< Default option. 31 | ) 32 | 33 | type clientOpts struct { 34 | listenOn IPType 35 | acceptOnly IPType 36 | ifaces []net.Interface 37 | } 38 | 39 | // ClientOption fills the option struct to configure intefaces, etc. 40 | type ClientOption func(*clientOpts) 41 | 42 | // SelectIPTraffic selects the type of IP packets (IPv4, IPv6, or both) this 43 | // instance listens for. 44 | // This does not guarantee that only mDNS entries of this sepcific 45 | // type passes. E.g. typical mDNS packets distributed via IPv4, may contain 46 | // both DNS A and AAAA entries. 47 | func SelectIPTraffic(t IPType) ClientOption { 48 | return func(o *clientOpts) { 49 | o.listenOn = t 50 | } 51 | } 52 | 53 | // SelectIPRecordType selects the type of IPs entries should only contain. 54 | func SelectIPRecordType(t IPType) ClientOption { 55 | return func(o *clientOpts) { 56 | o.acceptOnly = t 57 | } 58 | } 59 | 60 | // SelectIfaces selects the interfaces to query for mDNS records 61 | func SelectIfaces(ifaces []net.Interface) ClientOption { 62 | return func(o *clientOpts) { 63 | o.ifaces = ifaces 64 | } 65 | } 66 | 67 | // Resolver acts as entry point for service lookups and to browse the DNS-SD. 68 | type Resolver struct { 69 | c *client 70 | 71 | shutdownCtx context.Context 72 | shutdownCtxCancel func() 73 | shutdownLock sync.Mutex 74 | shutdownEnd *sync.WaitGroup 75 | isShutdown bool 76 | } 77 | 78 | // NewResolver creates a new resolver and joins the UDP multicast groups to 79 | // listen for mDNS messages. 80 | func NewResolver(logger golog.Logger, options ...ClientOption) (*Resolver, error) { 81 | // Apply default configuration and load supplied options. 82 | var conf = clientOpts{ 83 | listenOn: IPv4AndIPv6, 84 | acceptOnly: IPv4AndIPv6, 85 | } 86 | for _, o := range options { 87 | if o != nil { 88 | o(&conf) 89 | } 90 | } 91 | 92 | shutdownCtx, shutdownCtxCancel := context.WithCancel(context.Background()) 93 | var shutdownEnd sync.WaitGroup 94 | c, err := newClient(shutdownCtx, &shutdownEnd, conf, logger) 95 | if err != nil { 96 | shutdownCtxCancel() 97 | return nil, err 98 | } 99 | return &Resolver{ 100 | c: c, 101 | shutdownCtx: shutdownCtx, 102 | shutdownCtxCancel: shutdownCtxCancel, 103 | shutdownEnd: &shutdownEnd, 104 | }, nil 105 | } 106 | 107 | // Browse for all services of a given type in a given domain. 108 | func (r *Resolver) Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry) error { 109 | params := defaultParams("", service, "local") 110 | if domain != "" { 111 | params.Domain = domain 112 | } 113 | params.Entries = entries 114 | params.isBrowsing = true 115 | 116 | return r.startClient(ctx, params) 117 | } 118 | 119 | func (r *Resolver) startClient(ctx context.Context, params *lookupParams) error { 120 | ctx, cancel := context.WithCancel(ctx) 121 | r.c.mainloop(ctx, params) 122 | err := r.c.query(params) 123 | if err != nil { 124 | // cancel mainloop 125 | cancel() 126 | return err 127 | } 128 | // If previous probe was ok, it should be fine now. In case of an error later on, 129 | // the entries' queue is closed. 130 | r.shutdownEnd.Add(1) 131 | managedGo(func() { 132 | if err := r.c.periodicQuery(ctx, params); err != nil { 133 | cancel() 134 | } 135 | }, r.shutdownEnd.Done) 136 | 137 | return nil 138 | } 139 | 140 | // Lookup a specific service by its name and type in a given domain. 141 | func (r *Resolver) Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry) error { 142 | params := defaultParams(instance, service, domain) 143 | params.Entries = entries 144 | ctx, cancel := context.WithCancel(ctx) 145 | panicCapturingGo(func() { r.c.mainloop(ctx, params) }) 146 | err := r.c.query(params) 147 | if err != nil { 148 | // cancel mainloop 149 | cancel() 150 | return err 151 | } 152 | // If previous probe was ok, it should be fine now. In case of an error later on, 153 | // the entries' queue is closed. 154 | panicCapturingGo(func() { 155 | if err := r.c.periodicQuery(ctx, params); err != nil { 156 | cancel() 157 | } 158 | }) 159 | 160 | return nil 161 | } 162 | 163 | // Shutdown closes the client and waits for all goroutines to finish. 164 | func (r *Resolver) Shutdown() { 165 | r.shutdownLock.Lock() 166 | defer r.shutdownLock.Unlock() 167 | if r.isShutdown { 168 | return 169 | } 170 | 171 | r.shutdownCtxCancel() 172 | 173 | r.c.shutdown() 174 | 175 | // Wait for goroutines to finish. 176 | r.shutdownEnd.Wait() 177 | r.isShutdown = true 178 | } 179 | 180 | // defaultParams returns a default set of QueryParams. 181 | func defaultParams(instance, service, domain string) *lookupParams { 182 | return newLookupParams(instance, service, domain, false, make(chan *ServiceEntry)) 183 | } 184 | 185 | // Client structure encapsulates both IPv4/IPv6 UDP connections. 186 | type client struct { 187 | shutdownCtx context.Context 188 | shutdownEnd *sync.WaitGroup 189 | ipv4conn *ipv4.PacketConn 190 | ipv4Ifaces []net.Interface 191 | ipv6conn *ipv6.PacketConn 192 | ipv6Ifaces []net.Interface 193 | acceptOnly IPType 194 | logger golog.Logger 195 | inboundBufferSize int 196 | } 197 | 198 | var errNoPositiveMTUFound = errors.New("no positive MTU found") 199 | 200 | // Client structure constructor 201 | func newClient( 202 | shutdownCtx context.Context, 203 | shutdownEnd *sync.WaitGroup, 204 | opts clientOpts, 205 | logger golog.Logger, 206 | ) (*client, error) { 207 | ifaces := opts.ifaces 208 | if len(ifaces) == 0 { 209 | ifaces = listMulticastInterfaces() 210 | } 211 | // IPv4 interfaces 212 | var ipv4conn *ipv4.PacketConn 213 | var ipv4Ifaces []net.Interface 214 | var ipv4Err, ipv6Err error 215 | if (opts.listenOn & IPv4) > 0 { 216 | ipv4conn, ipv4Ifaces, ipv4Err = joinUdp4Multicast(ifaces) 217 | if opts.listenOn == IPv4 && ipv4Err != nil { 218 | return nil, ipv4Err 219 | } 220 | } 221 | // IPv6 interfaces 222 | var ipv6conn *ipv6.PacketConn 223 | var ipv6Ifaces []net.Interface 224 | if (opts.listenOn & IPv6) > 0 { 225 | ipv6conn, ipv6Ifaces, ipv6Err = joinUdp6Multicast(ifaces) 226 | if opts.listenOn == IPv6 && ipv6Err != nil { 227 | return nil, ipv6Err 228 | } 229 | } 230 | 231 | if opts.listenOn == IPv4AndIPv6 && ipv4conn == nil && ipv6conn == nil { 232 | return nil, fmt.Errorf("failed to listen on a socket; error: %w", multierr.Combine(ipv4Err, ipv6Err)) 233 | } 234 | 235 | inboundBufferSize := 0 236 | for _, ifc := range ipv4Ifaces { 237 | if ifc.MTU > inboundBufferSize { 238 | inboundBufferSize = ifc.MTU 239 | } 240 | } 241 | for _, ifc := range ipv6Ifaces { 242 | if ifc.MTU > inboundBufferSize { 243 | inboundBufferSize = ifc.MTU 244 | } 245 | } 246 | if inboundBufferSize == 0 { 247 | return nil, errNoPositiveMTUFound 248 | } 249 | 250 | return &client{ 251 | shutdownCtx: shutdownCtx, 252 | shutdownEnd: shutdownEnd, 253 | ipv4conn: ipv4conn, 254 | ipv4Ifaces: ipv4Ifaces, 255 | ipv6conn: ipv6conn, 256 | ipv6Ifaces: ipv6Ifaces, 257 | acceptOnly: opts.acceptOnly, 258 | logger: logger, 259 | 260 | // https://www.rfc-editor.org/rfc/rfc6762.html#section-17 261 | // Multicast DNS messages carried by UDP may be up to the IP MTU of the 262 | // physical interface, less the space required for the IP header (20 263 | // bytes for IPv4; 40 bytes for IPv6) and the UDP header (8 bytes). 264 | inboundBufferSize: inboundBufferSize - 20 - 8, 265 | }, nil 266 | } 267 | 268 | // Start listeners and waits for the shutdown signal from exit channel 269 | func (c *client) mainloop(ctx context.Context, params *lookupParams) { 270 | // start listening for responses 271 | msgCh := make(chan *dns.Msg, 32) 272 | if c.ipv4conn != nil { 273 | c.shutdownEnd.Add(1) 274 | managedGo(func() { c.recv(ctx, c.ipv4conn, msgCh) }, c.shutdownEnd.Done) 275 | } 276 | if c.ipv6conn != nil { 277 | c.shutdownEnd.Add(1) 278 | managedGo(func() { c.recv(ctx, c.ipv6conn, msgCh) }, c.shutdownEnd.Done) 279 | } 280 | 281 | c.shutdownEnd.Add(1) 282 | managedGo(func() { 283 | // Iterate through channels from listeners goroutines 284 | var entries, sentEntries map[string]*ServiceEntry 285 | sentEntries = make(map[string]*ServiceEntry) 286 | for { 287 | select { 288 | case <-c.shutdownCtx.Done(): 289 | // Context expired. Notify subscriber that we are done here. 290 | params.done() 291 | return 292 | case <-ctx.Done(): 293 | // Context expired. Notify subscriber that we are done here. 294 | params.done() 295 | return 296 | case msg := <-msgCh: 297 | entries = make(map[string]*ServiceEntry) 298 | sections := append(msg.Answer, msg.Ns...) 299 | sections = append(sections, msg.Extra...) 300 | 301 | for _, answer := range sections { 302 | switch rr := answer.(type) { 303 | case *dns.PTR: 304 | if params.ServiceName() != rr.Hdr.Name { 305 | continue 306 | } 307 | if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Ptr { 308 | continue 309 | } 310 | if _, ok := entries[rr.Ptr]; !ok { 311 | entries[rr.Ptr] = NewServiceEntry( 312 | trimDot(strings.Replace(rr.Ptr, rr.Hdr.Name, "", -1)), 313 | params.Service, 314 | params.Domain) 315 | } 316 | entries[rr.Ptr].TTL = rr.Hdr.Ttl 317 | case *dns.SRV: 318 | if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name { 319 | continue 320 | } else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) { 321 | continue 322 | } 323 | if _, ok := entries[rr.Hdr.Name]; !ok { 324 | entries[rr.Hdr.Name] = NewServiceEntry( 325 | trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)), 326 | params.Service, 327 | params.Domain) 328 | } 329 | entries[rr.Hdr.Name].HostName = rr.Target 330 | entries[rr.Hdr.Name].Port = int(rr.Port) 331 | entries[rr.Hdr.Name].TTL = rr.Hdr.Ttl 332 | case *dns.TXT: 333 | if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name { 334 | continue 335 | } else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) { 336 | continue 337 | } 338 | if _, ok := entries[rr.Hdr.Name]; !ok { 339 | entries[rr.Hdr.Name] = NewServiceEntry( 340 | trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)), 341 | params.Service, 342 | params.Domain) 343 | } 344 | entries[rr.Hdr.Name].Text = rr.Txt 345 | entries[rr.Hdr.Name].TTL = rr.Hdr.Ttl 346 | } 347 | } 348 | // Associate IPs in a second round as other fields should be filled by now. 349 | for _, answer := range sections { 350 | switch rr := answer.(type) { 351 | case *dns.A: 352 | if (c.acceptOnly & IPv4) > 0 { 353 | for k, e := range entries { 354 | if e.HostName == rr.Hdr.Name { 355 | entries[k].AddrIPv4 = append(entries[k].AddrIPv4, rr.A) 356 | } 357 | } 358 | } 359 | case *dns.AAAA: 360 | if (c.acceptOnly & IPv6) > 0 { 361 | for k, e := range entries { 362 | if e.HostName == rr.Hdr.Name { 363 | entries[k].AddrIPv6 = append(entries[k].AddrIPv6, rr.AAAA) 364 | } 365 | } 366 | } 367 | } 368 | } 369 | } 370 | 371 | if len(entries) > 0 { 372 | for k, e := range entries { 373 | if e.TTL == 0 { 374 | delete(entries, k) 375 | delete(sentEntries, k) 376 | continue 377 | } 378 | if _, ok := sentEntries[k]; ok { 379 | continue 380 | } 381 | 382 | // If this is an DNS-SD query do not throw PTR away. 383 | // It is expected to have only PTR for enumeration 384 | if params.ServiceRecord.ServiceTypeName() != params.ServiceRecord.ServiceName() { 385 | // Require at least one resolved IP address for ServiceEntry 386 | // TODO: wait some more time as chances are high both will arrive. 387 | if len(e.AddrIPv4) == 0 && len(e.AddrIPv6) == 0 { 388 | continue 389 | } 390 | } 391 | // Submit entry to subscriber and cache it. 392 | // This is also a point to possibly stop probing actively for a 393 | // service entry. 394 | params.Entries <- e 395 | sentEntries[k] = e 396 | if !params.isBrowsing { 397 | params.disableProbing() 398 | } 399 | } 400 | } 401 | } 402 | }, c.shutdownEnd.Done) 403 | } 404 | 405 | // Shutdown client will close currently open connections and channel implicitly. 406 | func (c *client) shutdown() { 407 | if c.ipv4conn != nil { 408 | c.ipv4conn.Close() 409 | } 410 | if c.ipv6conn != nil { 411 | c.ipv6conn.Close() 412 | } 413 | } 414 | 415 | // Data receiving routine reads from connection, unpacks packets into dns.Msg 416 | // structures and sends them to a given msgCh channel 417 | func (c *client) recv(ctx context.Context, l interface{}, msgCh chan *dns.Msg) { 418 | var readFrom func([]byte) (n int, src net.Addr, err error) 419 | 420 | switch pConn := l.(type) { 421 | case *ipv6.PacketConn: 422 | readFrom = func(b []byte) (n int, src net.Addr, err error) { 423 | n, _, src, err = pConn.ReadFrom(b) 424 | return 425 | } 426 | case *ipv4.PacketConn: 427 | readFrom = func(b []byte) (n int, src net.Addr, err error) { 428 | n, _, src, err = pConn.ReadFrom(b) 429 | return 430 | } 431 | 432 | default: 433 | return 434 | } 435 | 436 | buf := make([]byte, 65536) 437 | var fatalErr error 438 | for { 439 | // Handles the following cases: 440 | // - ReadFrom aborts with error due to closed UDP connection -> causes ctx cancel 441 | // - ReadFrom aborts otherwise. 442 | // TODO: the context check can be removed. Verify! 443 | if ctx.Err() != nil || c.shutdownCtx.Err() != nil || fatalErr != nil { 444 | return 445 | } 446 | 447 | n, _, err := readFrom(buf) 448 | if err != nil { 449 | fatalErr = err 450 | continue 451 | } 452 | msg := new(dns.Msg) 453 | if err := msg.Unpack(buf[:n]); err != nil { 454 | // log.Printf("[WARN] mdns: Failed to unpack packet: %v", err) 455 | continue 456 | } 457 | select { 458 | case msgCh <- msg: 459 | // Submit decoded DNS message and continue. 460 | case <-ctx.Done(): 461 | // Abort. 462 | return 463 | case <-c.shutdownCtx.Done(): 464 | // Abort. 465 | return 466 | } 467 | } 468 | } 469 | 470 | // periodicQuery sens multiple probes until a valid response is received by 471 | // the main processing loop or some timeout/cancel fires. 472 | // TODO: move error reporting to shutdown function as periodicQuery is called from 473 | // go routine context. 474 | func (c *client) periodicQuery(ctx context.Context, params *lookupParams) error { 475 | bo := backoff.NewExponentialBackOff() 476 | bo.InitialInterval = 4 * time.Second 477 | bo.MaxInterval = 60 * time.Second 478 | bo.MaxElapsedTime = 0 479 | bo.Reset() 480 | 481 | var timer *time.Timer 482 | defer func() { 483 | if timer != nil { 484 | timer.Stop() 485 | } 486 | }() 487 | for { 488 | // Backoff and cancel logic. 489 | wait := bo.NextBackOff() 490 | if wait == backoff.Stop { 491 | return errors.New("periodicQuery: abort due to timeout") 492 | } 493 | if timer == nil { 494 | timer = time.NewTimer(wait) 495 | } else { 496 | timer.Reset(wait) 497 | } 498 | select { 499 | case <-timer.C: 500 | // Wait for next iteration. 501 | case <-params.stopProbing: 502 | // Chan is closed (or happened in the past). 503 | // Done here. Received a matching mDNS entry. 504 | return nil 505 | case <-c.shutdownCtx.Done(): 506 | return c.shutdownCtx.Err() 507 | case <-ctx.Done(): 508 | return ctx.Err() 509 | } 510 | // Do periodic query. 511 | if err := c.query(params); err != nil { 512 | return err 513 | } 514 | } 515 | } 516 | 517 | // Performs the actual query by service name (browse) or service instance name (lookup), 518 | // start response listeners goroutines and loops over the entries channel. 519 | func (c *client) query(params *lookupParams) error { 520 | var serviceName, serviceInstanceName string 521 | serviceName = fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain)) 522 | 523 | // send the query 524 | m := new(dns.Msg) 525 | if params.Instance != "" { // service instance name lookup 526 | serviceInstanceName = fmt.Sprintf("%s.%s", params.Instance, serviceName) 527 | m.Question = []dns.Question{ 528 | {Name: serviceInstanceName, Qtype: dns.TypeSRV, Qclass: dns.ClassINET}, 529 | {Name: serviceInstanceName, Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, 530 | } 531 | } else if len(params.Subtypes) > 0 { // service subtype browse 532 | m.SetQuestion(params.Subtypes[0], dns.TypePTR) 533 | } else { // service name browse 534 | m.SetQuestion(serviceName, dns.TypePTR) 535 | } 536 | m.RecursionDesired = false 537 | if err := c.sendQuery(m); err != nil { 538 | return err 539 | } 540 | 541 | return nil 542 | } 543 | 544 | // Pack the dns.Msg and write to available connections (multicast) 545 | func (c *client) sendQuery(msg *dns.Msg) error { 546 | b := make([]byte, c.inboundBufferSize) 547 | buf, err := msg.PackBuffer(b) 548 | if err != nil { 549 | return err 550 | } 551 | if c.ipv4conn != nil { 552 | for ifcIdx := range c.ipv4Ifaces { 553 | if err := c.ipv4conn.SetMulticastInterface(&c.ipv4Ifaces[ifcIdx]); err != nil { 554 | c.logger.Debugw("mdns: failed to set multicast interface", "error", err) 555 | } else { 556 | c.ipv4conn.WriteTo(buf, nil, ipv4Addr) 557 | } 558 | } 559 | } 560 | if c.ipv6conn != nil { 561 | for ifcIdx := range c.ipv6Ifaces { 562 | if err := c.ipv6conn.SetMulticastInterface(&c.ipv6Ifaces[ifcIdx]); err != nil { 563 | c.logger.Debugw("mdns: failed to set multicast interface", "error", err) 564 | } else { 565 | c.ipv6conn.WriteTo(buf, nil, ipv6Addr) 566 | } 567 | } 568 | } 569 | return nil 570 | } 571 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package zeroconf 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "net" 9 | "os" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/edaniels/golog" 15 | "github.com/miekg/dns" 16 | "golang.org/x/net/ipv4" 17 | "golang.org/x/net/ipv6" 18 | ) 19 | 20 | const ( 21 | // Number of Multicast responses sent for a query message (default: 1 < x < 9) 22 | multicastRepetitions = 2 23 | ) 24 | 25 | // Register a service by given arguments. This call will take the system's hostname 26 | // and lookup IP by that hostname. 27 | func Register( 28 | instance, service, domain string, 29 | port int, 30 | text []string, 31 | ifaces []net.Interface, 32 | logger golog.Logger, 33 | ) (*Server, error) { 34 | return register(instance, service, domain, port, text, ifaces, false, logger) 35 | } 36 | 37 | // RegisterDynamic registers a service by the given arguments. This call will take the system's hostname 38 | // and look up IPs as requests come in on a particular interface. 39 | func RegisterDynamic( 40 | instance, service, domain string, 41 | port int, 42 | text []string, 43 | ifaces []net.Interface, 44 | logger golog.Logger, 45 | ) (*Server, error) { 46 | return register(instance, service, domain, port, text, ifaces, true, logger) 47 | } 48 | 49 | func register( 50 | instance, service, domain string, 51 | port int, 52 | text []string, 53 | ifaces []net.Interface, 54 | dynamic bool, 55 | logger golog.Logger, 56 | ) (*Server, error) { 57 | entry := NewServiceEntry(instance, service, domain) 58 | entry.Port = port 59 | entry.Text = text 60 | 61 | if entry.Instance == "" { 62 | return nil, errors.New("missing service instance name") 63 | } 64 | if entry.Service == "" { 65 | return nil, errors.New("missing service name") 66 | } 67 | if entry.Domain == "" { 68 | entry.Domain = "local." 69 | } 70 | if entry.Port == 0 { 71 | return nil, errors.New("missing port") 72 | } 73 | 74 | var err error 75 | if entry.HostName == "" { 76 | entry.HostName, err = os.Hostname() 77 | if err != nil { 78 | return nil, errors.New("could not determine host") 79 | } 80 | } 81 | 82 | if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) { 83 | entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain)) 84 | } 85 | 86 | if len(ifaces) == 0 { 87 | ifaces = listMulticastInterfaces() 88 | } 89 | 90 | if !dynamic { 91 | for _, iface := range ifaces { 92 | v4, v6 := addrsForInterface(&iface) 93 | entry.AddrIPv4 = append(entry.AddrIPv4, v4...) 94 | entry.AddrIPv6 = append(entry.AddrIPv6, v6...) 95 | } 96 | 97 | if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil { 98 | return nil, errors.New("could not determine host IP addresses") 99 | } 100 | } 101 | 102 | return newServerForService(entry, ifaces, logger) 103 | } 104 | 105 | // RegisterProxy registers a service proxy. This call will skip the hostname/IP lookup and 106 | // will use the provided values. 107 | func RegisterProxy( 108 | instance, service, domain string, 109 | port int, 110 | host string, 111 | ips []string, 112 | text []string, 113 | ifaces []net.Interface, 114 | logger golog.Logger, 115 | ) (*Server, error) { 116 | entry := NewServiceEntry(instance, service, domain) 117 | entry.Port = port 118 | entry.Text = text 119 | entry.HostName = host 120 | 121 | if entry.Instance == "" { 122 | return nil, errors.New("missing service instance name") 123 | } 124 | if entry.Service == "" { 125 | return nil, errors.New("missing service name") 126 | } 127 | if entry.HostName == "" { 128 | return nil, errors.New("missing host name") 129 | } 130 | if entry.Domain == "" { 131 | entry.Domain = "local" 132 | } 133 | if entry.Port == 0 { 134 | return nil, errors.New("missing port") 135 | } 136 | 137 | if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) { 138 | entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain)) 139 | } 140 | 141 | for _, ip := range ips { 142 | ipAddr := net.ParseIP(ip) 143 | if ipAddr == nil { 144 | return nil, fmt.Errorf("failed to parse given IP: %v", ip) 145 | } else if ipv4 := ipAddr.To4(); ipv4 != nil { 146 | entry.AddrIPv4 = append(entry.AddrIPv4, ipAddr) 147 | } else if ipv6 := ipAddr.To16(); ipv6 != nil { 148 | entry.AddrIPv6 = append(entry.AddrIPv6, ipAddr) 149 | } else { 150 | return nil, fmt.Errorf("the IP is neither IPv4 nor IPv6: %#v", ipAddr) 151 | } 152 | } 153 | 154 | if len(ifaces) == 0 { 155 | ifaces = listMulticastInterfaces() 156 | } 157 | 158 | return newServerForService(entry, ifaces, logger) 159 | } 160 | 161 | func newServerForService(entry *ServiceEntry, ifaces []net.Interface, logger golog.Logger) (*Server, error) { 162 | s, err := newServer(ifaces, logger) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | s.service = entry 168 | s.startReceivers() 169 | s.shutdownEnd.Add(1) 170 | s.startupWait.Add(1) 171 | managedGo(s.probe, s.shutdownEnd.Done) 172 | s.startupWait.Wait() 173 | 174 | return s, nil 175 | } 176 | 177 | const ( 178 | qClassCacheFlush uint16 = 1 << 15 179 | ) 180 | 181 | // Server structure encapsulates both IPv4/IPv6 UDP connections 182 | type Server struct { 183 | service *ServiceEntry 184 | ipv4conn *ipv4.PacketConn 185 | ipv4Ifaces []net.Interface 186 | ipv6conn *ipv6.PacketConn 187 | ipv6Ifaces []net.Interface 188 | selectedIfaceIndexes map[int]struct{} 189 | logger golog.Logger 190 | inboundBufferSize int 191 | 192 | shutdownCtx context.Context 193 | shutdownCtxCancel func() 194 | shutdownLock sync.Mutex 195 | shutdownEnd sync.WaitGroup 196 | isShutdown bool 197 | ttl uint32 198 | startupWait sync.WaitGroup 199 | } 200 | 201 | // Constructs server structure 202 | func newServer(ifaces []net.Interface, logger golog.Logger) (*Server, error) { 203 | ipv4conn, ipv4Ifaces, err4 := joinUdp4Multicast(ifaces) 204 | if err4 != nil { 205 | logger.Debugw("no suitable IPv4 interface", "error", err4.Error()) 206 | } 207 | ipv6conn, ipv6Ifaces, err6 := joinUdp6Multicast(ifaces) 208 | if err6 != nil { 209 | logger.Debugw("no suitable IPv6 interface", "error", err6.Error()) 210 | } 211 | if err4 != nil && err6 != nil { 212 | // No supported interface left. 213 | return nil, errors.New("no supported interface") 214 | } 215 | inboundBufferSize := 0 216 | selectedIfaceIndexes := map[int]struct{}{} 217 | for _, ifc := range ipv4Ifaces { 218 | selectedIfaceIndexes[ifc.Index] = struct{}{} 219 | if ifc.MTU > inboundBufferSize { 220 | inboundBufferSize = ifc.MTU 221 | } 222 | } 223 | for _, ifc := range ipv6Ifaces { 224 | selectedIfaceIndexes[ifc.Index] = struct{}{} 225 | if ifc.MTU > inboundBufferSize { 226 | inboundBufferSize = ifc.MTU 227 | } 228 | } 229 | if inboundBufferSize == 0 { 230 | return nil, errNoPositiveMTUFound 231 | } 232 | 233 | shutdownCtx, shutdownCtxCancel := context.WithCancel(context.Background()) 234 | s := &Server{ 235 | ipv4conn: ipv4conn, 236 | ipv4Ifaces: ipv4Ifaces, 237 | ipv6conn: ipv6conn, 238 | ipv6Ifaces: ipv6Ifaces, 239 | selectedIfaceIndexes: selectedIfaceIndexes, 240 | ttl: 3200, 241 | shutdownCtx: shutdownCtx, 242 | shutdownCtxCancel: shutdownCtxCancel, 243 | logger: logger, 244 | 245 | // https://www.rfc-editor.org/rfc/rfc6762.html#section-17 246 | // Multicast DNS messages carried by UDP may be up to the IP MTU of the 247 | // physical interface, less the space required for the IP header (20 248 | // bytes for IPv4; 40 bytes for IPv6) and the UDP header (8 bytes). 249 | inboundBufferSize: inboundBufferSize - 20 - 8, 250 | } 251 | 252 | return s, nil 253 | } 254 | 255 | // startReceivers starts both IPv4/6 receiver loops and and waits for the shutdown signal from exit channel 256 | func (s *Server) startReceivers() { 257 | if s.ipv4conn != nil { 258 | s.startupWait.Add(1) 259 | s.shutdownEnd.Add(1) 260 | var nextInstance bool 261 | managedGo(func() { 262 | defer func() { 263 | nextInstance = true 264 | }() 265 | s.recv4(s.ipv4conn, !nextInstance) 266 | }, s.shutdownEnd.Done) 267 | } 268 | if s.ipv6conn != nil { 269 | s.shutdownEnd.Add(1) 270 | s.startupWait.Add(1) 271 | var nextInstance bool 272 | managedGo(func() { 273 | defer func() { 274 | nextInstance = true 275 | }() 276 | s.recv6(s.ipv6conn, !nextInstance) 277 | }, s.shutdownEnd.Done) 278 | } 279 | } 280 | 281 | // Shutdown closes all udp connections and unregisters the service 282 | func (s *Server) Shutdown() { 283 | s.shutdown() 284 | } 285 | 286 | // SetText updates and announces the TXT records 287 | func (s *Server) SetText(text []string) { 288 | s.service.Text = text 289 | s.announceText() 290 | } 291 | 292 | // TTL sets the TTL for DNS replies 293 | func (s *Server) TTL(ttl uint32) { 294 | s.ttl = ttl 295 | } 296 | 297 | // Shutdown server will close currently open connections & channel 298 | func (s *Server) shutdown() error { 299 | s.shutdownLock.Lock() 300 | defer s.shutdownLock.Unlock() 301 | if s.isShutdown { 302 | return errors.New("server is already shutdown") 303 | } 304 | 305 | err := s.unregister() 306 | 307 | s.shutdownCtxCancel() 308 | 309 | if s.ipv4conn != nil { 310 | s.ipv4conn.Close() 311 | } 312 | if s.ipv6conn != nil { 313 | s.ipv6conn.Close() 314 | } 315 | 316 | // Wait for connection and routines to be closed 317 | s.shutdownEnd.Wait() 318 | s.isShutdown = true 319 | 320 | return err 321 | } 322 | 323 | // recv is a long running routine to receive packets from an interface 324 | func (s *Server) recv4(c *ipv4.PacketConn, firstInstance bool) { 325 | if c == nil { 326 | return 327 | } 328 | firstRecv := firstInstance 329 | buf := make([]byte, 65536) 330 | for { 331 | select { 332 | case <-s.shutdownCtx.Done(): 333 | return 334 | default: 335 | if firstRecv { 336 | s.startupWait.Done() 337 | firstRecv = false 338 | } 339 | var ifIndex int 340 | n, cm, from, err := c.ReadFrom(buf) 341 | if err != nil { 342 | continue 343 | } 344 | if cm != nil { 345 | ifIndex = cm.IfIndex 346 | } 347 | _ = s.parsePacket(buf[:n], ifIndex, from) 348 | } 349 | } 350 | } 351 | 352 | // recv is a long running routine to receive packets from an interface 353 | func (s *Server) recv6(c *ipv6.PacketConn, firstInstance bool) { 354 | if c == nil { 355 | return 356 | } 357 | firstRecv := firstInstance 358 | buf := make([]byte, 65536) 359 | for { 360 | select { 361 | case <-s.shutdownCtx.Done(): 362 | return 363 | default: 364 | if firstRecv { 365 | s.startupWait.Done() 366 | firstRecv = false 367 | } 368 | var ifIndex int 369 | n, cm, from, err := c.ReadFrom(buf) 370 | if err != nil { 371 | continue 372 | } 373 | if cm != nil { 374 | ifIndex = cm.IfIndex 375 | } 376 | _ = s.parsePacket(buf[:n], ifIndex, from) 377 | } 378 | } 379 | } 380 | 381 | // parsePacket is used to parse an incoming packet 382 | func (s *Server) parsePacket(packet []byte, ifIndex int, from net.Addr) error { 383 | var msg dns.Msg 384 | if err := msg.Unpack(packet); err != nil { 385 | // log.Printf("[ERR] zeroconf: Failed to unpack packet: %v", err) 386 | return err 387 | } 388 | return s.handleQuery(&msg, ifIndex, from) 389 | } 390 | 391 | // handleQuery is used to handle an incoming query 392 | func (s *Server) handleQuery(query *dns.Msg, ifIndex int, from net.Addr) error { 393 | // Ignore questions with authoritative section for now 394 | if len(query.Ns) > 0 { 395 | return nil 396 | } 397 | 398 | // Handle each question 399 | var err error 400 | for _, q := range query.Question { 401 | resp := dns.Msg{} 402 | resp.SetReply(query) 403 | resp.Compress = true 404 | resp.RecursionDesired = false 405 | resp.Authoritative = true 406 | resp.Question = nil // RFC6762 section 6 "responses MUST NOT contain any questions" 407 | resp.Answer = []dns.RR{} 408 | resp.Extra = []dns.RR{} 409 | if err = s.handleQuestion(q, &resp, query, ifIndex); err != nil { 410 | // log.Printf("[ERR] zeroconf: failed to handle question %v: %v", q, err) 411 | continue 412 | } 413 | // Check if there is an answer 414 | if len(resp.Answer) == 0 { 415 | continue 416 | } 417 | 418 | if isUnicastQuestion(q) { 419 | // Send unicast 420 | if e := s.unicastResponse(&resp, ifIndex, from); e != nil { 421 | err = e 422 | } 423 | } else { 424 | // Send mulicast 425 | if e := s.multicastResponse(&resp, ifIndex); e != nil { 426 | err = e 427 | } 428 | } 429 | } 430 | 431 | return err 432 | } 433 | 434 | // RFC6762 7.1. Known-Answer Suppression 435 | func isKnownAnswer(resp *dns.Msg, query *dns.Msg) bool { 436 | if len(resp.Answer) == 0 || len(query.Answer) == 0 { 437 | return false 438 | } 439 | 440 | if resp.Answer[0].Header().Rrtype != dns.TypePTR { 441 | return false 442 | } 443 | answer := resp.Answer[0].(*dns.PTR) 444 | 445 | for _, known := range query.Answer { 446 | hdr := known.Header() 447 | if hdr.Rrtype != answer.Hdr.Rrtype { 448 | continue 449 | } 450 | ptr := known.(*dns.PTR) 451 | if ptr.Ptr == answer.Ptr && hdr.Ttl >= answer.Hdr.Ttl/2 { 452 | // log.Printf("skipping known answer: %v", ptr) 453 | return true 454 | } 455 | } 456 | 457 | return false 458 | } 459 | 460 | // handleQuestion is used to handle an incoming question 461 | func (s *Server) handleQuestion(q dns.Question, resp *dns.Msg, query *dns.Msg, ifIndex int) error { 462 | if s.service == nil { 463 | return nil 464 | } 465 | 466 | switch q.Name { 467 | case s.service.ServiceTypeName(): 468 | s.serviceTypeName(resp, s.ttl) 469 | if isKnownAnswer(resp, query) { 470 | resp.Answer = nil 471 | } 472 | 473 | case s.service.ServiceName(), s.service.NonServiceHostName(): 474 | s.composeBrowsingAnswers(resp, ifIndex) 475 | if isKnownAnswer(resp, query) { 476 | resp.Answer = nil 477 | } 478 | 479 | case s.service.ServiceInstanceName(): 480 | s.composeLookupAnswers(resp, s.ttl, ifIndex, false) 481 | default: 482 | // handle matching subtype query 483 | for _, subtype := range s.service.Subtypes { 484 | subtype = fmt.Sprintf("%s._sub.%s", subtype, s.service.ServiceName()) 485 | if q.Name == subtype { 486 | s.composeBrowsingAnswers(resp, ifIndex) 487 | if isKnownAnswer(resp, query) { 488 | resp.Answer = nil 489 | } 490 | break 491 | } 492 | } 493 | } 494 | 495 | return nil 496 | } 497 | 498 | func (s *Server) composeBrowsingAnswers(resp *dns.Msg, ifIndex int) { 499 | ptr := &dns.PTR{ 500 | Hdr: dns.RR_Header{ 501 | Name: s.service.ServiceName(), 502 | Rrtype: dns.TypePTR, 503 | Class: dns.ClassINET, 504 | Ttl: s.ttl, 505 | }, 506 | Ptr: s.service.ServiceInstanceName(), 507 | } 508 | resp.Answer = append(resp.Answer, ptr) 509 | 510 | txt := &dns.TXT{ 511 | Hdr: dns.RR_Header{ 512 | Name: s.service.ServiceInstanceName(), 513 | Rrtype: dns.TypeTXT, 514 | Class: dns.ClassINET, 515 | Ttl: s.ttl, 516 | }, 517 | Txt: s.service.Text, 518 | } 519 | srv := &dns.SRV{ 520 | Hdr: dns.RR_Header{ 521 | Name: s.service.ServiceInstanceName(), 522 | Rrtype: dns.TypeSRV, 523 | Class: dns.ClassINET, 524 | Ttl: s.ttl, 525 | }, 526 | Priority: 0, 527 | Weight: 0, 528 | Port: uint16(s.service.Port), 529 | Target: s.service.HostName, 530 | } 531 | resp.Extra = append(resp.Extra, srv, txt) 532 | 533 | resp.Extra = s.appendAddrs(resp.Extra, s.ttl, ifIndex, false) 534 | } 535 | 536 | func (s *Server) composeLookupAnswers(resp *dns.Msg, ttl uint32, ifIndex int, flushCache bool) { 537 | // From RFC6762 538 | // The most significant bit of the rrclass for a record in the Answer 539 | // Section of a response message is the Multicast DNS cache-flush bit 540 | // and is discussed in more detail below in Section 10.2, "Announcements 541 | // to Flush Outdated Cache Entries". 542 | ptr := &dns.PTR{ 543 | Hdr: dns.RR_Header{ 544 | Name: s.service.ServiceName(), 545 | Rrtype: dns.TypePTR, 546 | Class: dns.ClassINET, 547 | Ttl: ttl, 548 | }, 549 | Ptr: s.service.ServiceInstanceName(), 550 | } 551 | srv := &dns.SRV{ 552 | Hdr: dns.RR_Header{ 553 | Name: s.service.ServiceInstanceName(), 554 | Rrtype: dns.TypeSRV, 555 | Class: dns.ClassINET | qClassCacheFlush, 556 | Ttl: ttl, 557 | }, 558 | Priority: 0, 559 | Weight: 0, 560 | Port: uint16(s.service.Port), 561 | Target: s.service.HostName, 562 | } 563 | txt := &dns.TXT{ 564 | Hdr: dns.RR_Header{ 565 | Name: s.service.ServiceInstanceName(), 566 | Rrtype: dns.TypeTXT, 567 | Class: dns.ClassINET | qClassCacheFlush, 568 | Ttl: ttl, 569 | }, 570 | Txt: s.service.Text, 571 | } 572 | dnssd := &dns.PTR{ 573 | Hdr: dns.RR_Header{ 574 | Name: s.service.ServiceTypeName(), 575 | Rrtype: dns.TypePTR, 576 | Class: dns.ClassINET, 577 | Ttl: ttl, 578 | }, 579 | Ptr: s.service.ServiceName(), 580 | } 581 | resp.Answer = append(resp.Answer, srv, txt, ptr, dnssd) 582 | 583 | for _, subtype := range s.service.Subtypes { 584 | resp.Answer = append(resp.Answer, 585 | &dns.PTR{ 586 | Hdr: dns.RR_Header{ 587 | Name: subtype, 588 | Rrtype: dns.TypePTR, 589 | Class: dns.ClassINET, 590 | Ttl: ttl, 591 | }, 592 | Ptr: s.service.ServiceInstanceName(), 593 | }) 594 | } 595 | 596 | resp.Answer = s.appendAddrs(resp.Answer, ttl, ifIndex, flushCache) 597 | } 598 | 599 | func (s *Server) serviceTypeName(resp *dns.Msg, ttl uint32) { 600 | // From RFC6762 601 | // 9. Service Type Enumeration 602 | // 603 | // For this purpose, a special meta-query is defined. A DNS query for 604 | // PTR records with the name "_services._dns-sd._udp." yields a 605 | // set of PTR records, where the rdata of each PTR record is the two- 606 | // label name, plus the same domain, e.g., 607 | // "_http._tcp.". 608 | dnssd := &dns.PTR{ 609 | Hdr: dns.RR_Header{ 610 | Name: s.service.ServiceTypeName(), 611 | Rrtype: dns.TypePTR, 612 | Class: dns.ClassINET, 613 | Ttl: ttl, 614 | }, 615 | Ptr: s.service.ServiceName(), 616 | } 617 | resp.Answer = append(resp.Answer, dnssd) 618 | } 619 | 620 | // Perform probing & announcement 621 | // TODO: implement a proper probing & conflict resolution 622 | func (s *Server) probe() { 623 | q := new(dns.Msg) 624 | q.SetQuestion(s.service.ServiceInstanceName(), dns.TypePTR) 625 | q.RecursionDesired = false 626 | 627 | srv := &dns.SRV{ 628 | Hdr: dns.RR_Header{ 629 | Name: s.service.ServiceInstanceName(), 630 | Rrtype: dns.TypeSRV, 631 | Class: dns.ClassINET, 632 | Ttl: s.ttl, 633 | }, 634 | Priority: 0, 635 | Weight: 0, 636 | Port: uint16(s.service.Port), 637 | Target: s.service.HostName, 638 | } 639 | txt := &dns.TXT{ 640 | Hdr: dns.RR_Header{ 641 | Name: s.service.ServiceInstanceName(), 642 | Rrtype: dns.TypeTXT, 643 | Class: dns.ClassINET, 644 | Ttl: s.ttl, 645 | }, 646 | Txt: s.service.Text, 647 | } 648 | q.Ns = []dns.RR{srv, txt} 649 | 650 | randomizer := rand.New(rand.NewSource(time.Now().UnixNano())) 651 | 652 | for i := 0; i < multicastRepetitions; i++ { 653 | if err := s.multicastResponse(q, 0); err != nil { 654 | s.logger.Debugw("failed to send probe", "error", err.Error()) 655 | } 656 | if i == 0 { 657 | s.startupWait.Done() 658 | } 659 | if !selectContextOrWait(s.shutdownCtx, time.Duration(randomizer.Intn(250))*time.Millisecond) { 660 | return 661 | } 662 | } 663 | 664 | // From RFC6762 665 | // The Multicast DNS responder MUST send at least two unsolicited 666 | // responses, one second apart. To provide increased robustness against 667 | // packet loss, a responder MAY send up to eight unsolicited responses, 668 | // provided that the interval between unsolicited responses increases by 669 | // at least a factor of two with every response sent. 670 | timeout := 1 * time.Second 671 | for i := 0; i < multicastRepetitions; i++ { 672 | for intfIndex := range s.selectedIfaceIndexes { 673 | resp := new(dns.Msg) 674 | resp.MsgHdr.Response = true 675 | // TODO: make response authoritative if we are the publisher 676 | resp.Compress = true 677 | resp.Answer = []dns.RR{} 678 | resp.Extra = []dns.RR{} 679 | s.composeLookupAnswers(resp, s.ttl, intfIndex, true) 680 | if err := s.multicastResponse(resp, intfIndex); err != nil { 681 | s.logger.Debugw("failed to send announcement", "error", err.Error()) 682 | } 683 | } 684 | if !selectContextOrWait(s.shutdownCtx, timeout) { 685 | return 686 | } 687 | timeout *= 2 688 | } 689 | } 690 | 691 | // announceText sends a Text announcement with cache flush enabled 692 | func (s *Server) announceText() { 693 | resp := new(dns.Msg) 694 | resp.MsgHdr.Response = true 695 | 696 | txt := &dns.TXT{ 697 | Hdr: dns.RR_Header{ 698 | Name: s.service.ServiceInstanceName(), 699 | Rrtype: dns.TypeTXT, 700 | Class: dns.ClassINET | qClassCacheFlush, 701 | Ttl: s.ttl, 702 | }, 703 | Txt: s.service.Text, 704 | } 705 | 706 | resp.Answer = []dns.RR{txt} 707 | s.multicastResponse(resp, 0) 708 | } 709 | 710 | func (s *Server) unregister() error { 711 | resp := new(dns.Msg) 712 | resp.MsgHdr.Response = true 713 | resp.Answer = []dns.RR{} 714 | resp.Extra = []dns.RR{} 715 | s.composeLookupAnswers(resp, 0, 0, true) 716 | return s.multicastResponse(resp, 0) 717 | } 718 | 719 | func (s *Server) appendAddrs(list []dns.RR, ttl uint32, ifIndex int, flushCache bool) []dns.RR { 720 | v4 := s.service.AddrIPv4 721 | v6 := s.service.AddrIPv6 722 | if len(v4) == 0 && len(v6) == 0 { 723 | iface, _ := net.InterfaceByIndex(ifIndex) 724 | if iface != nil { 725 | a4, a6 := addrsForInterface(iface) 726 | v4 = append(v4, a4...) 727 | v6 = append(v6, a6...) 728 | } 729 | } 730 | if iface, _ := net.InterfaceByIndex(ifIndex); iface != nil { 731 | if (iface.Flags & net.FlagLoopback) > 0 { 732 | v4 = append(v4, net.IPv4(127, 0, 0, 1)) 733 | } 734 | } 735 | if ttl > 0 { 736 | // RFC6762 Section 10 says A/AAAA records SHOULD 737 | // use TTL of 120s, to account for network interface 738 | // and IP address changes. 739 | ttl = 120 740 | } 741 | var cacheFlushBit uint16 742 | if flushCache { 743 | cacheFlushBit = qClassCacheFlush 744 | } 745 | for _, ipv4 := range v4 { 746 | a := &dns.A{ 747 | Hdr: dns.RR_Header{ 748 | Name: s.service.HostName, 749 | Rrtype: dns.TypeA, 750 | Class: dns.ClassINET | cacheFlushBit, 751 | Ttl: ttl, 752 | }, 753 | A: ipv4, 754 | } 755 | list = append(list, a) 756 | } 757 | for _, ipv6 := range v6 { 758 | aaaa := &dns.AAAA{ 759 | Hdr: dns.RR_Header{ 760 | Name: s.service.HostName, 761 | Rrtype: dns.TypeAAAA, 762 | Class: dns.ClassINET | cacheFlushBit, 763 | Ttl: ttl, 764 | }, 765 | AAAA: ipv6, 766 | } 767 | list = append(list, aaaa) 768 | } 769 | return list 770 | } 771 | 772 | func addrsForInterface(iface *net.Interface) ([]net.IP, []net.IP) { 773 | var v4, v6, v6local []net.IP 774 | addrs, _ := iface.Addrs() 775 | for _, address := range addrs { 776 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 777 | if ipnet.IP.To4() != nil { 778 | v4 = append(v4, ipnet.IP) 779 | } else { 780 | switch ip := ipnet.IP.To16(); ip != nil { 781 | case ip.IsGlobalUnicast(): 782 | v6 = append(v6, ipnet.IP) 783 | case ip.IsLinkLocalUnicast(): 784 | v6local = append(v6local, ipnet.IP) 785 | } 786 | } 787 | } 788 | } 789 | if len(v6) == 0 { 790 | v6 = v6local 791 | } 792 | return v4, v6 793 | } 794 | 795 | // unicastResponse is used to send a unicast response packet 796 | func (s *Server) unicastResponse(resp *dns.Msg, ifIndex int, from net.Addr) error { 797 | b := make([]byte, s.inboundBufferSize) 798 | buf, err := resp.PackBuffer(b) 799 | if err != nil { 800 | return err 801 | } 802 | addr := from.(*net.UDPAddr) 803 | if addr.IP.To4() != nil { 804 | if ifIndex != 0 { 805 | var wcm ipv4.ControlMessage 806 | wcm.IfIndex = ifIndex 807 | _, err = s.ipv4conn.WriteTo(buf, &wcm, addr) 808 | } else { 809 | _, err = s.ipv4conn.WriteTo(buf, nil, addr) 810 | } 811 | return err 812 | } else { 813 | if ifIndex != 0 { 814 | var wcm ipv6.ControlMessage 815 | wcm.IfIndex = ifIndex 816 | _, err = s.ipv6conn.WriteTo(buf, &wcm, addr) 817 | } else { 818 | _, err = s.ipv6conn.WriteTo(buf, nil, addr) 819 | } 820 | return err 821 | } 822 | } 823 | 824 | // multicastResponse us used to send a multicast response packet 825 | func (s *Server) multicastResponse(msg *dns.Msg, ifIndex int) error { 826 | b := make([]byte, s.inboundBufferSize) 827 | buf, err := msg.PackBuffer(b) 828 | if err != nil { 829 | return err 830 | } 831 | if s.ipv4conn != nil { 832 | if ifIndex != 0 { 833 | iface, _ := net.InterfaceByIndex(ifIndex) 834 | if err := s.ipv4conn.SetMulticastInterface(iface); err != nil { 835 | s.logger.Debugw("mdns: failed to set multicast interface", "error", err) 836 | } else { 837 | s.ipv4conn.WriteTo(buf, nil, ipv4Addr) 838 | } 839 | } else { 840 | for ifcIdx := range s.ipv4Ifaces { 841 | if err := s.ipv4conn.SetMulticastInterface(&s.ipv4Ifaces[ifcIdx]); err != nil { 842 | s.logger.Debugw("mdns: failed to set multicast interface", "error", err) 843 | } else { 844 | s.ipv4conn.WriteTo(buf, nil, ipv4Addr) 845 | } 846 | } 847 | } 848 | } 849 | 850 | if s.ipv6conn != nil { 851 | if ifIndex != 0 { 852 | iface, _ := net.InterfaceByIndex(ifIndex) 853 | if err := s.ipv6conn.SetMulticastInterface(iface); err != nil { 854 | s.logger.Debugw("mdns: failed to set multicast interface", "error", err) 855 | } else { 856 | s.ipv6conn.WriteTo(buf, nil, ipv6Addr) 857 | } 858 | } else { 859 | for ifcIdx := range s.ipv6Ifaces { 860 | if err := s.ipv6conn.SetMulticastInterface(&s.ipv6Ifaces[ifcIdx]); err != nil { 861 | s.logger.Debugw("mdns: failed to set multicast interface", "error", err) 862 | } else { 863 | s.ipv6conn.WriteTo(buf, nil, ipv6Addr) 864 | } 865 | } 866 | } 867 | } 868 | return nil 869 | } 870 | 871 | func isUnicastQuestion(q dns.Question) bool { 872 | // From RFC6762 873 | // 18.12. Repurposing of Top Bit of qclass in Question Section 874 | // 875 | // In the Question Section of a Multicast DNS query, the top bit of the 876 | // qclass field is used to indicate that unicast responses are preferred 877 | // for this particular question. (See Section 5.4.) 878 | return q.Qclass&qClassCacheFlush != 0 879 | } 880 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | 4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 5 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 6 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 10 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 11 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 12 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 13 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 14 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 17 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= 18 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 19 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 20 | github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= 21 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 22 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 23 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 24 | github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= 25 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 26 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 27 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 28 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 29 | github.com/ashanbrown/forbidigo v1.1.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= 30 | github.com/ashanbrown/makezero v0.0.0-20201205152432-7b7cdbb3025a/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= 31 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 32 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 33 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 34 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 35 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 36 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 37 | github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= 38 | github.com/bombsimon/wsl/v3 v3.2.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= 39 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= 40 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 41 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 42 | github.com/charithe/durationcheck v0.0.6/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 45 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 46 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 47 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 48 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 49 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 50 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 51 | github.com/daixiang0/gci v0.2.8/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= 52 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= 56 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 57 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 58 | github.com/edaniels/golinters v0.0.4/go.mod h1:KzjC7OrCrRlFxufhH+kQ1Sdyzuj2eanHHzPaWxD3lgk= 59 | github.com/edaniels/golog v0.0.0-20220930140416-6e52e83a97fc h1:x43xs2HSLmj8OGqiVyefDyDgC0W1B8mbX1cfvkDjqqM= 60 | github.com/edaniels/golog v0.0.0-20220930140416-6e52e83a97fc/go.mod h1:Ms3gOPiAEPRrgN3kBOF8YIkT2JEXxPFk/HBx/FAkmgA= 61 | github.com/esimonov/ifshort v1.0.1/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= 62 | github.com/fatih/addlint v0.0.0-20190906181921-76b21bd409a2/go.mod h1:jDmgAsni5lF2hjg3Eozc5y+Uh9hE26oBfZ1fCLSet0U= 63 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 64 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 65 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 66 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 67 | github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= 68 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 69 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 70 | github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= 71 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 72 | github.com/go-critic/go-critic v0.5.4/go.mod h1:cjB4YGw+n/+X8gREApej7150Uyy1Tg8If6F2XOAUXNE= 73 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 74 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 75 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 76 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 77 | github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= 78 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 79 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 80 | github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= 81 | github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= 82 | github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 83 | github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= 84 | github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= 85 | github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= 86 | github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= 87 | github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= 88 | github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= 89 | github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= 90 | github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= 91 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 92 | github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 93 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 94 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 95 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 96 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 97 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 98 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 99 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 100 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 101 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 102 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 103 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 104 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 105 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 106 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 107 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 108 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 109 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= 110 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= 111 | github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= 112 | github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= 113 | github.com/golangci/golangci-lint v1.38.0/go.mod h1:Knp/sd5ATrVp7EOzWzwIIFH+c8hUfpW+oOQb8NvdZDo= 114 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= 115 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= 116 | github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= 117 | github.com/golangci/revgrep v0.0.0-20210208091834-cd28932614b5/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY= 118 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 119 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 121 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 122 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 123 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 124 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 125 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 129 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 130 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 131 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 132 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 133 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 134 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 135 | github.com/gookit/color v1.3.6/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= 136 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 137 | github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= 138 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 139 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= 140 | github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= 141 | github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw= 142 | github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0= 143 | github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI= 144 | github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= 145 | github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= 146 | github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= 147 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 148 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 149 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 150 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 151 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 152 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 153 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 154 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 155 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 156 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 157 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 158 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 159 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 160 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 161 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 162 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 163 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 164 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 165 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 166 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 167 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 168 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 169 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 170 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 171 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 172 | github.com/jgautheron/goconst v1.4.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= 173 | github.com/jingyugao/rowserrcheck v0.0.0-20210130005344-c6a0c12dd98d/go.mod h1:/EZlaYCnEX24i7qdVhT9du5JrtFWYRQr67bVgR7JJC8= 174 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= 175 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 176 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 177 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 178 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 179 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 180 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 181 | github.com/julz/importas v0.0.0-20210226073942-60b4fa260dd0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= 182 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 183 | github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 184 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 185 | github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 186 | github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 187 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 188 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 189 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 190 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 191 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 192 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 193 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 194 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 195 | github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= 196 | github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2GhLxSfY+oqUsvG30= 197 | github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= 198 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 199 | github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 200 | github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 201 | github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 202 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 203 | github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= 204 | github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= 205 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 206 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 207 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 208 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 209 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 210 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 211 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 212 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 213 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 214 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 215 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 216 | github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= 217 | github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= 218 | github.com/mgechev/revive v1.0.3/go.mod h1:POGGZagSo/0frdr7VeAifzS5Uka0d0GPiM35MsTO8nE= 219 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 220 | github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= 221 | github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 222 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 223 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 224 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 225 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 226 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 227 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 228 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 229 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 230 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 231 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 232 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 233 | github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= 234 | github.com/mozilla/tls-observatory v0.0.0-20201209171846-0547674fceff/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= 235 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 236 | github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= 237 | github.com/nbutton23/zxcvbn-go v0.0.0-20201221231540-e56b841a3c88/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= 238 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 239 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 240 | github.com/nishanths/exhaustive v0.1.0/go.mod h1:S1j9110vxV1ECdCudXRkeMnFQ/DQk9ajLT0Uf2MYZQQ= 241 | github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= 242 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 243 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 244 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 245 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 246 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 247 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 248 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 249 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 250 | github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= 251 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 252 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 253 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 254 | github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= 255 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 256 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 257 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 258 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 259 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 260 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 261 | github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= 262 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 263 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 264 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 265 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 266 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 267 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 268 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 269 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 270 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 271 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 272 | github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= 273 | github.com/quasilyte/go-ruleguard v0.3.0/go.mod h1:p2miAhLp6fERzFNbcuQ4bevXs8rgK//uCHsUDkumITg= 274 | github.com/quasilyte/go-ruleguard/dsl v0.0.0-20210106184943-e47d54850b18/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= 275 | github.com/quasilyte/go-ruleguard/dsl v0.0.0-20210115110123-c73ee1cbff1f/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= 276 | github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= 277 | github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= 278 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 279 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 280 | github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 281 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 282 | github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ= 283 | github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= 284 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 285 | github.com/sanposhiho/wastedassign v0.1.3/go.mod h1:LGpq5Hsv74QaqM47WtIsRSF/ik9kqk07kchgv66tLVE= 286 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 287 | github.com/securego/gosec/v2 v2.6.1/go.mod h1:I76p3NTHBXsGhybUW+cEQ692q2Vp+A0Z6ZLzDIZy+Ao= 288 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= 289 | github.com/shirou/gopsutil/v3 v3.21.1/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4= 290 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 291 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 292 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 293 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 294 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 295 | github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= 296 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 297 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 298 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 299 | github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= 300 | github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= 301 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 302 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 303 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 304 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 305 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 306 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 307 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 308 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 309 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 310 | github.com/ssgreg/nlreturn/v2 v2.1.0/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= 311 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 312 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 313 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 314 | github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 315 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 316 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 317 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 318 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 319 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 320 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 321 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 322 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 323 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 324 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 325 | github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= 326 | github.com/tetafro/godot v1.4.4/go.mod h1:FVDd4JuKliW3UgjswZfJfHq4vAx0bD/Jd5brJjGeaz4= 327 | github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= 328 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 329 | github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0= 330 | github.com/tommy-muehle/go-mnd/v2 v2.3.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= 331 | github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= 332 | github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= 333 | github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM= 334 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 335 | github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= 336 | github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY= 337 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 338 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 339 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 340 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 341 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 342 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 343 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 344 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 345 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 346 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 347 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 348 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 349 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 350 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 351 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 352 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 353 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 354 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 355 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 356 | go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= 357 | go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= 358 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 359 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 360 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 361 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 362 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 363 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 364 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 365 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 366 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 367 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 368 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 369 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 370 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 371 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 372 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 373 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 374 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 375 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 376 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 377 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 378 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 379 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 380 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 381 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 382 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 383 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 384 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 385 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 386 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 387 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 388 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 389 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 390 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 391 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 392 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 393 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 394 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 395 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 396 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 397 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 398 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 399 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 400 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 401 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 402 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 403 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 404 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 405 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 406 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 407 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 408 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 409 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 410 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 411 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 412 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 413 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 414 | golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= 415 | golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 416 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 417 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 418 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 419 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 420 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 421 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 422 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 423 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 424 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 425 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 426 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 427 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 428 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 429 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 430 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 431 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 432 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 433 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 434 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 435 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 436 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 437 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 438 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 462 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 463 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 464 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 465 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 466 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 467 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 468 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 469 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 470 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 471 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 472 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 473 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 474 | golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 475 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 476 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 477 | golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= 478 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 479 | golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 480 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 481 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 482 | golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 483 | golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 484 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 485 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 486 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 487 | golang.org/x/tools v0.0.0-20190529010454-aa71c3f32488/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 488 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 489 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 490 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 491 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 492 | golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 493 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 494 | golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 495 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 496 | golang.org/x/tools v0.0.0-20191026034945-b2104f82a97d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 497 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 498 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 499 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 500 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 501 | golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 502 | golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 503 | golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 504 | golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 505 | golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 506 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 507 | golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 508 | golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 509 | golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 510 | golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 511 | golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 512 | golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 513 | golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 514 | golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 515 | golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 516 | golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 517 | golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 518 | golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 519 | golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 520 | golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 521 | golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 522 | golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 523 | golang.org/x/tools v0.0.0-20210102185154-773b96fafca2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 524 | golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 525 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 526 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 527 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 528 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 529 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 530 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 531 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 532 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 533 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 534 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 535 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 536 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 537 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 538 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 539 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 540 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 541 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 542 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 543 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 544 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 545 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 546 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 547 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 548 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 549 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 550 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 551 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 552 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 553 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 554 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 555 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 556 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 557 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 558 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 559 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 560 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 561 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 562 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 563 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 564 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 565 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 566 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 567 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 568 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 569 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 570 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 571 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 572 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 573 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 574 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 575 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 576 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 577 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 578 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 579 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 580 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 581 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 582 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 583 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 584 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 585 | honnef.co/go/tools v0.1.2/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= 586 | mvdan.cc/gofumpt v0.1.0/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= 587 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= 588 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= 589 | mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= 590 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 591 | --------------------------------------------------------------------------------