Step 79. The Port Scanner — A Scout That Finds Open Doors
Level 1 — Programming and the Computer’s Insides | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Steps 77–78 complete; you know the principle of socket connections and threads. You know what it means when connect is refused (Connection refused).
- What you need: Python, and my own computer (127.0.0.1) as the scan target. We reuse Step 77’s server.py as "the server being scanned."
- Caution: ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
In Step 77 we opened a server, and a client knocked on its door. But think about it — if merely knocking on a door tells you "open/closed," wouldn’t knocking on every door one by one draw you a map of the building? That is a port scan. It is reconnaissance, the first step of a penetration test; attackers find weak services at open doors, and defenders close doors that have no reason to be open. Today we build this scanner ourselves. A commercial tool (nmap) does it in one line, but only someone who has built one truly reads that tool’s output.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain, with a tool you built yourself, that the principle of port scanning is "attempting a connection (connect)"
- Write a function that checks one port with connect_ex and settimeout
- Parallelize the scan with ThreadPoolExecutor and measure the speed difference
- Read the service name and version of an open port with banner grabbing
- Explain for yourself the legal boundary of scanning (my lab, authorized targets)
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 standard library socket + concurrent.futures (no installation) |
| Today’s functions | connect_ex(), settimeout(), ThreadPoolExecutor, pool.map(), recv() |
| Concepts needed | Port open/closed/filtered, timeouts, banner grabbing, thread pools |
| Today’s output | scanner.py + scan_result.txt — a recon tool for your own lab only |
2-1. The Scan’s Principle — Three Kinds of Answers
Attempt a connect to one port, and the answer is one of three.
- Connection succeeds → a server is alive on that port (open)
- Immediate refusal (Connection refused) → the door exists but nobody is waiting (closed)
- No answer at all (timeout) → a firewall pretends not to hear the knock (filtered)
Python has a function for this experiment: connect_ex. connect throws an exception on failure, but connect_ex returns a result-code number. 0 means success (open); anything else means failure. For a scanner, this one is far more convenient.
2-2. Timeouts — Not Waiting Forever
As we learned in Step 77, a connection attempt waits until the other side answers. Waiting blindly on a silent port means the scan never ends. So with settimeout(1) we set "wait 1 second, then give up." A scanner’s speed is in fact decided by how this waiting is managed — we confirm it with our bodies in 3-2.
2-3. Banner Grabbing — Hearing the Voice Behind the Door
Knowing a port is open is not enough. You need to know "which program is waiting." Fortunately, many services send their self-introduction (banner) first, the moment you connect. An SSH server immediately spits out a string like SSH-2.0-.... The technique of receiving and reading this first greeting is called banner grabbing. A service’s name and version are immediately a lead for vulnerability searches.
2-4. Thread Pools — A Staff Group with a Fixed Headcount
Knocking on a thousand ports one at a time accumulates the waiting as-is. Here we apply last chapter’s threads. But instead of creating threads without limit, we use ThreadPoolExecutor, a "staff pool with a fixed headcount." Hand it tasks (port numbers), and the workers inside the pool divide them up and process them.
3. Follow Along
3-1. The Single-Port Check Function — The Scanner’s Heart
Create scanner.py.
Input (scanner.py)
import socket
def check_port(ip, port):
s = socket.socket()
s.settimeout(1)
result = s.connect_ex((ip, port))
s.close()
return result == 0 # True means open
print(check_port("127.0.0.1", 9999)) # True if the server is running
print(check_port("127.0.0.1", 65000)) # almost certainly False
Input: first leave Step 77’s server.py running in terminal 1 (python server.py), then run this in terminal 2.
python scanner.py
Output (measured 2026-09-09):
True
False
How to read it: port 9999, where the echo server waits, is True; port 65000, where nobody lives, is False. Your server has become the scanner’s first catch. (Note: our echo server is designed to end after receiving one guest, so this single check consumes one connection and the server exits. Check the server window.)
Why: this confirms that the scan’s principle is the single line "try connecting and look at the result code." Not some grand technique — exactly last chapter’s connect itself.
3-2. Predict — The Time to Knock on 100 Ports in Order
Time to predict. Checking ports 1 through 100 in order, how many seconds will it take? Most ports are closed. The key is how fast a closed port’s answer comes back. Write down your prediction and measure.
Input (add to the bottom of scanner.py)
import time
if __name__ == "__main__":
start = time.time()
open_ports = []
for port in range(1, 101):
if check_port("127.0.0.1", port):
open_ports.append(port)
print("Sequential scan (1-100) open ports:", open_ports)
print("Sequential time taken:", round(time.time() - start, 1), "seconds")
Output (measured 2026-09-09):
Sequential scan (1-100) open ports: [22]
Sequential time taken: 30.9 seconds
How to read it: two things are visible. ① Port 22 was open — an SSH server was living on this computer. ② 30.9 seconds for 100 ports. That is, we waited about 0.3 seconds per closed port. (In the measured environment, refusals of closed ports didn’t come immediately and each held out until timeout — common in environments where security software holds connection attempts. In a Linux lab, refusals are immediate and it’s much faster.) Either way, the conclusion is the same: in a sequential scan, the waiting accumulates as-is.
Why: an experiment to feel in units of time "why parallelization is needed." How far off was your prediction?
3-3. Parallelizing with a Thread Pool — Overlapping the Waits
Let’s entrust the same work to a thread pool. Add a function to scanner.py and fix __main__.
Input (add to scanner.py)
from concurrent.futures import ThreadPoolExecutor
def scan(ip, start=1, end=1024, workers=100):
ports = range(start, end + 1)
with ThreadPoolExecutor(max_workers=workers) as pool:
results = pool.map(lambda p: (p, check_port(ip, p)), ports)
return [p for p, ok in results if ok]
Input (the __main__ part)
if __name__ == "__main__":
start = time.time()
print("Thread pool (1-1024) open ports:", scan("127.0.0.1"))
print("Thread pool time taken:", round(time.time() - start, 1), "seconds")
Output (measured 2026-09-09):
Thread pool (1-1024) open ports: [22, 135, 445]
Thread pool time taken: 3.4 seconds
How to read it: the range got 10 times wider (100→1024) yet the time shrank to a tenth (30.9→3.4 seconds). With 100 workers each waiting separately, the waits overlapped. And the newly caught 135 and 445 — those are Windows’ default service (RPC, file sharing) ports. If the question "why is this open?" arose even about your own computer, that is the moment the defender’s eye opens.
Why: if last chapter’s threads were "concurrency of service," today’s thread pool is "concurrency of task processing." Same technology, different face. Record the speed-comparison numbers in your notes.
3-4. Banner Grabbing — The Voice Behind the Door
Having found open ports, let’s ask their identity. Make a cousin function of check_port.
Input (add to scanner.py)
def grab_banner(ip, port):
try:
s = socket.socket()
s.settimeout(2)
s.connect((ip, port))
banner = s.recv(1024).decode(errors="ignore").strip()
s.close()
return banner
except Exception:
return ""
Input (continuing in __main__)
for p in scan("127.0.0.1"):
print(f"Port {p} banner: {grab_banner('127.0.0.1', p) or '(none)'}")
Output (measured 2026-09-09):
Port 22 banner: SSH-2.0-OpenSSH_for_Windows_9.5
Port 135 banner: (none)
Port 445 banner: (none)
How to read it: in port 22’s single banner line, everything is contained — the protocol (SSH-2.0), the program name (OpenSSH), the version (9.5), even the family (Windows). It’s a business card the service hands out voluntarily. Ports 135 and 445, on the other hand, are silent — services that say nothing on connection alone. errors="ignore" is a safety pin that keeps it from dying even when bytes that can’t be decoded into characters are mixed in.
Why: think about why this one business card is sensitive information. To someone who can search "known vulnerabilities of OpenSSH 9.5," a banner is a table of contents for an attack; to a defender, it’s an update checklist. It’s the same information — only the side from which it’s read differs.
3-5. Completion — Target Confirmation and Result Saving
Let’s gather the pieces into a finished product. The last part of scanner.py.
Input (the whole __main__ of scanner.py)
import sys
from datetime import datetime
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python scanner.py <IP of my lab>")
sys.exit(1)
target = sys.argv[1]
print(f"Target: {target} — double-check that this is your own lab equipment!")
open_ports = scan(target)
with open("scan_result.txt", "a", encoding="utf-8") as fp:
fp.write(f"=== {datetime.now()} | target {target} ===n")
for p in open_ports:
banner = grab_banner(target, p)
line = f"Port {p} open banner: {banner or '(none)'}"
print(line)
fp.write(line + "n")
fp.write("n")
Input: leave Step 77’s server.py running again (this time, change the scan to narrow the range to 9990–10010 with scan(target, 9990, 10010)):
python scanner.py 127.0.0.1
Output (measured 2026-09-09):
Target: 127.0.0.1 — double-check that this is your own lab equipment!
Port 9999 open banner: (none)
How to read it: taking the target as an argument at run time, and showing a confirmation warning first, is for mistake prevention. Our deliberately opened server was caught exactly. Our echo server is designed not to greet, so the banner is empty. (In this measurement, scanning the 9990–10010 range took 6.2 seconds.)
Why: taking arguments, a confirmation message, accumulating results — a tool’s "dignity" comes from details like these. Your first recon tool is complete.
4. Missions & Exercises
Mission — Completing the Scanner and Making a Baseline List
- Measure the times of the sequential scan and the thread-pool scan over the same range and build a comparison table
- Scan the full range of 127.0.0.1 (1–1024) and accumulate open ports and banners in scan_result.txt
- If the results contain numbers like 22, 80, 443, 135, 445, annotate what service each is
- Keep today’s scan results as "my computer’s normal-state list," and write in three lines why this list is needed
- Write with your own hand a comment at the top of the scanner file: "This tool is for my lab only"
Exercises
Q1. State the three results of a connect scan (open/closed/filtered) and the meaning of each.
Q2. Why does using connect_ex instead of connect make building a scanner easier?
Q3. Explain why the sequential scan in 3-2 was slow, from the perspective of "accumulation of waiting," and state how the thread pool solves it.
Q4. In banner grabbing, what kind of service yields an empty string, and what must you do to learn such a service’s identity?
5. Model Answers & Completion Criteria
Mission Model Answer
An example speed-comparison table (measured 2026-09-09 — varies by environment):
| Method | Range | Time |
|---|---|---|
| Sequential | 1–100 | 30.9 s |
| Thread pool (100) | 1–1024 | 3.4 s |
Examples of well-known port annotations: 22 SSH (remote access), 80 HTTP (web), 443 HTTPS (encrypted web), 135 Windows RPC, 445 Windows file sharing (SMB).
An example answer for item 4: "You must know which ports were originally open in order to notice the anomaly later — ‘a port that didn’t exist before has opened.’ In incident investigation, ‘an open port different from usual’ is a top-priority clue. Knowing the normal is the beginning of knowing the abnormal."
How to verify: ① Are both methods’ times recorded in a table? ② Do date and target accumulate together in scan_result.txt? ③ Is there a service-name annotation for each open port? ④ Is there a lab-only comment on the scanner’s first line? If all four are "yes," it’s complete.
Exercise Solutions
Q1 solution. A successful connection means open (a server is waiting); an immediate refusal means closed (the port exists but no service); no response/timeout means filtered (a firewall swallowed the response). Distinguishing the three is the beginning of interpreting scan results.
Q2 solution. connect throws failure as an exception, so every port needs try/except, but connect_ex returns failure as a result code (a number). You can write the scan loop as an ordinary if statement — "if 0, open; otherwise, next port" — which makes it the convenient choice for a scanner.
Q3 solution. A sequential scan moves to the next port only after one port’s wait (refusal or timeout) ends, so the waits add up as-is — 30.9 seconds for 100 ports in the measurement. A thread pool has different workers wait on several ports simultaneously, overlapping the waits, so the total time shrinks to about the level of "the single slowest one" — 3.4 seconds for 1024 ports in the same environment.
Q4 solution. Unlike services like SSH that "greet first," services like HTTP that answer only "when the guest speaks first" stay silent on connection alone. To such a service you must first send a word matching its protocol — for example, send HTTP GET / HTTP/1.0rnrn and a response containing server information comes back. The distinction itself — "services that speak first vs services that wait" — is knowledge.
Completion Criteria Checklist
- [ ] I can explain the three results of a connect scan (open/closed/filtered)
- [ ] I can explain why a timeout is needed
- [ ] I sped up the scan with a thread pool and recorded the times
- [ ] I read service names and versions with banner grabbing
- [ ] I confirmed that a deliberately opened port (9999) is caught by the scan
- [ ] I completed my lab-only scanner, the result file, and the baseline list
6. Common Pitfalls & Fixes
Wall 1. The scan is too slow
Symptom (measured 2026-09-09): knocking on 100 ports sequentially takes 30.9 seconds.
Cause: in environments where closed-port responses don’t come immediately (e.g., security software holding connections), the full timeout is consumed every time. The waiting accumulates as-is.
Fix: check settimeout and overlap the waits with ThreadPoolExecutor. If it’s still slow, narrow the range (1–100) and test first.
Wall 2. connect_ex throws an exception
Symptom: you used connect_ex, yet an exception pops out.
Cause: when the address itself is wrong (format error, name resolution failure), even connect_ex throws. "Returning a result code" applies only to the success/failure of the connection attempt.
Fix: wrap it in try/except and treat failures as False. A scanner must not die on any input.
Wall 3. Every banner is an empty string
Symptom (measured 2026-09-09): ports like 135 and 445 are open, but no banner is caught.
Cause: many services, like HTTP and SMB, answer only "when the guest speaks first." They stay silent on connection alone.
Fix: that’s normal. To such services you must send a first word matching the protocol. An empty banner itself is information: "this service is the type that doesn’t speak first."
Wall 4. I created too many threads
Symptom: raising workers into the thousands makes it slower, or errors occur.
Cause: threads are resources too. There is a limit to the simultaneous connections the operating system and network equipment can handle.
Fix: around 100 is plenty. For speed, adjusting the timeout and the range is more effective than the thread count.
Wall 5. I’m confused about which targets I may scan
Symptom: you start wanting to scan the router or a school computer "out of curiosity."
Cause: curiosity is good, but scanning without permission can violate the law depending on the jurisdiction. Curiosity and permission are separate things.
Fix: there is exactly one criterion — is it equipment I own and configured, or a target a practice platform explicitly permits? If neither, don’t do it. If you’re curious, install that service yourself in your lab and scan it.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Port scan | The reconnaissance act of probing open/closed by attempting connections to ports |
| Open/closed/filtered | Connection success / immediate refusal / no response — the scan’s three answers |
| Timeout | The cap on waiting — the value that decides a scanner’s speed |
| Banner grabbing | The technique of receiving a service’s self-introduction (name, version) that comes right after connecting |
| Thread pool | A worker group with a fixed headcount — gains speed by overlapping waits |
| Baseline list | A record of the usual open ports — the reference line for anomaly detection |
Today’s Functions
| Function | What it does |
|---|---|
connect_ex((ip, port)) |
Attempt a connection and get the result as a code (0 = open) |
settimeout(seconds) |
Set the cap on waiting |
ThreadPoolExecutor(max_workers=n) |
Create a thread pool of n workers |
pool.map(function, list) |
Automatically distribute tasks to the pool’s workers |
recv(1024) (for banners) |
Receive the first greeting that comes right after connecting |
The Instinct That Matters More Than Commands
The scanner you built today is the tool of "reconnaissance," the standard first step of a penetration test. Attackers use its results to decide the order of attack; defenders use the same results to decide which doors to close. A scanner can be a burglar’s flashlight or a guard’s flashlight — the distinction lies not in the flashlight but in you. And when you scan, the other side’s logs retain a vivid footprint: "short connections from one address to many ports." Intrusion detection systems watch for exactly that pattern. Knowing what a scan looks like is the first step to understanding detection rules.
Lastly, today’s method is called a "connect scan" — accurate, but it completes the connection to the end and leaves many records. Real-world tools use variants like the SYN scan, which judges by watching only the first step of the handshake — you will meet that principle in the next chapter, assembling packets by hand. The person who has built a tool is the tool’s true owner.
Once every box is checked, Step 79 is complete.