Command Palette

Search for a command to run...

0GitHub stars
Components

Port Scanner

Concurrent TCP port scanner in Go with banner grabbing and port range support.

Loading…

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-scanner

Usage

Scan default web and development ports on localhost:

go run main.go -host 127.0.0.1

Scan specific ports or port ranges:

go run main.go -host 192.168.1.1 -ports 22,80,443,8000-8080 -timeout 2s -workers 200

Flags

FlagDefaultDescription
-host127.0.0.1Target hostname or IP address to scan
-ports21,22,25,80,443,3000,8080Comma-separated list or ranges of ports
-timeout1.5sConnection timeout per attempt
-workers100Number of concurrent worker goroutines
-bannertrueAttempt 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"
	}
}
 

Was this helpful?

Command Palette

Search for a command to run...