DEV Community

Cover image for Nmap Go implementation - TCP port scan
b0r
b0r

Posted on

Nmap Go implementation - TCP port scan

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:

  1. n2map (noob network mapper)
    1. TCP connect scan technique
  2. n2map requirements
  3. Implementation
    1. Get target machine IP
    2. Connect to the target machine (hard-coded port)
    3. Connect to the target machine (user provided port)
    4. Connect to the target machine (multiple ports)
    5. Review
    6. Make it go brrrr (parallel)
  4. Conclusion

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 port 80

TCP open connection handshake

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:

TCP cant open connection

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
        }
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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)
    }

}
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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 and portEnd range, and send each port to one of the worker goroutines
    • create a channel used to pass data between manager and worker goroutines
  • print results

    • make results channel used by worker goroutines to pass the results to the manager goroutine
    • use for-range loop to iterate over results 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 the results channel

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

Discussion (0)