diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..69ca7db --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,8 @@ +service: + analyzed-paths: + - scanner/... + golangci-lint-version: 1.23.x + prepare: + - apt-get update && apt-get install -y libpcap-dev + suggested-changes: + disabled: true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ae01790 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ + +.PHONY: test + +test: + go test -v ./... diff --git a/hosts.go b/hosts.go index f973197..bb37293 100644 --- a/hosts.go +++ b/hosts.go @@ -9,11 +9,12 @@ import ( "github.com/vokomarov/netshark/scanner/host" ) -type HostsCommand struct { +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 { +// Execute will run the command +func (c *hostsCommand) Execute(_ []string) error { fmt.Printf("Scanning Hosts..\n") quit := make(chan os.Signal, 1) diff --git a/main.go b/main.go index ce78d8c..f0fd881 100644 --- a/main.go +++ b/main.go @@ -10,8 +10,8 @@ import ( 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"` + 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) { @@ -33,11 +33,11 @@ func main() { 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 } + + fmt.Printf("Error: %v\n", err) + os.Exit(1) + return } os.Exit(0) diff --git a/ports.go b/ports.go index bad95b9..b8f53a7 100644 --- a/ports.go +++ b/ports.go @@ -4,10 +4,11 @@ import ( "fmt" ) -type PortsCommand struct { +type portsCommand struct { } -func (c *PortsCommand) Execute(_ []string) error { +// Execute will run the command +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 index 1b0d4c4..85b800e 100644 --- a/scanner/host/host.go +++ b/scanner/host/host.go @@ -6,12 +6,15 @@ import ( "io" ) +// Host struct host information about discovered network client type Host struct { id string IP string MAC string } +// ID will generate unique MD5 hash of host by his properties +// and cache generated hash for future usage func (h *Host) ID() string { if h.id == "" { hash := md5.New() diff --git a/scanner/host/scanner.go b/scanner/host/scanner.go index 96c23ac..da4671c 100644 --- a/scanner/host/scanner.go +++ b/scanner/host/scanner.go @@ -13,29 +13,50 @@ import ( "github.com/google/gopacket/pcap" ) +// Scanner provide container for control local network scanning +// process and checking results type Scanner struct { - mu sync.RWMutex - unique map[string]bool - Hosts []*Host - stop chan struct{} - Done chan struct{} - Error error + mu sync.RWMutex + unique map[string]bool + Hosts []*Host + started bool + stopped bool + stop chan struct{} + Done chan struct{} + Error error } +// NewScanner will initialise new instance of Scanner 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{}), + mu: sync.RWMutex{}, + started: false, + stopped: false, + stop: make(chan struct{}), + unique: make(map[string]bool), + Hosts: make([]*Host, 0), + Done: make(chan struct{}), } } +// Stop perform manually stopping of scan process with blocking +// until stopping is not finished in case of scanning already started +// Safe to call before or after scanning started or stopped func (s *Scanner) Stop() { - s.stop <- struct{}{} - close(s.stop) - <-s.Done + if s.started { + s.stop <- struct{}{} + <-s.Done + } + + if !s.stopped { + close(s.stop) + + if !s.started { + close(s.Done) + } + } + + s.stopped = true } func (s *Scanner) finish(err error) { @@ -43,11 +64,13 @@ func (s *Scanner) finish(err error) { s.Error = err } - s.Done <- struct{}{} - close(s.Done) + if s.started && !s.stopped { + s.Done <- struct{}{} + close(s.Done) + } } -func (s *Scanner) HasHost(host *Host) bool { +func (s *Scanner) hasHost(host *Host) bool { s.mu.RLock() defer s.mu.RUnlock() @@ -58,8 +81,7 @@ func (s *Scanner) HasHost(host *Host) bool { return false } -// Push new detected Host to the list of all detected -func (s *Scanner) AddHost(host *Host) *Scanner { +func (s *Scanner) addHost(host *Host) *Scanner { s.mu.Lock() defer s.mu.Unlock() @@ -69,9 +91,10 @@ func (s *Scanner) AddHost(host *Host) *Scanner { return s } -// Detect system interfaces and go over each one to detect IP addresses -// and read/write ARP packets +// Scan will detect system interfaces and go over each one to detect +// IP addresses to read/write ARP packets // Blocked until every interfaces unable to write packets or stop call +// so typically should be run as a goroutine func (s *Scanner) Scan() { interfaces, err := net.Interfaces() if err != nil { @@ -161,6 +184,12 @@ func (s *Scanner) scanInterface(iface *net.Interface) error { // Push new Host once any correct response received // Work until 'stop' is closed. func (s *Scanner) listenARP(handle *pcap.Handle, iface *net.Interface) { + s.mu.Lock() + if !s.started { + s.started = true + } + s.mu.Unlock() + src := gopacket.NewPacketSource(handle, layers.LayerTypeEthernet) in := src.Packets() @@ -193,8 +222,8 @@ func (s *Scanner) listenARP(handle *pcap.Handle, iface *net.Interface) { MAC: fmt.Sprintf("%v", net.HardwareAddr(arp.SourceHwAddress)), } - if !s.HasHost(&host) { - s.AddHost(&host) + if !s.hasHost(&host) { + s.addHost(&host) } } } diff --git a/scanner/host/scanner_test.go b/scanner/host/scanner_test.go new file mode 100644 index 0000000..6a1efd8 --- /dev/null +++ b/scanner/host/scanner_test.go @@ -0,0 +1,184 @@ +package host + +import ( + "testing" + "time" +) + +func TestNewScanner(t *testing.T) { + scanner := NewScanner() + if scanner == nil { + t.Errorf("Scanner instance is empty") + return + } + + if scanner.unique == nil { + t.Errorf("Unique host registry is not initialised") + } + + if scanner.started == true { + t.Errorf("Scanner has wrong started flag") + } + + if scanner.stopped == true { + t.Errorf("Scanner has wrong stopped flag") + } + + if scanner.Hosts == nil { + t.Errorf("Host storage is not initialised") + } + + if scanner.Done == nil { + t.Errorf("Done channel is not created") + } + + if scanner.stop == nil { + t.Errorf("Stop channel is not created") + } +} + +func TestScanner_StopEmpty(t *testing.T) { + scanner := NewScanner() + if scanner == nil { + t.Errorf("Scanner instance is empty") + return + } + + scanner.Stop() + + select { + case <-scanner.stop: + default: + t.Errorf("stop channel must be closed once Stop method called") + } +} + +func TestScanner_StopStartedStopped(t *testing.T) { + scanner := NewScanner() + if scanner == nil { + t.Errorf("Scanner instance is empty") + return + } + + go scanner.Scan() + scanner.Stop() + + select { + case <-scanner.stop: + default: + t.Errorf("stop channel must be closed once Stop method called") + } + + select { + case <-scanner.Done: + default: + t.Errorf("done channel must be closed once Stop method finished") + } + + scanner.Stop() + + select { + case <-scanner.stop: + default: + t.Errorf("stop channel must be closed once Stop method called") + } + + select { + case <-scanner.Done: + default: + t.Errorf("done channel must be closed once Stop method finished") + } +} + +func TestScanner_StopWorking(t *testing.T) { + scanner := NewScanner() + if scanner == nil { + t.Errorf("Scanner instance is empty") + return + } + + // simulate fake scanner + go func(s *Scanner) { + s.started = true + <-s.stop + s.finish(nil) + }(scanner) + + time.Sleep(1 * time.Millisecond) + + scanner.Stop() + + select { + case <-scanner.stop: + default: + t.Errorf("stop channel must be closed once Stop method called") + } + + select { + case <-scanner.Done: + default: + t.Errorf("done channel must be closed once Stop method finished") + } +} + +func TestScanner_AddHost(t *testing.T) { + scanner := NewScanner() + if scanner == nil { + t.Errorf("Scanner instance is empty") + return + } + + host := Host{ + IP: "127.0.0.1", + MAC: "ff:ff:ff:ff:ff:ff", + } + + scanner.addHost(&host) + + if len(scanner.Hosts) != 1 { + t.Errorf("Host is not added") + } + + if host.ID() != scanner.Hosts[0].ID() { + t.Errorf("Host is added but changed") + } + + if host.IP != scanner.Hosts[0].IP { + t.Errorf("Host is added but changed IP") + } + + if host.MAC != scanner.Hosts[0].MAC { + t.Errorf("Host is added but changed MAC") + } + + if len(scanner.unique) != 1 { + t.Errorf("Host is not registered to unique registry") + } + + if _, ok := scanner.unique[host.ID()]; !ok { + t.Errorf("Host is not registered to unique registry") + } +} + +func TestScanner_HasHost(t *testing.T) { + scanner := NewScanner() + if scanner == nil { + t.Errorf("Scanner instance is empty") + return + } + + host := Host{ + IP: "127.0.0.1", + MAC: "ff:ff:ff:ff:ff:ff", + } + + if scanner.hasHost(&host) { + t.Errorf("Host is wrongly detected as already added") + } + + scanner.addHost(&host) + + if !scanner.hasHost(&host) { + t.Errorf("Host is not registered as already addded") + } +}