From 75f1c4b0ec991cd365a5794146f20edba02feb6a Mon Sep 17 00:00:00 2001 From: Volodymyr Komarov Date: Sun, 8 Mar 2020 17:27:38 +0200 Subject: [PATCH] Implement host scanning --- .gitignore | 3 +- README.md | 63 +++++++++- go.mod | 8 ++ go.sum | 11 ++ hosts.go | 64 ++++++++++ main.go | 44 +++++++ ports.go | 13 ++ scanner/host/host.go | 23 ++++ scanner/host/scanner.go | 267 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hosts.go create mode 100644 main.go create mode 100644 ports.go create mode 100644 scanner/host/host.go create mode 100644 scanner/host/scanner.go diff --git a/.gitignore b/.gitignore index 66fd13c..9808852 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +.idea # Test binary, built with `go test -c` *.test @@ -12,4 +13,4 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ diff --git a/README.md b/README.md index e324015..6667160 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,63 @@ # netshark -Command line tool to scan local network and opened ports +[![Build Status](https://travis-ci.org/vokomarov/netshark.svg?branch=master)](https://travis-ci.org/vokomarov/netshark) +[![Coverage Status](https://coveralls.io/repos/github/vokomarov/netshark/badge.svg?branch=master)](https://coveralls.io/github/vokomarov/netshark?branch=master) +[![GoDoc](https://godoc.org/github.com/DimitarPetrov/stegify?status.svg)](https://godoc.org/github.com/DimitarPetrov/stegify) +[![Go Report Card](https://goreportcard.com/badge/github.com/vokomarov/netshark)](https://goreportcard.com/report/github.com/vokomarov/netshark) + +## Overview + +`netshark` is a simple command line tool to scan your local network to available hosts and his opened ports. + +Features: +- Scan local network neighbor hosts +- Display client MAC address + +Coming soon: +- Scan opened TCP ports +- Specify scanning port ranges +- Use predefined port ranges to scan +- Multithreaded scan +- Shadow scan +- Use STDIN as source of hosts to support Unix pipes + +## Installation + +#### Installing from Source + +``` +go get -u github.com/vokomarov/netshark +``` + +#### Download + +Download binary for your system [here](https://github.com/vokomarov/netshark/releases). + +## Usage + +### As a command line tool + +```shell script +# Scan local network for neighbor hosts +$ netshark scan hosts [--timeout 15] + +# Scan given host for opened ports of all port range +$ netshark scan ports --host 192.168.1.1 # scan specific + +# Specify TCP port range from lower to higher number of ports +$ netshark scan ports --range 1,65535 +$ netshark scan ports --range-min 1024 # check registered ports and private ports range +$ netshark scan ports --range-max 1023 # check only well-known port range + +# Or use predefined port ranges +$ netshark scan ports --range known # scan ports from 0 to 1023 +$ netshark scan ports --range reg # scan ports from 1024 to 49151 +$ netshark scan ports --range private # scan ports from 49152 to 65535 +``` + +### Programmatically in your code + +`netshark` can be used programmatically. You can visit [godoc](https://godoc.org/github.com/vokomarov/netshark) to check API documentation. + +## License + +`netshark` is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c56d465 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/vokomarov/netshark + +go 1.14 + +require ( + github.com/google/gopacket v1.1.17 + github.com/jessevdk/go-flags v1.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6efbf1 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 h1:1Fzlr8kkDLQwqMP8GxrhptBLqZG/EDpiATneiZHY998= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/hosts.go b/hosts.go new file mode 100644 index 0000000..f973197 --- /dev/null +++ b/hosts.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "time" + + "github.com/vokomarov/netshark/scanner/host" +) + +type HostsCommand struct { + Timeout int `short:"t" long:"timeout" default:"5" description:"Timeout in seconds to wait for ARP responses."` +} + +func (c *HostsCommand) Execute(_ []string) error { + fmt.Printf("Scanning Hosts..\n") + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + + scanner := host.NewScanner() + + go scanner.Scan() + + timeout := time.NewTicker(time.Duration(c.Timeout) * time.Second) + + func() { + for { + fmt.Printf("found %d hosts...\r", len(scanner.Hosts)) + + time.Sleep(1 * time.Second) + + select { + case <-timeout.C: + scanner.Stop() + return + case <-quit: + scanner.Stop() + return + case <-scanner.Done: + + return + default: + } + } + }() + + // Clear line + fmt.Printf("%c[2K\r", 27) + + if scanner.Error != nil { + fmt.Printf("\n\r") + return scanner.Error + } + + fmt.Printf("\nFound %d hosts: \n", len(scanner.Hosts)) + + for _, h := range scanner.Hosts { + fmt.Printf(" [IP: %s] \t [MAC: %s] \n", h.IP, h.MAC) + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ce78d8c --- /dev/null +++ b/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + + "github.com/jessevdk/go-flags" +) + +var parser *flags.Parser + +type scanCommand struct { + HostsCommand HostsCommand `command:"hosts" description:"Scan all available neighbor hosts of current local network"` + PortsCommand PortsCommand `command:"ports" description:"Scan open ports on a host"` +} + +func registerCommands(parser *flags.Parser) { + _, _ = parser.AddCommand( + "scan", + "Network scanner", + "Perform scanning over network", + &scanCommand{}, + ) +} + +func init() { + parser = flags.NewParser(nil, flags.HelpFlag|flags.PassDoubleDash) + registerCommands(parser) +} + +func main() { + if _, err := parser.Parse(); err != nil { + if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { + os.Exit(0) + return + } else { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + return + } + } + + os.Exit(0) +} diff --git a/ports.go b/ports.go new file mode 100644 index 0000000..bad95b9 --- /dev/null +++ b/ports.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" +) + +type PortsCommand struct { +} + +func (c *PortsCommand) Execute(_ []string) error { + fmt.Printf("Scanning Ports..\n") + return nil +} diff --git a/scanner/host/host.go b/scanner/host/host.go new file mode 100644 index 0000000..1b0d4c4 --- /dev/null +++ b/scanner/host/host.go @@ -0,0 +1,23 @@ +package host + +import ( + "crypto/md5" + "fmt" + "io" +) + +type Host struct { + id string + IP string + MAC string +} + +func (h *Host) ID() string { + if h.id == "" { + hash := md5.New() + _, _ = io.WriteString(hash, h.IP+h.MAC) + h.id = fmt.Sprintf("%x", hash.Sum(nil)) + } + + return h.id +} diff --git a/scanner/host/scanner.go b/scanner/host/scanner.go new file mode 100644 index 0000000..96c23ac --- /dev/null +++ b/scanner/host/scanner.go @@ -0,0 +1,267 @@ +package host + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "sync" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" +) + +type Scanner struct { + mu sync.RWMutex + unique map[string]bool + Hosts []*Host + stop chan struct{} + Done chan struct{} + Error error +} + +func NewScanner() *Scanner { + return &Scanner{ + mu: sync.RWMutex{}, + stop: make(chan struct{}), + unique: make(map[string]bool), + Hosts: make([]*Host, 0), + Done: make(chan struct{}), + } +} + +func (s *Scanner) Stop() { + s.stop <- struct{}{} + close(s.stop) + <-s.Done +} + +func (s *Scanner) finish(err error) { + if err != nil { + s.Error = err + } + + s.Done <- struct{}{} + close(s.Done) +} + +func (s *Scanner) HasHost(host *Host) bool { + s.mu.RLock() + defer s.mu.RUnlock() + + if _, ok := s.unique[host.ID()]; ok { + return true + } + + return false +} + +// Push new detected Host to the list of all detected +func (s *Scanner) AddHost(host *Host) *Scanner { + s.mu.Lock() + defer s.mu.Unlock() + + s.Hosts = append(s.Hosts, host) + s.unique[host.ID()] = true + + return s +} + +// Detect system interfaces and go over each one to detect IP addresses +// and read/write ARP packets +// Blocked until every interfaces unable to write packets or stop call +func (s *Scanner) Scan() { + interfaces, err := net.Interfaces() + if err != nil { + s.finish(err) + return + } + + var wg sync.WaitGroup + + for i := range interfaces { + wg.Add(1) + + go func(iface net.Interface) { + defer wg.Done() + + if err := s.scanInterface(&iface); err != nil { + s.finish(fmt.Errorf("interface [%v] error: %w", iface.Name, err)) + return + } + }(interfaces[i]) + } + + // Wait for all interfaces' scans to complete. They'll try to run + // forever, but will stop on an error, so if we get past this Wait + // it means all attempts to write have failed. + wg.Wait() +} + +// Scans an individual interface's local network for machines using ARP requests/replies. +// +// Loops forever, sending packets out regularly. It returns an error if +// it's ever unable to write a packet. +func (s *Scanner) scanInterface(iface *net.Interface) error { + // We just look for IPv4 addresses, so try to find if the interface has one. + var addr *net.IPNet + + if addresses, err := iface.Addrs(); err != nil { + return err + } else { + for _, a := range addresses { + if IPNet, ok := a.(*net.IPNet); ok { + if IPv4 := IPNet.IP.To4(); IPv4 != nil { + addr = &net.IPNet{ + IP: IPv4, + Mask: IPNet.Mask[len(IPNet.Mask)-4:], + } + + break + } + } + } + } + + // Sanity-check that the interface has a good address. + if addr == nil { + return nil + } else if addr.IP[0] == 127 { + return nil + } else if addr.Mask[0] != 0xff || addr.Mask[1] != 0xff { + return nil + } + + // Open up a pcap handle for packet reads/writes. + handle, err := pcap.OpenLive(iface.Name, 65536, true, pcap.BlockForever) + if err != nil { + return err + } + defer handle.Close() + + // Start up a goroutine to read in packet data. + go s.listenARP(handle, iface) + + for { + // Write our scan packets out to the handle. + if err := writeARP(handle, iface, addr); err != nil { + return fmt.Errorf("error writing packets: %w", err) + } + + // We don't know exactly how long it'll take for packets to be + // sent back to us, but 10 seconds should be more than enough + // time ;) + time.Sleep(10 * time.Second) + } +} + +// Watches a handle for incoming ARP responses we might care about. +// Push new Host once any correct response received +// Work until 'stop' is closed. +func (s *Scanner) listenARP(handle *pcap.Handle, iface *net.Interface) { + src := gopacket.NewPacketSource(handle, layers.LayerTypeEthernet) + in := src.Packets() + + for { + var packet gopacket.Packet + + select { + case <-s.stop: + s.finish(nil) + return + case packet = <-in: + arpLayer := packet.Layer(layers.LayerTypeARP) + + if arpLayer == nil { + continue + } + + arp := arpLayer.(*layers.ARP) + + if arp.Operation != layers.ARPReply || bytes.Equal([]byte(iface.HardwareAddr), arp.SourceHwAddress) { + // This is a packet I sent. + continue + } + + // Note: we might get some packets here that aren't responses to ones we've sent, + // if for example someone else sends US an ARP request. Doesn't much matter, though... + // all information is good information :) + host := Host{ + IP: fmt.Sprintf("%v", net.IP(arp.SourceProtAddress)), + MAC: fmt.Sprintf("%v", net.HardwareAddr(arp.SourceHwAddress)), + } + + if !s.HasHost(&host) { + s.AddHost(&host) + } + } + } +} + +// writeARP writes an ARP request for each address on our local network to the +// pcap handle. +func writeARP(handle *pcap.Handle, iface *net.Interface, addr *net.IPNet) error { + // Set up all the layers' fields we can. + + eth := layers.Ethernet{ + SrcMAC: iface.HardwareAddr, + DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + EthernetType: layers.EthernetTypeARP, + } + + arp := layers.ARP{ + AddrType: layers.LinkTypeEthernet, + Protocol: layers.EthernetTypeIPv4, + HwAddressSize: 6, + ProtAddressSize: 4, + Operation: layers.ARPRequest, + SourceHwAddress: []byte(iface.HardwareAddr), + SourceProtAddress: []byte(addr.IP), + DstHwAddress: []byte{0, 0, 0, 0, 0, 0}, + } + + // Set up buffer and options for serialization. + buf := gopacket.NewSerializeBuffer() + + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + // Send one packet for every address. + for _, ip := range ips(addr) { + arp.DstProtAddress = ip + + if err := gopacket.SerializeLayers(buf, opts, ð, &arp); err != nil { + return err + } + + if err := handle.WritePacketData(buf.Bytes()); err != nil { + return err + } + } + + return nil +} + +// ips is a simple and not very good method for getting all IPv4 addresses from a +// net.IPNet. It returns all IPs it can over the channel it sends back, closing +// the channel when done. +func ips(n *net.IPNet) (out []net.IP) { + num := binary.BigEndian.Uint32([]byte(n.IP)) + mask := binary.BigEndian.Uint32([]byte(n.Mask)) + num &= mask + + for mask < 0xffffffff { + var buf [4]byte + + binary.BigEndian.PutUint32(buf[:], num) + out = append(out, net.IP(buf[:])) + mask++ + num++ + } + + return +}