Skip to content

Commit

Permalink
Add ARP scan
Browse files Browse the repository at this point in the history
  • Loading branch information
v-byte-cpu committed Mar 9, 2021
1 parent 21483d5 commit f853373
Show file tree
Hide file tree
Showing 34 changed files with 3,098 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sx
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# sx

## Purpose

The goal of this project is to create the fastest network scanner with clean and simple code.

Right now, only ARP scan is supported.

## Building

From the root of the source tree, run:

```
go build
```

## Using

```
./sx help
```
145 changes: 145 additions & 0 deletions command/arp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package command

import (
"bufio"
"context"
"errors"
"net"
"os"
"os/signal"
"strings"
"sync"
"time"

"github.com/spf13/cobra"
"github.com/v-byte-cpu/sx/pkg/ip"
"github.com/v-byte-cpu/sx/pkg/packet/afpacket"
"github.com/v-byte-cpu/sx/pkg/scan"
"github.com/v-byte-cpu/sx/pkg/scan/arp"
"go.uber.org/zap"
)

var errSrcIP = errors.New("invalid source IP")

var interfaceFlag string
var srcIPFlag string
var srcMACFlag string

func init() {
arpCmd.Flags().StringVarP(&interfaceFlag, "iface", "i", "", "set interface to send/receive packets")
arpCmd.Flags().StringVar(&srcIPFlag, "srcip", "", "set source IP address for generated packets")
arpCmd.Flags().StringVar(&srcMACFlag, "srcmac", "", "set source MAC address for generated packets")
rootCmd.AddCommand(arpCmd)
}

var arpCmd = &cobra.Command{
Use: "arp [flags] subnet",
Example: strings.Join([]string{"arp 192.168.0.1/24", "arp 10.0.0.1"}, "\n"),
Short: "Perform ARP scan",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("requires one ip subnet argument")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) (err error) {
dstSubnet, err := ip.ParseIPNet(args[0])
if err != nil {
return err
}

var iface *net.Interface
var srcIP net.IP

if len(interfaceFlag) > 0 {
if iface, err = net.InterfaceByName(interfaceFlag); err != nil {
return err
}
} else {
if iface, srcIP, err = ip.GetSubnetInterface(dstSubnet); err != nil {
return err
}
}

if len(srcIPFlag) > 0 {
if srcIP = net.ParseIP(srcIPFlag); srcIP == nil {
return errSrcIP
}
}

srcMAC := iface.HardwareAddr
if len(srcMACFlag) > 0 {
if srcMAC, err = net.ParseMAC(srcMACFlag); err != nil {
return err
}
}

r := &scan.Range{Subnet: dstSubnet, Interface: iface, SrcIP: srcIP, SrcMAC: srcMAC}
return startEngine(r)
},
}

func startEngine(r *scan.Range) error {
bw := bufio.NewWriter(os.Stdout)
defer bw.Flush()
logger, err := zap.NewProduction()
if err != nil {
return err
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

// setup network interface to read/write packets
rw, err := afpacket.NewPacketSource(r.Interface.Name)
if err != nil {
return err
}
defer rw.Close()
err = rw.SetBPFFilter(arp.BPFFilter(r))
if err != nil {
return err
}

m := arp.NewScanMethod(ctx)

// setup result logging
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for result := range m.Results() {
// TODO extract it
if jsonFlag {
data, err := result.MarshalJSON()
if err != nil {
logger.Error("arp", zap.Error(err))
}
bw.Write(data)
} else {
bw.WriteString(result.String())
}
bw.WriteByte('\n')
}
}()

// start scan
engine := scan.SetupEngine(rw, m)
done, errc := engine.Start(ctx, r)
go func() {
defer cancel()
<-done
<-time.After(300 * time.Millisecond)
}()

// error logging
wg.Add(1)
go func() {
defer wg.Done()
for err := range errc {
logger.Error("arp", zap.Error(err))
}
}()
wg.Wait()
return nil
}
25 changes: 25 additions & 0 deletions command/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package command

import (
"os"

"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "sx",
Short: "Fast, modern, easy-to-use network scanner",
Version: "0.1.0",
}

var jsonFlag bool

func init() {
rootCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "enable JSON output")
}

func Main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/v-byte-cpu/sx

go 1.15

require (
github.com/golang/mock v1.5.0
github.com/google/gopacket v1.1.20-0.20210304165259-20562ffb40f8
github.com/google/wire v0.5.0
github.com/mailru/easyjson v0.7.7
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
)
339 changes: 339 additions & 0 deletions go.sum

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/v-byte-cpu/sx/command"

func main() {
command.Main()
}
59 changes: 59 additions & 0 deletions pkg/ip/ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ip

import (
"errors"
"net"
)

var (
ErrInvalidAddr = errors.New("invalid IP subnet/host")
ErrSubnetInterface = errors.New("no directly connected interfaces to destination subnet")
)

func Inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}

func DupIP(ip net.IP) net.IP {
dup := make([]byte, 4)
copy(dup, ip.To4())
return dup
}

func ParseIPNet(subnet string) (*net.IPNet, error) {
_, result, err := net.ParseCIDR(subnet)
if err == nil {
return result, err
}
// try to parse host IP address instead
ipAddr := net.ParseIP(subnet)
if ipAddr == nil {
return nil, ErrInvalidAddr
}
return &net.IPNet{IP: ipAddr.To4(), Mask: net.CIDRMask(32, 32)}, nil
}

func GetSubnetInterface(dstSubnet *net.IPNet) (*net.Interface, net.IP, error) {
dstSubnetIP := dstSubnet.IP.Mask(dstSubnet.Mask)
ifaces, err := net.Interfaces()
if err != nil {
return nil, nil, err
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return nil, nil, err
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.Contains(dstSubnetIP) {
return &iface, ipnet.IP.To4(), nil
}
}
}
return nil, nil, ErrSubnetInterface
}
99 changes: 99 additions & 0 deletions pkg/ip/ip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package ip

import (
"net"
"testing"

"github.com/stretchr/testify/assert"
)

func TestInc(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input net.IP
expected net.IP
}{
{
name: "ZeroNet",
input: net.IPv4(0, 0, 0, 0),
expected: net.IPv4(0, 0, 0, 1),
},
{
name: "Inc3rd",
input: net.IPv4(1, 1, 0, 255),
expected: net.IPv4(1, 1, 1, 0),
},
{
name: "Inc2nd",
input: net.IPv4(1, 1, 255, 255),
expected: net.IPv4(1, 2, 0, 0),
},
{
name: "Inc1st",
input: net.IPv4(1, 255, 255, 255),
expected: net.IPv4(2, 0, 0, 0),
},
}

for _, vtt := range tests {
tt := vtt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
Inc(tt.input)
assert.Equal(t, tt.expected, tt.input)
})
}
}

func TestDupIP(t *testing.T) {
t.Parallel()
ipAddr := net.IPv4(192, 168, 0, 1).To4()

dupAddr := DupIP(ipAddr)
assert.Equal(t, ipAddr, dupAddr)

dupAddr[3]++
assert.Equal(t, net.IPv4(192, 168, 0, 1).To4(), ipAddr)
}

func TestParseIPNetWithError(t *testing.T) {
t.Parallel()
_, err := ParseIPNet("")
assert.Error(t, err)
}

func TestParseIPNet(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
expected *net.IPNet
}{
{
name: "subnet",
in: "192.168.0.1/24",
expected: &net.IPNet{
IP: net.IPv4(192, 168, 0, 0).To4(),
Mask: net.CIDRMask(24, 32),
},
},
{
name: "host",
in: "10.0.0.1",
expected: &net.IPNet{
IP: net.IPv4(10, 0, 0, 1).To4(),
Mask: net.CIDRMask(32, 32),
},
},
}
for _, vtt := range tests {
tt := vtt
t.Run(tt.name, func(t *testing.T) {
result, err := ParseIPNet(tt.in)
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)

})
}
}
Loading

0 comments on commit f853373

Please sign in to comment.