005 Project 5: Concurrent Port Scanner

005 Build a Concurrent Port Scanner

This CLI scans a host across a port range using a bounded worker pool.

ports range -> jobs channel -> workers dial tcp -> open ports channel -> sorted output

Full main.go

package main

import (
    "flag"
    "fmt"
    "net"
    "os"
    "sort"
    "sync"
    "time"
)

func scan(host string, timeout time.Duration, jobs <-chan int, openPorts chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for p := range jobs {
        addr := fmt.Sprintf("%s:%d", host, p)
        conn, err := net.DialTimeout("tcp", addr, timeout)
        if err == nil {
            _ = conn.Close()
            openPorts <- p
        }
    }
}

func main() {
    host := flag.String("host", "127.0.0.1", "target host")
    from := flag.Int("from", 1, "start port")
    to := flag.Int("to", 1024, "end port")
    workers := flag.Int("w", 200, "worker count")
    timeout := flag.Duration("t", 300*time.Millisecond, "dial timeout")
    flag.Parse()

    if *from < 1 || *to > 65535 || *from > *to {
        fmt.Fprintln(os.Stderr, "invalid port range")
        os.Exit(2)
    }

    jobs := make(chan int, *workers)
    openPorts := make(chan int, 1024)
    var wg sync.WaitGroup

    for i := 0; i < *workers; i++ {
        wg.Add(1)
        go scan(*host, *timeout, jobs, openPorts, &wg)
    }

    go func() {
        for p := *from; p <= *to; p++ {
            jobs <- p
        }
        close(jobs)
        wg.Wait()
        close(openPorts)
    }()

    var open []int
    for p := range openPorts {
        open = append(open, p)
    }
    sort.Ints(open)

    fmt.Printf("open ports on %s:\n", *host)
    for _, p := range open {
        fmt.Println(p)
    }
}

Run

go run . -host scanme.nmap.org -from 1 -to 2000 -w 300 -t 200ms

What to Extend

  1. CIDR support for scanning multiple hosts.
  2. Banner grabbing for open ports.
  3. Rate limiting to avoid overwhelming targets.

Step-by-Step Explanation

  1. Model jobs, workers, and outputs explicitly.
  2. Bound concurrency using worker pools and buffered channels.
  3. Use sync.WaitGroup for lifecycle control.
  4. Aggregate worker results in one place.
  5. Verify behavior under both normal and failure paths.

Code Anatomy

  • Producer pushes jobs into a channel.
  • Workers consume jobs and emit results.
  • Aggregator merges results and prints summary.

Learning Goals

  • Build leak-free goroutine patterns.
  • Balance throughput and resource limits.
  • Understand fan-out/fan-in architecture.