Skip to content

Commit 876471b

Browse files
added arpspoof package and CLI tool, several improvements and refactorings
1 parent 0b361e8 commit 876471b

File tree

10 files changed

+697
-70
lines changed

10 files changed

+697
-70
lines changed

arpspoof/arpspoof.go

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

cmd/marpspoof/main.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"net/netip"
7+
"os"
8+
"os/signal"
9+
"regexp"
10+
"time"
11+
12+
"github.com/rs/zerolog"
13+
"github.com/shadowy-pycoder/colors"
14+
"github.com/shadowy-pycoder/mshark/arpspoof"
15+
"github.com/shadowy-pycoder/mshark/network"
16+
)
17+
18+
var (
19+
app = "marpspoof"
20+
ipPortPattern = regexp.MustCompile(
21+
`\b(?:\d{1,3}\.){3}\d{1,3}(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?\b`,
22+
)
23+
)
24+
25+
func root(args []string) error {
26+
conf := &arpspoof.ARPSpoofConfig{}
27+
flags := flag.NewFlagSet(app, flag.ExitOnError)
28+
flags.StringVar(
29+
&conf.Targets,
30+
"t",
31+
"",
32+
"Targets for ARP spoofing. Example: \"targets 10.0.0.1,10.0.0.5-10,192.168.1.*,192.168.10.0/24\" (Default: entire subnet)",
33+
)
34+
gw := flags.String("g", "", "IPv4 address of custom gateway (Default: default gateway)")
35+
flags.StringVar(&conf.Interface, "i", "", "The name of the network interface. Example: eth0 (Default: default interface)")
36+
flags.BoolVar(&conf.FullDuplex, "f", false, "Run ARP spoofing in fullduplex mode")
37+
flags.BoolVar(&conf.Debug, "d", false, "Enable debug logging")
38+
flags.BoolFunc("I", "Display list of interfaces and exit.", func(flagValue string) error {
39+
if err := network.DisplayInterfaces(); err != nil {
40+
fmt.Fprintf(os.Stderr, "%s: %v\n", app, err)
41+
os.Exit(2)
42+
}
43+
os.Exit(0)
44+
return nil
45+
})
46+
if err := flags.Parse(args); err != nil {
47+
return err
48+
}
49+
if *gw != "" {
50+
ip, err := netip.ParseAddr(*gw)
51+
if err != nil {
52+
return err
53+
}
54+
conf.Gateway = &ip
55+
}
56+
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false}
57+
output.FormatTimestamp = func(i any) string {
58+
ts, _ := time.Parse(time.RFC3339, i.(string))
59+
return colors.Gray(colors.WrapBrackets(ts.Format(time.TimeOnly))).String()
60+
}
61+
output.FormatMessage = func(i any) string {
62+
if i == nil || i == "" {
63+
return ""
64+
}
65+
s := i.(string)
66+
result := ipPortPattern.ReplaceAllStringFunc(s, func(match string) string {
67+
return colors.Gray(match).String()
68+
})
69+
return result
70+
}
71+
logger := zerolog.New(output).With().Timestamp().Logger()
72+
conf.Logger = &logger
73+
arpspoofer, err := arpspoof.NewARPSpoofer(conf)
74+
if err != nil {
75+
return err
76+
}
77+
go arpspoofer.Start()
78+
quit := make(chan os.Signal, 1)
79+
signal.Notify(quit, os.Interrupt)
80+
<-quit
81+
return arpspoofer.Stop()
82+
}
83+
84+
func main() {
85+
if err := root(os.Args[1:]); err != nil {
86+
fmt.Fprintf(os.Stderr, "%s: %v\n", app, err)
87+
os.Exit(2)
88+
}
89+
}

cmd/mshark/cli.go

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ package main
33
import (
44
"flag"
55
"fmt"
6-
"net"
76
"os"
87
"path/filepath"
98
"slices"
109
"strings"
11-
"text/tabwriter"
1210
"time"
1311

1412
ms "github.com/shadowy-pycoder/mshark"
1513
"github.com/shadowy-pycoder/mshark/mpcap"
1614
"github.com/shadowy-pycoder/mshark/mpcapng"
15+
"github.com/shadowy-pycoder/mshark/network"
1716
)
1817

1918
const app string = "mshark"
@@ -62,21 +61,6 @@ func (f *ExtFlag) UnmarshalText(b []byte) error {
6261
return nil
6362
}
6463

65-
func displayInterfaces() error {
66-
w := new(tabwriter.Writer)
67-
w.Init(os.Stdout, 0, 0, 2, ' ', tabwriter.TabIndent)
68-
ifaces, err := net.Interfaces()
69-
if err != nil {
70-
return fmt.Errorf("failed to get network interfaces: %v", err)
71-
}
72-
fmt.Fprintln(w, "Index\tName\tFlags")
73-
fmt.Fprintln(w, "0\tany\tUP")
74-
for _, iface := range ifaces {
75-
fmt.Fprintf(w, "%d\t%s\t%s\n", iface.Index, iface.Name, strings.ToUpper(iface.Flags.String()))
76-
}
77-
return w.Flush()
78-
}
79-
8064
func createFile(app, ext string) (*os.File, error) {
8165
path := fmt.Sprintf("./%s_%s.%s", app, time.Now().UTC().Format("20060102_150405"), ext)
8266
f, err := os.OpenFile(filepath.FromSlash(path), os.O_CREATE|os.O_WRONLY, 0o644)
@@ -105,7 +89,7 @@ func root(args []string) error {
10589
packetBuffer := flags.Int("b", 8192, "The maximum size of packet queue.")
10690
flags.StringVar(&conf.Expr, "e", "", `BPF filter expression. Example: "ip proto tcp".`)
10791
flags.BoolFunc("D", "Display list of interfaces and exit.", func(flagValue string) error {
108-
if err := displayInterfaces(); err != nil {
92+
if err := network.DisplayInterfaces(); err != nil {
10993
fmt.Fprintf(os.Stderr, "%s: %v\n", app, err)
11094
os.Exit(2)
11195
}
@@ -130,7 +114,7 @@ func root(args []string) error {
130114
}
131115

132116
// getting network interface from the provided name
133-
in, err := ms.InterfaceByName(*iface)
117+
in, err := network.InterfaceByName(*iface)
134118
if err != nil {
135119
return err
136120
}

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ go 1.24.1
44

55
require (
66
github.com/magefile/mage v1.15.0
7+
github.com/malfunkt/iprange v0.9.0
78
github.com/mdlayher/packet v1.1.2
89
github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5
10+
github.com/rs/zerolog v1.34.0
11+
github.com/shadowy-pycoder/colors v0.0.1
912
github.com/stretchr/testify v1.9.0
1013
golang.org/x/net v0.28.0
1114
golang.org/x/text v0.27.0
@@ -14,7 +17,10 @@ require (
1417
require (
1518
github.com/davecgh/go-spew v1.1.1 // indirect
1619
github.com/josharian/native v1.1.0 // indirect
20+
github.com/mattn/go-colorable v0.1.13 // indirect
21+
github.com/mattn/go-isatty v0.0.19 // indirect
1722
github.com/mdlayher/socket v0.4.1 // indirect
23+
github.com/pkg/errors v0.9.1 // indirect
1824
github.com/pmezard/go-difflib v1.0.0 // indirect
1925
golang.org/x/sync v0.16.0 // indirect
2026
golang.org/x/sys v0.24.0 // indirect

go.sum

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
12
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
35
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
46
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
57
github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A=
@@ -8,20 +10,37 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL
810
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
911
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
1012
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
13+
github.com/malfunkt/iprange v0.9.0 h1:VCs0PKLUPotNVQTpVNszsut4lP7OCGNBwX+lOYBrnVQ=
14+
github.com/malfunkt/iprange v0.9.0/go.mod h1:TRGqO/f95gh3LOndUGTL46+W0GXA91WTqyZ0Quwvt4U=
15+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
16+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
17+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
18+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
19+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
1120
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
1221
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
1322
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
1423
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
1524
github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 h1:p4VuaitqUAqSZSomd7Wb4BPV/Jj7Hno2/iqtfX7DZJI=
1625
github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5/go.mod h1:zIAoVKeWP0mz4zXY50UYQt6NLg2uwKRswMDcGEqOms4=
26+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
27+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1728
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1829
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
30+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
31+
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
32+
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
33+
github.com/shadowy-pycoder/colors v0.0.1 h1:weCj/YIOupqy4BSP8KuVzr20fC+cuAv/tArz7bhhkP4=
34+
github.com/shadowy-pycoder/colors v0.0.1/go.mod h1:lkrJS1PY2oVigNLTT6pkbF7B/v0YcU2LD5PZnss1Q4U=
1935
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2036
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2137
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
2238
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
2339
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
2440
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
41+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
42+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2544
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
2645
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2746
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=

mshark.go

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -140,31 +140,6 @@ func (mw *Writer) WriteHeader(c *Config) error {
140140
return err
141141
}
142142

143-
// InterfaceByName returns the interface specified by name.
144-
func InterfaceByName(name string) (*net.Interface, error) {
145-
var (
146-
in *net.Interface
147-
err error
148-
)
149-
if name == "any" {
150-
in = &net.Interface{Index: 0, Name: "any"}
151-
} else {
152-
in, err = net.InterfaceByName(name)
153-
if err != nil {
154-
return nil, fmt.Errorf("unknown interface %s: %v", name, err)
155-
}
156-
ok := true &&
157-
// Look for an Ethernet interface.
158-
len(in.HardwareAddr) == 6 &&
159-
// Look for up, multicast, broadcast.
160-
in.Flags&(net.FlagUp|net.FlagMulticast|net.FlagBroadcast) != 0
161-
if !ok {
162-
return nil, fmt.Errorf("interface %s is not up", name)
163-
}
164-
}
165-
return in, nil
166-
}
167-
168143
// OpenLive opens a live capture based on the given configuration and writes
169144
// all captured packets to the given PacketWriters.
170145
func OpenLive(conf *Config, pw ...PacketWriter) error {

mshark_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package mshark
33
import (
44
"io"
55
"testing"
6+
7+
"github.com/shadowy-pycoder/mshark/network"
68
)
79

810
func BenchmarkOpenLive(b *testing.B) {
911
b.ResetTimer()
10-
in, err := InterfaceByName("any")
12+
in, err := network.InterfaceByName("any")
1113
if err != nil {
1214
b.Fatal(err)
1315
}

network/network.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Package network provides utility functions to extract some data about network
2+
package network
3+
4+
import (
5+
"bufio"
6+
"fmt"
7+
"net"
8+
"net/netip"
9+
"os"
10+
"os/exec"
11+
"strings"
12+
"text/tabwriter"
13+
)
14+
15+
// InterfaceByName returns the interface specified by name.
16+
func InterfaceByName(name string) (*net.Interface, error) {
17+
var (
18+
in *net.Interface
19+
err error
20+
)
21+
if name == "any" {
22+
in = &net.Interface{Index: 0, Name: "any"}
23+
} else {
24+
in, err = net.InterfaceByName(name)
25+
if err != nil {
26+
return nil, fmt.Errorf("unknown interface %s: %v", name, err)
27+
}
28+
ok := true &&
29+
// Look for an Ethernet interface.
30+
len(in.HardwareAddr) == 6 &&
31+
// Look for up, multicast, broadcast.
32+
in.Flags&(net.FlagUp|net.FlagMulticast|net.FlagBroadcast) != 0
33+
if !ok {
34+
return nil, fmt.Errorf("interface %s is not up", name)
35+
}
36+
}
37+
return in, nil
38+
}
39+
40+
func DisplayInterfaces() error {
41+
w := new(tabwriter.Writer)
42+
w.Init(os.Stdout, 0, 0, 2, ' ', tabwriter.TabIndent)
43+
ifaces, err := net.Interfaces()
44+
if err != nil {
45+
return fmt.Errorf("failed to get network interfaces: %v", err)
46+
}
47+
fmt.Fprintln(w, "Index\tName\tFlags")
48+
fmt.Fprintln(w, "0\tany\tUP")
49+
for _, iface := range ifaces {
50+
fmt.Fprintf(w, "%d\t%s\t%s\n", iface.Index, iface.Name, strings.ToUpper(iface.Flags.String()))
51+
}
52+
return w.Flush()
53+
}
54+
55+
func GetDefaultInterface() (*net.Interface, error) {
56+
f, err := os.Open("/proc/net/route")
57+
if err != nil {
58+
return nil, err
59+
}
60+
defer f.Close()
61+
62+
defaultInterface := ""
63+
scanner := bufio.NewScanner(f)
64+
for scanner.Scan() {
65+
line := scanner.Text()
66+
fields := strings.Fields(line)
67+
if len(fields) >= 2 && fields[1] == "00000000" {
68+
defaultInterface = fields[0]
69+
break
70+
}
71+
}
72+
return net.InterfaceByName(defaultInterface)
73+
}
74+
75+
func GetDefaultGatewayIPv4() (netip.Addr, error) {
76+
cmd := exec.Command("sh", "-c", "ip route show 0.0.0.0/0 | awk '{print $3}'")
77+
ipRaw, err := cmd.Output()
78+
if err != nil {
79+
return netip.Addr{}, err
80+
}
81+
ip, err := netip.ParseAddr(strings.TrimRight(string(ipRaw), "\n"))
82+
if err != nil {
83+
return netip.Addr{}, err
84+
}
85+
if !ip.Is4() {
86+
return netip.Addr{}, fmt.Errorf("only IPv4 is supported")
87+
}
88+
return ip, nil
89+
}
90+
91+
func GetGatewayIPv4FromInterface(iface string) (netip.Addr, error) {
92+
cmd := exec.Command("sh", "-c", fmt.Sprintf("ip route show dev %s", iface))
93+
routes, err := cmd.Output()
94+
if err != nil {
95+
return netip.Addr{}, err
96+
}
97+
for line := range strings.Lines(string(routes)) {
98+
fields := strings.Fields(line)
99+
if len(fields) > 2 && fields[1] == "via" {
100+
ip, err := netip.ParseAddr(fields[2])
101+
if err != nil {
102+
continue
103+
}
104+
if !ip.Is4() {
105+
continue
106+
}
107+
return ip, nil
108+
}
109+
}
110+
return netip.Addr{}, fmt.Errorf("gateway IPv4 not found for %s", iface)
111+
}
112+
113+
func GetIPv4PrefixFromInterface(iface *net.Interface) (netip.Prefix, error) {
114+
addrs, err := iface.Addrs()
115+
if err != nil {
116+
return netip.Prefix{}, err
117+
}
118+
for _, a := range addrs {
119+
ipPrefix, err := netip.ParsePrefix(a.String())
120+
if err != nil {
121+
return netip.Prefix{}, err
122+
}
123+
if ipPrefix.Addr().Is4() {
124+
return ipPrefix, nil
125+
}
126+
}
127+
return netip.Prefix{}, fmt.Errorf("no IPv4 prefix found")
128+
}

0 commit comments

Comments
 (0)