Nmap Go implementation - TCP port scan
b0r
Posted on December 17, 2021
I remember when I first started learning about computer networks. It was from the excellent Computer Networks, 5th Edition, Andrew S. Tanenbaum book. Book is accompanied by the University of Washington video course taught by David Wetherall. Video materials are available at: π Tanenbaum, Wetherall Computer Networks 5e, Video Notes. π
Be sure to check them out, they are great!!
After reading a bunch of posts on dev.to on Nmap topic, e.g.:
I have decided to refresh my knowledge on computer networks by re-implementing Nmaps` basic functionalities in Go.
This is first post in the series of posts I'll use to record that journey.
Table of Contents:
n2map (noob network mapper)
This project is called n2map (noob network mapper) and it will implement a subset of Nmaps` functionalities. That's port scanning functionality for now.
If we take a look at the Nmap documentation for port scanning we'll see that is supports a dozen or so port scan techniques:
- TCP SYN scan (half-open scanning, doesn't open a full TCP connection)
- TCP connect scan (full scanning, connect to port on the target machine)
- UDP scans (sends UDP packet to every targeted port)
- and many more..
n2map first functionality will add support to perform port scanning for single IP address. It will use a TCP connect scan technique.
TCP connect scan technique
In detail description of TCP connect scan technique is described in port scan techniques part in the Nmap documentation.
For this post, I think it's enough to know that a TCP connect scan technique is a technique that performs a standard TCP three-way handshake to establish the connection, and does the same for closing the connection.
Let's see what packets are sent if connect:
- from
localhost
(192.168.1.6
) - to target machine on the
http://scanme.nmap.org/
(45.33.32.156
) on port80
We see green
rectangle that represents packets sent to establish the connection. We also see red
rectangles representing the packets sent to close the connection.
In case a connection can't be established, we'll see following packets sent over the network:
n2map requirements
I would like to use n2map the same way I use nmap:
- via terminal
- by providing target machine IP address
- by providing a port range to scan
- by using the TCP connect scan technique
- this is currently only supported scan technique in n2map so no additional flags are added at the moment
The run command should looks like this: n2map -p 80-433 127.0.0.1
Implementation
Get target machine IP
First thing we need to have to perform a port scan is a target machine IP address. IP address will be provided as an argument to the n2map
runtime as follows: n2map 127.0.0.1
Following Go code shows how to implement that.
package main
import (
"flag"
"fmt"
"os"
"time"
)
func main() {
flag.Parse()
// IP is provided as an argument at position 0
if ip := flag.Arg(0); ip != "" {
dt := time.Now()
fmt.Printf("Starting n2map v0.1 at %s\n", dt.Format(time.UnixDate))
fmt.Printf("n2map scan report for %s\n", ip)
fmt.Printf("PORT\tSTATE\n")
} else {
fmt.Println("error : IP address not provided")
os.Exit(1)
}
}
Result
% go run main.go 127.0.0.1
Starting n2map v0.1 at Fri Dec 17 13:27:55 CET 2021
n2map scan report for 127.0.0.1
PORT STATE
Connect to the target machine (hard-coded port)
Once we have a target machine IP address we are ready to make a connection. Luckily, Go provides a Dial
function that can be used to connect to a target machine. It can be used like this: Dial("tcp", "198.51.100.1:80")
Let's update our code to reflect the changes:
port := 80
addr := fmt.Sprintf("%s:%d", ip, port)
_, err := net.Dial("tcp", addr)
if err == nil {
fmt.Printf("%s\t%s\t\n", port, "open")
} else {
fmt.Printf("%s\t%s\t\n", port, "closed")
}
Result
% go run main.go 127.0.0.1
Starting n2map v0.1 at Fri Dec 17 13:27:55 CET 2021
n2map scan report for 127.0.0.1
PORT STATE
80 closed
Notice that we have hard-coded port
value 80
, which means we need to rerun the n2map
for each port we want to scan. We can do better than that.
Connect to the target machine (user provided port)
To make it possible to provide port we will introduce a new -p
flag.
func main() {
var port string
flag.StringVar(&port, "p", "80", "port to scan")
flag.Parse()
if ip := flag.Arg(0); ip != "" {
dt := time.Now()
fmt.Printf("Starting n2map v0.1 at %s\n", dt.Format(time.UnixDate))
fmt.Printf("n2map scan report for %s : [%s]\n", ip, port)
fmt.Printf("PORT\tSTATE\n")
addr := fmt.Sprintf("%s:%d", ip, port)
_, err := net.Dial("tcp", addr)
if err == nil {
fmt.Printf("%d\t%s\t\n", port, "open")
} else {
fmt.Printf("%d\t%s\t\n", port, "closed")
}
} else {
fmt.Println("error : IP address not provided")
os.Exit(1)
}
}
Now we are able to use n2map with provided port as:
n2map -p 80 127.0.0.1
Result
% go run main.go -p 80 127.0.0.1
Starting n2map v0.1 at Fri Dec 17 13:27:55 CET 2021
n2map scan report for 127.0.0.1 : [80]
PORT STATE
80 closed
This is still now good enough! We want to be able to scan not only one port per n2map
run but multiple ports.
Connect to the target machine (multiple ports)
To make that possible we are going to extend the existing port
flag functionality to include a range of ports to scan by:
- extending provided port runtime argument to support port ranges like
80-100
(scan ports >= 80 and <= 100) - making a
Dial
call for each port in provided range
func main() {
var portRangeFlag string
flag.StringVar(&portRangeFlag, "p", "80", "port range to scan")
flag.Parse()
if ip := flag.Arg(0); ip != "" {
dt := time.Now()
fmt.Printf("Starting n2map v0.1 at %s\n", dt.Format(time.UnixDate))
fmt.Printf("n2map scan report for %s : [%s]\n", ip, portRangeFlag)
fmt.Printf("PORT\tSTATE\n")
portRange := strings.Split(portRangeFlag, "-")
startPort, _ := strconv.Atoi(portRange[0])
endPort, _ := strconv.Atoi(portRange[1])
for port := startPort; port <= endPort; port++ {
addr := fmt.Sprintf("%s:%d", ip, port)
_, err := net.Dial("tcp", addr)
if err == nil {
fmt.Printf("%d\t%s\t\n", port, "open")
} else {
fmt.Printf("%d\t%s\t\n", port, "closed")
}
}
} else {
fmt.Println("error : IP address not provided")
os.Exit(1)
}
}
Now we are able to use n2map with provided port as:
n2map -p 80-81 127.0.0.1
Result
% go run main.go -p 80-81 127.0.0.1
Starting n2map v0.1 at Fri Dec 17 13:46:36 CET 2021
n2map scan report for 127.0.0.1 : [80-81]
PORT STATE
80 closed
81 open
Review
Nice! If we check the initial requirements:
I would like to use n2map the same way I use nmap:
- via terminal
- by providing target machine IP address
- by providing a port range to scan
- by using the TCP connect scan technique
- this is currently only supported scan technique in n2map so no additional flags are added at the moment
we can see that all the requirements are implemented.
Make it go brrrr (parallel)
Not in the requirement list, but non less important is the time this implementation takes to scan multiple ports, or even all 65535 ports. It's very, very slow.
If you have read any of my previous posts on Go Channel Patterns you should have enough knowledge to make improvements needed to speed up this process.
Battle plan for that is:
-
scan ports (make TCP connection)
- create a worker goroutines that will do the
net.Dial
call for specific port - create a manager goroutine that will iterate over a
portStart
andportEnd
range, and send each port to one of the worker goroutines - create a channel used to pass data between manager and worker goroutines
- create a worker goroutines that will do the
-
print results
- make
results
channel used by worker goroutines to pass the results to the manager goroutine - use
for-range
loop to iterate overresults
channel and print out the results - create a new supervisor goroutine that will close the
results
channel once there are no more ports to process by worker goroutines - use
sync.WaitGroup
to tell supervisor when to close theresults
channel
- make
Conclusion
In this post, TCP connect scan port scanning technique was described (as described by Nmap). In addition, simple implementation was provided.
Readers are encouraged to try to speed the port scanning process up by using one of the Go concurrency primitives (goroutines, channels...).
Resources:
- Header Photo by Ricardo Esquivel from Pexels
Posted on December 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.