Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ I haven't done any proper performance testing, but a SYN scan of a single host,
You'll need to install libpcap.

- On Linux, install `libpcap` with your package manager
- On OSX, `brew install libpcap`
- On macOS, `brew install libpcap`
- On Windows, install [WinPcap](https://www.winpcap.org/)

**Note:** SYN scanning now works on Linux, macOS, and Windows with proper routing support.

Then just:

```
Expand All @@ -29,8 +31,8 @@ Use the specified scan type. The options are:

| Type | Description |
|------------|-------------|
| `syn` | A SYN/stealth scan. Most efficient scan type, using only a partial TCP handshake. Requires root privileges.
| `connect` | A less detailed scan using full TCP handshakes, though does not require root privileges.
| `syn` | A SYN/stealth scan. Most efficient scan type, using only a partial TCP handshake. Requires root privileges. **Supported on Linux, macOS, and Windows.**
| `connect` | A less detailed scan using full TCP handshakes, though does not require root privileges. **Works on all platforms.**
| `device` | Attempt to identify device MAC address and manufacturer where possible. Useful for listing devices on a LAN.

The default is a SYN scan.
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.12
require (
github.com/google/gopacket v1.1.17
github.com/mostlygeek/arp v0.0.0-20170424181311-541a2129847a
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.3.0
Expand Down
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mostlygeek/arp v0.0.0-20170424181311-541a2129847a h1:AfneHvfmYgUIcgdUrrDFklLdEzQAvG9AKRTe1x1mx/0=
github.com/mostlygeek/arp v0.0.0-20170424181311-541a2129847a/go.mod h1:jZxafo9CAqaKFQE4zitrg5QNlA6CXUsjwXPlIppF3tk=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
Expand All @@ -45,7 +43,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
230 changes: 230 additions & 0 deletions scan/routing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package scan

import (
"fmt"
"net"
"os/exec"
"runtime"
"strings"
)

// RouteInfo contains routing information for a destination
type RouteInfo struct {
Interface *net.Interface
Gateway net.IP
SrcIP net.IP
}

// GetRoute returns routing information for the given destination IP
func GetRoute(dst net.IP) (*RouteInfo, error) {
switch runtime.GOOS {
case "linux":
return getRouteLinux(dst)
case "darwin":
return getRouteDarwin(dst)
case "windows":
return getRouteWindows(dst)
default:
return nil, fmt.Errorf("routing not supported on %s", runtime.GOOS)
}
}

// getRouteLinux gets routing info on Linux using ip route command
func getRouteLinux(dst net.IP) (*RouteInfo, error) {
cmd := exec.Command("ip", "route", "get", dst.String())
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get route: %v", err)
}

return parseLinuxRoute(string(output))
}

// getRouteDarwin gets routing info on macOS using route command
func getRouteDarwin(dst net.IP) (*RouteInfo, error) {
cmd := exec.Command("route", "-n", "get", dst.String())
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get route: %v", err)
}

return parseDarwinRoute(string(output))
}

// getRouteWindows gets routing info on Windows using route command
func getRouteWindows(dst net.IP) (*RouteInfo, error) {
// Fall back to default interface for Windows
iface, err := GetDefaultInterface()
if err != nil {
return nil, err
}

// Get source IP from interface
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("could not get interface addresses: %v", err)
}

var srcIP net.IP
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
srcIP = ipnet.IP
break
}
}
}

if srcIP == nil {
return nil, fmt.Errorf("could not determine source IP")
}

return &RouteInfo{
Interface: iface,
Gateway: nil, // Simplified for Windows
SrcIP: srcIP,
}, nil
}

// parseLinuxRoute parses Linux 'ip route get' output
func parseLinuxRoute(output string) (*RouteInfo, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) == 0 {
return nil, fmt.Errorf("no route output")
}

fields := strings.Fields(lines[0])
var gateway net.IP
var interfaceName string
var srcIP net.IP

for i, field := range fields {
switch field {
case "via":
if i+1 < len(fields) {
gateway = net.ParseIP(fields[i+1])
}
case "dev":
if i+1 < len(fields) {
interfaceName = fields[i+1]
}
case "src":
if i+1 < len(fields) {
srcIP = net.ParseIP(fields[i+1])
}
}
}

if interfaceName == "" {
return nil, fmt.Errorf("could not determine interface")
}

iface, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, fmt.Errorf("could not get interface %s: %v", interfaceName, err)
}

if srcIP == nil {
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("could not get interface addresses: %v", err)
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
srcIP = ipnet.IP
break
}
}
}
}

return &RouteInfo{
Interface: iface,
Gateway: gateway,
SrcIP: srcIP,
}, nil
}

// parseDarwinRoute parses macOS 'route -n get' output
func parseDarwinRoute(output string) (*RouteInfo, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
var gateway net.IP
var interfaceName string

for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "gateway:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
gateway = net.ParseIP(parts[1])
}
} else if strings.HasPrefix(line, "interface:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
interfaceName = parts[1]
}
}
}

if interfaceName == "" {
return nil, fmt.Errorf("could not determine interface")
}

iface, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, fmt.Errorf("could not get interface %s: %v", interfaceName, err)
}

addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("could not get interface addresses: %v", err)
}

var srcIP net.IP
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
srcIP = ipnet.IP
break
}
}
}

if srcIP == nil {
return nil, fmt.Errorf("could not determine source IP")
}

return &RouteInfo{
Interface: iface,
Gateway: gateway,
SrcIP: srcIP,
}, nil
}

// GetDefaultInterface returns the default network interface
func GetDefaultInterface() (*net.Interface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}

for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}

addrs, err := iface.Addrs()
if err != nil {
continue
}

for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
return &iface, nil
}
}
}

return nil, fmt.Errorf("no suitable network interface found")
}
89 changes: 89 additions & 0 deletions scan/routing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package scan

import (
"net"
"runtime"
"testing"

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

func TestGetDefaultInterface(t *testing.T) {
iface, err := GetDefaultInterface()
require.NoError(t, err)
assert.NotNil(t, iface)
assert.NotEmpty(t, iface.Name)
}

func TestGetRoute(t *testing.T) {
// Test with a well-known public IP
dst := net.ParseIP("8.8.8.8")
require.NotNil(t, dst)

routeInfo, err := GetRoute(dst)

// On Windows, this might fail because we don't have route commands
// but on Linux/macOS it should work
if runtime.GOOS == "windows" {
// On Windows, we expect this to fail gracefully
if err != nil {
t.Logf("Expected failure on Windows: %v", err)
return
}
}

if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
require.NoError(t, err)
assert.NotNil(t, routeInfo)
assert.NotNil(t, routeInfo.Interface)
assert.NotNil(t, routeInfo.SrcIP)
assert.NotEmpty(t, routeInfo.Interface.Name)

t.Logf("Route info for %s:", dst.String())
t.Logf(" Interface: %s", routeInfo.Interface.Name)
t.Logf(" Source IP: %s", routeInfo.SrcIP.String())
if routeInfo.Gateway != nil {
t.Logf(" Gateway: %s", routeInfo.Gateway.String())
}
}
}

func TestParseLinuxRoute(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Linux-specific test")
}

// Test parsing Linux route output
output := "192.168.1.1 via 192.168.1.254 dev eth0 src 192.168.1.100 uid 1000"

routeInfo, err := parseLinuxRoute(output)
require.NoError(t, err)

assert.Equal(t, "192.168.1.254", routeInfo.Gateway.String())
assert.Equal(t, "192.168.1.100", routeInfo.SrcIP.String())
assert.Equal(t, "eth0", routeInfo.Interface.Name)
}func TestP
arseDarwinRoute(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("macOS-specific test")
}

// Test parsing macOS route output
output := ` route to: 8.8.8.8
destination: default
mask: default
gateway: 192.168.1.1
interface: en0
flags: <UP,GATEWAY,DONE,STATIC>
recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire
0 0 0 28 8 0 1500 0`

routeInfo, err := parseDarwinRoute(output)
require.NoError(t, err)

assert.Equal(t, "192.168.1.1", routeInfo.Gateway.String())
assert.Equal(t, "en0", routeInfo.Interface.Name)
// Source IP will be determined from interface, so we just check it's not nil
assert.NotNil(t, routeInfo.SrcIP)
}
11 changes: 5 additions & 6 deletions scan/scan-syn.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"github.com/google/gopacket/routing"
""
"github.com/mostlygeek/arp"
"github.com/phayes/freeport"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -239,14 +239,13 @@ func (s *SynScanner) scanHost(job hostJob) (Result, error) {
default:
}

router, err := routing.New()
if err != nil {
return result, err
}
networkInterface, gateway, srcIP, err := router.Route(job.ip)
routeInfo, err := GetRoute(job.ip)
if err != nil {
return result, err
}
networkInterface := routeInfo.Interface
gateway := routeInfo.Gateway
srcIP := routeInfo.SrcIP

handle, err := pcap.OpenLive(networkInterface.Name, 65535, true, pcap.BlockForever)
if err != nil {
Expand Down