Let’s Build a “Simple and Fast” Port Scanner in Go
What is a Port Scanner?
Simply a port scanner is an application or a tool, that use to identify the open ports in a network or a server. In a nutshell, port scanners send data packets to relevant port numbers and check for responses to determine whether a particular port is open or closed.
Why Golang?
Golang is a programming language developed by Google, that is open-source, compiled, and statically typed. It was developed to be a readable, efficient, and high-performing programming language with simplicity in mind.
Go has concurrency support, which allows for functions to run simultaneously and independently, known as goroutines. These goroutines require only 2 kB of memory each, making the language scalable and efficient for running multiple concurrent processes.
In our case, When using a port scanner, data packets must be sent to specific port numbers and responses must be checked in order to determine whether a port is open or closed. However, scanning a large number of ports can be a time-consuming process, as each one must be scanned individually.
However, by utilizing Go’s concurrency support, namely “go routines,” we are able to speed up the port scanning process. This feature allows for the concurrent scanning of ports by running multiple processes simultaneously, resulting in a faster scanning time.
Let’s Build It
importing packages
package main
import (
"bufio"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
)
These are the Go packages we are gonna use for this application
- “bufio” — This package implements buffered I/O. It provides a way to read and write data in chunks, which can improve performance when dealing with large amounts of data.
- “fmt” — This package provides formatting and printing functionality for Go programs. It allows for the creation of formatted output strings, as well as the printing of data to the console or other output streams.
- “net” — This package provides a set of functions and types for working with network connections, including TCP, UDP, and IP. It also provides support for working with DNS and other network protocols.
- “os” — This package provides a set of functions and types for working with the operating system, including file and directory operations, process management, and environment variables.
- “strconv” — This package provides functions for converting strings to other types, such as integers and floats, as well as for formatting numbers as strings.
- “strings” — This package provides functions for working with strings, including searching, replacing, and splitting.
- “time” — This package provides functions for working with dates and times, including parsing, formatting, and calculating time differences.
Reading user inputs
reader := bufio.NewReader(os.Stdin)
First defines the “reader” to reads the inputs from the standard input stream
fmt.Print("Enter the address: ")
address, _ := reader.ReadString('\n')
address = strings.TrimSpace(address)
fmt.Print("Enter the protocol (tcp/udp): ")
protocol, _ := reader.ReadString('\n')
protocol = strings.TrimSpace(protocol)
fmt.Print("Enter the starting port: ")
startPortStr, _ := reader.ReadString('\n')
startPortStr = strings.TrimSpace(startPortStr)
startPort, err := strconv.Atoi(startPortStr)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Print("Enter the ending port: ")
endPortStr, _ := reader.ReadString('\n')
endPortStr = strings.TrimSpace(endPortStr)
endPort, err := strconv.Atoi(endPortStr)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
Then after using the “reader” to reading the user inputs one by one and save the user inputs in variables of “address”, “protocol”, “startPortStr” and “endPortStr”.
Here I validate the inputs, once after the user inputs the values to the system.
Calling the function
openPorts := scanPorts(address, protocol, startPort, endPort)
if len(openPorts) == 0 {
fmt.Println("No open ports found")
} else {
fmt.Printf("Open ports: %v\n", openPorts)
}
Here, the “scanPorts” function is invoked after the user inputs the required parameters. These input values are passed into the “scanPorts” function, which will return an array of open ports if any are found.
Writing the scanPorts function
func scanPorts(address string, protocol string, startPort int, endPort int) []int {
openPorts := []int{}
results := make(chan int)
startTime := time.Now()
for port := startPort; port <= endPort; port++ {
go func(port int) {
target := fmt.Sprintf("%s:%d", address, port)
conn, err := net.Dial(protocol, target)
if err == nil {
conn.Close()
results <- port
} else {
results <- 0
}
}(port)
}
for i := 0; i < endPort-startPort+1; i++ {
port := <-results
if port != 0 {
openPorts = append(openPorts, port)
}
}
endTime := time.Now()
duration := endTime.Sub(startTime)
fmt.Print(" \n")
fmt.Printf("Scanned %d ports in %s\n", endPort-startPort+1, duration)
return openPorts
}
In here there are 4 main processes are happening,
- Initialize the “openPorts” array and “results” channel
- Using a for loop, iterate through the range of port numbers between startPort and endPort. Within the loop, create a new goroutine for each port number using the “go” keyword.
- within the go routing this is the main line of code, which use for scanning the port
conn, err := net.Dial(protocol, target)
This line attempts to establish a network connection to the target
address and port using the net.Dial()
function from the net
package. The protocol
argument specifies the type of network protocol to use (e.g. tcp
or udp
). The function returns a connection object conn
and an error err
.
If the system found any open ports in the network, it sends to the results channel. results <- port
In our function, This line immediately calls the anonymous function with the port
argument, creating a new goroutine that will execute concurrently with the main program.
}(port)
Overall, this section of code scans a range of ports concurrently by creating a new goroutine for each port using the go
keyword. Each goroutine attempts to establish a network connection to the address and port specified by the address
and port
variables using the net.Dial()
function, and sends the port
number to the results
channel if the connection is successful. If the connection fails, a value of 0
is sent to the results
channel instead.
Results
The primary benefit of utilizing go routines for concurrency is that it allows for the full utilization of processing power while the scanning process is in progress. This results in a significantly reduced amount of time required for the entire scanning process compared to tools that scan one port at a time.
In a test scenario that was performed on my PC, it took approximately 7 seconds to scan through 10,000 TCP ports using go routines. However, for the same scenario, it took me ten times longer to complete the scanning process without using the concurrency feature.
you can find the code here