🚪 Analogy
A port scan is walking down a hallway and trying every doorknob to see which open. An open port is an unlocked door with someone working behind it; closed is a locked door that answers “no one home”; filtered is a door with a guard who says nothing at all, so you can’t even tell it’s there. The scanner is just a very fast, very patient person trying thousands of knobs — which is exactly what goroutines are good at.
How a connect scan works
The simplest scan is the TCP connect scan: ask the OS to open a normal connection to host:port. The kernel runs the three-way handshake for you, so you need no special privileges:
sequenceDiagram participant S as Scanner participant T as Target:port S->>T: SYN alt port open T->>S: SYN-ACK S->>T: ACK (handshake complete → OPEN) else port closed T->>S: RST (refused → CLOSED) else firewalled Note over T: packet dropped → (timeout → FILTERED) end
- Open — the handshake completes;
net.Dialreturns a usable connection. - Closed — the host replies with RST;
net.Dialfails fast with “connection refused”. - Filtered — a firewall drops the probe;
net.Dialhangs until your timeout fires. That timeout is the knob that separates “slow but open” from “filtered.”
Go’s net package gives you the connect scan essentially for free — the skill is doing it concurrently and politely.
See it: the concurrent scan engine
Here is the worker-pool scanner with a simulated network so it runs in the sandbox — real net.Dial is fenced below. Note the bounded workers, the open/closed/filtered classification, and the deterministic sorted report:
package main
import (
"fmt"
"sort"
"sync"
)
type status int
const (
closed status = iota
open
filtered
)
// simulateDial stands in for net.DialTimeout so this runs offline.
// Pretend 22, 80, 443 are open; 8080 is firewalled; everything else closed.
func simulateDial(port int) status {
switch port {
case 22, 80, 443:
return open
case 8080:
return filtered
default:
return closed
}
}
func main() {
const workers = 50
ports := make(chan int)
type res struct {
port int
st status
}
out := make(chan res)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for p := range ports {
out <- res{p, simulateDial(p)}
}
}()
}
go func() {
for p := 1; p <= 1024; p++ {
ports <- p
}
close(ports)
}()
go func() { wg.Wait(); close(out) }()
var openPorts []int
for r := range out {
if r.st == open {
openPorts = append(openPorts, r.port)
}
}
sort.Ints(openPorts)
fmt.Printf("scanned 1024 ports, %d open:\n", len(openPorts))
for _, p := range openPorts {
fmt.Printf(" %d/tcp open\n", p)
}
}
The real thing: net.DialTimeout
Swap the simulated dial for the real one and you have a working scanner. The sandbox can’t open real connections, so this is fenced — but it’s the whole change:
import (
"fmt"
"net"
"time"
)
// scanPort returns true if a TCP handshake to host:port completes.
func scanPort(host string, port int, timeout time.Duration) bool {
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return false // refused (closed) or timed out (filtered)
}
conn.Close()
return true
}
Drop scanPort into the worker body above, give it a 1–2 second timeout, and respect the scope guard and a rate limit. That’s a complete, polite scanner in ~40 lines — the Black Hat Go classic, done idiomatically.
Banner grabbing: what’s behind the door
Knowing a port is open is half the story; banner grabbing identifies the service. Many protocols send a greeting on connect (SSH, SMTP, FTP) or answer a crafted request (HTTP). Read the first bytes and you’ve fingerprinted the software and version:
conn, _ := net.DialTimeout("tcp", addr, 2*time.Second)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
buf := make([]byte, 256)
n, _ := conn.Read(buf) // SSH/SMTP/FTP announce themselves here
fmt.Printf("%s banner: %q\n", addr, buf[:n])
// For HTTP, write "HEAD / HTTP/1.0\r\n\r\n" first, then read the Server header.
A defender uses the same technique to inventory their own network and spot outdated, vulnerable versions before an attacker does.
Defending against scans
🐹 You can't stop scanning — so have little to find
The internet scans every public IP constantly; you will be scanned within minutes of going live. So the real defense is minimizing exposure: close unused ports, bind services to localhost or a private network, put admin interfaces behind a VPN, and expose only what must be public. Then layer detection and rate-limiting — an IDS, fail2ban, or connection-rate caps that flag a host hitting 1,000 ports in a second. A smaller attack surface beats a hidden one every time.
⚠️ A connect scan is loud and logged
The TCP connect scan completes a full handshake, which means the target’s services log the connection — every probe shows up. It’s the opposite of stealthy. (Raw SYN scans that never complete the handshake are quieter but need root and raw sockets via golang.org/x/net or gopacket.) For authorized testing this is fine and even desirable; just know that “I scanned 65k ports” is plainly visible in the target’s logs, and an aggressive unthrottled scan can itself look like — or cause — a denial of service.
See also
- Building security tools — the worker-pool skeleton this fills in.
- DNS enumeration — discovering hosts before you scan their ports.
- Worker pool & rate limiting — the concurrency behind a fast, polite scanner.
- TCP sockets — the
netpackage the scanner is built on.
Next: turning a list of open ports into a picture of the network — network recon & service detection.
Related topics
The anatomy of a Go security tool — static cross-compiled builds, stripped binaries, embedded assets, and a concurrent worker-pool skeleton with rate limiting and structured logging you can reuse for any scanner.
offensiveDNS EnumerationMapping a target's attack surface through DNS — record types and lookups, concurrent subdomain brute-forcing, zone transfers as a misconfiguration, and the defenses that limit what DNS reveals.
offensiveHTTP ReconnaissanceProfiling web targets in Go — a custom HTTP client, fingerprinting tech from headers, content and path discovery, and the response-hardening defenses (security headers, generic errors, rate limits) that blunt it.
Check your understanding
Score: 0 / 51. What does a basic TCP 'connect' scan actually do to decide a port is open?
A connect scan uses the OS TCP stack (net.Dial) to attempt a full handshake. If it completes, the port is open and listening; a refused connection (RST) means closed; a timeout usually means filtered (a firewall dropped the packet). It needs no special privileges, unlike a raw SYN scan.
2. Why is a bounded worker pool essential for a port scanner?
Scanning is massively parallel I/O. Without a cap you'd spawn a goroutine and socket per port×host — exhausting file descriptors and hammering the target. A fixed worker pool keeps in-flight probes bounded; a per-dial timeout stops slow/filtered ports from stalling the scan.
3. What is 'banner grabbing' and why does it matter?
Many services announce themselves (SSH version, SMTP greeting, HTTP Server header). Reading that banner identifies the exact software and version — which an attacker maps to known CVEs and a defender uses to find outdated services. Hiding or genericizing banners is a mild defense (security through obscurity, not a real control).
4. A connect scan reports a port as 'filtered'. What usually causes that?
Open → handshake completes. Closed → the host sends RST (connection refused) quickly. Filtered → a firewall drops the packet with no reply, so you just hit your timeout. Distinguishing these is why a sensible per-connection timeout matters: too short and you mislabel slow-but-open ports as filtered.
5. What is the best defense against attackers scanning your services?
You can't stop the internet from scanning you, so the real defense is having little to find: close unused ports, put services behind a firewall/VPN, and expose only what must be public. Layer detection (IDS, fail2ban, connection-rate limits) on top. Port-knocking and odd ports are obscurity — minor delay, not protection.
Comments
Sign in with GitHub to join the discussion.