Skip to content

Commit f853373

Browse files
committed
Add ARP scan
1 parent 21483d5 commit f853373

34 files changed

+3098
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sx

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
11
# sx
2+
3+
## Purpose
4+
5+
The goal of this project is to create the fastest network scanner with clean and simple code.
6+
7+
Right now, only ARP scan is supported.
8+
9+
## Building
10+
11+
From the root of the source tree, run:
12+
13+
```
14+
go build
15+
```
16+
17+
## Using
18+
19+
```
20+
./sx help
21+
```

command/arp.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package command
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"errors"
7+
"net"
8+
"os"
9+
"os/signal"
10+
"strings"
11+
"sync"
12+
"time"
13+
14+
"github.com/spf13/cobra"
15+
"github.com/v-byte-cpu/sx/pkg/ip"
16+
"github.com/v-byte-cpu/sx/pkg/packet/afpacket"
17+
"github.com/v-byte-cpu/sx/pkg/scan"
18+
"github.com/v-byte-cpu/sx/pkg/scan/arp"
19+
"go.uber.org/zap"
20+
)
21+
22+
var errSrcIP = errors.New("invalid source IP")
23+
24+
var interfaceFlag string
25+
var srcIPFlag string
26+
var srcMACFlag string
27+
28+
func init() {
29+
arpCmd.Flags().StringVarP(&interfaceFlag, "iface", "i", "", "set interface to send/receive packets")
30+
arpCmd.Flags().StringVar(&srcIPFlag, "srcip", "", "set source IP address for generated packets")
31+
arpCmd.Flags().StringVar(&srcMACFlag, "srcmac", "", "set source MAC address for generated packets")
32+
rootCmd.AddCommand(arpCmd)
33+
}
34+
35+
var arpCmd = &cobra.Command{
36+
Use: "arp [flags] subnet",
37+
Example: strings.Join([]string{"arp 192.168.0.1/24", "arp 10.0.0.1"}, "\n"),
38+
Short: "Perform ARP scan",
39+
Args: func(cmd *cobra.Command, args []string) error {
40+
if len(args) != 1 {
41+
return errors.New("requires one ip subnet argument")
42+
}
43+
return nil
44+
},
45+
RunE: func(cmd *cobra.Command, args []string) (err error) {
46+
dstSubnet, err := ip.ParseIPNet(args[0])
47+
if err != nil {
48+
return err
49+
}
50+
51+
var iface *net.Interface
52+
var srcIP net.IP
53+
54+
if len(interfaceFlag) > 0 {
55+
if iface, err = net.InterfaceByName(interfaceFlag); err != nil {
56+
return err
57+
}
58+
} else {
59+
if iface, srcIP, err = ip.GetSubnetInterface(dstSubnet); err != nil {
60+
return err
61+
}
62+
}
63+
64+
if len(srcIPFlag) > 0 {
65+
if srcIP = net.ParseIP(srcIPFlag); srcIP == nil {
66+
return errSrcIP
67+
}
68+
}
69+
70+
srcMAC := iface.HardwareAddr
71+
if len(srcMACFlag) > 0 {
72+
if srcMAC, err = net.ParseMAC(srcMACFlag); err != nil {
73+
return err
74+
}
75+
}
76+
77+
r := &scan.Range{Subnet: dstSubnet, Interface: iface, SrcIP: srcIP, SrcMAC: srcMAC}
78+
return startEngine(r)
79+
},
80+
}
81+
82+
func startEngine(r *scan.Range) error {
83+
bw := bufio.NewWriter(os.Stdout)
84+
defer bw.Flush()
85+
logger, err := zap.NewProduction()
86+
if err != nil {
87+
return err
88+
}
89+
90+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
91+
defer cancel()
92+
93+
// setup network interface to read/write packets
94+
rw, err := afpacket.NewPacketSource(r.Interface.Name)
95+
if err != nil {
96+
return err
97+
}
98+
defer rw.Close()
99+
err = rw.SetBPFFilter(arp.BPFFilter(r))
100+
if err != nil {
101+
return err
102+
}
103+
104+
m := arp.NewScanMethod(ctx)
105+
106+
// setup result logging
107+
var wg sync.WaitGroup
108+
wg.Add(1)
109+
go func() {
110+
defer wg.Done()
111+
for result := range m.Results() {
112+
// TODO extract it
113+
if jsonFlag {
114+
data, err := result.MarshalJSON()
115+
if err != nil {
116+
logger.Error("arp", zap.Error(err))
117+
}
118+
bw.Write(data)
119+
} else {
120+
bw.WriteString(result.String())
121+
}
122+
bw.WriteByte('\n')
123+
}
124+
}()
125+
126+
// start scan
127+
engine := scan.SetupEngine(rw, m)
128+
done, errc := engine.Start(ctx, r)
129+
go func() {
130+
defer cancel()
131+
<-done
132+
<-time.After(300 * time.Millisecond)
133+
}()
134+
135+
// error logging
136+
wg.Add(1)
137+
go func() {
138+
defer wg.Done()
139+
for err := range errc {
140+
logger.Error("arp", zap.Error(err))
141+
}
142+
}()
143+
wg.Wait()
144+
return nil
145+
}

command/root.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package command
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var rootCmd = &cobra.Command{
10+
Use: "sx",
11+
Short: "Fast, modern, easy-to-use network scanner",
12+
Version: "0.1.0",
13+
}
14+
15+
var jsonFlag bool
16+
17+
func init() {
18+
rootCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "enable JSON output")
19+
}
20+
21+
func Main() {
22+
if err := rootCmd.Execute(); err != nil {
23+
os.Exit(1)
24+
}
25+
}

go.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/v-byte-cpu/sx
2+
3+
go 1.15
4+
5+
require (
6+
github.com/golang/mock v1.5.0
7+
github.com/google/gopacket v1.1.20-0.20210304165259-20562ffb40f8
8+
github.com/google/wire v0.5.0
9+
github.com/mailru/easyjson v0.7.7
10+
github.com/spf13/cobra v1.1.3
11+
github.com/stretchr/testify v1.7.0
12+
go.uber.org/multierr v1.6.0 // indirect
13+
go.uber.org/zap v1.16.0
14+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
15+
)

go.sum

Lines changed: 339 additions & 0 deletions
Large diffs are not rendered by default.

main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main
2+
3+
import "github.com/v-byte-cpu/sx/command"
4+
5+
func main() {
6+
command.Main()
7+
}

pkg/ip/ip.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package ip
2+
3+
import (
4+
"errors"
5+
"net"
6+
)
7+
8+
var (
9+
ErrInvalidAddr = errors.New("invalid IP subnet/host")
10+
ErrSubnetInterface = errors.New("no directly connected interfaces to destination subnet")
11+
)
12+
13+
func Inc(ip net.IP) {
14+
for j := len(ip) - 1; j >= 0; j-- {
15+
ip[j]++
16+
if ip[j] > 0 {
17+
break
18+
}
19+
}
20+
}
21+
22+
func DupIP(ip net.IP) net.IP {
23+
dup := make([]byte, 4)
24+
copy(dup, ip.To4())
25+
return dup
26+
}
27+
28+
func ParseIPNet(subnet string) (*net.IPNet, error) {
29+
_, result, err := net.ParseCIDR(subnet)
30+
if err == nil {
31+
return result, err
32+
}
33+
// try to parse host IP address instead
34+
ipAddr := net.ParseIP(subnet)
35+
if ipAddr == nil {
36+
return nil, ErrInvalidAddr
37+
}
38+
return &net.IPNet{IP: ipAddr.To4(), Mask: net.CIDRMask(32, 32)}, nil
39+
}
40+
41+
func GetSubnetInterface(dstSubnet *net.IPNet) (*net.Interface, net.IP, error) {
42+
dstSubnetIP := dstSubnet.IP.Mask(dstSubnet.Mask)
43+
ifaces, err := net.Interfaces()
44+
if err != nil {
45+
return nil, nil, err
46+
}
47+
for _, iface := range ifaces {
48+
addrs, err := iface.Addrs()
49+
if err != nil {
50+
return nil, nil, err
51+
}
52+
for _, addr := range addrs {
53+
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.Contains(dstSubnetIP) {
54+
return &iface, ipnet.IP.To4(), nil
55+
}
56+
}
57+
}
58+
return nil, nil, ErrSubnetInterface
59+
}

pkg/ip/ip_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package ip
2+
3+
import (
4+
"net"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestInc(t *testing.T) {
11+
t.Parallel()
12+
tests := []struct {
13+
name string
14+
input net.IP
15+
expected net.IP
16+
}{
17+
{
18+
name: "ZeroNet",
19+
input: net.IPv4(0, 0, 0, 0),
20+
expected: net.IPv4(0, 0, 0, 1),
21+
},
22+
{
23+
name: "Inc3rd",
24+
input: net.IPv4(1, 1, 0, 255),
25+
expected: net.IPv4(1, 1, 1, 0),
26+
},
27+
{
28+
name: "Inc2nd",
29+
input: net.IPv4(1, 1, 255, 255),
30+
expected: net.IPv4(1, 2, 0, 0),
31+
},
32+
{
33+
name: "Inc1st",
34+
input: net.IPv4(1, 255, 255, 255),
35+
expected: net.IPv4(2, 0, 0, 0),
36+
},
37+
}
38+
39+
for _, vtt := range tests {
40+
tt := vtt
41+
t.Run(tt.name, func(t *testing.T) {
42+
t.Parallel()
43+
Inc(tt.input)
44+
assert.Equal(t, tt.expected, tt.input)
45+
})
46+
}
47+
}
48+
49+
func TestDupIP(t *testing.T) {
50+
t.Parallel()
51+
ipAddr := net.IPv4(192, 168, 0, 1).To4()
52+
53+
dupAddr := DupIP(ipAddr)
54+
assert.Equal(t, ipAddr, dupAddr)
55+
56+
dupAddr[3]++
57+
assert.Equal(t, net.IPv4(192, 168, 0, 1).To4(), ipAddr)
58+
}
59+
60+
func TestParseIPNetWithError(t *testing.T) {
61+
t.Parallel()
62+
_, err := ParseIPNet("")
63+
assert.Error(t, err)
64+
}
65+
66+
func TestParseIPNet(t *testing.T) {
67+
t.Parallel()
68+
tests := []struct {
69+
name string
70+
in string
71+
expected *net.IPNet
72+
}{
73+
{
74+
name: "subnet",
75+
in: "192.168.0.1/24",
76+
expected: &net.IPNet{
77+
IP: net.IPv4(192, 168, 0, 0).To4(),
78+
Mask: net.CIDRMask(24, 32),
79+
},
80+
},
81+
{
82+
name: "host",
83+
in: "10.0.0.1",
84+
expected: &net.IPNet{
85+
IP: net.IPv4(10, 0, 0, 1).To4(),
86+
Mask: net.CIDRMask(32, 32),
87+
},
88+
},
89+
}
90+
for _, vtt := range tests {
91+
tt := vtt
92+
t.Run(tt.name, func(t *testing.T) {
93+
result, err := ParseIPNet(tt.in)
94+
assert.NoError(t, err)
95+
assert.Equal(t, tt.expected, result)
96+
97+
})
98+
}
99+
}

0 commit comments

Comments
 (0)