├── .gitignore ├── LICENSE ├── NIC.go ├── README.md ├── apple.go ├── contrib └── embed-oui.go ├── go.mod ├── go.sum ├── gui.go ├── intel.go ├── listen_bsd.go ├── listen_linux.go ├── main.go ├── mndp.go ├── mux.go ├── ouiDatabase.go ├── state_bsd.go ├── state_linux.go ├── stringSlice.go └── ubiquiti-discovery.go /.gitignore: -------------------------------------------------------------------------------- 1 | /pnmap 2 | /oui.go 3 | **.pcap 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anders Brander 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 | -------------------------------------------------------------------------------- /NIC.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // NIC contains information about an ethernet station. 9 | type NIC struct { 10 | MAC string `json:"MAC"` 11 | IPs stringSlice `json:"IPs"` 12 | Hostnames stringSlice `json:"Hostnames"` 13 | UserAgents stringSlice `json:"UserAgents"` 14 | Vendor stringSlice `json:"Vendor"` 15 | Applications stringSlice `json:"Applications"` 16 | Seen int `json:"Seen"` 17 | LastSeen time.Time `json:"LastSeen"` 18 | FirstSeen time.Time `json:"FirstSeen"` 19 | } 20 | 21 | func mac(addr []byte) string { 22 | if len(addr) == 0 { 23 | return "" 24 | } 25 | 26 | mac := fmt.Sprintf("%02x", addr[0]) 27 | for i := 1; i < len(addr); i++ { 28 | mac += fmt.Sprintf(":%02x", addr[i]) 29 | } 30 | 31 | return mac 32 | } 33 | 34 | func newNIC(addr []byte) *NIC { 35 | return &NIC{MAC: mac(addr)} 36 | } 37 | 38 | func (n *NIC) String() string { 39 | output := "" 40 | 41 | output += fmt.Sprintf("[yellow]First seen[reset]: [white]%s[reset] ([white]%s[reset] ago)\n", n.FirstSeen.UTC().String(), time.Since(n.FirstSeen).Round(time.Second).String()) 42 | output += fmt.Sprintf("[yellow]Last seen[reset]: [white]%s[reset] ([white]%s[reset] ago)\n", n.LastSeen.UTC().String(), time.Since(n.LastSeen).Round(time.Second).String()) 43 | output += fmt.Sprintf("[yellow]Packets[reset]: [white]%d[reset]\n\n", n.Seen) 44 | 45 | output += fmt.Sprintf("[yellow]OUI Vendor[reset]: [white]%s[reset]\n", OUIVendor(n.MAC)) 46 | output += fmt.Sprintf("[yellow]IPS[reset]: %s\n", n.IPs.String()) 47 | output += fmt.Sprintf("[yellow]Hostnames[reset]: %s\n", n.Hostnames.String()) 48 | output += fmt.Sprintf("[yellow]User agents[reset]: %s\n", n.UserAgents.String()) 49 | output += fmt.Sprintf("[yellow]Vendor[reset]: %s\n", n.Vendor.String()) 50 | output += fmt.Sprintf("[yellow]Applications[reset]: %s", n.Applications.String()) 51 | 52 | return output 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pnmap 2 | ===== 3 | 4 | Passive Network Mapper is an entirely passive network mapper. It will 5 | passively and undetectable gather information about hosts and clients 6 | participating in an ethernet segment. 7 | 8 | Features 9 | -------- 10 | 11 | - Undetectable by network participants 12 | - Does not require promiscuous mode 13 | - Supports wired and wireless networks 14 | - Supports encrypted WiFi-networks 15 | - Detects IPv4 addresses of hosts 16 | - Detects IPv6 addresses of hosts 17 | - Detects IPv6 neighbor discovery 18 | - Detects IPv4 and IPv6 DHCP client vendor, hostnames and IPs 19 | - Detects DHCP servers 20 | - Detects public IPv4 address of natted network 21 | - Detects DHCP hostnames 22 | - Detects DHCP vendors 23 | - Detects Cisco Network hardware 24 | - Detects Mikrotek routers 25 | - Detects SSDP user agents 26 | - Detects Chrome OS 27 | - Detects clients running Spotify and Spotify Connect speakers 28 | - Detects Sonos speakers 29 | - Detects Dropbox clients 30 | - Detects HASP License Managers 31 | - Detects MDNS services 32 | - Detects macOS SSH servers 33 | - Detects iOS and macOS hardware models 34 | - Detects Chromecast and AirPlay clients and servers 35 | - Detects various file-sharing services 36 | - Detects Glen Dimplex Nobø Energy Control hubs 37 | - Detects WS-Discovery clients 38 | - Detects Ubiquiti Discover clients 39 | - Detects TeamViewer 40 | - Detects Mediaroom displays 41 | - Detects Minecraft clients 42 | - Detects Steam 43 | - Detects VNC 44 | - Detects NetBIOS (basic) 45 | - Displays ethernet OUI vendors 46 | - no cgo needed. 47 | 48 | Requirements 49 | ------------ 50 | 51 | A working Go environment is required for compiling, and a Linux, BSD or 52 | macOS host is required for running. 53 | 54 | Compiling 55 | --------- 56 | 57 | The usual `go mod download`, `go generate` and `go build` should suffice. 58 | 59 | Running 60 | ------- 61 | List network interfaces by invoking `./pnmap list`. 62 | 63 | Monitoring a live network can be done like `./pnmap monitor -i eno1`. 64 | 65 | Replaying a pcap file: `./pnmap simulate capture-file.pcap`. 66 | -------------------------------------------------------------------------------- /apple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // appleModels is different Apple models and their known IDs. 9 | // Sources: 10 | // https://www.theiphonewiki.com/wiki/Models 11 | // https://everymac.com/systems/by_capability/mac-specs-by-machine-model-machine-id.html 12 | var appleModels = map[string][]string{ 13 | "Apple TV (1st generation)": nil, // unknown id 14 | "Apple TV (2nd generation)": {"K66AP"}, 15 | "Apple TV (3rd generation)": {"J33AP", "J33IAP"}, 16 | "Apple TV (4th generation)": {"J42dAP"}, 17 | "Apple TV 4K": {"J105aAP"}, 18 | "Apple TV 4K (2nd generation)": {"J305AP"}, 19 | 20 | "iPad": {"K48AP"}, 21 | "iPad 2": {"K93AP", "K94AP", "K95AP", "K93AAP"}, 22 | "iPad (3rd generation)": {"J1AP", "J2AP", "J2AAP"}, 23 | "iPad (4th generation)": {"P101AP", "P102AP", "P103AP"}, 24 | "iPad (5th generation)": {"J71sAP", "J71tAP", "J72sAP", "J72tAP"}, 25 | "iPad (6th generation)": {"J71bAP", "J72bAP"}, 26 | "iPad (7th generation)": {"J171AP", "J172AP"}, 27 | "iPad (8th generation)": {"J171aAP", "J172aAP"}, 28 | "iPad (9th generation)": {"J181AP", "J182AP"}, 29 | 30 | "iPad Air": {"J71AP", "J72AP", "J73AP"}, 31 | "iPad Air 2": {"J81AP", "J82AP"}, 32 | "iPad Air (3rd generation)": {"J217AP", "J218AP"}, 33 | "iPad Air (4th generation)": {"J307AP", "J308AP"}, 34 | "iPad Air (5th generation)": {"J407AP", "J408AP"}, 35 | 36 | "iPad Pro (12.9-inch)": {"J98aAP", "J99aAP"}, 37 | "iPad Pro (9.7-inch)": {"J127AP", "J128AP"}, 38 | "iPad Pro (12.9-inch) (2nd generation)": {"J120AP", "J121AP"}, 39 | "iPad Pro (10.5-inch)": {"J207AP", "J208AP"}, 40 | "iPad Pro (11-inch)": {"J317AP", "J317xAP", "J318AP", "J318xAP"}, 41 | "iPad Pro (12.9-inch) (3rd generation)": {"J320AP", "J320xAP", "J321AP", "J321xAP"}, 42 | "iPad Pro (11-inch) (2nd generation)": {"J417AP", "J418AP"}, 43 | "iPad Pro (12.9-inch) (4th generation)": {"J420AP", "J421AP"}, 44 | "iPad Pro (11-inch) (3rd generation)": {"J517AP", "J517xAP", "J518AP", "J518xAP"}, 45 | "iPad Pro (12.9-inch) (5th generation)": {"J522AP", "J522xAP", "J523AP", "J523xAP"}, 46 | 47 | "iPad mini": {"P105AP", "P106AP", "P107AP"}, 48 | "iPad mini 2": {"J85AP", "J86AP", "J87AP"}, 49 | "iPad mini 3": {"J85mAP", "J86mAP", "J87mAP"}, 50 | "iPad mini 4": {"J96AP", "J97AP"}, 51 | "iPad mini (5th generation)": {"J210AP", "J211AP"}, 52 | "iPad mini (6th generation)": {"J310AP", "J311AP"}, 53 | 54 | "iPhone": {"M68AP"}, 55 | "iPhone 3G": {"N82AP"}, 56 | "iPhone 3GS": {"N88AP"}, 57 | "iPhone 4": {"N90AP", "N90bAP", "N92AP"}, 58 | "iPhone 4S": {"N94AP"}, 59 | "iPhone 5": {"N41AP", "N42AP"}, 60 | "iPhone 5c": {"N48AP", "N49AP"}, 61 | "iPhone 5s": {"N51AP", "N53AP"}, 62 | "iPhone 6": {"N61AP"}, 63 | "iPhone 6 Plus": {"N56AP"}, 64 | "iPhone 6s": {"N71AP", "N71mAP"}, 65 | "iPhone 6s Plus": {"N66AP", "N66mAP"}, 66 | "iPhone SE (1st generation)": {"N69AP", "N69uAP"}, 67 | "iPhone 7": {"D10AP", "D101AP"}, 68 | "iPhone 7 Plus": {"D11AP", "D111AP"}, 69 | "iPhone 8": {"D20AP", "D20AAP", "D201AP", "D201AAP"}, 70 | "iPhone 8 Plus": {"D21AP", "D21AAP", "D211AP", "D211AAP"}, 71 | "iPhone X": {"D22AP", "D221AP"}, 72 | "iPhone XR": {"N841AP"}, 73 | "iPhone XS": {"D321AP"}, 74 | "iPhone XS Max": {"D331pAP"}, 75 | "iPhone 11": {"N104AP"}, 76 | "iPhone 11 Pro": {"D421AP"}, 77 | "iPhone 11 Pro Max": {"D431AP"}, 78 | "iPhone SE (2nd generation)": {"D79AP"}, 79 | "iPhone 12 mini": {"D52gAP"}, 80 | "iPhone 12": {"D53gAP"}, 81 | "iPhone 12 Pro": {"D53pAP"}, 82 | "iPhone 12 Pro Max": {"D54pAP"}, 83 | "iPhone 13 Mini": {"D16AP"}, 84 | "iPhone 13": {"D17AP"}, 85 | "iPhone 13 Pro": {"D63AP"}, 86 | "iPhone 13 Pro Max": {"D64AP"}, 87 | "iPhone SE (3rd generation)": {"D49AP"}, 88 | "iPhone 14": {"D27AP"}, 89 | "iPhone 14 Plus": {"D28AP"}, 90 | "iPhone 14 Pro": {"D73AP"}, 91 | "iPhone 14 Pro Max": {"D74AP"}, 92 | 93 | "eMac G4": {"PowerMac4,4", "PowerMac6,4"}, 94 | "iBook G3": {"PowerBook2,1", "PowerBook4,1", "PowerBook4,2", "PowerBook4,3"}, 95 | "iBook G4": {"PowerBook6,3", "PowerBook6,5", "PowerBook6,7"}, 96 | "iMac 17-inch": {"iMac4,2", "iMac5,2"}, 97 | "iMac 17/20-inch": {"iMac4,1", "iMac5,1"}, 98 | "iMac 20/24-inch": {"iMac7,1", "iMac8,1", "iMac9,1"}, 99 | "iMac 21.5-inch": {"iMac11,2", "iMac12,1", "iMac13,1", "iMac14,1", "iMac14,3", "iMac14,4", "iMac16,1", "iMac16,2", "iMac18,1", "iMac18,2", "iMac19,2"}, 100 | "iMac 21.5/27-inch": {"iMac10,1"}, 101 | "iMac 24-inch": {"iMac6,1"}, 102 | "iMac 27-inch": {"iMac11,1", "iMac11,3", "iMac12,2", "iMac13,2", "iMac14,2", "iMac15,1", "iMac17,1", "iMac18,3", "iMac19,1"}, 103 | "iMac G3": {"iMac,1", "PowerMac2,1", "PowerMac2,2", "PowerMac4,1"}, 104 | "iMac G4": {"PowerMac4,2", "PowerMac4,5", "PowerMac6,1", "PowerMac6,3"}, 105 | "iMac G5": {"PowerMac8,1", "PowerMac8,2", "PowerMac12,1"}, 106 | "iMac Pro": {"iMacPro1,1"}, 107 | "iMac M1": {"iMac21,1", "iMac21,2"}, 108 | "Mac Studio M1 Max": {"Mac13,1"}, 109 | "Mac Studio M1 Ultra": {"Mac13,2"}, 110 | "Mac Mini G4": {"PowerMac10,1", "PowerMac10,2"}, 111 | "Mac Mini Intel": {"Macmini1,1", "Macmini2,1", "Macmini3,1", "Macmini4,1", "Macmini5,1", "Macmini5,2", "Macmini5,3", "Macmini6,1", "Macmini6,2", "Macmini7,1", "Macmini8,1"}, 112 | "Mac Mini M1": {"Macmini9,1"}, 113 | "Mac Mini M2": {"Mac14,3"}, 114 | "Mac Mini M2 Pro": {"Mac14,12"}, 115 | "Mac Pro": {"MacPro1,1*", "MacPro1,1", "MacPro2,1", "MacPro3,1", "MacPro4,1", "MacPro5,1", "MacPro6,1", "MacPro7,1"}, 116 | "MacBook 12-inch": {"MacBook8,1", "MacBook9,1", "MacBook10,1"}, 117 | "MacBook 13-inch": {"MacBook1,1", "MacBook2,1", "MacBook3,1", "MacBook4,1", "MacBook5,1", "MacBook5,2", "MacBook6,1", "MacBook7,1"}, 118 | "MacBook Air 11-inch": {"MacBookAir3,1", "MacBookAir4,1", "MacBookAir5,1", "MacBookAir6,1", "MacBookAir7,1"}, 119 | "MacBook Air 13-inch": {"MacBookAir1,1", "MacBookAir2,1", "MacBookAir3,2", "MacBookAir4,2", "MacBookAir5,2", "MacBookAir6,2", "MacBookAir7,2", "MacBookAir8,1", "MacBookAir8,2", "MacBookAir9,1"}, 120 | "MacBook Air M1 13-inch": {"MacBookAir10,1"}, 121 | "MacBook Air M2 13-inch": {"Mac14,2"}, 122 | "MacBook Pro 13-inch": {"MacBookPro5,5", "MacBookPro7,1", "MacBookPro8,1", "MacBookPro9,2", "MacBookPro10,2", "MacBookPro11,1", "MacBookPro12,1", "MacBookPro13,1", "MacBookPro14,1", "MacBookPro15,2", "MacBookPro15,4", "MacBookPro16,2", "MacBookPro16,4"}, 123 | "MacBook Pro 13-inch Touch": {"MacBookPro13,2", "MacBookPro14,2"}, 124 | "MacBook Pro 15-inch": {"MacBookPro1,1", "MacBookPro2,2", "MacBookPro5,1", "MacBookPro5,3", "MacBookPro5,4", "MacBookPro6,2", "MacBookPro8,2", "MacBookPro9,1", "MacBookPro10,1", "MacBookPro11,2", "MacBookPro11,3", "MacBookPro11,4", "MacBookPro11,5"}, 125 | "MacBook Pro 15-inch Touch": {"MacBookPro13,3", "MacBookPro14,3", "MacBookPro15,1", "MacBookPro15,3", ""}, 126 | "MacBook Pro 16-inch": {"MacBookPro16,1"}, 127 | "MacBook Pro 17-inch": {"MacBookPro1,2", "MacBookPro2,1", "MacBookPro5,2", "MacBookPro6,1", "MacBookPro8,3"}, 128 | "MacBook Pro 15/17-inch": {"MacBookPro3,1", "MacBookPro4,1"}, 129 | "MacBook Pro M1 13-inch": {"MacBookPro17,1"}, 130 | "MacBook Pro M1 Pro 14-inch": {"MacBookPro18,3"}, 131 | "MacBook Pro M1 Max 14-inch": {"MacBookPro18,4"}, 132 | "MacBook Pro M1 Pro 16-inch": {"MacBookPro18,1"}, 133 | "MacBook Pro M1 Max 16-inch": {"MacBookPro18,2"}, 134 | "MacBook Pro M2 13-inch": {"Mac14,7"}, 135 | "MacBook Pro M2 Pro 14-inch": {"Mac14,9"}, 136 | "MacBook Pro M2 Max 14-inch": {"Mac14,5"}, 137 | "MacBook Pro M2 Pro 16-inch": {"Mac14,10"}, 138 | "MacBook Pro M2 Max 16-inch": {"Mac14,6"}, 139 | "Power Macintosh/Mac Server G3": {"PowerMac1,1"}, 140 | "Power Macintosh/Mac Server G4": {"PowerMac1,2", "PowerMac3,1", "PowerMac3,3", "PowerMac3,4", "PowerMac3,5", "PowerMac3,6", "PowerMac5,1"}, 141 | "Power Macintosh G5": {"PowerMac7,2", "PowerMac7,3", "PowerMac9,1", "PowerMac11,2"}, 142 | "PowerBook G3": {"PowerBook1,1", "PowerBook3,1"}, 143 | "PowerBook G4": {"PowerBook3,2", "PowerBook3,3", "PowerBook3,4", "PowerBook3,5", "PowerBook5,1", "PowerBook5,2", "PowerBook5,3", "PowerBook5,4", "PowerBook5,5", "PowerBook5,6", "PowerBook5,7", "PowerBook5,8", "PowerBook5,9", "PowerBook6,1", "PowerBook6,2"}, 144 | "Xserve G4": {"RackMac1,1"}, 145 | "Xserve G5": {"RackMac3,1"}, 146 | "Xserve Intel": {"Xserve1,1", "Xserve2,1", "Xserve3,1"}, 147 | } 148 | 149 | var appleReverse map[string]string 150 | 151 | func init() { 152 | appleReverse = make(map[string]string) 153 | 154 | for h, ids := range appleModels { 155 | for _, id := range ids { 156 | u := strings.ToUpper(id) 157 | 158 | existing, found := appleReverse[u] 159 | if found { 160 | fmt.Printf("Apple ID '%s' belongs to both '%s' and '%s'\n", id, h, existing) 161 | } 162 | 163 | appleReverse[u] = h 164 | } 165 | } 166 | } 167 | 168 | func appleHumanModel(id string) string { 169 | human, found := appleReverse[strings.ToUpper(id)] 170 | if found { 171 | return human 172 | } 173 | 174 | return id 175 | } 176 | -------------------------------------------------------------------------------- /contrib/embed-oui.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/csv" 8 | "fmt" 9 | "html/template" 10 | "io" 11 | "log" 12 | "net/http" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | var ouiTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. 18 | package main 19 | 20 | var ouiToVendor = map[string]string{ 21 | {{ range $key, $value := . }} "{{ $key }}": "{{ $value }}", 22 | {{ end }} 23 | } 24 | `)) 25 | 26 | func main() { 27 | resp, err := http.Get("https://standards-oui.ieee.org/oui/oui.csv") 28 | if err != nil { 29 | log.Fatalf("%s", err.Error()) 30 | } 31 | defer resp.Body.Close() 32 | 33 | toVendor := make(map[string]string) 34 | 35 | out, err := os.Create("oui.go") 36 | if err != nil { 37 | log.Fatalf("%s", err.Error()) 38 | } 39 | 40 | r := csv.NewReader(resp.Body) 41 | 42 | // Burn the header. 43 | _, _ = r.Read() 44 | 45 | for { 46 | record, err := r.Read() 47 | if err == io.EOF { 48 | break 49 | } 50 | 51 | if err != nil { 52 | log.Fatalf("%s", err.Error()) 53 | } 54 | 55 | if len(record[1]) != 6 { 56 | panic(record) 57 | } 58 | 59 | lower := strings.ToLower(record[1]) 60 | mac := fmt.Sprintf("%c%c:%c%c:%c%c", lower[0], lower[1], lower[2], lower[3], lower[4], lower[5]) 61 | vendor := strings.TrimSpace(record[2]) 62 | 63 | toVendor[mac] = vendor 64 | } 65 | toVendor["52:54:00"] = "QEMU" 66 | 67 | ouiTemplate.Execute(out, toVendor) 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abrander/pnmap 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.9.0 7 | github.com/golang/protobuf v1.5.4 8 | github.com/google/gopacket v1.1.19 9 | github.com/mdlayher/raw v0.1.0 10 | github.com/miekg/dns v1.1.68 11 | github.com/rivo/tview v0.42.0 12 | github.com/spf13/cobra v1.10.1 13 | ) 14 | 15 | require ( 16 | github.com/clipperhouse/uax29/v2 v2.2.0 // indirect 17 | github.com/gdamore/encoding v1.0.1 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/josharian/native v1.1.0 // indirect 20 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 21 | github.com/mattn/go-runewidth v0.0.19 // indirect 22 | github.com/mdlayher/packet v1.1.2 // indirect 23 | github.com/mdlayher/socket v0.5.1 // indirect 24 | github.com/rivo/uniseg v0.4.7 // indirect 25 | github.com/spf13/pflag v1.0.10 // indirect 26 | golang.org/x/mod v0.28.0 // indirect 27 | golang.org/x/net v0.44.0 // indirect 28 | golang.org/x/sync v0.17.0 // indirect 29 | golang.org/x/sys v0.36.0 // indirect 30 | golang.org/x/term v0.35.0 // indirect 31 | golang.org/x/text v0.29.0 // indirect 32 | golang.org/x/tools v0.37.0 // indirect 33 | google.golang.org/protobuf v1.33.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= 2 | github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 4 | github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 5 | github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= 6 | github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys= 7 | github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo= 8 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 9 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 13 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 14 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 15 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 16 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 17 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 18 | github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 19 | github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 20 | github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 21 | github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 22 | github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= 23 | github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= 24 | github.com/mdlayher/raw v0.1.0 h1:K4PFMVy+AFsp0Zdlrts7yNhxc/uXoPVHi9RzRvtZF2Y= 25 | github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5s9Sg= 26 | github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= 27 | github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 28 | github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= 29 | github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 30 | github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= 31 | github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= 32 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 33 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 34 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 35 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 36 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 37 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 38 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 39 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 40 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 43 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 44 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 45 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 46 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 47 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 48 | golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= 49 | golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 52 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 53 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 54 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 55 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 56 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 57 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 61 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 70 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 72 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 73 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 74 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= 75 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 78 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 79 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 80 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 81 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 82 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 83 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 84 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 85 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 86 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 87 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 88 | golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= 89 | golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 90 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 93 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | -------------------------------------------------------------------------------- /gui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | const ips = "ips" 12 | const hostnames = "hostnames" 13 | const useragents = "useragents" 14 | const vendor = "vendor" 15 | const applications = "applications" 16 | const seen = "seen" 17 | const lastseen = "lastseen" 18 | const firstseen = "firstseen" 19 | 20 | type gui struct { 21 | app *tview.Application 22 | hostList *tview.List 23 | details *tview.TextView 24 | secondary string 25 | 26 | nics map[string]*NIC 27 | } 28 | 29 | func newGUI() *gui { 30 | g := &gui{ 31 | app: tview.NewApplication(), 32 | hostList: tview.NewList(), 33 | details: tview.NewTextView(), 34 | secondary: ips, 35 | nics: make(map[string]*NIC), 36 | } 37 | 38 | flex := tview.NewFlex() 39 | 40 | flex.SetDirection(tview.FlexColumn) 41 | flex.AddItem(g.hostList, 0, 30, true) 42 | flex.AddItem(g.details, 0, 70, false) 43 | 44 | g.hostList.SetBorder(true) 45 | g.hostList.SetBorderColor(tcell.ColorGray) 46 | g.hostList.SetTitle(" Stations ") 47 | g.hostList.SetTitleColor(tcell.ColorGreenYellow) 48 | g.hostList.SetMainTextColor(tcell.ColorGreen) 49 | g.hostList.SetSecondaryTextColor(tcell.ColorWhite) 50 | g.hostList.SetSelectedTextColor(tcell.ColorBlack) 51 | g.hostList.SetSelectedBackgroundColor(tcell.ColorGreen) 52 | 53 | g.details.SetBorder(true) 54 | g.details.SetBorderColor(tcell.ColorGray) 55 | g.details.SetTitleColor(tcell.ColorGreenYellow) 56 | g.details.SetTextColor(tcell.ColorWhite) 57 | g.details.SetDynamicColors(true) 58 | 59 | g.app.SetRoot(flex, true) 60 | 61 | g.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 62 | switch event.Key() { 63 | case tcell.KeyRune: 64 | switch event.Rune() { 65 | case '1': 66 | g.secondary = ips 67 | case '2': 68 | g.secondary = hostnames 69 | case '3': 70 | g.secondary = useragents 71 | case '4': 72 | g.secondary = vendor 73 | case '5': 74 | g.secondary = applications 75 | case '6': 76 | g.secondary = seen 77 | case '7': 78 | g.secondary = lastseen 79 | case '8': 80 | g.secondary = firstseen 81 | } 82 | 83 | go func() { 84 | for _, nic := range g.nics { 85 | g.updateNIC(nic) 86 | } 87 | }() 88 | 89 | } 90 | return event 91 | }) 92 | 93 | return g 94 | } 95 | 96 | func (g *gui) Run() error { 97 | return g.app.Run() 98 | } 99 | 100 | func (g *gui) selectHost() { 101 | selected := g.hostList.GetCurrentItem() 102 | 103 | mac, _ := g.hostList.GetItemText(selected) 104 | 105 | nic, found := g.nics[mac[0:17]] 106 | 107 | if !found { 108 | // This should not happen 109 | return 110 | } 111 | 112 | g.details.SetTitle(" " + nic.MAC + " ") 113 | g.details.SetText(nic.String()) 114 | } 115 | 116 | func (g *gui) updateNIC(nic *NIC) { 117 | defer g.app.Draw() 118 | 119 | var sec string 120 | 121 | switch g.secondary { 122 | default: 123 | sec = fmt.Sprintf(" %v", nic.IPs) 124 | case ips: 125 | sec = fmt.Sprintf(" %v", nic.IPs) 126 | case hostnames: 127 | sec = fmt.Sprintf(" %v", nic.Hostnames) 128 | case useragents: 129 | sec = fmt.Sprintf(" %v", nic.UserAgents) 130 | case vendor: 131 | sec = fmt.Sprintf(" %v", nic.Vendor) 132 | case applications: 133 | sec = fmt.Sprintf(" %v", nic.Applications) 134 | case seen: 135 | sec = fmt.Sprintf(" %v", nic.Seen) 136 | case lastseen: 137 | sec = fmt.Sprintf(" %v", nic.LastSeen) 138 | case firstseen: 139 | sec = fmt.Sprintf(" %v", nic.FirstSeen) 140 | } 141 | 142 | g.nics[nic.MAC] = nic 143 | 144 | selected := g.hostList.GetCurrentItem() 145 | 146 | for i := 0; i < g.hostList.GetItemCount(); i++ { 147 | main, _ := g.hostList.GetItemText(i) 148 | if main[0:17] == nic.MAC { 149 | g.hostList.RemoveItem(i) 150 | g.hostList.InsertItem(i, main, sec, 0, g.selectHost) 151 | 152 | g.hostList.SetCurrentItem(selected) 153 | 154 | // If the current item is selected, update details view. 155 | if selected == i { 156 | g.selectHost() 157 | } 158 | 159 | return 160 | } 161 | } 162 | 163 | g.hostList.AddItem(nic.MAC+" "+OUIVendor(nic.MAC), sec, 0, g.selectHost) 164 | } 165 | -------------------------------------------------------------------------------- /intel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "encoding/json" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "strings" 12 | "unicode/utf8" 13 | 14 | "github.com/golang/protobuf/proto" 15 | "github.com/google/gopacket" 16 | "github.com/google/gopacket/layers" 17 | "github.com/miekg/dns" 18 | ) 19 | 20 | type intel struct { 21 | NICCollection map[string]*NIC 22 | hostChan chan *NIC 23 | 24 | mux mux 25 | } 26 | 27 | func newIntel() *intel { 28 | i := &intel{ 29 | NICCollection: make(map[string]*NIC), 30 | mux: newMux(), 31 | } 32 | 33 | i.mux.add(layers.LayerTypeARP, i.arp) 34 | i.mux.add(layers.LayerTypeDHCPv4, i.dhcpv4) 35 | i.mux.add(layers.LayerTypeDHCPv6, i.dhcpv6) 36 | i.mux.add(layers.LayerTypeIPv4, i.ipv4) 37 | i.mux.add(layers.LayerTypeIPv6, i.ipv6) 38 | i.mux.add(layers.LayerTypeUDP, i.udp) 39 | i.mux.add(layers.LayerTypeCiscoDiscoveryInfo, i.ciscoDiscoveryInfo) 40 | i.mux.add(layerTypeMNDP, i.mndp) 41 | i.mux.add(layers.LayerTypeICMPv6NeighborAdvertisement, i.ipv6NeighborAdvertisement) 42 | i.mux.add(layerTypeUDiscovery, i.ubiquitiDiscovery) 43 | 44 | return i 45 | } 46 | 47 | func (i *intel) getNIC(addr []byte) *NIC { 48 | mac := mac(addr) 49 | 50 | nic, found := i.NICCollection[mac] 51 | if !found { 52 | n := newNIC(addr) 53 | i.NICCollection[mac] = n 54 | nic = n 55 | } 56 | 57 | i.hostChan <- nic 58 | 59 | return nic 60 | } 61 | 62 | func (i *intel) findNIC(ip net.IP) *NIC { 63 | needle := ip.String() 64 | 65 | for _, nic := range i.NICCollection { 66 | for _, ip := range nic.IPs { 67 | if ip == needle { 68 | return nic 69 | } 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func (i *intel) dhcpv4(source net.HardwareAddr, layer gopacket.Layer) bool { 77 | nic := i.getNIC(source) 78 | nic.Applications.add("dhcpv4") 79 | 80 | dhcpv4 := layer.(*layers.DHCPv4) 81 | if dhcpv4.Operation != layers.DHCPOpRequest { 82 | return false 83 | } 84 | 85 | for _, o := range dhcpv4.Options { 86 | switch o.Type { 87 | case layers.DHCPOptMessageType: 88 | if layers.DHCPMsgType(o.Data[0]) == layers.DHCPMsgTypeOffer { 89 | nic.Applications.add("dhcpv4-server") 90 | } 91 | case layers.DHCPOptClassID: 92 | nic.Vendor.add(string(o.Data)) 93 | case layers.DHCPOptHostname: 94 | nic.Hostnames.add(string(o.Data)) 95 | case layers.DHCPOpt(81): // Client FQDN 96 | nic.Hostnames.add(string(o.Data)) 97 | case layers.DHCPOptRequestIP: 98 | nic.IPs.add(net.IP(o.Data).String()) 99 | case layers.DHCPOptServerID: // Abuse client requests to recognize server. 100 | if server := i.findNIC(net.IP(o.Data)); server != nil { 101 | server.Applications.add("dhcpv4-server") 102 | } 103 | } 104 | } 105 | 106 | return true 107 | } 108 | 109 | func (i *intel) dhcpv6(source net.HardwareAddr, layer gopacket.Layer) bool { 110 | nic := i.getNIC(source) 111 | nic.Applications.add("dhcpv6") 112 | 113 | return true 114 | } 115 | 116 | func (i *intel) arp(source net.HardwareAddr, layer gopacket.Layer) bool { 117 | arp := layer.(*layers.ARP) 118 | 119 | nic := i.getNIC(source) 120 | 121 | if len(arp.SourceProtAddress) == 4 { 122 | ip := fmt.Sprintf("%d.%d.%d.%d", arp.SourceProtAddress[0], arp.SourceProtAddress[1], arp.SourceProtAddress[2], arp.SourceProtAddress[3]) 123 | if ip != "0.0.0.0" { 124 | nic.IPs.add(ip) 125 | } 126 | 127 | return true 128 | } 129 | 130 | return false 131 | } 132 | 133 | func (i *intel) ipv6(source net.HardwareAddr, layer gopacket.Layer) bool { 134 | ipv6 := layer.(*layers.IPv6) 135 | 136 | nic := i.getNIC(source) 137 | 138 | if ip := ipv6.SrcIP.String(); ip != "::" { 139 | nic.IPs.add(ip) 140 | } 141 | 142 | return false 143 | } 144 | 145 | func (i *intel) ipv4(source net.HardwareAddr, layer gopacket.Layer) bool { 146 | ipv4 := layer.(*layers.IPv4) 147 | 148 | nic := i.getNIC(source) 149 | 150 | ip := ipv4.SrcIP.String() 151 | if ip != "0.0.0.0" { 152 | nic.IPs.add(ip) 153 | } 154 | 155 | return false 156 | } 157 | 158 | func (i *intel) udp(source net.HardwareAddr, layer gopacket.Layer) bool { 159 | udp := layer.(*layers.UDP) 160 | nic := i.getNIC(source) 161 | 162 | switch udp.DstPort { 163 | 164 | // NBNS 165 | case 137: 166 | nic.Applications.add("NetBIOS-Name-Service") 167 | return true 168 | 169 | // NBDS - SMB 170 | case 138: 171 | nic.Applications.add("NetBIOS-Datagram-Service") 172 | return true 173 | 174 | // SSDP 175 | case 1900: 176 | req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(udp.Payload))) 177 | if err != nil { 178 | return false 179 | } 180 | 181 | ua := req.Header.Get("user-agent") 182 | if ua != "" { 183 | nic.UserAgents.add(ua) 184 | } 185 | 186 | return true 187 | 188 | // HASP License Manager 189 | case 1947: 190 | nic.Applications.add("HASP-License-Manager") 191 | 192 | return true 193 | 194 | // WS-Discovery 195 | case 3702: 196 | nic.Applications.add("WS-Discovery") 197 | 198 | return true 199 | 200 | // Multicast-DNS 201 | case 5353: 202 | msg := new(dns.Msg) 203 | 204 | dnsParts := func(in string) []string { 205 | in = strings.TrimSuffix(in, ".local.") 206 | parts := []string{""} 207 | part := 0 208 | 209 | var r rune 210 | for i, w := 0, 0; i < len(in); i += w { 211 | r, w = utf8.DecodeRuneInString(in[i:]) 212 | if r == '\\' { 213 | var w2 int 214 | r, w2 = utf8.DecodeRuneInString(in[i+w:]) 215 | w += w2 216 | parts[part] += string(r) 217 | } else if r == '.' { 218 | parts = append(parts, "") 219 | part++ 220 | } else { 221 | parts[part] += string(r) 222 | } 223 | } 224 | 225 | return parts 226 | } 227 | 228 | if err := msg.Unpack(udp.Payload); err != nil { 229 | return false 230 | } 231 | 232 | if !msg.Response { 233 | return true 234 | } 235 | 236 | m := map[string]string{ 237 | "_sftp-ssh": "SSH", 238 | "_smb": "Samba", 239 | "_ipp": "IPP", 240 | "_ipps": "IPPS", 241 | "_pdl-datastream": "PDL-socket", 242 | "_afpovertcp": "AFP", // Apple Filing Protocol 243 | "_raop": "AirPlay-RAOP", // Remote Audio Output Protocol 244 | "_airplay": "AirPlay-display", 245 | "_companion-link": "AirPlay-client", 246 | "_services": "", 247 | "_nvstream_dbd": "NVidia-Gamestream", 248 | "_homekit": "homekit?", 249 | "_ePCL": "ePCL?", 250 | "_universal": "universal?", 251 | "_print": "print?", 252 | "_wfds-print": "wfds-print?", 253 | "_printer": "LPR-printer", 254 | "_http": "HTTP-server", 255 | "_scanner": "Scanner", 256 | "_http-alt": "HTTP-server-alt", 257 | "_uscan": "uscan?", 258 | "_privet": "Privet", 259 | "_uscans": "uscans?", 260 | "_soundtouch": "SoundTouch", // Bose 261 | "_googlecast": "Chromecast", 262 | "_spotify-connect": "Spotify-Connect", 263 | "_teamviewer": "TeamViewer", 264 | "_rfb": "VNC", 265 | "_adisk": "TimeCapsule", 266 | "_telnet": "Telnet", 267 | "_sonos": "Sonos", 268 | "_cros_p2p": "ChromeOS", 269 | } 270 | 271 | for _, answer := range msg.Answer { 272 | names := dnsParts(answer.Header().Name) 273 | switch rr := answer.(type) { 274 | case *dns.A: 275 | name := strings.TrimSuffix(rr.Header().Name, ".local.") 276 | 277 | nic.Hostnames.add(name) 278 | 279 | case *dns.PTR: 280 | app, found := m[names[0]] 281 | if !found { 282 | app = names[0] 283 | } 284 | 285 | if strings.HasSuffix(rr.Header().Name, ".arpa.") { 286 | break 287 | } 288 | 289 | if app != "" { 290 | nic.Applications.add(app) 291 | } 292 | 293 | case *dns.SRV: 294 | if len(names) < 2 { 295 | break 296 | } 297 | 298 | app, found := m[names[1]] 299 | if !found { 300 | app = names[1] 301 | } 302 | 303 | if app != "" { 304 | nic.Applications.add(app) 305 | } 306 | 307 | if names[0][0] != '_' { 308 | nic.Hostnames.add(names[0]) 309 | } 310 | 311 | case *dns.TXT: 312 | nic.Hostnames.add(names[0]) 313 | if names[1] == "_device-info" { 314 | if strings.HasPrefix(rr.Txt[0], "model=") { 315 | nic.Vendor.add(appleHumanModel(rr.Txt[0][6:])) 316 | } else { 317 | nic.Vendor.add(rr.Txt[0]) 318 | } 319 | } 320 | } 321 | } 322 | 323 | // Mediaroom set top box 324 | case 8082: 325 | if bytes.Contains(udp.Payload, []byte("x-type: display")) { 326 | nic.Applications.add("Mediaroom") 327 | 328 | return true 329 | } 330 | 331 | case 10000, 10001: 332 | // Nobø Hub 333 | // https://www.glendimplex.se/media/15650/nobo-hub-api-v-1-1-integration-for-advanced-users.pdf 334 | if bytes.Contains(udp.Payload, []byte("__NOBOHUB__")) { 335 | nic.Vendor.add("Glen-Dimplex") 336 | nic.Applications.add("nobo") 337 | 338 | return true 339 | } 340 | 341 | // Dropbox 342 | case 17500: 343 | dummy := make(map[string]interface{}) 344 | err := json.Unmarshal(udp.Payload, &dummy) 345 | if err == nil { 346 | // If we can decode a JSON payload, we assume it's 347 | // from Dropbox. 348 | nic.Applications.add("Dropbox") 349 | 350 | return true 351 | } 352 | 353 | // Raknet for Minecraft client 354 | case 19133: 355 | nic.Applications.add("Minecraft") 356 | 357 | return true 358 | 359 | // Steam client 360 | case 27036: 361 | nic.Applications.add("Steam") 362 | 363 | if len(udp.Payload) < 40 { 364 | return false 365 | } 366 | 367 | hlen, err := binary.ReadUvarint(bytes.NewBuffer(udp.Payload[8:])) 368 | if err != nil { 369 | return false 370 | } 371 | 372 | // 8: skip 8 byts of signature 373 | buf := proto.NewBuffer(udp.Payload[8:]) 374 | if err != nil { 375 | return false 376 | } 377 | 378 | // signature + header length + header + body length 379 | offset := 8 + 4 + hlen + 4 380 | 381 | if offset > uint64(len(udp.Payload)) { 382 | return false 383 | } 384 | 385 | buf.SetBuf(udp.Payload[offset:]) 386 | 387 | for value, err := buf.DecodeVarint(); err == nil; value, err = buf.DecodeVarint() { 388 | number := value >> 3 389 | typ := value & 0x7 390 | 391 | var str string 392 | 393 | switch typ { 394 | case 0: 395 | _, err = buf.DecodeVarint() 396 | case 1: 397 | _, err = buf.DecodeFixed64() 398 | case 2: 399 | str, err = buf.DecodeStringBytes() 400 | case 5: 401 | _, err = buf.DecodeFixed32() 402 | default: 403 | return false 404 | } 405 | 406 | if err != nil { 407 | break 408 | } 409 | 410 | switch number { 411 | case 4: 412 | nic.Hostnames.add(str) 413 | 414 | case 20, 21: 415 | if str != "0.0.0.0" { 416 | nic.IPs.add(str) 417 | } 418 | } 419 | } 420 | 421 | return true 422 | 423 | // Spotify 424 | case 57621: 425 | if bytes.HasPrefix(udp.Payload, []byte("SpotUdp")) { 426 | nic.Applications.add("Spotify") 427 | 428 | return true 429 | } 430 | } 431 | 432 | return false 433 | } 434 | 435 | func (i *intel) ciscoDiscoveryInfo(source net.HardwareAddr, layer gopacket.Layer) bool { 436 | d := layer.(*layers.CiscoDiscoveryInfo) 437 | nic := i.getNIC(source) 438 | 439 | for _, a := range d.Addresses { 440 | nic.IPs.add(a.String()) 441 | } 442 | 443 | for _, a := range d.MgmtAddresses { 444 | nic.IPs.add(a.String()) 445 | } 446 | 447 | nic.Vendor.add(d.Platform) 448 | nic.Hostnames.add(d.DeviceID) 449 | 450 | for _, v := range strings.Split(d.Version, "\n") { 451 | if strings.HasPrefix(v, "Cisco IOS Software") { 452 | nic.Applications.add("Cisco IOS") 453 | } 454 | 455 | switch { 456 | case strings.HasPrefix(v, "Technical Support:"): 457 | case strings.HasPrefix(v, "Copyright (c) "): 458 | 459 | default: 460 | nic.UserAgents.add(v) 461 | } 462 | } 463 | 464 | return false 465 | } 466 | 467 | func (i *intel) ipv6NeighborAdvertisement(source net.HardwareAddr, layer gopacket.Layer) bool { 468 | na := layer.(*layers.ICMPv6NeighborAdvertisement) 469 | nic := i.getNIC(source) 470 | 471 | nic.IPs.add(na.TargetAddress.String()) 472 | 473 | return true 474 | } 475 | 476 | func (i *intel) mndp(source net.HardwareAddr, layer gopacket.Layer) bool { 477 | mndp := layer.(*MNDP) 478 | nic := i.getNIC(source) 479 | 480 | nic.Vendor.add("MikroTek") 481 | nic.Vendor.add(mndp.Board) 482 | nic.Hostnames.add(mndp.Identity) 483 | nic.UserAgents.add(mndp.Platform + "/" + mndp.Version) 484 | 485 | nic.Applications.add("router") 486 | 487 | return true 488 | } 489 | 490 | func (i *intel) ubiquitiDiscovery(source net.HardwareAddr, layer gopacket.Layer) bool { 491 | ubnt := layer.(*UDiscovery) 492 | nic := i.getNIC(source) 493 | 494 | nic.Applications.add("ubnt-discover") 495 | nic.Vendor.add("Ubiquiti") 496 | nic.Vendor.add(ubnt.Model) 497 | nic.UserAgents.add(ubnt.Software) 498 | nic.IPs.add(ubnt.IP) 499 | 500 | return true 501 | } 502 | 503 | func (i *intel) NewPacket(packet gopacket.Packet) bool { 504 | if ethernetLayer := packet.Layer(layers.LayerTypeEthernet); ethernetLayer != nil { 505 | ethernet := ethernetLayer.(*layers.Ethernet) 506 | 507 | nic := i.getNIC(ethernet.SrcMAC) 508 | 509 | nic.LastSeen = packet.Metadata().Timestamp 510 | if nic.FirstSeen.IsZero() { 511 | nic.FirstSeen = packet.Metadata().Timestamp 512 | } 513 | nic.Seen++ 514 | 515 | return i.mux.process(packet) 516 | } 517 | 518 | return false 519 | } 520 | -------------------------------------------------------------------------------- /listen_bsd.go: -------------------------------------------------------------------------------- 1 | // +build darwin dragonfly freebsd netbsd openbsd 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/google/gopacket" 11 | "github.com/google/gopacket/bsdbpf" 12 | "github.com/google/gopacket/layers" 13 | ) 14 | 15 | func listen(deviceName string, out chan gopacket.Packet) { 16 | options := &bsdbpf.Options{ 17 | ReadBufLen: 32767, 18 | Promisc: false, 19 | Immediate: true, 20 | PreserveLinkAddr: true, 21 | } 22 | 23 | sniffer, err := bsdbpf.NewBPFSniffer(deviceName, options) 24 | if err != nil { 25 | log.Fatalf("error: %s", err.Error()) 26 | } 27 | 28 | for { 29 | buffer, ci, err := sniffer.ReadPacketData() 30 | if err != nil { 31 | if e, ok := err.(syscall.Errno); ok && e.Temporary() { 32 | continue 33 | } 34 | 35 | // This will happen from time to time - we simply continue and hope for the best :-) 36 | if err.Error() == "BPF captured frame received with corrupted BpfHdr struct." { 37 | continue 38 | } 39 | 40 | log.Fatalf("ReadPacketData: %s", err.Error()) 41 | } 42 | 43 | packet := gopacket.NewPacket(buffer[0:ci.CaptureLength], layers.LayerTypeEthernet, gopacket.Default) 44 | packet.Metadata().CaptureInfo = ci 45 | packet.Metadata().Timestamp = time.Now() 46 | 47 | // If we're unable to decode the packet, continue in silence. 48 | _, failure := packet.Layer(gopacket.LayerTypeDecodeFailure).(*gopacket.DecodeFailure) 49 | if failure { 50 | continue 51 | } 52 | 53 | filter(packet, out) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /listen_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "syscall" 7 | "time" 8 | 9 | "github.com/google/gopacket" 10 | "github.com/google/gopacket/layers" 11 | "github.com/mdlayher/raw" 12 | ) 13 | 14 | func listen(deviceName string, out chan gopacket.Packet) { 15 | intf, err := net.InterfaceByName(deviceName) 16 | if err != nil { 17 | log.Printf("error: %s", err.Error()) 18 | return 19 | } 20 | 21 | conn, err := raw.ListenPacket(intf, syscall.ETH_P_ALL, nil) 22 | if err != nil { 23 | log.Printf("error: %s", err.Error()) 24 | return 25 | } 26 | 27 | buffer := make([]byte, 65536) 28 | 29 | for { 30 | l, _, err := conn.ReadFrom(buffer) 31 | if err != nil { 32 | log.Fatalf("error: %s", err.Error()) 33 | break 34 | } 35 | 36 | packet := gopacket.NewPacket(buffer[0:l], layers.LayerTypeEthernet, gopacket.Default) 37 | packet.Metadata().Timestamp = time.Now() 38 | packet.Metadata().CaptureInfo.CaptureLength = l 39 | packet.Metadata().CaptureInfo.Length = l 40 | 41 | filter(packet, out) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | 14 | "github.com/google/gopacket" 15 | "github.com/google/gopacket/layers" 16 | "github.com/google/gopacket/pcapgo" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | var ( 21 | rootCmd = &cobra.Command{ 22 | Use: os.Args[0], 23 | } 24 | 25 | interfaces *[]string 26 | 27 | hostInterfaces []net.Interface 28 | 29 | unknown string 30 | dissectOnly bool 31 | 32 | unknownFile *os.File 33 | unknownWriter *pcapgo.Writer 34 | 35 | statefile = getStateFile() 36 | ) 37 | 38 | func init() { 39 | hostInterfaces, _ = net.Interfaces() 40 | 41 | listCmd := &cobra.Command{ 42 | Use: "list", 43 | Short: "List network interfaces", 44 | Run: list, 45 | } 46 | rootCmd.AddCommand(listCmd) 47 | 48 | monitorCmd := &cobra.Command{ 49 | Use: "monitor", 50 | Short: "Monitor all interfaces for Probe Requests", 51 | Run: monitor, 52 | PreRun: setupWriter, 53 | PostRun: tearDownWriter, 54 | } 55 | monitorCmd.Flags().StringVarP(&unknown, "unknown", "u", "", "Path to write unknown packets to") 56 | rootCmd.AddCommand(monitorCmd) 57 | 58 | simulateCmd := &cobra.Command{ 59 | Use: "simulate", 60 | Short: "", 61 | Run: simulate, 62 | Args: cobra.MinimumNArgs(1), 63 | PreRun: setupWriter, 64 | PostRun: tearDownWriter, 65 | } 66 | simulateCmd.Flags().StringVarP(&unknown, "unknown", "u", "", "Path to write unknown packets to") 67 | simulateCmd.Flags().BoolVarP(&dissectOnly, "dissect-only", "d", false, "Only dissect packets") 68 | rootCmd.AddCommand(simulateCmd) 69 | 70 | interfaces = monitorCmd.PersistentFlags().StringArrayP("interface", "i", []string{"all"}, "Interface(s) to monitor") 71 | } 72 | 73 | func setupWriter(_ *cobra.Command, _ []string) { 74 | var err error 75 | if unknown != "" { 76 | unknownFile, err = os.OpenFile(unknown, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 77 | if err != nil { 78 | log.Fatalf("%s", err) 79 | } 80 | 81 | unknownWriter = pcapgo.NewWriter(unknownFile) 82 | 83 | pos, _ := unknownFile.Seek(0, 2) 84 | if pos == 0 { 85 | err = unknownWriter.WriteFileHeader(65536, layers.LinkTypeEthernet) 86 | if err != nil { 87 | log.Fatalf("%s", err) 88 | } 89 | } 90 | } 91 | } 92 | 93 | func tearDownWriter(_ *cobra.Command, _ []string) { 94 | if unknownFile != nil { 95 | unknownFile.Close() 96 | } 97 | } 98 | 99 | func list(_ *cobra.Command, _ []string) { 100 | for _, i := range hostInterfaces { 101 | fmt.Printf("%10s %17s %s\n", i.Name, i.HardwareAddr.String(), OUIVendor(i.HardwareAddr.String())) 102 | } 103 | 104 | os.Exit(0) 105 | } 106 | 107 | func monitor(_ *cobra.Command, _ []string) { 108 | packets := make(chan gopacket.Packet, 10) 109 | 110 | if len(*interfaces) == 1 && (*interfaces)[0] == "all" { 111 | *interfaces = []string{} 112 | 113 | for _, i := range hostInterfaces { 114 | *interfaces = append(*interfaces, i.Name) 115 | } 116 | } 117 | 118 | for _, i := range *interfaces { 119 | go listen(i, packets) 120 | } 121 | 122 | i := newIntel() 123 | g := newGUI() 124 | 125 | state, _ := ioutil.ReadFile(statefile) 126 | _ = json.Unmarshal(state, &i.NICCollection) 127 | 128 | i.hostChan = make(chan *NIC, 10) 129 | 130 | go func() { 131 | last := time.Now() 132 | 133 | for packet := range packets { 134 | if !i.NewPacket(packet) && unknownWriter != nil { 135 | _ = unknownWriter.WritePacket(packet.Metadata().CaptureInfo, packet.Data()) 136 | } 137 | now := time.Now() 138 | if now.Sub(last).Seconds() > 10 { 139 | _ = os.Mkdir(filepath.Dir(statefile), 0700) 140 | f, _ := os.Create(statefile) 141 | j, _ := json.Marshal(i.NICCollection) 142 | _, _ = fmt.Fprintf(f, "%s", j) 143 | _ = f.Close() 144 | last = time.Now() 145 | } 146 | } 147 | }() 148 | 149 | go func() { 150 | for nic := range i.hostChan { 151 | g.updateNIC(nic) 152 | } 153 | }() 154 | 155 | // Needs to be run in a Goroutine because of limited number of channels 156 | go func() { 157 | for _, nic := range i.NICCollection { 158 | i.hostChan <- nic 159 | } 160 | }() 161 | 162 | _ = g.Run() 163 | } 164 | 165 | func simulate(_ *cobra.Command, args []string) { 166 | packets := make(chan gopacket.Packet, 10) 167 | 168 | i := newIntel() 169 | i.hostChan = make(chan *NIC, 10) 170 | 171 | go func() { 172 | for _, a := range args { 173 | f, err := os.Open(a) 174 | if err != nil { 175 | log.Fatalf("%s", err) 176 | } 177 | 178 | reader, err := pcapgo.NewReader(f) 179 | if err != nil { 180 | log.Fatalf("%s", err) 181 | } 182 | 183 | for { 184 | data, ci, err := reader.ReadPacketData() 185 | if err == io.EOF { 186 | break 187 | } 188 | 189 | packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.Default) 190 | packet.Metadata().Timestamp = ci.Timestamp 191 | packet.Metadata().CaptureInfo = ci 192 | 193 | filter(packet, packets) 194 | } 195 | 196 | f.Close() 197 | } 198 | 199 | close(packets) 200 | }() 201 | 202 | if dissectOnly { 203 | go func() { 204 | for range i.hostChan { 205 | } 206 | }() 207 | 208 | for packet := range packets { 209 | if !i.NewPacket(packet) && unknownWriter != nil { 210 | _ = unknownWriter.WritePacket(packet.Metadata().CaptureInfo, packet.Data()) 211 | } 212 | } 213 | 214 | return 215 | } 216 | 217 | g := newGUI() 218 | 219 | go func() { 220 | for packet := range packets { 221 | if !i.NewPacket(packet) && unknownWriter != nil { 222 | _ = unknownWriter.WritePacket(packet.Metadata().CaptureInfo, packet.Data()) 223 | } 224 | } 225 | }() 226 | 227 | go func() { 228 | for nic := range i.hostChan { 229 | g.updateNIC(nic) 230 | } 231 | }() 232 | 233 | err := g.Run() 234 | if err != nil { 235 | log.Fatal(err.Error()) 236 | } 237 | } 238 | 239 | func main() { 240 | err := rootCmd.Execute() 241 | if err != nil { 242 | log.Fatal(err.Error()) 243 | } 244 | } 245 | 246 | func filter(packet gopacket.Packet, out chan gopacket.Packet) { 247 | if ethernetLayer := packet.Layer(layers.LayerTypeEthernet); ethernetLayer != nil { 248 | eth := ethernetLayer.(*layers.Ethernet) 249 | ipv4 := packet.Layer(layers.LayerTypeIPv4) 250 | ipv6 := packet.Layer(layers.LayerTypeIPv6) 251 | 252 | switch { 253 | // Throw away packets with no source. 254 | case eth.SrcMAC.String() == "00:00:00:00:00:00": 255 | 256 | // We're only interested in group traffic. 257 | case eth.DstMAC[0]&0x01 > 0: 258 | out <- packet 259 | 260 | // ... or IPv4 broadcast traffic. 261 | case ipv4 != nil && ipv4.(*layers.IPv4).DstIP.Equal(net.IPv4bcast): 262 | out <- packet 263 | 264 | case ipv4 != nil && ipv4.(*layers.IPv4).DstIP.IsLinkLocalMulticast(): 265 | out <- packet 266 | 267 | // ... or IPv6 broadcast traffic: 268 | case ipv6 != nil && ipv6.(*layers.IPv6).DstIP.IsLinkLocalMulticast(): 269 | out <- packet 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /mndp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/google/gopacket" 7 | "github.com/google/gopacket/layers" 8 | ) 9 | 10 | var layerTypeMNDP = gopacket.RegisterLayerType(0x1001, gopacket.LayerTypeMetadata{Name: "MNDP", Decoder: gopacket.DecodeFunc(decodeMNDP)}) 11 | 12 | func init() { 13 | layers.RegisterUDPPortLayerType(layers.UDPPort(5678), layerTypeMNDP) 14 | } 15 | 16 | type MNDP struct { 17 | unknownHeader1 [2]byte 18 | unknownHeader2 [2]byte 19 | 20 | MAC string 21 | Identity string 22 | Version string 23 | Platform string 24 | Uptime uint16 25 | SoftwareID string 26 | Board string 27 | Interface string 28 | 29 | contents []byte 30 | } 31 | 32 | func (m *MNDP) LayerType() gopacket.LayerType { return layerTypeMNDP } 33 | func (m *MNDP) LayerContents() []byte { return m.contents } 34 | func (m *MNDP) LayerPayload() []byte { return nil } 35 | 36 | func decodeMNDP(data []byte, p gopacket.PacketBuilder) error { 37 | m := &MNDP{} 38 | 39 | // Well... 40 | copy(m.unknownHeader1[:], data) 41 | copy(m.unknownHeader2[:], data[2:]) 42 | 43 | rest := data[4:] 44 | 45 | for len(rest) > 4 { 46 | var payload []byte 47 | typ := binary.BigEndian.Uint16(rest) 48 | length := binary.BigEndian.Uint16(rest[2:]) 49 | 50 | rest = rest[4:] 51 | 52 | if len(rest) >= int(length) { 53 | payload = rest[:length] 54 | 55 | rest = rest[length:] 56 | } 57 | 58 | switch typ { 59 | case 1: // MAC address, string 60 | m.MAC = string(payload) 61 | case 5: // Identity, string 62 | m.Identity = string(payload) 63 | case 7: // Version, string 64 | m.Version = string(payload) 65 | case 8: // Platform, string 66 | m.Platform = string(payload) 67 | case 10: // Uptime, uint16 68 | m.Uptime = binary.BigEndian.Uint16(payload) 69 | case 11: // Software ID, string 70 | m.SoftwareID = string(payload) 71 | case 12: // Board, string 72 | m.Board = string(payload) 73 | case 13: // unknown, unseen 74 | case 14: // unknown, uint8 75 | case 15: // unknown, unseen 76 | case 16: // interface name, string 77 | m.Interface = string(payload) 78 | case 17: // unknown 79 | } 80 | } 81 | 82 | p.AddLayer(m) 83 | 84 | return p.NextDecoder(gopacket.DecodeFragment) 85 | } 86 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/google/gopacket" 7 | "github.com/google/gopacket/layers" 8 | ) 9 | 10 | type mux map[gopacket.LayerType]func(source net.HardwareAddr, layer gopacket.Layer) bool 11 | 12 | func newMux() mux { 13 | return make(mux) 14 | } 15 | 16 | func (m mux) add(layerType gopacket.LayerType, fun func(source net.HardwareAddr, layer gopacket.Layer) bool) { 17 | m[layerType] = fun 18 | } 19 | 20 | func (m mux) process(packet gopacket.Packet) bool { 21 | var source net.HardwareAddr 22 | 23 | if ethernetLayer := packet.Layer(layers.LayerTypeEthernet); ethernetLayer != nil { 24 | ethernet := ethernetLayer.(*layers.Ethernet) 25 | 26 | source = ethernet.SrcMAC 27 | } 28 | 29 | recognized := false 30 | for t, f := range m { 31 | if l := packet.Layer(t); l != nil { 32 | r := f(source, l) 33 | if r { 34 | recognized = true 35 | } 36 | } 37 | } 38 | 39 | return recognized 40 | } 41 | -------------------------------------------------------------------------------- /ouiDatabase.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run contrib/embed-oui.go 4 | 5 | var ( 6 | privates = map[byte]bool{ 7 | '2': true, 8 | '6': true, 9 | 'a': true, 10 | 'e': true, 11 | } 12 | ) 13 | 14 | // OUIVendor will return the owner of a MAC address. 15 | func OUIVendor(mac string) string { 16 | if mac == "" { 17 | return "" 18 | } 19 | 20 | vendor := ouiToVendor[mac[0:8]] 21 | 22 | if vendor == "" && len(mac) > 1 && privates[mac[1]] { 23 | return "LAA (LOCALLY ADMINISTERED)" 24 | } 25 | 26 | return vendor 27 | } 28 | -------------------------------------------------------------------------------- /state_bsd.go: -------------------------------------------------------------------------------- 1 | // +build darwin dragonfly freebsd netbsd openbsd 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | func getStateFile() string { 11 | homedir, _ := os.UserHomeDir() 12 | return fmt.Sprintf("%s/.pnmap/state.json", homedir) 13 | } 14 | -------------------------------------------------------------------------------- /state_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func getStateFile() string { 13 | homedir, _ := os.UserHomeDir() 14 | gateways := findGateways() 15 | 16 | if len(gateways) == 1 { 17 | mac := findMacFromIPInArpTable(gateways[0]) 18 | return fmt.Sprintf("%s/.pnmap/state-%s-%s.json", homedir, mac, gateways[0]) 19 | } 20 | 21 | // If we have multiple - or zero - gateways, we fall-back to a generic state file. 22 | return fmt.Sprintf("%s/.pnmap/state.json", homedir) 23 | } 24 | 25 | // find gateways 26 | func findGateways() []string { 27 | file, err := os.Open("/proc/net/route") 28 | if err != nil { 29 | log.Fatalf("Failed to open /proc/net/route") 30 | } 31 | 32 | defer file.Close() 33 | 34 | scanner := bufio.NewScanner(file) 35 | scanner.Split(bufio.ScanLines) 36 | 37 | var gateways []string 38 | scanner.Scan() // skip first line as it's a header 39 | for scanner.Scan() { 40 | s := strings.Fields(scanner.Text()) 41 | if s[1] == "00000000" && s[7] == "00000000" { 42 | octet0, _ := strconv.ParseInt(s[2][6:8], 16, 64) 43 | octet1, _ := strconv.ParseInt(s[2][4:6], 16, 64) 44 | octet2, _ := strconv.ParseInt(s[2][2:4], 16, 64) 45 | octet3, _ := strconv.ParseInt(s[2][0:2], 16, 64) 46 | gateway := fmt.Sprintf("%d.%d.%d.%d", octet0, octet1, octet2, octet3) 47 | gateways = append(gateways, gateway) 48 | } 49 | } 50 | return gateways 51 | } 52 | 53 | // find the gateway in the arp table 54 | func findMacFromIPInArpTable(ip string) string { 55 | file, err := os.Open("/proc/net/arp") 56 | if err != nil { 57 | log.Fatalf("Failed to open /proc/net/route") 58 | } 59 | 60 | defer file.Close() 61 | 62 | scanner := bufio.NewScanner(file) 63 | scanner.Split(bufio.ScanLines) 64 | 65 | scanner.Scan() // skip first line as it's a header 66 | 67 | for scanner.Scan() { 68 | s := strings.Fields(scanner.Text()) 69 | if s[0] == ip { 70 | return string(s[3]) // we don't expect to find more than one 71 | } 72 | } 73 | return "" 74 | } 75 | -------------------------------------------------------------------------------- /stringSlice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // stringSlice is a slice of strings containing unique strings. 8 | type stringSlice []string 9 | 10 | func (s *stringSlice) add(value string) { 11 | if len(value) == 0 { 12 | return 13 | } 14 | 15 | for _, v := range *s { 16 | if v == value { 17 | return 18 | } 19 | } 20 | 21 | *s = append(*s, value) 22 | } 23 | 24 | func (s *stringSlice) String() string { 25 | all := strings.Join([]string(*s), "[reset], [white]") 26 | 27 | return "[reset][[white]" + all + "[reset]]" 28 | } 29 | -------------------------------------------------------------------------------- /ubiquiti-discovery.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | 7 | "github.com/google/gopacket" 8 | "github.com/google/gopacket/layers" 9 | ) 10 | 11 | var layerTypeUDiscovery = gopacket.RegisterLayerType(0x1002, gopacket.LayerTypeMetadata{Name: "ubqt-discovery", Decoder: gopacket.DecodeFunc(decodeUbiquityDiscovery)}) 12 | 13 | func init() { 14 | layers.RegisterUDPPortLayerType(layers.UDPPort(10001), layerTypeUDiscovery) 15 | } 16 | 17 | type UDiscovery struct { 18 | Software string 19 | IP string 20 | Uptime int 21 | Name string 22 | Model string 23 | Series string 24 | } 25 | 26 | func (m *UDiscovery) LayerType() gopacket.LayerType { return layerTypeUDiscovery } 27 | func (m *UDiscovery) LayerContents() []byte { return nil } 28 | func (m *UDiscovery) LayerPayload() []byte { return nil } 29 | 30 | func decodeUbiquityDiscovery(data []byte, p gopacket.PacketBuilder) error { 31 | u := &UDiscovery{} 32 | 33 | dataLength := int(binary.BigEndian.Uint16(data[2:])) 34 | 35 | if dataLength+4 != len(data) { 36 | 37 | return p.NextDecoder(gopacket.DecodePayload) 38 | } 39 | 40 | rest := data[4:] 41 | 42 | for len(rest) >= 4 { 43 | var payload []byte 44 | 45 | typ := rest[0] 46 | length := binary.BigEndian.Uint16(rest[1:]) 47 | 48 | rest = rest[3:] 49 | 50 | if len(rest) >= int(length) { 51 | payload = rest[:length] 52 | 53 | rest = rest[length:] 54 | } else { 55 | // malformed. 56 | return p.NextDecoder(gopacket.DecodePayload) 57 | } 58 | 59 | // Well, this is based on samples from a handful Ubiquiti 60 | // hardware. YMMV. 61 | switch typ { 62 | case 1: // MAC address, 6 bytes 63 | case 2: // MAC+IP? Legacy, 10 bytes 64 | case 3: // HW/SW, string 65 | u.Software = string(payload) 66 | case 4: // IP address, 4 bytes 67 | u.IP = net.IP(payload).String() 68 | case 10: // Uptime, uint32 69 | u.Uptime = int(binary.BigEndian.Uint32(payload)) 70 | case 11: // name, string 71 | u.Name = string(payload) 72 | case 12: // model, string 73 | u.Model = string(payload) 74 | case 14: // 1 byte..? 75 | case 16: // 2 bytes..? 76 | case 20: // series, string 77 | u.Series = string(payload) 78 | } 79 | } 80 | 81 | p.AddLayer(u) 82 | 83 | return p.NextDecoder(gopacket.DecodeFragment) 84 | } 85 | --------------------------------------------------------------------------------