Skip to main content

Command Palette

Search for a command to run...

Concurrent Port Scanning in F# with Tasks

Updated
3 min read
Concurrent Port Scanning in F# with Tasks

Introduction

Scanning one TCP port at a time is simple, but it is inefficient. Network operations spend most of their time waiting for remote hosts to respond. Instead of scanning ports sequentially, we can launch multiple connection attempts concurrently and significantly reduce the total scan time.

In .NET, the Task Parallel Library (TPL) provides a lightweight mechanism for running large numbers of asynchronous operations without creating hundreds or thousands of operating system threads.


Why Concurrent Scanning?

A sequential scanner performs the following workflow:

Scan Port 1
Wait
Scan Port 2
Wait
Scan Port 3
Wait
...

This approach wastes time because the CPU sits idle while waiting for network responses.

A concurrent scanner launches many connection attempts simultaneously:

Port 1  ─┐
Port 2  ─┼─> Running Together
Port 3  ─┤
...
Port N  ─┘

The result is a much faster scan.


The F# Scanner

let scanPort port =
    task {
        try
            use client = new TcpClient()

            do! client.ConnectAsync("scanme.nmap.org", port)

            printfn "%d open" port
        with
        | _ -> ()
    }

What Happens Here?

Statement Purpose
task {} Creates an asynchronous Task
TcpClient() Creates a TCP client
ConnectAsync() Attempts a non-blocking connection
printfn Displays open ports
try/with Ignores failed connections

If the connection succeeds, the port is considered open.


Launching 1024 Concurrent Scans

[1 .. 1024]
|> List.map scanPort
|> Task.WhenAll
|> fun t -> t.Wait()

This compact pipeline performs three important operations.

1. Generate Port Numbers

[1 .. 1024]

Creates a list of ports from 1 through 1024.

2. Create Scan Tasks

List.map scanPort

Transforms:

[1;2;3;...;1024]

into:

[Task1;Task2;Task3;...;Task1024]

Each task represents one asynchronous connection attempt.

3. Wait for Completion

Task.WhenAll

Combines all scan tasks into a single master task.

fun t -> t.Wait()

Blocks until every scan operation finishes.


Execution Flow

Ports List
     |
     v
Create Scan Tasks
     |
     v
Task1 Task2 Task3 ... Task1024
     |
     v
Task.WhenAll
     |
     v
Single Master Task
     |
     v
Wait()

Why Tasks Instead of Threads?

Creating 1024 operating system threads would be expensive.

Tasks provide:

  • Lower memory consumption
  • Better scalability
  • Efficient I/O scheduling
  • Simpler concurrency management
  • Integration with async network APIs

For network-heavy workloads such as port scanners, Tasks are typically the preferred solution in modern .NET applications.


Final Thoughts

Concurrent scanning demonstrates a fundamental principle of network programming: don't wait on one connection when you can wait on many simultaneously.

Using TcpClient.ConnectAsync() together with Task.WhenAll() allows F# developers to build scalable network tools with surprisingly little code while taking advantage of the .NET runtime's efficient asynchronous I/O infrastructure.