How often do you type telnet some-host 443 just to see if a port is open? That little reachability test — can I reach this TCP port on this host? — is something you can do natively in Python with the socket module, and once you can, you can sweep hundreds of ports across dozens of hosts in seconds. Today we build a real port reachability checker and, along the way, demystify what a socket actually is.
You do not need deep network-programming theory here. As a network engineer you already know what TCP, ports, and the three-way handshake are. We are just driving them from Python.
What a Socket Is (in 30 Seconds)
A socket is an endpoint for network communication — the programmatic equivalent of plugging a cable into a jack. To test a TCP port, you create a socket, try to connect it to (host, port), and see whether the connection succeeds. A successful connect means the port is open and something accepted the handshake. That is the whole idea.
import socket
# AF_INET = IPv4, SOCK_STREAM = TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2) # don't wait forever on a filtered port
result = s.connect_ex(("1.1.1.1", 443)) # returns 0 on success
s.close()
print("OPEN" if result == 0 else "CLOSED/FILTERED")
The key choice: connect_ex instead of connect. The plain connect raises an exception on failure; connect_ex returns an error code (0 = success) so you can branch cleanly without a try/except for the common case. For a scanner, connect_ex is exactly right.
Always Set a Timeout
This is the rule that separates a usable scanner from one that hangs. A closed port usually refuses fast, but a filtered port (firewall silently dropping) will let your connect attempt sit until the OS default timeout — which can be 30+ seconds. settimeout(2) caps that. Tune it to your network: 1–2 seconds for a LAN, more across a WAN.
The Cleaner Pattern: Context Managers
Remember with from the files lesson in Week 1? Sockets support it too, so they always get closed even if something goes wrong:
import socket
def check_port(host, port, timeout=2):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
return s.connect_ex((host, port)) == 0
print(check_port("1.1.1.1", 443)) # True
print(check_port("1.1.1.1", 8443)) # False (likely)
That is a complete, reusable port checker in five lines. Everything else today is built on it.
Resolving Names and Grabbing the Local View
The socket module also wraps DNS and local identity, handy for inventory scripts:
import socket
# Forward lookup: name -> IP
print(socket.gethostbyname("one.one.one.one")) # 1.1.1.1
# Reverse lookup: IP -> name (may raise socket.herror if no PTR)
try:
print(socket.gethostbyaddr("1.1.1.1")[0])
except socket.herror:
print("no PTR record")
# What does THIS box think it is?
print(socket.gethostname())
Cisco Context: A Service Health Checker
Put it together: scan the management-plane ports you care about across a device list and produce a matrix. Is SSH up on every switch? Did someone leave Telnet (23) open where they should not have? This answers both.
import socket
SERVICES = {22: "SSH", 23: "Telnet", 443: "HTTPS", 830: "NETCONF"}
def check_port(host, port, timeout=1.5):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
return s.connect_ex((host, port)) == 0
devices = ["1.1.1.1", "8.8.8.8"]
# header
print(f"{'Host':16}" + "".join(f"{name:9}" for name in SERVICES.values()))
for host in devices:
row = f"{host:16}"
for port, name in SERVICES.items():
row += f"{'open' if check_port(host, port) else '-':9}"
print(row)
Run this and a row where Telnet shows open is an instant audit finding. The same structure, pointed at your real management subnet, becomes a daily exposure check.
Going Faster: A Note on Concurrency
Checking ports one at a time is fine for a handful of hosts but slow for hundreds, because each check spends most of its time waiting. That waiting is the perfect case for concurrency. We are keeping today single-threaded to stay focused, but here is the shape of the speedup using the standard library’s thread pool — tuck it away for when you need it:
from concurrent.futures import ThreadPoolExecutor
targets = [("1.1.1.1", 443), ("8.8.8.8", 443), ("1.1.1.1", 22)]
with ThreadPoolExecutor(max_workers=50) as pool:
results = pool.map(lambda t: (t, check_port(*t)), targets)
for (host, port), is_open in results:
print(host, port, "open" if is_open else "closed")
Exercises
- Warm-up. Write
is_open(host, port)usingconnect_exand a 2-second timeout. Test it against a port you know is open and one you know is closed. - Range scan. Scan ports 20–25 on a host and print only the open ones.
- Service map. Given the dict
{22:"SSH", 80:"HTTP", 443:"HTTPS"}, check one host and print"SSH: open"/"HTTP: closed"style lines using the friendly names. - Resolve then scan. Take a hostname, resolve it to an IP with
gethostbyname, then check whether 443 is open on the resolved address. Handle the case where the name does not resolve. - Challenge. Write
first_open(host, ports)that returns the first open port from a list (useful for “is this device reachable on SSH or Telnet?”), orNoneif none are open. Then use it to classify a list of hosts as"ssh","telnet-only", or"unreachable".
Answers
Show answers
1. Warm-up
import socket
def is_open(host, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(2)
return s.connect_ex((host, port)) == 0
print(is_open("1.1.1.1", 443)) # True
print(is_open("1.1.1.1", 444)) # False
2. Range scan
host = "1.1.1.1"
for port in range(20, 26):
if is_open(host, port):
print(f"{port} open")
3. Service map
services = {22: "SSH", 80: "HTTP", 443: "HTTPS"}
host = "1.1.1.1"
for port, name in services.items():
print(f"{name}: {'open' if is_open(host, port) else 'closed'}")
4. Resolve then scan
import socket
name = "one.one.one.one"
try:
ip = socket.gethostbyname(name)
print(f"{name} -> {ip}: 443 {'open' if is_open(ip, 443) else 'closed'}")
except socket.gaierror:
print(f"{name} did not resolve")
gaierror is the “get address info” error — the exception DNS resolution raises on failure.
5. Challenge
def first_open(host, ports):
for p in ports:
if is_open(host, p):
return p
return None
hosts = ["1.1.1.1", "8.8.8.8", "192.0.2.1"]
for h in hosts:
p = first_open(h, [22, 23])
if p == 22:
label = "ssh"
elif p == 23:
label = "telnet-only"
else:
label = "unreachable"
print(f"{h:15} {label}")
Returning early from the loop on the first hit is the idiom — no need to scan the rest once you have an answer.
Previously: subprocess. Coming tomorrow — Paramiko: your first real SSH session to a router, driven entirely from Python.