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.



