Port Scanner
Concurrent TCP port scanner in Go with banner grabbing and port range support.
Features
- Concurrent port scanning powered by Go goroutines and worker pools.
- Supports single ports, comma-separated lists, and port ranges (
1-1024). - Automatic service banner grabbing on open TCP ports.
- Service lookup table mapping common ports to known protocols.
- Zero external dependencies: compiles to a fast, standalone native binary.
Installation
Install-Module cnpx -Scope CurrentUser -Force; Import-Module cnpx; cnpx add @chadnpc/port-scannerUsage
Scan default web and development ports on localhost:
go run main.go -host 127.0.0.1Scan specific ports or port ranges:
go run main.go -host 192.168.1.1 -ports 22,80,443,8000-8080 -timeout 2s -workers 200Flags
| Flag | Default | Description |
|---|---|---|
-host | 127.0.0.1 | Target hostname or IP address to scan |
-ports | 21,22,25,80,443,3000,8080 | Comma-separated list or ranges of ports |
-timeout | 1.5s | Connection timeout per attempt |
-workers | 100 | Number of concurrent worker goroutines |
-banner | true | Attempt service banner grab on open ports |
Source code
package main
import (
"flag"
"fmt"
"net"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
)
// ScanResult holds the outcome of scanning a single TCP port.
type ScanResult struct {
Port int
Open bool
Banner string
}
func main() {
hostFlag := flag.String("host", "127.0.0.1", "Target hostname or IP address to scan")
portsFlag := flag.String("ports", "21,22,25,80,443,3000,8080", "Comma-separated list or ranges of ports (e.g. 80,443 or 1-1024)")
timeoutFlag := flag.Duration("timeout", 1500*time.Millisecond, "Timeout for each port connection attempt")
workersFlag := flag.Int("workers", 100, "Number of concurrent scanning workers")
bannerFlag := flag.Bool("banner", true, "Attempt service banner grab on open ports")
flag.Parse()
target := *hostFlag
ports, err := parsePorts(*portsFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing ports: %v\n", err)
os.Exit(1)
}
fmt.Printf("[+] Scanning %s (%d ports)...\n", target, len(ports))
startTime := time.Now()
results := scanPorts(target, ports, *workersFlag, *timeoutFlag, *bannerFlag)
elapsed := time.Since(startTime)
openCount := 0
for _, res := range results {
if res.Open {
openCount++
service := lookupService(res.Port)
bannerText := ""
if res.Banner != "" {
bannerText = fmt.Sprintf(" (%s)", res.Banner)
}
fmt.Printf("[+] %-6s OPEN %s%s\n", fmt.Sprintf("%d/tcp", res.Port), service, bannerText)
}
}
fmt.Printf("[*] Scan complete in %s (%d open ports).\n", elapsed.Round(time.Millisecond), openCount)
}
func parsePorts(input string) ([]int, error) {
portSet := make(map[int]struct{})
parts := strings.Split(input, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if strings.Contains(part, "-") {
rangeParts := strings.SplitN(part, "-", 2)
start, err := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
if err != nil {
return nil, fmt.Errorf("invalid port range start: %s", rangeParts[0])
}
end, err := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
if err != nil {
return nil, fmt.Errorf("invalid port range end: %s", rangeParts[1])
}
for p := start; p <= end; p++ {
if p >= 1 && p <= 65535 {
portSet[p] = struct{}{}
}
}
} else {
p, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("invalid port number: %s", part)
}
if p >= 1 && p <= 65535 {
portSet[p] = struct{}{}
}
}
}
var ports []int
for p := range portSet {
ports = append(ports, p)
}
sort.Ints(ports)
return ports, nil
}
func scanPorts(host string, ports []int, workers int, timeout time.Duration, grabBanner bool) []ScanResult {
portsChan := make(chan int, len(ports))
resultsChan := make(chan ScanResult, len(ports))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for port := range portsChan {
resultsChan <- scanSinglePort(host, port, timeout, grabBanner)
}
}()
}
for _, port := range ports {
portsChan <- port
}
close(portsChan)
wg.Wait()
close(resultsChan)
var results []ScanResult
for res := range resultsChan {
results = append(results, res)
}
sort.Slice(results, func(i, j int) bool {
return results[i].Port < results[j].Port
})
return results
}
func scanSinglePort(host string, port int, timeout time.Duration, grabBanner bool) ScanResult {
address := net.JoinHostPort(host, strconv.Itoa(port))
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
return ScanResult{Port: port, Open: false}
}
defer conn.Close()
banner := ""
if grabBanner {
banner = readBanner(conn, timeout)
}
return ScanResult{Port: port, Open: true, Banner: banner}
}
func readBanner(conn net.Conn, timeout time.Duration) string {
_ = conn.SetReadDeadline(time.Now().Add(timeout))
buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil || n == 0 {
return ""
}
clean := strings.TrimSpace(string(buf[:n]))
clean = strings.ReplaceAll(clean, "\r", "")
clean = strings.ReplaceAll(clean, "\n", " ")
return clean
}
func lookupService(port int) string {
switch port {
case 21:
return "ftp"
case 22:
return "ssh"
case 23:
return "telnet"
case 25:
return "smtp"
case 53:
return "dns"
case 80:
return "http"
case 110:
return "pop3"
case 143:
return "imap"
case 443:
return "https"
case 3000:
return "node/react"
case 3306:
return "mysql"
case 5432:
return "postgresql"
case 6379:
return "redis"
case 8080:
return "http-alt"
default:
return "unknown"
}
}