mirror of
				https://github.com/maride/pancap.git
				synced 2025-10-10 19:36:51 +00:00 
			
		
		
		
	Get rid of LinkType-style branching: different LinkTypes may use the same Protocol modules.
This commit is contained in:
		
							parent
							
								
									c2aa435a6d
								
							
						
					
					
						commit
						f0b5ab140e
					
				| @ -1,11 +1,8 @@ | |||||||
| package ethernet | package analyze | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"git.darknebu.la/maride/pancap/ethernet/arp" | 	"git.darknebu.la/maride/pancap/protocol" | ||||||
| 	"git.darknebu.la/maride/pancap/ethernet/dhcpv4" |  | ||||||
| 	"git.darknebu.la/maride/pancap/ethernet/dns" |  | ||||||
| 	"github.com/google/gopacket" | 	"github.com/google/gopacket" | ||||||
| 	"github.com/google/gopacket/layers" |  | ||||||
| 	"log" | 	"log" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -22,33 +19,23 @@ func Analyze(source *gopacket.PacketSource) error { | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if packet.Layer(layers.LayerTypeDNS) != nil { | 		// Iterate over all possible protocols | ||||||
| 			// Handle DNS packet | 		for _, p := range protocol.Protocols { | ||||||
| 			handleErr(dns.ProcessDNSPacket(packet)) | 			// Check if this protocol can handle this packet | ||||||
| 		} | 			if p.CanAnalyze(packet) { | ||||||
| 
 | 				handleErr(p.Analyze(packet)) | ||||||
| 		if packet.Layer(layers.LayerTypeARP) != nil { | 			} | ||||||
| 			// Handle ARP packet |  | ||||||
| 			handleErr(arp.ProcessARPPacket(packet)) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if packet.Layer(layers.LayerTypeDHCPv4) != nil { |  | ||||||
| 			// Handle DHCP (v4) packet |  | ||||||
| 			handleErr(dhcpv4.HandleDHCPv4Packet(packet)) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	// After processing all packets, print summary |  | ||||||
| 	printSummary() |  | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Prints all the summaries. | // Prints all the summaries. | ||||||
| func printSummary() { | func PrintSummary() { | ||||||
| 	arp.PrintARPSummary() | 	for _, p := range protocol.Protocols { | ||||||
| 	dns.PrintDNSSummary() | 		p.PrintSummary() | ||||||
| 	dhcpv4.PrintDHCPv4Summary() | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Handles an error, if err is not nil. | // Handles an error, if err is not nil. | ||||||
| @ -58,3 +45,4 @@ func handleErr(err error) { | |||||||
| 		log.Printf("Encountered error while examining packets, continuing anyway. Error: %s", err.Error()) | 		log.Printf("Encountered error while examining packets, continuing anyway. Error: %s", err.Error()) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
							
								
								
									
										23
									
								
								analyzer.go
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								analyzer.go
									
									
									
									
									
								
							| @ -1,23 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"git.darknebu.la/maride/pancap/ethernet" |  | ||||||
| 	"github.com/google/gopacket" |  | ||||||
| 	"github.com/google/gopacket/layers" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Analyzes the given packet source |  | ||||||
| func analyzePCAP(source *gopacket.PacketSource, linkType layers.LinkType) error { |  | ||||||
| 	// Switch over link type to determine correct module to ask for analysis |  | ||||||
| 	switch linkType { |  | ||||||
| 	case layers.LinkTypeEthernet: |  | ||||||
| 		// Ethernet |  | ||||||
| 		return ethernet.Analyze(source) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// if we reach this point, the given PCAP contains a link type we can't handle (yet). |  | ||||||
| 	errorMsg := fmt.Sprintf("Asked for link type %s (ID %d), but not supported by pancap. :( sorry!", linkType.String(), linkType) |  | ||||||
| 	return errors.New(errorMsg) |  | ||||||
| } |  | ||||||
| @ -1,21 +0,0 @@ | |||||||
| package dhcpv4 |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"git.darknebu.la/maride/pancap/common" |  | ||||||
| 	"github.com/google/gopacket/layers" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	requestMAC []string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Processes the DHCP request packet handed over |  | ||||||
| func processRequestPacket(dhcppacket layers.DHCPv4) { |  | ||||||
| 	requestMAC = common.AppendIfUnique(dhcppacket.ClientHWAddr.String(), requestMAC) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Generates the summary of all DHCP request packets |  | ||||||
| func generateRequestSummary() string { |  | ||||||
| 	return fmt.Sprintf("%d unique DHCP requests\n%s", len(requestMAC), common.GenerateTree(requestMAC)) |  | ||||||
| } |  | ||||||
							
								
								
									
										8
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								main.go
									
									
									
									
									
								
							| @ -3,6 +3,7 @@ package main | |||||||
| import ( | import ( | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"git.darknebu.la/maride/pancap/analyze" | ||||||
| 	"git.darknebu.la/maride/pancap/output" | 	"git.darknebu.la/maride/pancap/output" | ||||||
| 	"log" | 	"log" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| @ -19,19 +20,22 @@ func main() { | |||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	// Open the given PCAP | 	// Open the given PCAP | ||||||
| 	packetSource, linkType, fileErr := openPCAP() | 	packetSource, _, fileErr := openPCAP() | ||||||
| 	if fileErr != nil { | 	if fileErr != nil { | ||||||
| 		// Encountered problems with the PCAP - permission and/or existance error | 		// Encountered problems with the PCAP - permission and/or existance error | ||||||
| 		log.Fatalf("Error occured while opeining specified file: %s", fileErr.Error()) | 		log.Fatalf("Error occured while opeining specified file: %s", fileErr.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Start analyzing | 	// Start analyzing | ||||||
| 	analyzeErr := analyzePCAP(packetSource, linkType) | 	analyzeErr := analyze.Analyze(packetSource) | ||||||
| 	if analyzeErr != nil { | 	if analyzeErr != nil { | ||||||
| 		// Mh, encountered some problems while analyzing file | 		// Mh, encountered some problems while analyzing file | ||||||
| 		log.Fatalf("Error occurred while analyzing: %s", analyzeErr.Error()) | 		log.Fatalf("Error occurred while analyzing: %s", analyzeErr.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Show user analysis | ||||||
|  | 	analyze.PrintSummary() | ||||||
|  | 
 | ||||||
| 	// Finalize output | 	// Finalize output | ||||||
| 	output.Finalize() | 	output.Finalize() | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,8 +15,15 @@ var ( | |||||||
| 	devices []arpDevice | 	devices []arpDevice | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Called on every ARP packet | type Protocol struct {} | ||||||
| func ProcessARPPacket(packet gopacket.Packet) error { | 
 | ||||||
|  | // Checks if the given packet is an ARP packet we can process | ||||||
|  | func (p *Protocol) CanAnalyze(packet gopacket.Packet) bool { | ||||||
|  | 	return packet.Layer(layers.LayerTypeARP) != nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Analyzes the given ARP packet | ||||||
|  | func (p *Protocol) Analyze(packet gopacket.Packet) error { | ||||||
| 	var arppacket layers.ARP | 	var arppacket layers.ARP | ||||||
| 
 | 
 | ||||||
| 	// Decode raw packet into ARP | 	// Decode raw packet into ARP | ||||||
| @ -28,7 +35,7 @@ func ProcessARPPacket(packet gopacket.Packet) error { | |||||||
| 
 | 
 | ||||||
| 	// Convert MAC address byte array to string | 	// Convert MAC address byte array to string | ||||||
| 	sourceAddr := net.HardwareAddr(arppacket.SourceHwAddress).String() | 	sourceAddr := net.HardwareAddr(arppacket.SourceHwAddress).String() | ||||||
| 	participant := getStatOrCreate(sourceAddr) | 	participant := p.getStatOrCreate(sourceAddr) | ||||||
| 
 | 
 | ||||||
| 	// Raise stats | 	// Raise stats | ||||||
| 	if arppacket.Operation == layers.ARPRequest { | 	if arppacket.Operation == layers.ARPRequest { | ||||||
| @ -37,27 +44,27 @@ func ProcessARPPacket(packet gopacket.Packet) error { | |||||||
| 		participant.askedList = common.AppendIfUnique(net.IP(arppacket.DstProtAddress).String(), participant.askedList) | 		participant.askedList = common.AppendIfUnique(net.IP(arppacket.DstProtAddress).String(), participant.askedList) | ||||||
| 
 | 
 | ||||||
| 		// Add device entry | 		// Add device entry | ||||||
| 		addDeviceEntry(sourceAddr, net.IP(arppacket.SourceProtAddress).String()) | 		p.addDeviceEntry(sourceAddr, net.IP(arppacket.SourceProtAddress).String()) | ||||||
| 	} else { | 	} else { | ||||||
| 		// Response packet | 		// Response packet | ||||||
| 		participant.answered++ | 		participant.answered++ | ||||||
| 		participant.answeredList = common.AppendIfUnique(net.IP(arppacket.SourceProtAddress).String(), participant.answeredList) | 		participant.answeredList = common.AppendIfUnique(net.IP(arppacket.SourceProtAddress).String(), participant.answeredList) | ||||||
| 
 | 
 | ||||||
| 		// Add device entry | 		// Add device entry | ||||||
| 		addDeviceEntry(sourceAddr, net.IP(arppacket.SourceProtAddress).String()) | 		p.addDeviceEntry(sourceAddr, net.IP(arppacket.SourceProtAddress).String()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Print a summary after all packets are processed | // Print a summary after all packets are processed | ||||||
| func PrintARPSummary() { | func (p *Protocol) PrintSummary() { | ||||||
| 	output.PrintBlock("ARP traffic summary", generateTrafficStats()) | 	output.PrintBlock("ARP traffic summary", p.generateTrafficStats()) | ||||||
| 	output.PrintBlock("ARP LAN overview", generateLANOverview()) | 	output.PrintBlock("ARP LAN overview", p.generateLANOverview()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates an answer regarding the ARP traffic | // Generates an answer regarding the ARP traffic | ||||||
| func generateTrafficStats() string { | func (p *Protocol) generateTrafficStats() string { | ||||||
| 	var tmparr []string | 	var tmparr []string | ||||||
| 
 | 
 | ||||||
| 	// Iterate over all participants | 	// Iterate over all participants | ||||||
| @ -83,7 +90,7 @@ func generateTrafficStats() string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates an overview over all connected devices in the LAN | // Generates an overview over all connected devices in the LAN | ||||||
| func generateLANOverview() string { | func (p *Protocol) generateLANOverview() string { | ||||||
| 	var tmparr []string | 	var tmparr []string | ||||||
| 
 | 
 | ||||||
| 	// iterate over all devices | 	// iterate over all devices | ||||||
| @ -96,7 +103,7 @@ func generateLANOverview() string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Returns the arpStats object for the given MAC address, or creates a new one | // Returns the arpStats object for the given MAC address, or creates a new one | ||||||
| func getStatOrCreate(macaddr string) *arpStats { | func (p *Protocol) getStatOrCreate(macaddr string) *arpStats { | ||||||
| 	// Try to find the given macaddr | 	// Try to find the given macaddr | ||||||
| 	for i := 0; i < len(arpStatsList); i++ { | 	for i := 0; i < len(arpStatsList); i++ { | ||||||
| 		if arpStatsList[i].macaddr == macaddr { | 		if arpStatsList[i].macaddr == macaddr { | ||||||
| @ -115,7 +122,7 @@ func getStatOrCreate(macaddr string) *arpStats { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Adds a new entry to the devices array, checking if there may be a collision (=ARP Spoofing) | // Adds a new entry to the devices array, checking if there may be a collision (=ARP Spoofing) | ||||||
| func addDeviceEntry(macaddr string, ipaddr string) { | func (p *Protocol) addDeviceEntry(macaddr string, ipaddr string) { | ||||||
| 	if ipaddr == "0.0.0.0" { | 	if ipaddr == "0.0.0.0" { | ||||||
| 		// Possible ARP request if sender doesn't have an IP address yet. Ignore. | 		// Possible ARP request if sender doesn't have an IP address yet. Ignore. | ||||||
| 		return | 		return | ||||||
| @ -6,11 +6,28 @@ import ( | |||||||
| 	"github.com/google/gopacket/layers" | 	"github.com/google/gopacket/layers" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Called on every DHCP (v4) packet | type Protocol struct { | ||||||
| func HandleDHCPv4Packet(packet gopacket.Packet) error { | 	hostnames []hostname | ||||||
|  | 	networkSetup map[layers.DHCPOpt][]byte | ||||||
|  | 	requestMAC []string | ||||||
|  | 	responses []dhcpResponse | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Checks if the given packet is a DHCP packet we can process | ||||||
|  | func (p *Protocol) CanAnalyze(packet gopacket.Packet) bool { | ||||||
|  | 	return packet.Layer(layers.LayerTypeDHCPv4) != nil && packet.Layer(layers.LayerTypeEthernet) != nil && packet.Layers()[2].LayerPayload() != nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Analyzes the given DHCP packet | ||||||
|  | func (p *Protocol) Analyze(packet gopacket.Packet) error { | ||||||
| 	var dhcppacket layers.DHCPv4 | 	var dhcppacket layers.DHCPv4 | ||||||
| 	var ethernetpacket layers.Ethernet | 	var ethernetpacket layers.Ethernet | ||||||
| 
 | 
 | ||||||
|  | 	// Check if it's the first run - init networkSetup map then | ||||||
|  | 	if p.networkSetup == nil { | ||||||
|  | 		p.networkSetup = make(map[layers.DHCPOpt][]byte) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// For some reason I can't find an explanation for, | 	// For some reason I can't find an explanation for, | ||||||
| 	// 	packet.Layer(layers.LayerTypeDHCPv4).LayerContents(), which effectively is | 	// 	packet.Layer(layers.LayerTypeDHCPv4).LayerContents(), which effectively is | ||||||
| 	// 	packet.Layers()[3].layerContents(), is empty, but | 	// 	packet.Layers()[3].layerContents(), is empty, but | ||||||
| @ -35,23 +52,23 @@ func HandleDHCPv4Packet(packet gopacket.Packet) error { | |||||||
| 	// Examine packet further | 	// Examine packet further | ||||||
| 	if dhcppacket.Operation == layers.DHCPOpRequest { | 	if dhcppacket.Operation == layers.DHCPOpRequest { | ||||||
| 		// Request packet | 		// Request packet | ||||||
| 		processRequestPacket(dhcppacket) | 		p.processRequestPacket(dhcppacket) | ||||||
| 	} else { | 	} else { | ||||||
| 		// Response/Offer packet | 		// Response/Offer packet | ||||||
| 		processResponsePacket(dhcppacket, ethernetpacket) | 		p.processResponsePacket(dhcppacket, ethernetpacket) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check for Hostname DHCP option (12) | 	// Check for Hostname DHCP option (12) | ||||||
| 	checkForHostname(dhcppacket) | 	p.checkForHostname(dhcppacket) | ||||||
| 	checkForNetworkInfos(dhcppacket) | 	p.checkForNetworkInfos(dhcppacket) | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Print summary after all packets are processed | // Print summary after all packets are processed | ||||||
| func PrintDHCPv4Summary() { | func (p *Protocol) PrintSummary() { | ||||||
| 	output.PrintBlock("DHCP Network Overview", generateNetworkSummary()) | 	output.PrintBlock("DHCP Network Overview", p.generateNetworkSummary()) | ||||||
| 	output.PrintBlock("DHCP Requests", generateRequestSummary()) | 	output.PrintBlock("DHCP Requests", p.generateRequestSummary()) | ||||||
| 	output.PrintBlock("DHCP Responses/Offers", generateResponseSummary()) | 	output.PrintBlock("DHCP Responses/Offers", p.generateResponseSummary()) | ||||||
| 	output.PrintBlock("DHCP Hostnames", generateHostnamesSummary()) | 	output.PrintBlock("DHCP Hostnames", p.generateHostnamesSummary()) | ||||||
| } | } | ||||||
| @ -7,25 +7,21 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | func (p *Protocol) checkForHostname(dhcppacket layers.DHCPv4) { | ||||||
| 	hostnames []hostname |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func checkForHostname(dhcppacket layers.DHCPv4) { |  | ||||||
| 	// Search for "Hostname" option (ID 12) in DHCP Packet Options | 	// Search for "Hostname" option (ID 12) in DHCP Packet Options | ||||||
| 	for _, o := range dhcppacket.Options { | 	for _, o := range dhcppacket.Options { | ||||||
| 		if o.Type == layers.DHCPOptHostname { | 		if o.Type == layers.DHCPOptHostname { | ||||||
| 			// found it. Let's see if it's a request or response | 			// found it. Let's see if it's a request or response | ||||||
| 			if dhcppacket.Operation == layers.DHCPOpRequest { | 			if dhcppacket.Operation == layers.DHCPOpRequest { | ||||||
| 				// request, not granted yet. | 				// request, not granted yet. | ||||||
| 				addHostname(hostname{ | 				p.addHostname(hostname{ | ||||||
| 					hostname:       string(o.Data), | 					hostname:       string(o.Data), | ||||||
| 					requestedByMAC: dhcppacket.ClientHWAddr.String(), | 					requestedByMAC: dhcppacket.ClientHWAddr.String(), | ||||||
| 					granted:        false, | 					granted:        false, | ||||||
| 				}) | 				}) | ||||||
| 			} else { | 			} else { | ||||||
| 				// Response, DHCP issued this hostname | 				// Response, DHCP issued this hostname | ||||||
| 				addHostname(hostname{ | 				p.addHostname(hostname{ | ||||||
| 					hostname:       string(o.Data), | 					hostname:       string(o.Data), | ||||||
| 					requestedByMAC: "", | 					requestedByMAC: "", | ||||||
| 					granted:        true, | 					granted:        true, | ||||||
| @ -40,11 +36,11 @@ func checkForHostname(dhcppacket layers.DHCPv4) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates the list of all hostnames encountered. | // Generates the list of all hostnames encountered. | ||||||
| func generateHostnamesSummary() string { | func (p *Protocol) generateHostnamesSummary() string { | ||||||
| 	var tmparr []string | 	var tmparr []string | ||||||
| 
 | 
 | ||||||
| 	// Construct meaningful text | 	// Construct meaningful text | ||||||
| 	for _, h := range hostnames { | 	for _, h := range p.hostnames { | ||||||
| 		answer := "" | 		answer := "" | ||||||
| 
 | 
 | ||||||
| 		// check what kind of answer we need to construct | 		// check what kind of answer we need to construct | ||||||
| @ -76,11 +72,11 @@ func generateHostnamesSummary() string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Adds the given hostname to the hostname array, or patches an existing entry if found | // Adds the given hostname to the hostname array, or patches an existing entry if found | ||||||
| func addHostname(tmph hostname) { | func (p *Protocol) addHostname(tmph hostname) { | ||||||
| 	// see if we have an existing entry for this hostname | 	// see if we have an existing entry for this hostname | ||||||
| 	for i := 0; i < len(hostnames); i++ { | 	for i := 0; i < len(p.hostnames); i++ { | ||||||
| 		// get ith hostname in the list | 		// get ith hostname in the list | ||||||
| 		h := hostnames[i] | 		h := p.hostnames[i] | ||||||
| 
 | 
 | ||||||
| 		// ... and check if it's the one requested | 		// ... and check if it's the one requested | ||||||
| 		if h.hostname == tmph.hostname { | 		if h.hostname == tmph.hostname { | ||||||
| @ -97,13 +93,13 @@ func addHostname(tmph hostname) { | |||||||
| 				// Received a response for this hostname, check if it was granted | 				// Received a response for this hostname, check if it was granted | ||||||
| 				if h.hostname == tmph.hostname { | 				if h.hostname == tmph.hostname { | ||||||
| 					// granted, everything is fine. | 					// granted, everything is fine. | ||||||
| 					hostnames[i].granted = true | 					p.hostnames[i].granted = true | ||||||
| 				} else { | 				} else { | ||||||
| 					// Received a different hostname than the one requested by the MAC. Report that. | 					// Received a different hostname than the one requested by the MAC. Report that. | ||||||
| 					log.Printf("Client %s asked for hostname '%s' but was given '%s' by DHCP server", h.requestedByMAC, tmph.hostname, h.hostname) | 					log.Printf("Client %s asked for hostname '%s' but was given '%s' by DHCP server", h.requestedByMAC, tmph.hostname, h.hostname) | ||||||
| 					hostnames[i].deniedHostname = hostnames[i].hostname | 					p.hostnames[i].deniedHostname = p.hostnames[i].hostname | ||||||
| 					hostnames[i].hostname = tmph.hostname | 					p.hostnames[i].hostname = tmph.hostname | ||||||
| 					hostnames[i].granted = false | 					p.hostnames[i].granted = false | ||||||
| 				} | 				} | ||||||
| 				// in either case, it's a response by the DHCP server - hostname is granted in this context | 				// in either case, it's a response by the DHCP server - hostname is granted in this context | ||||||
| 
 | 
 | ||||||
| @ -114,5 +110,5 @@ func addHostname(tmph hostname) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// We didn't find the desired hostname, append given object to the list | 	// We didn't find the desired hostname, append given object to the list | ||||||
| 	hostnames = append(hostnames, tmph) | 	p.hostnames = append(p.hostnames, tmph) | ||||||
| } | } | ||||||
| @ -11,7 +11,6 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	networkSetup = make(map[layers.DHCPOpt][]byte) |  | ||||||
| 	watchedOpts = []layers.DHCPOpt{ | 	watchedOpts = []layers.DHCPOpt{ | ||||||
| 		layers.DHCPOptSubnetMask, // Option 1 | 		layers.DHCPOptSubnetMask, // Option 1 | ||||||
| 		layers.DHCPOptRouter, // Option 3 | 		layers.DHCPOptRouter, // Option 3 | ||||||
| @ -23,6 +22,19 @@ var ( | |||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Generates the summary of relevant DHCP options | ||||||
|  | func (p *Protocol) generateNetworkSummary() string { | ||||||
|  | 	// It's also possible to use strings.Builder here, but it produces code which is longer than this solution :shrug: | ||||||
|  | 	summary := fmt.Sprintf("Subnet Mask: %s\n", formatIP(p.networkSetup[layers.DHCPOptSubnetMask])) | ||||||
|  | 	summary = fmt.Sprintf("%sBroadcast: %s\n", summary, formatIP(p.networkSetup[layers.DHCPOptBroadcastAddr])) | ||||||
|  | 	summary = fmt.Sprintf("%sRouter: %s\n", summary, formatIP(p.networkSetup[layers.DHCPOptRouter])) | ||||||
|  | 	summary = fmt.Sprintf("%sDNS Server: %s\n", summary, formatIP(p.networkSetup[layers.DHCPOptDNS])) | ||||||
|  | 	summary = fmt.Sprintf("%sNTP Server: %s\n", summary, formatIP(p.networkSetup[layers.DHCPOptNTPServers])) | ||||||
|  | 	summary = fmt.Sprintf( "%sLease Time: %s\n", summary, formatDate(p.networkSetup[layers.DHCPOptLeaseTime])) | ||||||
|  | 	summary = fmt.Sprintf("%sRenewal Time: %s\n", summary, formatDate(p.networkSetup[layers.DHCPOptT1])) | ||||||
|  | 	return summary | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Looks for information specifying the setup of the network. This includes | // Looks for information specifying the setup of the network. This includes | ||||||
| //  - Option  1: Subnet Mask | //  - Option  1: Subnet Mask | ||||||
| //  - Option  3: Router address | //  - Option  3: Router address | ||||||
| @ -31,7 +43,7 @@ var ( | |||||||
| //  - Option 42: NTP Server address | //  - Option 42: NTP Server address | ||||||
| //  - Option 51: IP Address Lease time | //  - Option 51: IP Address Lease time | ||||||
| //  - Option 58: IP Renewal time | //  - Option 58: IP Renewal time | ||||||
| func checkForNetworkInfos(dhcppacket layers.DHCPv4) { | func (p *Protocol) checkForNetworkInfos(dhcppacket layers.DHCPv4) { | ||||||
| 	// Check if it is a DHCP request | 	// Check if it is a DHCP request | ||||||
| 	if dhcppacket.Operation == layers.DHCPOpRequest { | 	if dhcppacket.Operation == layers.DHCPOpRequest { | ||||||
| 		// We can ignore requests, they won't help us here | 		// We can ignore requests, they won't help us here | ||||||
| @ -42,12 +54,29 @@ func checkForNetworkInfos(dhcppacket layers.DHCPv4) { | |||||||
| 	for _, o := range dhcppacket.Options { | 	for _, o := range dhcppacket.Options { | ||||||
| 		if isRelevantOption(o) { | 		if isRelevantOption(o) { | ||||||
| 			// Found DHCP option to be watched, let's watch it | 			// Found DHCP option to be watched, let's watch it | ||||||
| 			saveOption(o) | 			p.saveOption(o) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Saves the given option in the networkSetup map, and informs the user if the value changes | ||||||
|  | func (p *Protocol) saveOption(opt layers.DHCPOption) { | ||||||
|  | 	// check if we already stored this value | ||||||
|  | 	if p.networkSetup[opt.Type] != nil { | ||||||
|  | 		// We already stored a value, let's check if it's the same as the new one | ||||||
|  | 		if !bytes.Equal(p.networkSetup[opt.Type], opt.Data) { | ||||||
|  | 			// Already stored a value and it's different from our new value - inform user and overwrite value later | ||||||
|  | 			log.Printf("Received different values for DHCP Option %s (ID %d). (Old: %s, New. %s)", opt.Type.String(), opt.Type, p.networkSetup[opt.Type], opt.Data) | ||||||
|  | 		} else { | ||||||
|  | 			// Exactly this value was already stored, no need to overwrite it | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	p.networkSetup[opt.Type] = opt.Data | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Checks if the given DHCPOption is part of the watchlist | // Checks if the given DHCPOption is part of the watchlist | ||||||
| func isRelevantOption(opt layers.DHCPOption) bool { | func isRelevantOption(opt layers.DHCPOption) bool { | ||||||
| 	// Iterate over all DHCP options in our watchlist | 	// Iterate over all DHCP options in our watchlist | ||||||
| @ -62,23 +91,6 @@ func isRelevantOption(opt layers.DHCPOption) bool { | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Saves the given option in the networkSetup map, and informs the user if the value changes |  | ||||||
| func saveOption(opt layers.DHCPOption) { |  | ||||||
| 	// check if we already stored this value |  | ||||||
| 	if networkSetup[opt.Type] != nil { |  | ||||||
| 		// We already stored a value, let's check if it's the same as the new one |  | ||||||
| 		if !bytes.Equal(networkSetup[opt.Type], opt.Data) { |  | ||||||
| 			// Already stored a value and it's different from our new value - inform user and overwrite value later |  | ||||||
| 			log.Printf("Received different values for DHCP Option %s (ID %d). (Old: %s, New. %s)", opt.Type.String(), opt.Type, networkSetup[opt.Type], opt.Data) |  | ||||||
| 		} else { |  | ||||||
| 			// Exactly this value was already stored, no need to overwrite it |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	networkSetup[opt.Type] = opt.Data |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Formats the given byte array as string representing the IP address, or returns an error (as string) | // Formats the given byte array as string representing the IP address, or returns an error (as string) | ||||||
| func formatIP(rawIP []byte) string { | func formatIP(rawIP []byte) string { | ||||||
| 	// Check if we even have an IP | 	// Check if we even have an IP | ||||||
| @ -139,17 +151,3 @@ func formatDate(rawDate []byte) string { | |||||||
| 
 | 
 | ||||||
| 	return formattedDate | 	return formattedDate | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // Generates the summary of relevant DHCP options |  | ||||||
| func generateNetworkSummary() string { |  | ||||||
| 	// It's also possible to use strings.Builder here, but it produces code which is longer than this solution :shrug: |  | ||||||
| 	summary := fmt.Sprintf("Subnet Mask: %s\n", formatIP(networkSetup[layers.DHCPOptSubnetMask])) |  | ||||||
| 	summary = fmt.Sprintf("%sBroadcast: %s\n", summary, formatIP(networkSetup[layers.DHCPOptBroadcastAddr])) |  | ||||||
| 	summary = fmt.Sprintf("%sRouter: %s\n", summary, formatIP(networkSetup[layers.DHCPOptRouter])) |  | ||||||
| 	summary = fmt.Sprintf("%sDNS Server: %s\n", summary, formatIP(networkSetup[layers.DHCPOptDNS])) |  | ||||||
| 	summary = fmt.Sprintf("%sNTP Server: %s\n", summary, formatIP(networkSetup[layers.DHCPOptNTPServers])) |  | ||||||
| 	summary = fmt.Sprintf("%sLease Time: %s\n", summary, formatDate(networkSetup[layers.DHCPOptLeaseTime])) |  | ||||||
| 	summary = fmt.Sprintf("%sRenewal Time: %s\n", summary, formatDate(networkSetup[layers.DHCPOptT1])) |  | ||||||
| 	return summary |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
							
								
								
									
										17
									
								
								protocol/dhcpv4/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								protocol/dhcpv4/request.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | package dhcpv4 | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"git.darknebu.la/maride/pancap/common" | ||||||
|  | 	"github.com/google/gopacket/layers" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Processes the DHCP request packet handed over | ||||||
|  | func (p *Protocol) processRequestPacket(dhcppacket layers.DHCPv4) { | ||||||
|  | 	p.requestMAC = common.AppendIfUnique(dhcppacket.ClientHWAddr.String(), p.requestMAC) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Generates the summary of all DHCP request packets | ||||||
|  | func (p *Protocol) generateRequestSummary() string { | ||||||
|  | 	return fmt.Sprintf("%d unique DHCP requests\n%s", len(p.requestMAC), common.GenerateTree(p.requestMAC)) | ||||||
|  | } | ||||||
| @ -7,20 +7,16 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | func (p *Protocol) processResponsePacket(dhcppacket layers.DHCPv4, ethernetpacket layers.Ethernet) { | ||||||
| 	responses []dhcpResponse | 	p.addResponseEntry(dhcppacket.ClientIP.String(), dhcppacket.YourClientIP.String(), dhcppacket.ClientHWAddr.String(), ethernetpacket.SrcMAC.String()) | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func processResponsePacket(dhcppacket layers.DHCPv4, ethernetpacket layers.Ethernet) { |  | ||||||
| 	addResponseEntry(dhcppacket.ClientIP.String(), dhcppacket.YourClientIP.String(), dhcppacket.ClientHWAddr.String(), ethernetpacket.SrcMAC.String()) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates the summary of all DHCP offer packets | // Generates the summary of all DHCP offer packets | ||||||
| func generateResponseSummary() string { | func (p *Protocol) generateResponseSummary() string { | ||||||
| 	var tmpaddr []string | 	var tmpaddr []string | ||||||
| 
 | 
 | ||||||
| 	// Iterate over all responses | 	// Iterate over all responses | ||||||
| 	for _, r := range responses { | 	for _, r := range p.responses { | ||||||
| 		addition := "" | 		addition := "" | ||||||
| 
 | 
 | ||||||
| 		if r.askedFor { | 		if r.askedFor { | ||||||
| @ -35,7 +31,7 @@ func generateResponseSummary() string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Adds a new response entry. If an IP address was already issued or a MAC asks multiple times for DNS, the case is examined further | // Adds a new response entry. If an IP address was already issued or a MAC asks multiple times for DNS, the case is examined further | ||||||
| func addResponseEntry(newIP string, yourIP string, destMAC string, serverMAC string) { | func (p *Protocol) addResponseEntry(newIP string, yourIP string, destMAC string, serverMAC string) { | ||||||
| 	// Check if client asked for a specific address (which was granted by the DHCP server) | 	// Check if client asked for a specific address (which was granted by the DHCP server) | ||||||
| 	askedFor := false | 	askedFor := false | ||||||
| 	if newIP == "0.0.0.0" { | 	if newIP == "0.0.0.0" { | ||||||
| @ -44,7 +40,7 @@ func addResponseEntry(newIP string, yourIP string, destMAC string, serverMAC str | |||||||
| 		askedFor = true | 		askedFor = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, r := range responses { | 	for _, r := range p.responses { | ||||||
| 		// Check for interesting cases | 		// Check for interesting cases | ||||||
| 		if r.destMACAddr == destMAC { | 		if r.destMACAddr == destMAC { | ||||||
| 			// The same client device received multiple IP addresses, let's examine further | 			// The same client device received multiple IP addresses, let's examine further | ||||||
| @ -71,7 +67,7 @@ func addResponseEntry(newIP string, yourIP string, destMAC string, serverMAC str | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Add a response entry - even if we found some "strange" behavior before. | 	// Add a response entry - even if we found some "strange" behavior before. | ||||||
| 	responses = append(responses, dhcpResponse{ | 	p.responses = append(p.responses, dhcpResponse{ | ||||||
| 		destMACAddr:   destMAC, | 		destMACAddr:   destMAC, | ||||||
| 		newIPAddr:     newIP, | 		newIPAddr:     newIP, | ||||||
| 		serverMACAddr: serverMAC, | 		serverMACAddr: serverMAC, | ||||||
| @ -19,7 +19,7 @@ var ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Called on every DNS packet to process response(s) | // Called on every DNS packet to process response(s) | ||||||
| func processDNSAnswer(answers []layers.DNSResourceRecord) { | func (p *Protocol) processDNSAnswer(answers []layers.DNSResourceRecord) { | ||||||
| 	for _, answer := range answers { | 	for _, answer := range answers { | ||||||
| 		// Raise stats | 		// Raise stats | ||||||
| 		numAnswers++ | 		numAnswers++ | ||||||
| @ -35,7 +35,7 @@ func processDNSAnswer(answers []layers.DNSResourceRecord) { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Process type answers | 		// Process type answers | ||||||
| 		processType(answerType, answer.Type) | 		p.processType(answerType, answer.Type) | ||||||
| 
 | 
 | ||||||
| 		// Append full domain and base domain | 		// Append full domain and base domain | ||||||
| 		answerDomains = common.AppendIfUnique(name, answerDomains) | 		answerDomains = common.AppendIfUnique(name, answerDomains) | ||||||
| @ -63,12 +63,12 @@ func processDNSAnswer(answers []layers.DNSResourceRecord) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates a summary of all DNS answers | // Generates a summary of all DNS answers | ||||||
| func generateDNSAnswerSummary() string { | func (p *Protocol) generateDNSAnswerSummary() string { | ||||||
| 	summary := "" | 	summary := "" | ||||||
| 
 | 
 | ||||||
| 	// Overall question stats | 	// Overall question stats | ||||||
| 	summary = fmt.Sprintf("%s%d DNS answers in total\n", summary, numAnswers) | 	summary = fmt.Sprintf("%s%d DNS answers in total\n", summary, numAnswers) | ||||||
| 	summary = fmt.Sprintf("%s%s records\n", summary, generateDNSTypeSummary(answerType)) | 	summary = fmt.Sprintf("%s%s records\n", summary, p.generateDNSTypeSummary(answerType)) | ||||||
| 	summary = fmt.Sprintf("%s%d unique domains of %d base domains, of which are %d private (non-ICANN) TLDs.\n", summary, len(answerDomains), len(answerBaseDomains), len(answerPrivateDomains)) | 	summary = fmt.Sprintf("%s%d unique domains of %d base domains, of which are %d private (non-ICANN) TLDs.\n", summary, len(answerDomains), len(answerBaseDomains), len(answerPrivateDomains)) | ||||||
| 
 | 
 | ||||||
| 	// Output base domains answered with | 	// Output base domains answered with | ||||||
| @ -17,7 +17,7 @@ var ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Processes the given dnstype and raises its stats in the given array | // Processes the given dnstype and raises its stats in the given array | ||||||
| func processType(typearr map[layers.DNSType]int, dnstype layers.DNSType) { | func (p *Protocol) processType(typearr map[layers.DNSType]int, dnstype layers.DNSType) { | ||||||
| 	typearr[dnstype]++ | 	typearr[dnstype]++ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -36,7 +36,7 @@ func ipIsPrivate(ip net.IP) bool { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates a summary string for DNS types in the given array | // Generates a summary string for DNS types in the given array | ||||||
| func generateDNSTypeSummary(typearr map[layers.DNSType]int) string { | func (p *Protocol) generateDNSTypeSummary(typearr map[layers.DNSType]int) string { | ||||||
| 	var answerarr []string | 	var answerarr []string | ||||||
| 
 | 
 | ||||||
| 	// Iterate over all possible DNS types | 	// Iterate over all possible DNS types | ||||||
| @ -6,8 +6,14 @@ import ( | |||||||
| 	"github.com/google/gopacket/layers" | 	"github.com/google/gopacket/layers" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Called on every DNS packet | type Protocol struct {} | ||||||
| func ProcessDNSPacket(packet gopacket.Packet) error { | 
 | ||||||
|  | func (p *Protocol) CanAnalyze(packet gopacket.Packet) bool { | ||||||
|  | 	return packet.Layer(layers.LayerTypeDNS) != nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Analyzes the given DHCP packet | ||||||
|  | func (p *Protocol) Analyze(packet gopacket.Packet) error { | ||||||
| 	var dnspacket layers.DNS | 	var dnspacket layers.DNS | ||||||
| 
 | 
 | ||||||
| 	// Decode raw packet into DNS | 	// Decode raw packet into DNS | ||||||
| @ -18,15 +24,15 @@ func ProcessDNSPacket(packet gopacket.Packet) error { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Further process the packet | 	// Further process the packet | ||||||
| 	processDNSQuestion(dnspacket.Questions) | 	p.processDNSQuestion(dnspacket.Questions) | ||||||
| 	processDNSAnswer(dnspacket.Answers) | 	p.processDNSAnswer(dnspacket.Answers) | ||||||
| 
 | 
 | ||||||
| 	// No error encountered, return clean | 	// No error encountered, return clean | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Print a summary after all DNS packets were processed | // Print a summary after all DNS packets were processed | ||||||
| func PrintDNSSummary() { | func (p *Protocol) PrintSummary() { | ||||||
| 	output.PrintBlock("DNS Request Summary", generateDNSQuestionSummary()) | 	output.PrintBlock("DNS Request Summary", p.generateDNSQuestionSummary()) | ||||||
| 	output.PrintBlock("DNS Response Summary", generateDNSAnswerSummary()) | 	output.PrintBlock("DNS Response Summary", p.generateDNSAnswerSummary()) | ||||||
| } | } | ||||||
| @ -17,7 +17,7 @@ var ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Called on every DNS packet to process questions | // Called on every DNS packet to process questions | ||||||
| func processDNSQuestion(questions []layers.DNSQuestion) { | func (p *Protocol) processDNSQuestion(questions []layers.DNSQuestion) { | ||||||
| 	// Iterate over all questions | 	// Iterate over all questions | ||||||
| 	for _, question := range questions { | 	for _, question := range questions { | ||||||
| 		// Raise stats | 		// Raise stats | ||||||
| @ -34,7 +34,7 @@ func processDNSQuestion(questions []layers.DNSQuestion) { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Process type questions | 		// Process type questions | ||||||
| 		processType(questionType, question.Type) | 		p.processType(questionType, question.Type) | ||||||
| 
 | 
 | ||||||
| 		// Append full domain and base domain | 		// Append full domain and base domain | ||||||
| 		questionDomains = common.AppendIfUnique(name, questionDomains) | 		questionDomains = common.AppendIfUnique(name, questionDomains) | ||||||
| @ -52,12 +52,12 @@ func processDNSQuestion(questions []layers.DNSQuestion) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generates a summary of all DNS questions | // Generates a summary of all DNS questions | ||||||
| func generateDNSQuestionSummary() string { | func (p *Protocol) generateDNSQuestionSummary() string { | ||||||
| 	summary := "" | 	summary := "" | ||||||
| 
 | 
 | ||||||
| 	// Overall question stats | 	// Overall question stats | ||||||
| 	summary = fmt.Sprintf("%s%d DNS questions in total\n", summary, numQuestions) | 	summary = fmt.Sprintf("%s%d DNS questions in total\n", summary, numQuestions) | ||||||
| 	summary = fmt.Sprintf("%s%s records\n", summary, generateDNSTypeSummary(questionType)) | 	summary = fmt.Sprintf("%s%s records\n", summary, p.generateDNSTypeSummary(questionType)) | ||||||
| 	summary = fmt.Sprintf("%s%d unique domains of %d base domains, of which are %d private (non-ICANN) TLDs.\n", summary, len(questionDomains), len(questionBaseDomains), len(questionPrivateDomains)) | 	summary = fmt.Sprintf("%s%d unique domains of %d base domains, of which are %d private (non-ICANN) TLDs.\n", summary, len(questionDomains), len(questionBaseDomains), len(questionPrivateDomains)) | ||||||
| 
 | 
 | ||||||
| 	// Output base domains asked for | 	// Output base domains asked for | ||||||
							
								
								
									
										15
									
								
								protocol/index.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								protocol/index.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | package protocol | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"git.darknebu.la/maride/pancap/protocol/arp" | ||||||
|  | 	"git.darknebu.la/maride/pancap/protocol/dhcpv4" | ||||||
|  | 	"git.darknebu.la/maride/pancap/protocol/dns" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	Protocols = []Protocol{ | ||||||
|  | 		&arp.Protocol{}, | ||||||
|  | 		&dhcpv4.Protocol{}, | ||||||
|  | 		&dns.Protocol{}, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
							
								
								
									
										9
									
								
								protocol/protocol.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								protocol/protocol.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | package protocol | ||||||
|  | 
 | ||||||
|  | import "github.com/google/gopacket" | ||||||
|  | 
 | ||||||
|  | type Protocol interface { | ||||||
|  | 	CanAnalyze(gopacket.Packet) bool | ||||||
|  | 	Analyze(gopacket.Packet) error | ||||||
|  | 	PrintSummary() | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user