diff --git a/README.md b/README.md index 833f57c..2a8ae56 100644 --- a/README.md +++ b/README.md @@ -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: ``` @@ -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. diff --git a/go.mod b/go.mod index e24a94b..4f96bbb 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 2b8e89b..66b51f6 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/scan/routing.go b/scan/routing.go new file mode 100644 index 0000000..3ca2e9c --- /dev/null +++ b/scan/routing.go @@ -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") +} \ No newline at end of file diff --git a/scan/routing_test.go b/scan/routing_test.go new file mode 100644 index 0000000..f1a2834 --- /dev/null +++ b/scan/routing_test.go @@ -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: + 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) +} \ No newline at end of file diff --git a/scan/scan-syn.go b/scan/scan-syn.go index 96a18c3..4a967b6 100644 --- a/scan/scan-syn.go +++ b/scan/scan-syn.go @@ -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" @@ -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 {