Building a High Performance Port Scanner with Golang

Golang has a fantastic multi-threading API which allows for all sorts of opportunities for building high performance tools, including one of my favorites, a port scanner!

Side Note: If you also like async/await style programming and Ruby, you might like this.

I’ve written about building a port scanner several times before; and this implementation is the fastest one I’ve been able to create thus far. Utilizing the multi-threaded runtime and the baked-in concurrency model provided by Golang, asynchronous I/O performance is just beautiful.👌

What does that end up looking like to actually code, and how fast does it go?

📦 Assembling our Package

Like any other language, we’ll pull in some libraries that have encapsulated the stuff we’re interested in orchestrating into a useful program.

In the world of Golang, we’ll import a bunch of packages — and at the very top of our script, we’ll declare it as our “main” package.

package main
import (
"context"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"sync"
"time"
    "golang.org/x/sync/semaphore"
)
// more code ...

Unlike object-orientated languages, there’s no concept of a class wherein we add methods inside of. Instead, we can just attach functions to any type, ultimately giving us a method.

So, let’s make a type called PortScanner which will be struct type containing two fields: ip and lock.

type PortScanner struct {
ip string
lock *semaphore.Weighted
}

The ip will be the IP address of the host on the network we’re interested in scanning. The lock will act as a threshold that will limit the number of go routines that will be running at any given time.

To set our lock based on the limits of the operating system, we may opt to use the value of the ulimit command. This can be captured to be parsed using the os/exec package.

Note: ulimit is a builtin shell command used to manage various resource restrictions. The -n flag will reveal the maximum number of open files allowed; we can capture the output of this command and convert it to the right type.

To abstract that whole process away of getting the value from the command-line, I decided to put it in a function called Ulimit.

func Ulimit() int64 {
out, err := exec.Command("ulimit", "-n").Output()
    if err != nil {
panic(err)
}
    s := strings.TrimSpace(string(out))
i, err := strconv.ParseInt(s, 10, 64)

if err != nil {
panic(err)
}
    return i
}

Since we’re building a port scanner, we’re going to need another function that can actually, ya’ know, scan the port. The net package includes a convenient method to make a connection with an allotted timeout value.

func ScanPort(ip string, port int, timeout time.Duration) {
target := fmt.Sprintf("%s:%d", ip, port)
conn, err := net.DialTimeout("tcp", target, timeout)

if err != nil {
if strings.Contains(err.Error(), "too many open files") {
time.Sleep(timeout)
ScanPort(ip, port, timeout)
} else {
fmt.Println(port, "closed")
}
return
}

conn.Close()
fmt.Println(port, "open")
}

As you probably noticed, most of the ScanPort function deals with the err from net.DialTimeout. This is because, as with any port scanner, we have some errors to juggle:

🤹🏻‍ Three Essential Errors to Juggle:

  • 🛑connect: connection refused is an error that will occur when a port is not open to connect to. This is a pretty typical response for most ports, and this response that is enforced with most REJECT firewall rules.
  • dial: i/o timeout is an error produced by net.DialTimeout when an operation didn’t complete within the allotted timeout value. This may happen for various reasons including a DROP firewall rule where the target doesn’t explicitly tell you a port isn’t open from the scanner’ perspective. We really don’t want to wait around forever to get nothing.
  • 📑socket: too many open files is an error that will occur when we try to connect to a port, but we have way too many connections (files, really) already open and need to schedule the execution to happen again, but just a little later, continuously delaying its execution until it’s able to be processed.

Based on the result of the error for the connection attempt, if the attempt failed as a result of too many files being open on the system, we’ll delay the execution of that port scan. Once the delay returns, we can again retry it until the connections fails because of a timeout or, better yet, a successful connection.

To use the ScanPort method for a range of ports, we’ll implement a method on the PortScanner struct that will scan a given range of ports (from the first f to the last one l) along with a Duration to use as a timeout. We’ll call this function Start, as it will kick-off the actual connection attempts later on.

func (ps *PortScanner) Start(f, l int, timeout time.Duration) {
wg := sync.WaitGroup{}
defer wg.Wait()

for port := f; port <= l; port++ {
wg.Add(1)
ps.lock.Acquire(context.TODO(), 1)
        go func(port int) {
defer ps.lock.Release(1)
defer wg.Done()
ScanPort(ps.ip, port, timeout)
}(port)
}
}

Limiting Go Routines for Performance

While it is be easy to just spawn off a new go routine for every connection attempt, this isn’t the most efficient way of utilizing our computer’s resources. Instead, it’s much better to bound our connections attempts to a set size based on our shell’s limits. The lock in our port scanner “locks down” the concurrent connection attempts, and thus wastes significantly less time by not trying to exceed its threshold.

The Main Function

All that’s left is to write our program’s main function to use the code we just implemented to scan all of the ports on our local host.

func main() {
ps := &PortScanner{
ip: "127.0.0.1",
lock: semaphore.NewWeighted(Ulimit()),
}
    ps.Start(1, 65535, 500*time.Millisecond)
}

🔨 Compiling the Program

Compiling our port scanner is as trivial as running the following command:

$ go build port_scanner.go

Which provides us with a binary which we can execute to perform our port scan now:

$ ./port_scanner

But, how fast does it go? Let’s use the time command on unix-like systems to give us a quick benchmark for an idea:

$ time ./port_scanner
...
real 0m31.384s
user 0m4.766s
sys 0m7.653s

Let’s do the math on that: 65535 ports / 31 seconds is about 2000 ports a second. That’s incredibly fast, and 2x faster than the single-threaded Ruby version I’ve made before! 🚀

📃 Source Code

Below is the full source code for the scanner (copy+paste friendly for testing):

👋 Conclusion

The multi-threaded runtime in Golang coupled with an asynchronous design allows for some incredible performance for a simple port scanner. Feel free to hack on it some more or let me know where it could be improved.

Until next time, that’s all folks!