Practical Port Probing: Build a Fast and Safe TCP Port Scanner in C#
TL-DR - This guide teaches you how to build a fast and safe TCP port scanner in C#. You’ll learn how port scanning works, why it matters in cybersecurity, and how to implement timeouts, concurrency limits, and async scanning. You’ll also get an improved version with banner grabbing and JSON export. Perfect for developers, students, and security beginners.
Port scanning is one of the most fundamental skills in cybersecurity and network operations. From discovering exposed services to diagnosing connectivity issues, scanning is the first step toward understanding a system’s attack surface. Although many people rely on tools like nmap, writing your own scanner helps you truly understand how TCP works.
In this guide, you’ll build a fast and safe TCP port scanner in C#. You’ll learn about async operations, concurrency, timeouts, and real-world scanning behavior. Everything is written in simple and friendly language for intermediate learners.
Why Port Scanning Matters
Port scanning reveals which services a machine is exposing to the network. This includes web servers, SSH daemons, databases, file servers, and more. Knowing what is open helps you manage risk and verify that deployments behave as expected.
In cybersecurity, attackers always map ports before launching attacks. By learning to scan responsibly, you gain the same visibility that attackers have, allowing you to prevent issues before they become incidents.
Important Reminder: Only scan systems you own or have permission to test. Unauthorized scanning can be illegal and disruptive.
Real-World Use Cases
Security Assessments: Security teams use port scanning to discover reachable services before running vulnerability scanners. This step helps identify weak points early and shape the scope of deeper testing.
DevOps / SRE: During deployments or network configuration changes, scanning verifies whether the correct ports are open or closed. It’s a reliable way to confirm that firewall rules and service bindings behave correctly.
Incident Response: If unusual activity is detected, scanning helps responders quickly identify unexpected services listening on a host. This can reveal unauthorized ports opened by malware or misconfigurations.
Education: Building a scanner teaches how TCP behaves, how firewalls respond, and how timeouts affect scanning. These concepts are best learned by hands-on experimentation.
Design Goals for Our Scanner
Our scanner aims to be:
Simple – makes learning easy
Fast – uses async tasks for concurrency
Safe – limits parallel connections
Configurable – port ranges, timeouts, concurrency
Informative – prints open ports with service names
This makes it practical for real use while still being beginner-friendly.
Project Setup
Create a new C# console project:
dotnet new console -n PortScanner
cd PortScanner
Replace Program.cs with the full code below.
How TCP Port Scanning Works
When you try to connect to a TCP port, three things can happen:
Port OPEN: SYN → SYN/ACK → Connected
Port CLOSED: SYN → RST
Port FILTERED: SYN → (No response) → Timeout
ASCII Diagram
[Scanner] --- SYN ---> [Target]
[Target] --- SYN/ACK -> (Open)
[Scanner] --- SYN ---> [Target]
[Target] --- RST ----> (Closed)
[Scanner] --- SYN ---> [Target]
[Target] --- ??? ----> (No reply / Dropped) = Filtered
Complete C# Code
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
class PortScanner
{
static async Task<int> Main(string[] args)
{
if (args.Length < 3)
{
Console.WriteLine(”Usage: PortScanner <host> <startPort> <endPort> [concurrency=100] [timeoutMs=500]”);
return 1;
}
string host = args[0];
if (!int.TryParse(args[1], out int startPort) ||
!int.TryParse(args[2], out int endPort) ||
startPort < 1 || endPort > 65535 || startPort > endPort)
{
Console.WriteLine(”Invalid port range.”);
return 1;
}
int concurrency = args.Length >= 4 && int.TryParse(args[3], out int c)
? Math.Max(1, c) : 100;
int timeoutMs = args.Length >= 5 && int.TryParse(args[4], out int t)
? Math.Max(1, t) : 500;
IPAddress[] addresses;
try
{
addresses = await Dns.GetHostAddressesAsync(host);
if (addresses.Length == 0) throw new Exception(”No addresses found.”);
}
catch (Exception ex)
{
Console.WriteLine($”Host resolution failed: {ex.Message}”);
return 1;
}
IPAddress target = addresses[0];
Console.WriteLine($”Scanning {host} [{target}] ports {startPort}-{endPort} with concurrency {concurrency} and timeout {timeoutMs}ms”);
var openPorts = new List<int>();
var tasks = new List<Task>();
using var sem = new SemaphoreSlim(concurrency);
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int port = startPort; port <= endPort; port++)
{
await sem.WaitAsync();
int p = port;
tasks.Add(Task.Run(async () =>
{
try
{
bool isOpen = await ScanPortAsync(target, p, timeoutMs);
if (isOpen)
{
lock (openPorts)
{
openPorts.Add(p);
}
Console.WriteLine($”[OPEN] {p} {ServiceNameForPort(p)}”);
}
}
finally
{
sem.Release();
}
}));
}
await Task.WhenAll(tasks);
sw.Stop();
Console.WriteLine();
Console.WriteLine($”Scan complete in {sw.Elapsed.TotalSeconds:F2}s. Open ports: {openPorts.Count}”);
openPorts.Sort();
foreach (var port in openPorts)
Console.WriteLine($” - {port} {ServiceNameForPort(port)}”);
return 0;
}
static async Task<bool> ScanPortAsync(IPAddress ip, int port, int timeoutMs)
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(ip, port);
var delayTask = Task.Delay(timeoutMs);
var completed = await Task.WhenAny(connectTask, delayTask);
if (completed != connectTask)
return false; // timeout
await connectTask; // catch exceptions
return client.Connected;
}
static string ServiceNameForPort(int port)
{
return port switch
{
21 => “ftp”,
22 => “ssh”,
23 => “telnet”,
25 => “smtp”,
53 => “dns”,
80 => “http”,
110 => “pop3”,
143 => “imap”,
443 => “https”,
3306 => “mysql”,
3389 => “rdp”,
_ => “”
};
}
}
How the Scanner Works
The scanner begins by resolving the target host into an IP address. This ensures scanning works with domain names, internal hosts, or external websites. If resolution fails, the scanner exits gracefully.
Next, it uses a SemaphoreSlim to control concurrency. This prevents overloading the network or your own machine with too many simultaneous TCP connection attempts. Each port scan runs as an async task that respects the concurrency limit.
For each port, the scanner attempts a TCP connection. If the connection is successful before the timeout, the port is considered open. Timeouts are treated as filtered or unreachable ports. Once scanning is complete, the results are printed in a sorted list.
Common Mistakes in Port Scanning
1. Not Using Timeouts: Without timeouts, connect attempts may hang for seconds. This can turn a small scan into a long-running operation.
2. Too Much Concurrency: Many beginners start scanning with thousands of parallel tasks. This crashes their own system before it affects the target.
3. Not Closing Sockets: Failing to dispose TCP clients causes ephemeral port exhaustion. Proper resource cleanup is essential.
4. Scanning Without Permission: Unauthorized scanning can trigger firewalls, IDS systems, or legal consequences. Always scan responsibly.
Advanced Scanner Features (Banner Grabbing, JSON Output, Retries)
Below is an expanded version of your scanner that includes:
Banner Grabbing: Captures a small amount of data from open ports to identify services.
JSON Output: Generates machine-readable results.
Retry Logic: Retries ports that behave inconsistently.
1. Banner Grabbing Example
static async Task<string> GrabBannerAsync(TcpClient client, int timeoutMs)
{
try
{
client.ReceiveTimeout = timeoutMs;
using var stream = client.GetStream();
byte[] buffer = new byte[256];
int bytes = await stream.ReadAsync(buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer, 0, bytes).Trim();
}
catch
{
return “”;
}
}
2. JSON Output Example
var result = new {
Host = host,
Target = target.ToString(),
DurationSeconds = sw.Elapsed.TotalSeconds,
OpenPorts = openPorts.Select(p => new {
Port = p,
Service = ServiceNameForPort(p),
Banner = banners[p]
})
};
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions {
WriteIndented = true
}));
3. Retry Logic Example
async Task<bool> ScanWithRetry(IPAddress ip, int port, int timeout)
{
for (int i = 0; i < 2; i++)
{
if (await ScanPortAsync(ip, port, timeout))
return true;
await Task.Delay(50);
}
return false;
}
Conclusion
Building your own TCP port scanner is one of the best ways to learn real-world networking and cybersecurity concepts. You now understand how TCP behaves, how firewalls react to probes, and how concurrency and timeouts affect scanning speed. With the advanced features added in this guide, you have a scanner that is not only educational but also practical for real diagnostics.
This project is a foundation you can build on. You can extend it with UDP scanning, SYN scanning, service fingerprinting, or integration into security automation pipelines. The important part is that you now understand the fundamentals deeply—and from here, you can grow into more advanced tools and techniques.



