Search code examples
gonetworkingtcpport-scanning

Unreliable results when scanning multiple ports concurrently


Background:

I was reading through Black Hat Go where the author presents a simple port scanner that uses go routines1:

package main

import (
    "fmt"
    "net"
)

func main() {
    for i := 1; i <= 9000; i++ {
        go func(j int) {
            address := fmt.Sprintf("127.0.0.1:%d", j)
            conn, err := net.Dial("tcp", address)
            if err != nil {
                return
            }
            conn.Close()
            fmt.Printf("%d open\n", j)
        }(i)
    }
}

And then he mentions the following:

Scanning an excessive number of hosts or ports simultaneously may cause network or system limitations to skew your results.

To test it, I started 2 php servers2 on ports 8000 and 8500 and ran the above code to scan my local ports.

Each time it gave me inconsistent results. Sometimes it'd detect both the open ports, sometimes it would not.

Question:

  1. Are the inconsistent results due to some limitations in TCP?

  2. Is there a way to calculate the optimal number of ports that can be scanned in parallel so that the results remain correct?

Edit:

I seem to have missed out waitgroups in the above code.

Besides that, is there anything else (OS limitation or protocol limitation) that prevents concurrent port scans over a large range?


Solution

  • Your main function will exit as soon as the for loop has finished. If the main function exits, so do all goroutines started by it. You need to wait for the goroutines to finish. That can be achieved this via sync.WaitGroup, for example.

    package main
    
    import (
        "fmt"
        "net"
        "sync"
        "time"
    )
    
    func main() {
    
        // This will help you to keep track of the goroutines
        var wg sync.WaitGroup
    
        for i := 1; i <= 9000; i++ {
    
            // Increment the counter for each goroutine you start.
            wg.Add(1)
    
            go func(j int) {
    
                // Make sure the wait group counter is decremented when the goroutine exits
                defer wg.Done()
    
                address := fmt.Sprintf("127.0.0.1:%d", j)
                conn, err := net.DialTimeout("tcp", address, 2 * time.Second)
                if err != nil {
                   return
                }
                conn.Close()
                fmt.Printf("%d open\n", j)
            }(i)
        }
    
        // Wait for all goroutines to finish before exiting main
        wg.Wait()
    }
    

    EDIT: For me, it turned out that the code as is did not work due to a lack of file descriptors. The following function works reliably.

    It needs better error handling, but does the trick

    package main
    
    import (
        "fmt"
        "log"
        "net"
        "sync"
        "time"
    )
    
    var minPort = 1
    var maxPort = 65535
    var timeout = 2 * time.Second
    const parallel = 50
    
    func main(){
        fmt.Println("portscan called")
    
        // Create a buffered channel with a size equal to the number of goroutines
        ctrl := make(chan int, parallel)
    
        // Keep track of the currently active goroutines
        var wg sync.WaitGroup
    
        for p := 1; p <= parallel; p++ {
            wg.Add(1)
    
            // Start a goroutine...
            go func(p int) {
                log.Printf("Starting goroutine %d", p)
    
                // ...listening to the control channel.
                // For every value this goroutine reads from the
                // channel...
                for i := range ctrl {
                    address := fmt.Sprintf("127.0.0.1:%d", i)
    
                    // ...try to conncet to the port.
                    conn, err := net.DialTimeout("tcp", address, timeout)
                    if err == nil {
                        conn.Close()
                        log.Printf("[%3d]: %5d open", p, i)
                    }
                    // TBD: ERROR HANDLING!!!
                }
    
                // If the channel is closed, this goroutine is done.
                wg.Done()
                log.Printf("[%3d]: Exiting", p)
            }(p)
        }
    
        // Fill the control channel with values.
        // If the channel is full, the write operation
        // to the channel will block until one of the goroutines
        // reads a value from it.
        for i := minPort; i <= maxPort; i++ {
            ctrl <- i
        }
        // We have sent all values, so the channel can be closed.
        // The goroutines will finish their current connection attempt,
        // notice that the channel is closed and will in turn call wg.Done().
        close(ctrl)
    
        // When all goroutines have announced that they are done, we can exit.
        wg.Wait()
    }