<#
.SYNOPSIS
Scans a subnet for open TCP ports.
.DESCRIPTION
Scans all usable IPs in a CIDR subnet for one or more open TCP ports.
Supports optional multithreading (PS 5.1+ only).
.PARAMETER CIDR
Subnet in CIDR format (e.g., 192.168.1.0/24)
.PARAMETER Port
One or more TCP ports to test. (e.g., -Port 22,80,443)
.PARAMETER Timeout
Connection timeout per port, in milliseconds.
.PARAMETER Multi
Optional. Enable multithreaded scanning.
- Just "-m" = use ½ of logical threads
- "-m 4" = use 4 threads (up to logical thread count)
- "-m 1" or "-m 0" = fallback to single-threaded
.EXAMPLE
.\Scan-TcpSubnet.ps1 -CIDR "192.168.0.0/24" -Port 22,80,443 -m
.NOTES
Author: PowerShell GPT 🔨🤖🔧 & Pat
Version: 3.1
#>
[CmdletBinding()]
param (
[Alias('c')]
[Parameter(Mandatory = $true)]
[ValidatePattern('^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$')]
[string]$CIDR,
[Alias('p')]
[ValidateRange(1, 65535)]
[int[]]$Port = 22,
[Alias('t')]
[ValidateRange(1, 10000)]
[int]$Timeout = 1000,
[Alias('m')]
[int]$Multi
)
# ----------------- Helper Functions ------------------
function Get-UsableIPsFromCIDR {
param ([string]$cidr)
$parts = $cidr -split "/"
$baseIP = $parts[0]
$prefixLength = [int]$parts[1]
$ipBytes = [System.Net.IPAddress]::Parse($baseIP).GetAddressBytes()
[array]::Reverse($ipBytes)
$ipInt = [BitConverter]::ToUInt32($ipBytes, 0)
$hostBits = 32 - $prefixLength
$hostCount = [math]::Pow(2, $hostBits) - 2
$startIP = $ipInt + 1
0..($hostCount - 1) | ForEach-Object {
$ip = $startIP + $_
$bytes = [BitConverter]::GetBytes([uint32]$ip)
[array]::Reverse($bytes)
[System.Net.IPAddress]::new($bytes)
}
}
function Test-TcpPort {
param (
[string]$IP,
[int]$Port,
[int]$Timeout
)
$client = New-Object System.Net.Sockets.TcpClient
try {
$async = $client.BeginConnect($IP, $Port, $null, $null)
$success = $async.AsyncWaitHandle.WaitOne($Timeout, $false)
if ($success -and $client.Connected) {
$client.EndConnect($async)
return $true
}
} catch { }
finally {
$client.Close()
}
return $false
}
# ----------------- CPU & PS Check ------------------
$cpuInfo = Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 NumberOfCores, NumberOfLogicalProcessors
$physicalCores = $cpuInfo.NumberOfCores
$logicalCores = $cpuInfo.NumberOfLogicalProcessors
$psVer = $PSVersionTable.PSVersion.Major + ($PSVersionTable.PSVersion.Minor / 10)
Write-Host "🧠 CPU: $physicalCores core(s), $logicalCores logical thread(s)" -ForegroundColor DarkGray
if ($psVer -lt 5.1) {
Write-Warning "PowerShell version must be 5.1 or higher for multithreading. Running single-threaded."
$Multi = 1
}
# Determine thread count logic
if ($PSBoundParameters.ContainsKey('Multi')) {
if (-not $Multi -or $Multi -lt 2) {
$Multi = 1
Write-Host "🔂 Multithreading requested but under 2. Defaulting to single-threaded." -ForegroundColor Yellow
} elseif ($Multi -gt $logicalCores) {
Write-Warning "Requested $Multi threads exceeds logical core count ($logicalCores). Using $logicalCores threads."
$Multi = $logicalCores
} else {
Write-Host "🔁 Multithreading enabled: using $Multi thread(s)" -ForegroundColor Cyan
}
} else {
$Multi = 1
Write-Host "🔂 Running single-threaded (default)" -ForegroundColor DarkGray
}
# ----------------- MAIN ------------------
Clear-Host
Write-Host "`n🔍 Scanning $CIDR for open TCP ports: $($Port -join ', ')" -ForegroundColor Yellow
$allIPs = Get-UsableIPsFromCIDR -cidr $CIDR
$tasks = foreach ($ipObj in $allIPs) {
foreach ($p in $Port) {
[PSCustomObject]@{ IP = $ipObj.ToString(); Port = $p }
}
}
$results = @()
$total = $tasks.Count
$count = 0
if ($Multi -eq 1) {
foreach ($task in $tasks) {
$count++
$percent = [math]::Round(($count / $total) * 100)
Write-Progress -Activity "Scanning $CIDR" -Status "$count of $total" -PercentComplete $percent
if (Test-TcpPort -IP $task.IP -Port $task.Port -Timeout $Timeout) {
Write-Host "✔️ Open TCP port $($task.Port) found: $($task.IP)" -ForegroundColor Green
$results += $task
}
}
} else {
$jobs = @()
foreach ($task in $tasks) {
while (@(Get-Job -State 'Running').Count -ge $Multi) {
Start-Sleep -Milliseconds 100
}
$jobs += Start-ThreadJob -ArgumentList $task.IP, $task.Port, $Timeout -ScriptBlock {
param($ip, $port, $timeout)
$client = New-Object System.Net.Sockets.TcpClient
try {
$async = $client.BeginConnect($ip, $port, $null, $null)
$success = $async.AsyncWaitHandle.WaitOne($timeout, $false)
if ($success -and $client.Connected) {
$client.EndConnect($async)
return [PSCustomObject]@{
IP = $ip
Port = $port
}
}
} catch {}
finally {
$client.Close()
}
}
}
Write-Host "`n⏳ Waiting for jobs to finish..." -ForegroundColor DarkGray
Wait-Job $jobs
foreach ($job in $jobs) {
$result = Receive-Job $job
if ($result) {
Write-Host "✔️ Open TCP port $($result.Port) found: $($result.IP)" -ForegroundColor Green
$results += $result
}
Remove-Job $job
}
}
Write-Host "`n✅ Scan complete. Found $($results.Count) open TCP service(s)." -ForegroundColor Yellow
if ($results.Count -gt 0) {
$results | Sort-Object IP, Port | Format-Table -AutoSize
}