-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
335 lines (303 loc) · 10.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
package main
import (
"encoding/binary"
"encoding/json"
"flag"
"fmt"
interfaceprint "ldgo/interfacePrint"
"log"
"os"
"sort"
"strings"
"time"
"github.com/BadPixel89/colourtext"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
type SwitchData struct {
Protocol string
SwitchName string
Interface string
VLAN string
SwitchModel string
VTPDomain string
}
func (sd SwitchData) String() string {
b := &strings.Builder{}
fmt.Fprintln(b, "protocol : ", sd.Protocol)
fmt.Fprintln(b, "switch name : ", sd.SwitchName)
fmt.Fprintln(b, "interface : ", sd.Interface)
fmt.Fprintln(b, "VLAN : ", sd.VLAN)
fmt.Fprintln(b, "model : ", sd.SwitchModel)
fmt.Fprintln(b, "VTP Domain : ", sd.VTPDomain)
return b.String()
}
var help = flag.Bool("h", false, "Display the help text and usage of each flag")
var listAdaptors = flag.Bool("l", false, "List the current network interfaces and exit the program. No packets will be captured")
var nicIndex = flag.Int("i", 9999, "Select the index of the network adapter you want to listen on, as displayed by the list flag. -i 0 will listen on the first adapter in the list")
var nicName = flag.String("n", "", "Select the network adapter to listen on by name. Case sensitive. This can be any substring found in the name or description and the first result is chosen. For example, '-n Intel' will return the first adapter where the name or description contains 'Intel'. Default behaviour is an empty string")
var protocol = flag.String("p", "", "Choose to listen on only one protocol. Valid choices are '-p cdp' '-p lldp' '-p icmp'. Default action is to listen for CDP and LLDP. Sepcifying LLDP will set the timeout to 31s unless a none default timeout is chosen. The purpose of ICMP is to enable testing on the given interface. Run a ping from a separate terminal while GoLD is listening for ICMP and you should see a request and a reply per ping")
var outFile = flag.String("o", "", "Choose the directory in which to write the output file, default is the current working directory as returned by os.Getwd() https://pkg.go.dev/os#Getwd")
var timeout = flag.Int("t", 61, "Set how long to listen for in seconds. CDP announces every 60s, LLDP every 30s. Specifying LLDP will set the timeout to 31s if the value is default")
var version = flag.Bool("v", false, "Display version")
const lldpOnlyFilter = "ether[12:2]==0x88cc"
const cdpOnlyFilter = "ether[20:2]==0x2000"
const defaultFilter = "ether[12:2]==0x88cc or ether[20:2]==0x2000"
const icmpFIlter = "icmp[icmptype] == icmp-echo or icmp[icmptype] == icmp-echoreply"
const blurb = "cross platform link discovery"
const kofi = "https://ko-fi.com/dktools"
const versionString = "version: 0.4a"
const repoString = "https://github.com/BadPixel89/ldGO"
func main() {
flag.Parse()
log.SetFlags(0)
colourtext.EnableVirtualConsoleStdout()
colourtext.EnableVirtualConsoleStderr()
if len(os.Args) == 1 {
colourtext.PrintInfo("No flags passed, use -h to see helptext")
*listAdaptors = true
}
if *help {
fmt.Fprintf(os.Stdout, "To run this software you will need:\n\nWindows:\nnPcap https://npcap.com/#download.\n\nLinux:\nlibpcap0.8 to run, libpcap-dev to build\n\nIn some environments you will need to run as admin or sudo\n\n")
flag.CommandLine.Usage()
os.Exit(0)
return
}
if *version {
PrintVersionBanner()
os.Exit(0)
return
}
devicePresent, devName := FindNetworkDevice()
if *listAdaptors {
os.Exit(0)
return
}
if !devicePresent {
if *nicIndex != 9999 || devName != "" {
fmt.Fprintf(os.Stderr, "[exit] %v\n", "invalid network adapter chosen "+*nicName)
os.Exit(1)
return
}
fmt.Fprintf(os.Stderr, "[exit] %v\n", "no arguments passed, use the -h flag to see CLI usage")
os.Exit(1)
}
colourtext.PrintSuccess("device found: " + devName)
handle, err := pcap.OpenLive(devName, 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err.Error())
}
defer handle.Close()
var filter string = defaultFilter
switch *protocol {
case "CDP":
filter = cdpOnlyFilter
case "cdp":
filter = cdpOnlyFilter
case "LLDP":
filter = lldpOnlyFilter
case "lldp":
filter = lldpOnlyFilter
if *timeout == 61 {
*timeout = 31
}
case "icmp":
filter = icmpFIlter
case "ICMP":
filter = icmpFIlter
}
err = handle.SetBPFFilter(filter)
if err != nil {
log.Fatal(err.Error())
}
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
killtime := time.After(time.Duration(*timeout) * time.Second)
go func() {
<-killtime
if *protocol == "icmp" || *protocol == "ICMP" {
colourtext.PrintTime("complete")
os.Exit(0)
}
colourtext.PrintTime("no packets captured")
os.Exit(0)
}()
colourtext.PrintSuccess("starting listener with filter '" + filter + "'")
// this method returns and quits as soon as it finds any packet matching the filter
switchInfo := StartListening(*packetSource)
colourtext.PrintColour(colourtext.Cyan, switchInfo.String())
err = WriteSwitchDataStructAsJson(switchInfo)
if err != nil {
log.Fatal(err.Error())
}
handle.Close()
os.Exit(0)
}
func StartListening(source gopacket.PacketSource) SwitchData {
for packet := range source.Packets() {
icmpLayer := packet.Layer(layers.LayerTypeICMPv4)
cdpLayer := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo)
lldpLayer := packet.Layer(layers.LayerTypeLinkLayerDiscovery)
lldpLayerInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo)
if cdpLayer != nil {
return ParseCDPToStruct(cdpLayer)
}
if lldpLayer != nil && lldpLayerInfo != nil {
var lldpInfo SwitchData = ParseLLDPToStruct(lldpLayer, lldpLayerInfo)
// this will happen when you plug in the network interface while listening, the packets capture will be incomplete
if lldpInfo.VLAN != "[fail]" {
return lldpInfo
}
}
if icmpLayer != nil {
ParseICMPPacket(icmpLayer)
}
}
return SwitchData{}
}
func ParseICMPPacket(icmplayer gopacket.Layer) {
echo, _ := icmplayer.(*layers.ICMPv4)
if echo.TypeCode.Type() == layers.ICMPv4TypeEchoRequest {
colourtext.PrintInfo("ping request")
}
if echo.TypeCode.Type() == layers.ICMPv4TypeEchoReply {
colourtext.PrintInfo("ping reply")
}
}
func ParseCDPToStruct(cdpLayer gopacket.Layer) SwitchData {
cdp, _ := cdpLayer.(*layers.CiscoDiscoveryInfo)
data := SwitchData{
Protocol: "CDP",
SwitchName: cdp.DeviceID,
Interface: cdp.PortID,
VLAN: fmt.Sprint(cdp.NativeVLAN),
SwitchModel: string(cdp.Platform),
VTPDomain: string(cdp.VTPDomain),
}
return data
}
func ParseLLDPToStruct(lldpLayer gopacket.Layer, lldpLayerInfo gopacket.Layer) SwitchData {
lldp, _ := lldpLayer.(*layers.LinkLayerDiscovery)
lldpInfo, _ := lldpLayerInfo.(*layers.LinkLayerDiscoveryInfo)
data := SwitchData{
Protocol: "LLDP",
SwitchName: lldpInfo.SysName,
Interface: string(lldp.PortID.ID),
VLAN: "[fail]",
SwitchModel: "[fail]",
VTPDomain: ParseLLDPManagementIP(lldpInfo.MgmtAddress.Address),
}
for _, val := range lldpInfo.OrgTLVs {
if val.SubType == 1 && val.OUI == layers.IEEEOUI8021 {
data.VLAN = fmt.Sprint(binary.BigEndian.Uint16(val.Info))
}
/*if val.SubType == 3 && val.OUI == layers.IEEEOUI8021 {
data.VLANName = string(val.Info)
}*/
if val.SubType == 10 && val.OUI == layers.IEEEOUIMedia {
data.SwitchModel = string(val.Info)
}
}
return data
}
func ParseLLDPManagementIP(addr []byte) string {
var ip string
if len(addr) == 4 {
ip = fmt.Sprint(addr[0])
for _, octet := range addr[1:] {
ip += "." + fmt.Sprint(octet)
}
} else {
ip = "0.0.0.0"
}
return ip
}
func FindNetworkDevice() (bool, string) {
devices, err := pcap.FindAllDevs()
if err != nil {
colourtext.PrintError("no devices found: MUST HAVE RELEVANT PACKET CAPTURE LIBRARY INSTALLED. In some environments you may need to run the program as admin/sudo. Pass -h for help text")
colourtext.PrintError(err.Error())
os.Exit(1)
}
sort.Slice(devices, func(i, j int) bool {
return devices[i].Name < devices[j].Name
})
if *listAdaptors {
/*
colourtext.PrintInfo("devices: ")
header := fmt.Sprintf("%10s %29s %13s", "name", "description", "IP")
colourtext.PrintColour(colourtext.Cyan, header)
for i, dev := range devices {
switch len(dev.Addresses) {
case 0:
line := fmt.Sprintf("%-5s %-20s | %-20s |", "["+fmt.Sprint(i)+"]", dev.Name, dev.Description)
colourtext.PrintColour(colourtext.Cyan, line)
case 1:
line := fmt.Sprintf("%-5s %-20s | %-20s |", "["+fmt.Sprint(i)+"]", dev.Name, dev.Description)
colourtext.PrintColour(colourtext.Cyan, line)
case 2:
line := fmt.Sprintf("%-5s %-20s | %-20s | %-20s", "["+fmt.Sprint(i)+"]", dev.Name, dev.Description, dev.Addresses[1].IP.String())
//line := "[" + fmt.Sprint(i) + "] " + dev.Name + " | " + dev.Description + " | " + dev.Addresses[1].IP.String()
colourtext.PrintColour(colourtext.Cyan, line)
}
}
*/
interfaceprint.PrintInterfaces(devices)
return false, ""
}
found := false
var devName string = ""
if *nicIndex != 9999 {
if *nicIndex < len(devices) {
devName = devices[*nicIndex].Name
found = true
return found, devName
}
}
if *nicName != "" {
for _, device := range devices {
if strings.Contains(device.Name, *nicName) {
found = true
devName = device.Name
return found, devName
}
if strings.Contains(device.Description, *nicName) {
found = true
devName = device.Name
break
}
}
}
return found, devName
}
func WriteSwitchDataStructAsJson(data SwitchData) error {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
if *outFile == "" {
dir, err := os.Getwd()
if err != nil {
return err
}
err = os.WriteFile(dir+"/switch-data.json", jsonData, 0644)
if err != nil {
return err
}
return nil
}
err = os.WriteFile(*outFile, jsonData, 0644)
if err != nil {
return err
}
return nil
}
func PrintVersionBanner() {
// this looks like a mess becuase of the escape characters for backslashes. Prints nice.
colourtext.PrintColour(colourtext.Yellow, " _ __________________ | "+blurb)
colourtext.PrintColour(colourtext.Yellow, "| | __| |___ ____/_ __ \\ |")
colourtext.PrintColour(colourtext.Yellow, "| |/ _` |__ / __ _ / / / | "+repoString)
colourtext.PrintColour(colourtext.Yellow, "| | (_| | / /_/ / / /_/ / | "+kofi)
colourtext.PrintColour(colourtext.Yellow, "|_|\\__,_| \\____/ \\____/ | "+versionString)
os.Exit(0)
}