diff --git a/LIBRARY_USAGE.md b/LIBRARY_USAGE.md new file mode 100644 index 0000000..8352fea --- /dev/null +++ b/LIBRARY_USAGE.md @@ -0,0 +1,211 @@ +# Furious Library Usage Guide + +This guide helps you use Furious as a Go library in your own projects. + +## Quick Start + +### Installation + +```bash +go get github.com/liamg/furious +``` + +### Simple Example + +```go +package main + +import ( + "fmt" + "log" + + "github.com/liamg/furious/scan" +) + +func main() { + // Check if specific ports are open + portStatus, err := scan.QuickPortCheck("example.com", 80, 443) + if err != nil { + log.Fatal(err) + } + + for port, isOpen := range portStatus { + fmt.Printf("Port %d: %s\n", port, map[bool]string{true: "OPEN", false: "CLOSED"}[isOpen]) + } +} +``` + +## Common Issues and Solutions + +### Issue 1: "Host is down" when it should be up + +**Problem**: Your code shows "Host is down" but the furious CLI tool shows the host as up. + +**Cause**: Incorrect timeout parameter. You're likely passing an integer instead of `time.Duration`. + +**Wrong**: +```go +// This creates a 6000 nanosecond timeout (0.006ms)! +scanner := scan.NewConnectScanner(targetIterator, 6000, 1000) +``` + +**Correct**: +```go +// This creates a 6000 millisecond timeout (6 seconds) +timeout := time.Duration(6000) * time.Millisecond +scanner := scan.NewConnectScanner(targetIterator, timeout, 1000) + +// Or use the library helpers: +results, err := scan.SimpleConnectScanWithTimeout("target.com", []int{80, 443}, 6000) +``` + +### Issue 2: Permission denied for SYN scans + +**Problem**: SYN scans fail with permission errors. + +**Solution**: SYN scans require root privileges. Either: + +1. Run as root: `sudo go run main.go` +2. Use connect scans instead: `scan.SimpleConnectScan(target, ports)` +3. Check privileges in code: + +```go +if os.Geteuid() != 0 { + log.Println("SYN scan requires root privileges, falling back to connect scan") + // Use connect scan instead +} +``` + +### Issue 3: Scans are too slow or too fast + +**Problem**: Default settings don't match your needs. + +**Solution**: Use custom configuration: + +```go +config := scan.LibraryConfig{ + TimeoutMS: 10000, // 10 second timeout for slow networks + Workers: 50, // Fewer workers for rate-limited targets + ScanType: "connect", + Retries: 3, // More retries for unreliable networks +} + +scanner, err := scan.NewScannerFromConfig(target, config) +``` + +### Issue 4: Context cancellation + +**Problem**: Need to cancel scans or set timeouts. + +**Solution**: Use context properly: + +```go +// With timeout +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() + +results, err := scanner.Scan(ctx, ports) + +// With manual cancellation +ctx, cancel := context.WithCancel(context.Background()) +go func() { + time.Sleep(10 * time.Second) + cancel() // Cancel after 10 seconds +}() + +results, err := scanner.Scan(ctx, ports) +``` + +## API Reference + +### Simple Functions + +#### `QuickPortCheck(target string, ports ...int) (map[int]bool, error)` +Quickly check if specific ports are open. + +#### `IsHostUp(target string) (bool, error)` +Check if a host is reachable by testing common ports. + +#### `SimpleConnectScan(target string, ports []int) ([]Result, error)` +Perform a connect scan with default settings. + +#### `SimpleSynScan(target string, ports []int) ([]Result, error)` +Perform a SYN scan with default settings (requires root). + +#### `ScanCommonPorts(target string) ([]Result, error)` +Scan the most common ports on a target. + +### Configuration-Based API + +#### `LibraryConfig` +```go +type LibraryConfig struct { + TimeoutMS int // Timeout in milliseconds + Workers int // Number of parallel workers + ScanType string // "syn", "connect", or "device" + Retries int // Number of retries +} +``` + +#### `NewScannerFromConfig(target string, config LibraryConfig) (Scanner, error)` +Create a scanner with custom configuration. + +#### `DefaultLibraryConfig() LibraryConfig` +Get sensible default configuration. + +### Manual Scanner API + +For advanced usage, you can use the scanners directly: + +```go +// Connect Scanner +targetIterator := scan.NewTargetIterator(target) +timeout := time.Duration(timeoutMS) * time.Millisecond +scanner := scan.NewConnectScanner(targetIterator, timeout, workers) + +// SYN Scanner (requires root) +scanner := scan.NewSynScanner(targetIterator, timeout, workers) + +// Device Scanner +scanner := scan.NewDeviceScanner(targetIterator, timeout) +``` + +## Examples + +See the `examples/` directory for complete working examples: + +- `examples/library_usage.go` - Comprehensive examples of all library features +- Simple port checking +- Host up detection +- Custom configuration +- Manual scanner usage +- Error handling + +## Best Practices + +1. **Always handle errors**: Network operations can fail in many ways +2. **Use appropriate timeouts**: Balance speed vs reliability +3. **Consider rate limiting**: Use fewer workers for rate-limited targets +4. **Use connect scans by default**: They don't require root privileges +5. **Test with known hosts first**: Verify your code works with reliable targets +6. **Use context for cancellation**: Allow users to cancel long-running scans + +## Performance Tips + +1. **Adjust worker count**: More workers = faster scans, but may trigger rate limiting +2. **Use appropriate timeouts**: Shorter timeouts = faster scans, but may miss slow responses +3. **Use SYN scans when possible**: Faster than connect scans, but requires root +4. **Batch operations**: Scan multiple ports at once rather than one by one +5. **Use retries for reliability**: Especially important for unreliable networks + +## Troubleshooting + +If you're still having issues: + +1. Test with the CLI tool first: `furious -s connect target.com` +2. Check your timeout values: Make sure they're reasonable (1000-10000ms) +3. Verify network connectivity: Can you ping the target? +4. Check for rate limiting: Try with fewer workers or longer timeouts +5. Enable debug logging: Use the verbose flag or add logging to your code + +For more help, see the examples or open an issue on GitHub. \ No newline at end of file diff --git a/README.md b/README.md index 833f57c..520d807 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,91 @@ If you installed using go, your user has the environment variables required to l sudo env "PATH=$PATH" furious ``` +## Library Usage + +Furious can be used as a Go library in your own projects: + +### Simple Usage + +```go +package main + +import ( + "fmt" + "log" + + "github.com/liamg/furious/scan" +) + +func main() { + // Quick port check + portStatus, err := scan.QuickPortCheck("example.com", 80, 443, 22) + if err != nil { + log.Fatal(err) + } + + for port, isOpen := range portStatus { + status := "CLOSED" + if isOpen { + status = "OPEN" + } + fmt.Printf("Port %d: %s\n", port, status) + } + + // Check if host is up + isUp, err := scan.IsHostUp("example.com") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Host is up: %v\n", isUp) +} +``` + +### Advanced Usage + +```go +// Custom configuration +config := scan.LibraryConfig{ + TimeoutMS: 5000, // 5 second timeout + Workers: 100, // 100 parallel workers + ScanType: "connect", + Retries: 2, // 2 retries for reliability +} + +scanner, err := scan.NewScannerFromConfig("target.com", config) +if err != nil { + log.Fatal(err) +} + +if err := scanner.Start(); err != nil { + log.Fatal(err) +} + +results, err := scanner.Scan(context.Background(), []int{80, 443, 22}) +if err != nil { + log.Fatal(err) +} + +for _, result := range results { + fmt.Printf("Host: %s, Open ports: %v\n", result.Host, result.Open) +} +``` + +### Fixed Manual Scanner Usage + +If you prefer the manual approach, make sure to use `time.Duration` correctly: + +```go +// CORRECT: Use time.Duration for timeout +targetIterator := scan.NewTargetIterator("192.168.3.5") +timeout := time.Duration(6000) * time.Millisecond // 6 seconds +scanner := scan.NewConnectScanner(targetIterator, timeout, 1000) + +// The rest of your code... +``` + +The issue in your original code was passing `6000` as an integer instead of a proper `time.Duration`. This caused the timeout to be interpreted as 6000 nanoseconds (0.006ms) instead of 6000 milliseconds. + ## SYN/Connect scans are slower than nmap! They're not in my experience, but with default arguments furious scans nearly six times as many ports as nmap does by default. diff --git a/examples/go.mod b/examples/go.mod new file mode 100644 index 0000000..87ac506 --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,9 @@ +module furious-examples + +go 1.12 + +require ( + github.com/liamg/furious v0.0.0 +) + +replace github.com/liamg/furious => ../ \ No newline at end of file diff --git a/examples/library_usage.go b/examples/library_usage.go new file mode 100644 index 0000000..2427b19 --- /dev/null +++ b/examples/library_usage.go @@ -0,0 +1,206 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/liamg/furious/scan" +) + +func main() { + fmt.Println("Furious Library Usage Examples") + fmt.Println("==============================") + + // Example 1: Simple port check + fmt.Println("\n1. Simple Port Check:") + simplePortCheck() + + // Example 2: Check if host is up + fmt.Println("\n2. Host Up Check:") + hostUpCheck() + + // Example 3: Scan common ports + fmt.Println("\n3. Common Ports Scan:") + commonPortsScan() + + // Example 4: Custom configuration + fmt.Println("\n4. Custom Configuration:") + customConfigScan() + + // Example 5: Manual scanner usage (original approach, fixed) + fmt.Println("\n5. Manual Scanner Usage (Fixed):") + manualScannerUsage() + + // Example 6: SYN scan (requires root) + fmt.Println("\n6. SYN Scan (requires root):") + synScanExample() +} + +// Example 1: Simple port check +func simplePortCheck() { + target := "8.8.8.8" + ports := []int{53, 80, 443} + + portStatus, err := scan.QuickPortCheck(target, ports...) + if err != nil { + log.Printf("Error: %v", err) + return + } + + fmt.Printf("Port status for %s:\n", target) + for port, isOpen := range portStatus { + status := "CLOSED" + if isOpen { + status = "OPEN" + } + fmt.Printf(" Port %d: %s\n", port, status) + } +} + +// Example 2: Check if host is up +func hostUpCheck() { + targets := []string{"8.8.8.8", "1.1.1.1", "192.0.2.1"} // Last one is RFC5737 test IP + + for _, target := range targets { + isUp, err := scan.IsHostUp(target) + if err != nil { + log.Printf("Error checking %s: %v", target, err) + continue + } + + status := "DOWN" + if isUp { + status = "UP" + } + fmt.Printf("Host %s: %s\n", target, status) + } +} + +// Example 3: Scan common ports +func commonPortsScan() { + target := "scanme.nmap.org" + + results, err := scan.ScanCommonPorts(target) + if err != nil { + log.Printf("Error: %v", err) + return + } + + for _, result := range results { + fmt.Printf("Scan results for %s:\n", result.Host.String()) + if result.IsHostUp() { + fmt.Printf(" Host is UP (latency: %v)\n", result.Latency) + if len(result.Open) > 0 { + fmt.Printf(" Open ports: %v\n", result.Open) + } + } else { + fmt.Printf(" Host is DOWN\n") + } + } +} + +// Example 4: Custom configuration +func customConfigScan() { + target := "example.com" + ports := []int{80, 443, 8080, 8443} + + // Create custom configuration + config := scan.LibraryConfig{ + TimeoutMS: 5000, // 5 second timeout + Workers: 100, // 100 parallel workers + ScanType: "connect", + Retries: 2, // 2 retries for reliability + } + + scanner, err := scan.NewScannerFromConfig(target, config) + if err != nil { + log.Printf("Error creating scanner: %v", err) + return + } + + if err := scanner.Start(); err != nil { + log.Printf("Error starting scanner: %v", err) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + results, err := scanner.Scan(ctx, ports) + if err != nil { + log.Printf("Error scanning: %v", err) + return + } + + fmt.Printf("Custom scan results for %s:\n", target) + for _, result := range results { + if result.IsHostUp() { + fmt.Printf(" Host: %s (UP)\n", result.Host.String()) + fmt.Printf(" Open: %v\n", result.Open) + fmt.Printf(" Closed: %v\n", result.Closed) + } + } +} + +// Example 5: Manual scanner usage (original approach, but fixed) +func manualScannerUsage() { + target := "8.8.8.8" + ports := []int{53, 80, 443} + + // FIXED: Use time.Duration instead of raw integer + targetIterator := scan.NewTargetIterator(target) + timeout := time.Duration(6000) * time.Millisecond // 6 seconds + scanner := scan.NewConnectScanner(targetIterator, timeout, 1000) + + if err := scanner.Start(); err != nil { + log.Printf("Error starting scanner: %v", err) + return + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + results, err := scanner.Scan(ctx, ports) + if err != nil { + log.Printf("Error scanning: %v", err) + return + } + + fmt.Printf("Manual scanner results for %s:\n", target) + for _, result := range results { + // Use the scanner's output method or custom formatting + if result.IsHostUp() { + fmt.Printf(" Host %s is UP (latency: %v)\n", result.Host.String(), result.Latency) + for _, port := range result.Open { + fmt.Printf(" Port %d: OPEN\n", port) + } + } else { + fmt.Printf(" Host %s is DOWN\n", result.Host.String()) + } + } +} + +// Example 6: SYN scan (requires root privileges) +func synScanExample() { + target := "scanme.nmap.org" + ports := []int{22, 80, 443} + + // Note: This will only work if running as root + results, err := scan.SimpleSynScan(target, ports) + if err != nil { + log.Printf("SYN scan error (may need root privileges): %v", err) + return + } + + fmt.Printf("SYN scan results for %s:\n", target) + for _, result := range results { + if result.IsHostUp() { + fmt.Printf(" Host: %s (UP)\n", result.Host.String()) + fmt.Printf(" Open: %v\n", result.Open) + fmt.Printf(" Closed: %v\n", result.Closed) + fmt.Printf(" Filtered: %v\n", result.Filtered) + } + } +} \ No newline at end of file diff --git a/scan/library.go b/scan/library.go new file mode 100644 index 0000000..9b004dd --- /dev/null +++ b/scan/library.go @@ -0,0 +1,141 @@ +package scan + +import ( + "context" + "time" +) + +// LibraryConfig provides a simple configuration for library usage +type LibraryConfig struct { + TimeoutMS int // Timeout in milliseconds (default: 2000) + Workers int // Number of parallel workers (default: 500) + ScanType string // "syn", "connect", or "device" (default: "connect") + Retries int // Number of retries (default: 1) +} + +// DefaultLibraryConfig returns a sensible default configuration for library usage +func DefaultLibraryConfig() LibraryConfig { + return LibraryConfig{ + TimeoutMS: 2000, + Workers: 500, + ScanType: "connect", // Connect scan doesn't require root + Retries: 1, + } +} + +// NewScannerFromConfig creates a scanner from a configuration +func NewScannerFromConfig(target string, config LibraryConfig) (Scanner, error) { + ti := NewTargetIterator(target) + timeout := time.Duration(config.TimeoutMS) * time.Millisecond + + switch config.ScanType { + case "syn", "stealth": + return NewSynScanner(ti, timeout, config.Workers), nil + case "connect": + return NewConnectScanner(ti, timeout, config.Workers), nil + case "device": + return NewDeviceScanner(ti, timeout), nil + default: + return NewConnectScanner(ti, timeout, config.Workers), nil + } +} + +// SimpleConnectScan performs a simple connect scan with sensible defaults +func SimpleConnectScan(target string, ports []int) ([]Result, error) { + return SimpleConnectScanWithTimeout(target, ports, 2000) +} + +// SimpleConnectScanWithTimeout performs a connect scan with specified timeout in milliseconds +func SimpleConnectScanWithTimeout(target string, ports []int, timeoutMS int) ([]Result, error) { + config := DefaultLibraryConfig() + config.TimeoutMS = timeoutMS + config.ScanType = "connect" + + scanner, err := NewScannerFromConfig(target, config) + if err != nil { + return nil, err + } + + if err := scanner.Start(); err != nil { + return nil, err + } + + ctx := context.Background() + return scanner.Scan(ctx, ports) +} + +// SimpleSynScan performs a simple SYN scan with sensible defaults (requires root) +func SimpleSynScan(target string, ports []int) ([]Result, error) { + return SimpleSynScanWithTimeout(target, ports, 2000) +} + +// SimpleSynScanWithTimeout performs a SYN scan with specified timeout in milliseconds (requires root) +func SimpleSynScanWithTimeout(target string, ports []int, timeoutMS int) ([]Result, error) { + config := DefaultLibraryConfig() + config.TimeoutMS = timeoutMS + config.ScanType = "syn" + + scanner, err := NewScannerFromConfig(target, config) + if err != nil { + return nil, err + } + + if err := scanner.Start(); err != nil { + return nil, err + } + + ctx := context.Background() + return scanner.Scan(ctx, ports) +} + +// QuickPortCheck checks if specific ports are open on a target (uses connect scan) +func QuickPortCheck(target string, ports ...int) (map[int]bool, error) { + results, err := SimpleConnectScan(target, ports) + if err != nil { + return nil, err + } + + portStatus := make(map[int]bool) + + for _, result := range results { + // Initialize all ports as closed + for _, port := range ports { + portStatus[port] = false + } + + // Mark open ports as true + for _, openPort := range result.Open { + portStatus[openPort] = true + } + } + + return portStatus, nil +} + +// IsHostUp checks if a host is reachable by attempting to connect to common ports +func IsHostUp(target string) (bool, error) { + commonPorts := []int{22, 23, 25, 53, 80, 110, 143, 443, 993, 995} + + results, err := SimpleConnectScanWithTimeout(target, commonPorts, 3000) + if err != nil { + return false, err + } + + for _, result := range results { + if result.IsHostUp() { + return true, nil + } + } + + return false, nil +} + +// ScanCommonPorts scans the most common ports on a target +func ScanCommonPorts(target string) ([]Result, error) { + commonPorts := []int{ + 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, + 1723, 3306, 3389, 5432, 5900, 8080, 8443, + } + + return SimpleConnectScan(target, commonPorts) +} \ No newline at end of file diff --git a/scan/library_test.go b/scan/library_test.go new file mode 100644 index 0000000..ac063da --- /dev/null +++ b/scan/library_test.go @@ -0,0 +1,179 @@ +package scan + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultLibraryConfig(t *testing.T) { + config := DefaultLibraryConfig() + + assert.Equal(t, 2000, config.TimeoutMS) + assert.Equal(t, 500, config.Workers) + assert.Equal(t, "connect", config.ScanType) + assert.Equal(t, 1, config.Retries) +} + +func TestNewScannerFromConfig(t *testing.T) { + target := "127.0.0.1" + + // Test connect scanner creation + config := LibraryConfig{ + TimeoutMS: 1000, + Workers: 100, + ScanType: "connect", + Retries: 1, + } + + scanner, err := NewScannerFromConfig(target, config) + require.NoError(t, err) + assert.NotNil(t, scanner) + + // Verify it's a ConnectScanner + connectScanner, ok := scanner.(*ConnectScanner) + assert.True(t, ok, "Should create a ConnectScanner") + assert.Equal(t, time.Second, connectScanner.timeout) + assert.Equal(t, 100, connectScanner.maxRoutines) +} + +func TestNewScannerFromConfigSyn(t *testing.T) { + target := "127.0.0.1" + + // Test SYN scanner creation + config := LibraryConfig{ + TimeoutMS: 2000, + Workers: 200, + ScanType: "syn", + Retries: 2, + } + + scanner, err := NewScannerFromConfig(target, config) + require.NoError(t, err) + assert.NotNil(t, scanner) + + // Verify it's a SynScanner + synScanner, ok := scanner.(*SynScanner) + assert.True(t, ok, "Should create a SynScanner") + assert.Equal(t, 2*time.Second, synScanner.timeout) + assert.Equal(t, 200, synScanner.maxRoutines) +} + +func TestNewScannerFromConfigDevice(t *testing.T) { + target := "127.0.0.1" + + // Test device scanner creation + config := LibraryConfig{ + TimeoutMS: 3000, + Workers: 50, // Not used for device scanner + ScanType: "device", + Retries: 1, + } + + scanner, err := NewScannerFromConfig(target, config) + require.NoError(t, err) + assert.NotNil(t, scanner) + + // Verify it's a DeviceScanner + deviceScanner, ok := scanner.(*DeviceScanner) + assert.True(t, ok, "Should create a DeviceScanner") + assert.Equal(t, 3*time.Second, deviceScanner.timeout) +} + +func TestSimpleConnectScan(t *testing.T) { + // Test with localhost (should be reachable) + target := "127.0.0.1" + ports := []int{22, 80} // Common ports that might be open + + results, err := SimpleConnectScan(target, ports) + require.NoError(t, err) + require.Len(t, results, 1) + + result := results[0] + assert.Equal(t, target, result.Host.String()) + + // Should account for all scanned ports + totalPorts := len(result.Open) + len(result.Closed) + len(result.Filtered) + assert.Equal(t, len(ports), totalPorts, "All scanned ports should be accounted for") +} + +func TestQuickPortCheck(t *testing.T) { + // Test with a reliable host + target := "8.8.8.8" + ports := []int{53, 80} // DNS should be open, HTTP might be + + portStatus, err := QuickPortCheck(target, ports...) + require.NoError(t, err) + + // Should have status for all requested ports + assert.Len(t, portStatus, len(ports)) + + for _, port := range ports { + _, exists := portStatus[port] + assert.True(t, exists, "Should have status for port %d", port) + } +} + +func TestIsHostUp(t *testing.T) { + // Test with known good host + isUp, err := IsHostUp("8.8.8.8") + require.NoError(t, err) + assert.True(t, isUp, "8.8.8.8 should be up") + + // Test with likely non-existent host + isUp, err = IsHostUp("192.0.2.1") // RFC5737 test IP + // This might error or return false, both are acceptable + if err == nil { + assert.False(t, isUp, "Test IP should not be up") + } +} + +func TestScanCommonPorts(t *testing.T) { + // Test with localhost + target := "127.0.0.1" + + results, err := ScanCommonPorts(target) + require.NoError(t, err) + require.Len(t, results, 1) + + result := results[0] + assert.Equal(t, target, result.Host.String()) + + // Should have scanned the common ports (22 ports defined in the function) + totalPorts := len(result.Open) + len(result.Closed) + len(result.Filtered) + assert.Equal(t, 22, totalPorts, "Should scan all common ports") +} + +func TestTimeoutConversion(t *testing.T) { + // Test that timeout conversion works correctly + config := LibraryConfig{ + TimeoutMS: 5000, // 5 seconds + Workers: 100, + ScanType: "connect", + } + + scanner, err := NewScannerFromConfig("127.0.0.1", config) + require.NoError(t, err) + + connectScanner := scanner.(*ConnectScanner) + expectedTimeout := 5 * time.Second + assert.Equal(t, expectedTimeout, connectScanner.timeout) +} + +func TestInvalidScanType(t *testing.T) { + // Test with invalid scan type - should default to connect + config := LibraryConfig{ + TimeoutMS: 1000, + Workers: 100, + ScanType: "invalid", + } + + scanner, err := NewScannerFromConfig("127.0.0.1", config) + require.NoError(t, err) + + // Should default to ConnectScanner + _, ok := scanner.(*ConnectScanner) + assert.True(t, ok, "Should default to ConnectScanner for invalid scan type") +} \ No newline at end of file