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 mainimport (
"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 mostREJECT
firewall rules. - ⏱
dial: i/o timeout
is an error produced bynet.DialTimeout
when an operation didn’t complete within the allottedtimeout
value. This may happen for various reasons including aDROP
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!