Repository: projectdiscovery/naabu (Go)

Issue:
Nil pointer error after concurrent scanning with goroutines
### Naabu version:
Naabu v2.3.4 (latest)

### Current Behavior:
When running Naabu to scan IPs and subdomains concurrently, it crashes with the following error:
```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x1023edb14]

goroutine 55 [running]:
net.(*IPConn).WriteTo(0x0?, {0x140009f1668?, 0x14001a40070?, 0x14000f3fe40?}, {0x104baf4f8?, 0x1400141d7a0?})
        /opt/homebrew/Cellar/go/1.24.1/libexec/src/net/iprawsock.go:171 +0x24
github.com/projectdiscovery/naabu/v2/pkg/scan.sendWithConn({0x14001a40070, 0xd}, {0x104bc0720, 0x0}, {0x14000f3fe40, 0x1, 0x1})
        /Users/walid/go/pkg/mod/github.com/projectdiscovery/naabu/v2@v2.3.4/pkg/scan/scan_unix.go:457 +0x1cc
github.com/projectdiscovery/naabu/v2/pkg/scan.sendAsyncTCP4(0x14001c100a0, {0x14001a40070, 0xd}, 0x140022316b0, 0x0)
        /Users/walid/go/pkg/mod/github.com/projectdiscovery/naabu/v2@v2.3.4/pkg/scan/scan_unix.go:244 +0x6fc
github.com/projectdiscovery/naabu/v2/pkg/scan.SendAsyncPkg(0x14001c100a0, {0x14001a40070, 0xd}, 0x140022316b0, 0x0)
        /Users/walid/go/pkg/mod/github.com/projectdiscovery/naabu/v2@v2.3.4/pkg/scan/scan_unix.go:163 +0xe4
github.com/projectdiscovery/naabu/v2/pkg/scan.TransportWriteWorker()
        /Users/walid/go/pkg/mod/github.com/projectdiscovery/naabu/v2@v2.3.4/pkg/scan/scan_unix.go:151 +0x4c
created by github.com/projectdiscovery/naabu/v2/pkg/scan.init.3 in goroutine 1
        /Users/walid/go/pkg/mod/github.com/projectdiscovery/naabu/v2@v2.3.4/pkg/scan/scan_unix.go:83 +0x3a4
exit status 2
```

### Steps To Reproduce:
Here's the Naabu config being used:
```
func ScanHost(ctx context.Context, host string, resultChann chan utils.ScanResult) error {
	options := runner.Options{
		Host:              goflags.StringSlice{host},
		ScanType:          "s",
		WithHostDiscovery: false,
		OnResult: func(hr *result.HostResult) {
			for _, port := range hr.Ports {
				resultChann <- utils.ScanResult{
					IP:       hr.IP,
					Port:     uint32(port.Port),
					Hostname: hr.Host,
					IsNew:    true,
				}
			}
		},
		Rate:     10000,
		TopPorts: "1000",
	}

	naabuRunner, err := runner.NewRunner(&options)
	if err != nil {
		return fmt.Errorf("Error running naabu: %v\n", err)
	}
	defer naabuRunner.Close()

	err = naabuRunner.RunEnumeration(ctx)
	if err != nil {
		return fmt.Errorf("Error enumerating: %v\n", err)
	}

	return nil
}
```
This function is called inside multiple goroutines, scanning subdomains and external IPs:
```
// Retrieve subdomains
	wg.Add(1)
	go func() {
		logger.Info("enumerating subdomains")

		subdomains, err := subfinder.EnumerateSubDomains(conf.Subfinder.Domains, ctx)
		if err != nil {
			errors <- fmt.Errorf("could not enumerate subdomains: %w", err)
			return
		}

		// Ensure subdomains is not nil before processing
		if subdomains == nil {
			errors <- fmt.Errorf("no subdomains retrieved")
			return
		}

		for _, subdomain := range subdomains {
			logger.Infof("Scanning %s...\n", subdomain)
			if err := naabu.ScanHost(ctx, subdomain, scanResultChann); err != nil {
				if strings.Contains(err.Error(), "no valid ipv4 or ipv6 targets were found") {
					logger.Warningf("no valid ipv4 or ipv6 targets were found: %s", subdomain)
					continue
				}
				errors <- fmt.Errorf("error retrieving open ports for subdomain %s: %v", subdomain, err)
			}
		}

		wg.Done()
	}()

	// Retrieve host details
	wg.Add(1)
	go func() {
		logger.Info("retrieving crowdstrike host details")

		hostDetails, hostErr := cs.GetHostDetails(ctx)
		if hostErr != nil {
			errors <- fmt.Errorf("could not get host details: %w", hostErr)
			return
		}

		// Ensure hostDetails is not nil before processing
		if hostDetails == nil {
			errors <- fmt.Errorf("no host details retrieved")
			return
		}

		// Scan open ports for the collected hosts
		for _, device := range hostDetails {
			if device.ExternalIP == "" || net.ParseIP(device.ExternalIP) == nil {
				logger.WithField("host", device.Hostname).Debug("skipping host without externalIP")
				continue
			}

			logger.Debugf("Scanning %s for %s...\n", device.ExternalIP, device.Hostname)

			logger.WithField("existingEntries", len(existingEntries)).Debug("scanning with naabu")

			err := naabu.ScanHost(ctx, device.ExternalIP, scanResultChann)
			if err != nil {
				if strings.Contains(err.Error(), "no valid ipv4 or ipv6 targets were found") {
					logger.Warningf("no valid ipv4 or ipv6 targets were found: %s\n", device.ExternalIP)
					continue
				}
				errors <- fmt.Errorf("error retrieving open ports for %s: %v", device.ExternalIP, err)
			}

			logger.WithField("existingEntries", len(existingEntries)).Debug("done scanning naabu")
		}
		wg.Done()
	}()

	go func() {
		wg.Wait()
		logger.Debug("closing doneChann")
		close(scanResultChann)
		close(doneChann)
		logger.Debug("closed doneChann")
	}()
```


Discussion:
Any update? For reference I am scanning around 450 subdomains and 200 IPs



Patch:
diff --git a/pkg/scan/scan_unix.go b/pkg/scan/scan_unix.go
index b416c708..bd550a40 100644
--- a/pkg/scan/scan_unix.go
+++ b/pkg/scan/scan_unix.go
@@ -250,8 +250,14 @@ func sendAsyncTCP4(listenHandler *ListenHandler, ip string, p *port.Port, pkgFla
 	if hasSourceIp && listenHandler.SourceHW != nil && iface != nil {
 		err = sendWithHandler(ip, iface, &eth, &ip4, &tcp)
 	} else {
+		if listenHandler.TcpConn4 == nil {
+			gologger.Debug().Msgf("TcpConn4 is nil, cannot send packet to %s:%d\n", ip, p.Port)
+			return
+		}
+
 		err = sendWithConn(ip, listenHandler.TcpConn4, &tcp)
 	}
+
 	if err != nil {
 		gologger.Debug().Msgf("Can not send packet to %s:%d port: %s\n", ip, p.Port, err)
 	}
@@ -289,6 +295,11 @@ func sendAsyncUDP4(listenHandler *ListenHandler, ip string, p *port.Port, pkgFla
 	if err != nil {
 		gologger.Debug().Msgf("Can not set network layer for %s:%d port: %s\n", ip, p.Port, err)
 	} else {
+		if listenHandler.UdpConn4 == nil {
+			gologger.Debug().Msgf("UdpConn4 is nil, cannot send packet to %s:%d\n", ip, p.Port)
+			return
+		}
+
 		err = sendWithConn(ip, listenHandler.UdpConn4, &udp)
 		if err != nil {
 			gologger.Debug().Msgf("Can not send packet to %s:%d port: %s\n", ip, p.Port, err)
@@ -345,6 +356,11 @@ func sendAsyncTCP6(listenHandler *ListenHandler, ip string, p *port.Port, pkgFla
 	if err != nil {
 		gologger.Debug().Msgf("Can not set network layer for %s:%d port: %s\n", ip, p.Port, err)
 	} else {
+		if listenHandler.TcpConn6 == nil {
+			gologger.Debug().Msgf("TcpConn6 is nil, cannot send packet to %s:%d\n", ip, p.Port)
+			return
+		}
+
 		err = sendWithConn(ip, listenHandler.TcpConn6, &tcp)
 		if err != nil {
 			gologger.Debug().Msgf("Can not send packet to %s:%d port: %s\n", ip, p.Port, err)
@@ -385,6 +401,11 @@ func sendAsyncUDP6(listenHandler *ListenHandler, ip string, p *port.Port, pkgFla
 	if err != nil {
 		gologger.Debug().Msgf("Can not set network layer for %s:%d port: %s\n", ip, p.Port, err)
 	} else {
+		if listenHandler.UdpConn6 == nil {
+			gologger.Debug().Msgf("UdpConn6 is nil, cannot send packet to %s:%d\n", ip, p.Port)
+			return
+		}
+
 		err = sendWithConn(ip, listenHandler.UdpConn6, &udp)
 		if err != nil {
 			gologger.Debug().Msgf("Can not send packet to %s:%d port: %s\n", ip, p.Port, err)
@@ -456,60 +477,60 @@ var defaultSerializeOptions = gopacket.SerializeOptions{
 
 // send sends the given layers as a single packet on the network.
 func sendWithConn(destIP string, conn net.PacketConn, l ...gopacket.SerializableLayer) error {
+	var err error
+
 	buf := gopacket.NewSerializeBuffer()
 	if err := gopacket.SerializeLayers(buf, defaultSerializeOptions, l...); err != nil {
 		return err
 	}
+	data := buf.Bytes()
+	addr := &net.IPAddr{IP: net.ParseIP(destIP)}
 
-	var (
-		retries int
-		err     error
-	)
+	for retries := 0; retries < maxRetries; retries++ {
+		_, err = conn.WriteTo(data, addr)
+		if err == nil {
+			return nil
+		}
 
-send:
-	if retries >= maxRetries {
-		return err
+		time.Sleep(time.Duration(sendDelayMsec) * time.Millisecond)
 	}
-	_, err = conn.WriteTo(buf.Bytes(), &net.IPAddr{IP: net.ParseIP(destIP)})
+
 	if err != nil {
-		retries++
-		// introduce a small delay to allow the network interface to flush the queue
-		time.Sleep(time.Duration(sendDelayMsec) * time.Millisecond)
-		goto send
+		return fmt.Errorf("could not send packet to %s: %s", destIP, err)
 	}
-	return err
+
+	return nil
 }
 
 func sendWithHandler(destIP string, iface *net.Interface, l ...gopacket.SerializableLayer) error {
+	var err error
+
 	buf := gopacket.NewSerializeBuffer()
 	if err := gopacket.SerializeLayers(buf, defaultSerializeOptions, l...); err != nil {
 		return err
 	}
-
-	var (
-		retries int
-		err     error
-	)
+	data := buf.Bytes()
 
 	// find the correct handler
 	handler, ok := handlers.InterfaceHandle[iface.Name]
-	if !ok {
+	if handler == nil || !ok {
 		return errors.New("could not find correct pcap handler")
 	}
 
-send:
+	for retries := 0; retries < maxRetries; retries++ {
+		err = handler.WritePacketData(data)
+		if err == nil {
+			return nil
+		}
 
-	if retries >= maxRetries {
-		return err
+		time.Sleep(time.Duration(sendDelayMsec) * time.Millisecond)
 	}
-	err = handler.WritePacketData(buf.Bytes())
+
 	if err != nil {
-		retries++
-		// introduce a small delay to allow the network interface to flush the queue
-		time.Sleep(time.Duration(sendDelayMsec) * time.Millisecond)
-		goto send
+		return fmt.Errorf("could not send packet to %s: %s", destIP, err)
 	}
-	return err
+
+	return nil
 }
 
 func (l *ListenHandler) TcpReadWorker4() {


Test patch:
diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go
index 7299fff8..3a81b433 100644
--- a/pkg/runner/runner_test.go
+++ b/pkg/runner/runner_test.go
@@ -6,7 +6,9 @@ import (
 	"log"
 	"net"
 	"os"
+	"runtime"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
@@ -874,3 +876,65 @@ func TestRunnerConnectVerification(t *testing.T) {
 	}
 	require.Equal(t, 1, count)
 }
+
+func TestConcurrentSYNScans(t *testing.T) {
+	if os.Geteuid() != 0 {
+		t.Skip("Test requires root privileges")
+	}
+
+	targets := []string{"127.0.0.1", "localhost", "scanme.sh"}
+	numGoroutines := runtime.NumCPU()
+
+	var wg sync.WaitGroup
+	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+	defer cancel()
+
+	errChan := make(chan error, numGoroutines)
+
+	for i := 0; i < numGoroutines; i++ {
+		wg.Add(1)
+		go func(target string) {
+			defer wg.Done()
+			select {
+			case <-ctx.Done():
+				return // Stop if context is cancelled
+			default:
+			}
+
+			options := &Options{
+				Host:     []string{target},
+				ScanType: SynScan,
+				Ports:    "80,443,8080",
+				Retries:  5,
+				Verbose:  false,
+				Silent:   true,
+			}
+
+			naabuRunner, err := NewRunner(options)
+			if err != nil {
+				t.Logf("Error creating runner for %s: %v", target, err)
+				errChan <- fmt.Errorf("runner creation failed for %s: %w", target, err)
+				return
+			}
+
+			defer func() {
+				closeErr := naabuRunner.Close()
+				if closeErr != nil {
+					t.Logf("Error closing runner for %s: %v", target, closeErr)
+				}
+			}()
+
+			runErr := naabuRunner.RunEnumeration(ctx)
+			if runErr != nil {
+				errChan <- fmt.Errorf("enumeration failed for %s: %w", target, runErr)
+			}
+		}(targets[i%len(targets)]) // cycle thru targets
+	}
+
+	wg.Wait()
+	close(errChan)
+
+	for err := range errChan {
+		t.Error(err)
+	}
+}