From 65136daa41be24825eaa70fcfdf725fe37949df4 Mon Sep 17 00:00:00 2001 From: Rob Gonnella Date: Tue, 26 Dec 2023 01:23:42 -0500 Subject: [PATCH] Adds option to make timing configurable --- README.md | 38 ++++++++++++- internal/cli/root.go | 110 ++++++++++++++++++++++++++++++++++++ internal/cli/root_test.go | 39 ++++++++++++- mock/scanner/scanner.go | 12 ++++ pkg/scanner/arpscan.go | 10 +++- pkg/scanner/fullscan.go | 5 ++ pkg/scanner/options.go | 11 +++- pkg/scanner/options_test.go | 3 + pkg/scanner/synscan.go | 12 +++- pkg/scanner/types.go | 1 + 10 files changed, 231 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7b37008..eb7cf5e 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,13 @@ sudo go-lanscan --no-progress --json # run only arp scanning (skip syn scanning) sudo go-lanscan --arp-only + +# set timing - this is how fast packets are sent to hosts +# default is 100µs between packets +# the faster you send packets (shorter the timing), the less accurate the results will be +sudo go-lanscan --timing 1ms # set to 1 millisecond +sudo go-lanscan --timing 500µs # set to 500 microseconds +sudo go-lanscan --timing 500us # alternate symbol for microseconds ``` ## Package Usage @@ -84,6 +91,31 @@ First you must install the following dependencies You can provide the following options to all scanners +- Provide specific timing duration + +This option is used to set a specific time to wait between sending packets +to hosts. The default is 100µs. The shorter the timing, the faster packets +will be sent, and the less accurate your results will be + +```go + timing := time.Microsecond * 200 + + fullScanner := scanner.NewFullScanner( + netInfo, + targets, + ports, + listenPort, + scanner.WithTiming(timing), + ) + + // or + fullScanner.SetTiming(timing) + + // or + option := scanner.WithTiming(timing) + option(fullScanner) +``` + - Provide channel for notifications when packet requests are sent to target ```go @@ -100,11 +132,11 @@ You can provide the following options to all scanners ) // or - option := scanner.WithRequestNotifications(requests) - option(requests) + synScanner.SetRequestNotifications(requests) // or - synScanner.SetRequestNotifications(requests) + option := scanner.WithRequestNotifications(requests) + option(synScanner) ``` - Provide your own idle timeout. If no packets are received from our targets diff --git a/internal/cli/root.go b/internal/cli/root.go index 273fe50..7a7d52e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -3,18 +3,98 @@ package cli import ( + "fmt" + "os" "strings" "time" + "github.com/jedib0t/go-pretty/table" "github.com/spf13/cobra" "github.com/robgonnella/go-lanscan/internal/core" + "github.com/robgonnella/go-lanscan/internal/logger" "github.com/robgonnella/go-lanscan/internal/util" "github.com/robgonnella/go-lanscan/pkg/network" "github.com/robgonnella/go-lanscan/pkg/oui" "github.com/robgonnella/go-lanscan/pkg/scanner" ) +func printConfiguration( + coreScanner scanner.Scanner, + targets []string, + cidr string, + ports []string, + ifaceName string, + listenPort uint16, + timing string, + vendorInfo, + printJson, + arpOnly, + progress bool, + outFile string, +) { + var configTable = table.NewWriter() + + configTable.SetOutputMirror(os.Stdout) + + configTable.AppendRow(table.Row{ + "scannerType", + fmt.Sprintf("%T", coreScanner), + }) + + configTable.AppendRow(table.Row{ + "targets", + targets, + }) + + configTable.AppendRow(table.Row{ + "cidr", + cidr, + }) + + configTable.AppendRow(table.Row{ + "ports", + ports, + }) + + configTable.AppendRow(table.Row{ + "interface", + ifaceName, + }) + + configTable.AppendRow(table.Row{ + "listenPort", + listenPort, + }) + + configTable.AppendRow(table.Row{ + "timing", + timing, + }) + + configTable.AppendRow(table.Row{ + "vendorInfo", + vendorInfo, + }) + + configTable.AppendRow(table.Row{ + "json", + printJson, + }) + + configTable.AppendRow(table.Row{ + "arpOnly", + arpOnly, + }) + + configTable.AppendRow(table.Row{ + "progress", + progress, + }) + + go configTable.Render() +} + func Root( runner core.Runner, userNet network.Network, @@ -23,6 +103,7 @@ func Root( var printJson bool var noProgress bool var ports string + var timing string var idleTimeoutSeconds int var listenPort uint16 var ifaceName string @@ -36,6 +117,8 @@ func Root( Short: "Scan your LAN!", Long: `CLI to scan your Local Area Network`, RunE: func(cmd *cobra.Command, args []string) error { + log := logger.New() + portList := strings.Split(ports, ",") if ifaceName != userNet.Interface().Name { @@ -74,6 +157,15 @@ func Root( coreScanner.IncludeVendorInfo(vendorRepo) } + timingDuration, err := time.ParseDuration(timing) + + if err != nil { + log.Error().Err(err).Msg("invalid timing value") + return err + } + + coreScanner.SetTiming(timingDuration) + portLen := util.PortTotal(portList) targetLen := util.TotalTargets(targets) @@ -92,10 +184,28 @@ func Root( outFile, ) + if !noProgress { + printConfiguration( + coreScanner, + targets, + userNet.Cidr(), + portList, + userNet.Interface().Name, + listenPort, + timing, + vendorInfo, + printJson, + arpOnly, + !noProgress, + outFile, + ) + } + return runner.Run() }, } + cmd.Flags().StringVar(&timing, "timing", "100µs", "set time between packet sends - the faster you send the less accurate the result will be") cmd.Flags().BoolVar(&printJson, "json", false, "output json instead of table text") cmd.Flags().BoolVar(&arpOnly, "arp-only", false, "only perform arp scanning (skip syn scanning)") cmd.Flags().BoolVar(&noProgress, "no-progress", false, "disable all output except for final results") diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index e5a047c..d03c8cf 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -18,7 +18,7 @@ func TestRootCommand(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - t.Run("initializes and runs full scanner", func(st *testing.T) { + t.Run("returns error if invalid timing duration is supplied", func(st *testing.T) { mockNetwork := mock_network.NewMockNetwork(ctrl) mockRunner := mock_core.NewMockRunner(ctrl) @@ -40,6 +40,41 @@ func TestRootCommand(t *testing.T) { mockNetwork.EXPECT().IPNet().AnyTimes().Return(mockIPNet) + cmd, err := cli.Root(mockRunner, mockNetwork, mockVendor) + + assert.NoError(st, err) + + cmd.SetArgs([]string{"--timing", "nope"}) + + err = cmd.Execute() + + assert.Error(st, err) + }) + + t.Run("initializes and runs full scanner and includes vendor info", func(st *testing.T) { + mockNetwork := mock_network.NewMockNetwork(ctrl) + + mockRunner := mock_core.NewMockRunner(ctrl) + + mockVendor := mock_oui.NewMockVendorRepo(ctrl) + + mockMAC, _ := net.ParseMAC("00:00:00:00:00:00") + + mockCidr := "172.17.1.1/32" + + _, mockIPNet, _ := net.ParseCIDR(mockCidr) + + mockNetwork.EXPECT().Interface().AnyTimes().Return(&net.Interface{ + Name: "test-interface", + HardwareAddr: mockMAC, + }) + + mockNetwork.EXPECT().Cidr().AnyTimes().Return(mockCidr) + + mockNetwork.EXPECT().IPNet().AnyTimes().Return(mockIPNet) + + mockVendor.EXPECT().UpdateVendors().Times(1) + mockRunner.EXPECT().Initialize( gomock.Any(), 1, @@ -56,7 +91,7 @@ func TestRootCommand(t *testing.T) { assert.NoError(st, err) - cmd.SetArgs([]string{}) + cmd.SetArgs([]string{"--vendor"}) err = cmd.Execute() diff --git a/mock/scanner/scanner.go b/mock/scanner/scanner.go index dee3c27..88061f7 100644 --- a/mock/scanner/scanner.go +++ b/mock/scanner/scanner.go @@ -117,6 +117,18 @@ func (mr *MockScannerMockRecorder) SetRequestNotifications(arg0 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestNotifications", reflect.TypeOf((*MockScanner)(nil).SetRequestNotifications), arg0) } +// SetTiming mocks base method. +func (m *MockScanner) SetTiming(arg0 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetTiming", arg0) +} + +// SetTiming indicates an expected call of SetTiming. +func (mr *MockScannerMockRecorder) SetTiming(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTiming", reflect.TypeOf((*MockScanner)(nil).SetTiming), arg0) +} + // Stop mocks base method. func (m *MockScanner) Stop() { m.ctrl.T.Helper() diff --git a/pkg/scanner/arpscan.go b/pkg/scanner/arpscan.go index 43fa2d8..e03f94b 100644 --- a/pkg/scanner/arpscan.go +++ b/pkg/scanner/arpscan.go @@ -30,6 +30,7 @@ type ArpScanner struct { requestNotifier chan *Request scanning bool lastPacketSentAt time.Time + timing time.Duration idleTimeout time.Duration vendorRepo oui.VendorRepo scanningMux *sync.RWMutex @@ -51,7 +52,8 @@ func NewArpScanner( cap: &defaultPacketCapture{}, networkInfo: networkInfo, resultChan: make(chan *ScanResult), - idleTimeout: time.Second * 5, + timing: defaultTiming, + idleTimeout: defaultIdleTimeout, scanning: false, lastPacketSentAt: time.Time{}, scanningMux: &sync.RWMutex{}, @@ -108,7 +110,7 @@ func (s *ArpScanner) Scan() error { go s.readPackets() - limiter := time.NewTicker(defaultAccuracy) + limiter := time.NewTicker(s.timing) defer limiter.Stop() if len(s.targets) == 0 { @@ -142,6 +144,10 @@ func (s *ArpScanner) Stop() { } } +func (s *ArpScanner) SetTiming(d time.Duration) { + s.timing = d +} + func (s *ArpScanner) SetRequestNotifications(c chan *Request) { s.requestNotifier = c } diff --git a/pkg/scanner/fullscan.go b/pkg/scanner/fullscan.go index caaaa1a..47b3195 100644 --- a/pkg/scanner/fullscan.go +++ b/pkg/scanner/fullscan.go @@ -148,6 +148,11 @@ func (s *FullScanner) Stop() { s.debug.Info().Msg("all scanners stopped") } +func (s *FullScanner) SetTiming(d time.Duration) { + s.arpScanner.SetTiming(d) + s.synScanner.SetTiming(d) +} + func (s *FullScanner) SetRequestNotifications(c chan *Request) { s.arpScanner.SetRequestNotifications(c) s.synScanner.SetRequestNotifications(c) diff --git a/pkg/scanner/options.go b/pkg/scanner/options.go index 94c2710..65012c0 100644 --- a/pkg/scanner/options.go +++ b/pkg/scanner/options.go @@ -11,7 +11,10 @@ import ( // How long to wait before sending next packet // the faster you send packets the more packets // will be missed when reading -const defaultAccuracy = time.Microsecond * 100 +const defaultTiming = time.Microsecond * 100 + +// If no packets are received for this period of time, exit with timeout +const defaultIdleTimeout = time.Second * 5 type ScannerOption = func(s Scanner) @@ -38,3 +41,9 @@ func WithPacketCapture(cap PacketCapture) ScannerOption { s.SetPacketCapture(cap) } } + +func WithTiming(duration time.Duration) ScannerOption { + return func(s Scanner) { + s.SetTiming(duration) + } +} diff --git a/pkg/scanner/options_test.go b/pkg/scanner/options_test.go index 0fbf11f..3003e32 100644 --- a/pkg/scanner/options_test.go +++ b/pkg/scanner/options_test.go @@ -30,6 +30,7 @@ func TestOptions(t *testing.T) { scanner.NewArpScanner( []string{}, netInfo, + scanner.WithTiming(time.Millisecond), scanner.WithIdleTimeout(time.Second*5), scanner.WithPacketCapture(testPacketCapture), scanner.WithRequestNotifications(requestNotifier), @@ -41,6 +42,7 @@ func TestOptions(t *testing.T) { []string{}, []string{}, 54321, + scanner.WithTiming(time.Millisecond), scanner.WithIdleTimeout(time.Second*5), scanner.WithPacketCapture(testPacketCapture), scanner.WithRequestNotifications(requestNotifier), @@ -52,6 +54,7 @@ func TestOptions(t *testing.T) { netInfo, []string{}, 54321, + scanner.WithTiming(time.Millisecond), scanner.WithIdleTimeout(time.Second*5), scanner.WithPacketCapture(testPacketCapture), scanner.WithRequestNotifications(requestNotifier), diff --git a/pkg/scanner/synscan.go b/pkg/scanner/synscan.go index 757a308..0537dd5 100644 --- a/pkg/scanner/synscan.go +++ b/pkg/scanner/synscan.go @@ -37,6 +37,7 @@ type SynScanner struct { requestNotifier chan *Request scanning bool lastPacketSentAt time.Time + timing time.Duration idleTimeout time.Duration scanningMux *sync.RWMutex packetSentAtMux *sync.RWMutex @@ -62,7 +63,8 @@ func NewSynScanner( ports: ports, listenPort: listenPort, resultChan: make(chan *ScanResult), - idleTimeout: time.Second * 5, + timing: defaultTiming, + idleTimeout: defaultIdleTimeout, scanning: false, lastPacketSentAt: time.Time{}, scanningMux: &sync.RWMutex{}, @@ -135,7 +137,7 @@ func (s *SynScanner) Scan() error { go s.readPackets() - limiter := time.NewTicker(defaultAccuracy) + limiter := time.NewTicker(s.timing) defer limiter.Stop() for _, target := range s.targets { @@ -170,6 +172,10 @@ func (s *SynScanner) Stop() { } } +func (s *SynScanner) SetTiming(d time.Duration) { + s.timing = d +} + func (s *SynScanner) SetRequestNotifications(c chan *Request) { s.requestNotifier = c } @@ -210,12 +216,14 @@ func (s *SynScanner) readPackets() { if err != nil { s.debug.Error().Err(err).Msg("syn: read packet error") + continue } err = parser.DecodeLayers(packetData, &decoded) if err != nil { s.debug.Error().Err(err).Msg("syn: decode packet error") + continue } synPacket := &SynPacket{} diff --git a/pkg/scanner/types.go b/pkg/scanner/types.go index a344ed0..b91f186 100644 --- a/pkg/scanner/types.go +++ b/pkg/scanner/types.go @@ -30,6 +30,7 @@ type Scanner interface { Scan() error Stop() Results() chan *ScanResult + SetTiming(d time.Duration) SetRequestNotifications(c chan *Request) SetIdleTimeout(d time.Duration) IncludeVendorInfo(repo oui.VendorRepo)