Please purchase the course to watch this video.

Full Course
Creating a simple port scanner is an excellent way to learn about networking in Go. This project involves using the net
package to identify open ports on a specified server, iterating through all possible ports and establishing TCP connections to check their status. Error handling is included, although more robust solutions would be advisable for production use. To improve efficiency, the implementation utilizes concurrency, allowing multiple ports to be scanned simultaneously. A timeout mechanism prevents the operation from hanging on unresponsive ports, while chunking techniques help manage the number of simultaneous connections to avoid overwhelming the server. This approach lays a foundational understanding of network programming, with references to more complex topics like HTTP requests hinted at for future exploration.
No links available for this lesson.
A common beginner networking project is a port scanner - a tool that checks which ports are open on a given server. This practical application of the net
package demonstrates network programming concepts and helps us understand how services are exposed on a network.
Note: Port scanning should only be performed on servers you own or have explicit permission to scan. Unauthorized port scanning may violate laws or terms of service in many jurisdictions.
Basic Port Scanner
Let's start by creating a simple port scanner that checks each port sequentially:
package main
import (
"fmt"
"net"
"os"
)
func main() {
// Get the host from command-line arguments
if len(os.Args) < 2 {
fmt.Println("Please provide a host to scan")
os.Exit(1)
}
host := os.Args[1]
// Scan all ports (0-65535)
for port := 1; port <= 65535; port++ {
// Format the address string (e.g., "example.com:80")
address := fmt.Sprintf("%s:%d", host, port)
// Try to establish a connection
conn, err := net.Dial("tcp", address)
// If successful, the port is open
if err == nil {
fmt.Printf("Port %d is open\n", port)
conn.Close()
}
}
}
This basic scanner:
- Takes a hostname or IP address as a command-line argument
- Iterates through all ports (1-65535)
- Attempts to establish a TCP connection to each port
- Reports which ports are open
To test this on your local machine:
$ go run main.go localhost
Adding Timeouts
Our basic scanner has a major problem: connecting to closed or filtered ports can take a long time to time out. Let's add a timeout to make the scanner more efficient:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a host to scan")
os.Exit(1)
}
host := os.Args[1]
for port := 1; port <= 65535; port++ {
address := fmt.Sprintf("%s:%d", host, port)
// Use DialTimeout instead of Dial
conn, err := net.DialTimeout("tcp", address, time.Second)
// Output the current port for progress monitoring
fmt.Printf("\rScanning port %d...", port)
if err == nil {
fmt.Printf("\nPort %d is open\n", port)
conn.Close()
}
}
fmt.Println("\nScan complete")
}
The key change is using net.DialTimeout
instead of net.Dial
, which allows us to specify a maximum time to wait for a connection to be established.
Concurrent Port Scanning
Our scanner is still quite slow, checking only one port per second. Let's use goroutines to scan multiple ports concurrently:
package main
import (
"fmt"
"net"
"os"
"sync"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a host to scan")
os.Exit(1)
}
host := os.Args[1]
var wg sync.WaitGroup
// Scan all ports concurrently
for port := 1; port <= 65535; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", host, p)
conn, err := net.DialTimeout("tcp", address, time.Second)
if err == nil {
fmt.Printf("Port %d is open\n", p)
conn.Close()
}
}(port)
}
wg.Wait()
fmt.Println("Scan complete")
}
However, this approach has a problem: it launches 65,535 goroutines simultaneously, which can overwhelm system resources and lead to errors like "too many open files" or connection timeouts.
Controlled Concurrency with Chunking
To limit the number of simultaneous connections, we'll use a "chunking" approach to process ports in batches:
package main
import (
"fmt"
"net"
"os"
"sync"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a host to scan")
os.Exit(1)
}
host := os.Args[1]
// Create a slice with all ports
var ports []int
for port := 1; port <= 65535; port++ {
ports = append(ports, port)
}
// Process ports in chunks to control concurrency
chunkSize := 100
for i := 0; i < len(ports); i += chunkSize {
end := i + chunkSize
if end > len(ports) {
end = len(ports)
}
chunk := ports[i:end]
var wg sync.WaitGroup
for _, port := range chunk {
wg.Add(1)
go func(p int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", host, p)
conn, err := net.DialTimeout("tcp", address, time.Second)
if err == nil {
fmt.Printf("Port %d is open\n", p)
conn.Close()
}
}(port)
}
wg.Wait()
fmt.Printf("\rScanned ports %d-%d", i+1, end)
}
fmt.Println("\nScan complete")
}
This approach:
- Creates a slice with all port numbers
- Processes ports in chunks (e.g., 100 ports at a time)
- Uses goroutines for concurrency within each chunk
- Waits for each chunk to complete before processing the next
A More Efficient Implementation with Worker Pools
For a more efficient implementation, we can use a worker pool pattern:
package main
import (
"fmt"
"net"
"os"
"sync"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a host to scan")
os.Exit(1)
}
host := os.Args[1]
// Number of worker goroutines
numWorkers := 100
// Create a channel to communicate ports to scan
ports := make(chan int, numWorkers)
// Create a WaitGroup to wait for all workers to finish
var wg sync.WaitGroup
// Launch worker goroutines
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Process ports from the channel
for port := range ports {
address := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", address, time.Second)
if err == nil {
fmt.Printf("Port %d is open\n", port)
conn.Close()
}
}
}()
}
// Send ports to the channel
for port := 1; port <= 65535; port++ {
ports <- port
}
// Close the channel when all ports have been sent
close(ports)
// Wait for all worker goroutines to finish
wg.Wait()
fmt.Println("Scan complete")
}
This implementation:
- Creates a fixed number of worker goroutines
- Uses a channel to distribute ports to the workers
- Provides better control over resource usage
Adding Progress Reporting
Let's enhance our port scanner with progress reporting:
package main
import (
"fmt"
"net"
"os"
"sync"
"sync/atomic"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a host to scan")
os.Exit(1)
}
host := os.Args[1]
// Number of worker goroutines
numWorkers := 100
// Total number of ports to scan
totalPorts := 65535
// Counters for progress and results
var scanned int64
var open []int
var mutex sync.Mutex
// Create a channel to communicate ports to scan
ports := make(chan int, numWorkers)
// Create a WaitGroup to wait for all workers to finish
var wg sync.WaitGroup
// Start a goroutine to display progress
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
default:
completed := atomic.LoadInt64(&scanned)
percent := float64(completed) / float64(totalPorts) * 100
fmt.Printf("\rProgress: %.2f%% (%d/%d)", percent, completed, totalPorts)
time.Sleep(200 * time.Millisecond)
}
}
}()
// Launch worker goroutines
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for port := range ports {
address := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", address, time.Second)
if err == nil {
mutex.Lock()
open = append(open, port)
mutex.Unlock()
conn.Close()
}
atomic.AddInt64(&scanned, 1)
}
}()
}
// Send ports to the channel
for port := 1; port <= totalPorts; port++ {
ports <- port
}
// Close the channel when all ports have been sent
close(ports)
// Wait for all worker goroutines to finish
wg.Wait()
// Signal the progress goroutine to stop
close(done)
// Clear the progress line and report results
fmt.Printf("\r%s\r", " ")
fmt.Printf("Scan of %s complete. Found %d open ports:\n", host, len(open))
for _, port := range open {
fmt.Printf(" %d\n", port)
}
}
This enhanced version:
- Displays real-time progress as a percentage
- Collects and reports all open ports at the end
- Uses atomic operations to safely update progress counters
Summary
In this lesson, we've learned:
- How to create a basic port scanner using Go's
net
package - How to add timeouts to prevent hanging on filtered ports
- How to use goroutines for concurrent scanning
- How to control concurrency to avoid overwhelming resources
- How to implement a worker pool pattern for efficient scanning
- How to add progress reporting to long-running operations
Port scanning is a practical application of network programming that demonstrates important concepts like:
- Establishing TCP connections
- Handling network timeouts
- Concurrent network operations
- Resource management
In the next lesson, we'll explore HTTP networking with Go's net/http
package, building on these foundational networking concepts.