Computing Foundations › How the Internet Works › Day 17
Hands-on lab — Day 17: TCP, UDP, and Ports
- ← Back to the Day 17 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-017-tcp-udp-and-ports/
Commands
Setup
cd labs/sections/computing-foundations/day-017-tcp-udp-and-ports Run
bash examples/inspect_ports.sh
bash starter/inspect_ports.sh Test
bash tests/run_tests.sh File tree
examples/inspect_ports.sh expected-output/FIELDS.md expected-output/sample-macos.txt metadata.yml README.md requirements/README.md security.md starter/inspect_ports.sh starter/ports-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 017 lab — See Ports and Connections
Lesson
- Lesson title: TCP, UDP, and Ports
- Day number: 17 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-017-tcp-udp-and-ports
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-017-tcp-udp-and-portswhen the site is running.
Purpose
Day 17's lesson explains the transport layer: TCP, UDP, and the port numbers
that let one machine run many services at once. This lab makes it concrete.
You interrogate your own machine to see which TCP ports it is listening
on, classify each port number into its IANA range, and then prove — with a
loopback connection test — the difference between a port that is open
(something is listening) and one that is closed (the "connection refused"
error you will meet constantly). Everything is read-only and local: the only
network action is 127.0.0.1 talking to itself.
Learning objectives
- List the listening TCP ports on your machine with
lsof(macOS) orss/netstat(Linux). - Classify a port number as well-known (0–1023), registered (1024–49151), or ephemeral (49152–65535), and name the service behind common well-known ports.
- Use
nc -zto probe a port on the loopback and read its exit status to tell open from closed. - Explain what "connection refused" means in terms of a listening socket.
- Run an automated test script and interpret its pass/fail output.
Prerequisites
- The Day 17 lesson (read it first — it explains ports, TCP, UDP, and sockets).
- Day 16 (IP addresses, DNS, and routing) for the idea of an IP address that a port number rides on top of.
- A terminal: Terminal.app (macOS), any terminal (Linux), or PowerShell/WSL (Windows).
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon), uses
lsof. - Linux — fully supported (uses
ssfromiproute2, ornetstatas a fallback). - Windows — use WSL and follow the Linux path, or the PowerShell
equivalents noted in
troubleshooting.md.
Hardware requirements
Any computer made in roughly the last 15 years. The lab only reads local system state and probes the loopback; it needs no minimum RAM, disk, or GPU.
Required software
bash(3.2 or newer — preinstalled on macOS and Linux).- A listening-socket viewer:
lsof(macOS, preinstalled) orss/netstat(Linux). nc(netcat) for the loopback probe — preinstalled on macOS; a one-line install on Linux (seerequirements/README.md).
Free and open-source options
Everything here is free and ships with your OS or installs from your package manager at no cost. No account, API key, or purchase is needed.
Installation
None. Copy this directory (or clone the repository) and change into it:
cd labs/sections/computing-foundations/day-017-tcp-udp-and-ports
If nc or ss is missing on Linux, see requirements/README.md for the
one-line install.
File structure
day-017-tcp-udp-and-ports/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── inspect_ports.sh ← YOUR working file (4 exercises)
│ └── ports-worksheet.md ← worksheet to fill in for your machine
├── examples/
│ └── inspect_ports.sh ← completed reference implementation
├── tests/
│ └── run_tests.sh ← automated checks (read-only, loopback-only)
├── expected-output/
│ ├── sample-macos.txt ← real captured run (macOS, Apple Silicon)
│ └── FIELDS.md ← required sections + platform differences
├── requirements/
│ └── README.md ← dependency statement
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished result first
bash examples/inspect_ports.sh
## 2. Your task: complete the four exercises in the starter, then run it
bash starter/inspect_ports.sh
## 3. Check your work
bash tests/run_tests.sh
What the commands do
bash examples/inspect_ports.sh— the reference script. It detects your OS (uname -s), lists listening TCP ports (lsof -nP -iTCP -sTCP:LISTENon macOS;ss -tlnp/netstat -tlnon Linux), classifies each port into its IANA range and guesses the service, then runs twonc -z -w 1 127.0.0.1 <port>probes to demonstrate one OPEN port and one closed port.bash starter/inspect_ports.sh— the same skeleton with four numbered exercises. Each comment names the exact command to run; you replace theEXERCISE Nplaceholder lines with real commands.bash tests/run_tests.sh— runs the reference script and checks: exit code 0, all four required section headers present, the loopback test targets127.0.0.1, and the script contains nosudo/curl/wgetand noncprobe against any non-loopback address.
Expected output
See expected-output/sample-macos.txt —
a real captured run:
=== Ports and Connections ===
Generated on: 2026-07-12
Operating system kernel: Darwin
--- Listening TCP ports ---
A 'listening' port is a service waiting for incoming connections.
PORT CLASS LIKELY SERVICE PROCESS
4321 registered - node
5000 registered common dev server / AirPlay on macOS ControlCe
7000 registered AirPlay receiver (macOS) ControlCe
...
--- Loopback connection test ---
127.0.0.1 is 'localhost' — this machine talking to itself, no network.
Testing 127.0.0.1:5000 ... OPEN (something is listening here)
Testing 127.0.0.1:1 ... closed (connection refused — nothing listening)
=== End of report ===
Your ports will differ — that is the point. expected-output/FIELDS.md
lists the required sections and describes Linux and Windows differences.
Validation steps
- Run
bash examples/inspect_ports.sh— it must exit without errors. - Confirm all four sections print:
Listening TCP ports,Well-known vs ephemeral,Loopback connection test, and the header/footer. - Confirm the loopback test prints one OPEN line and one closed line, both
against
127.0.0.1. - Fill in
starter/ports-worksheet.mdfor your own machine. - Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 9 checks, 0 failure(s). The command exits 0 on success
and non-zero on any failure, so it can run in CI. The tests confirm the script
is read-only and probes only the loopback.
Cleanup
Nothing to clean up: the scripts only read local state and probe 127.0.0.1;
they write no files and change no settings. To reset your work, restore the
starter from git: git checkout -- starter/inspect_ports.sh.
Troubleshooting
See troubleshooting.md for the full list (lsof shows
fewer ports without sudo — that is fine and expected; missing nc/ss;
IPv6-only listeners; the port-1 "connection refused" demonstration; Windows
notes).
Security notes
See security.md. Short version: read-only, loopback-only, no
sudo, and it never scans or contacts an external host — scanning machines
you do not own can be illegal, so this lab hard-codes 127.0.0.1.
Extension exercises
- Probe the IPv6 loopback for a service bound to
[::1]:nc -z -w 1 ::1 <port>. Which of your services answer on IPv6 but not IPv4? - Add a UDP listing to the report. On macOS:
lsof -nP -iUDP; on Linux:ss -ulnp. Notice UDP sockets have no "LISTEN" state — a clue to how UDP differs from TCP. - Extend the well-known-port lookup table in the script with three more
ports you care about (for example
3306MySQL,27017MongoDB,11434for a local model server) and re-run it.
Navigation
- Previous day: Day 16 — IP Addresses, DNS, and Routing
(
labs/sections/computing-foundations/day-016-ip-addresses-dns-and-routing/). - Next day: Day 18 — HTTP: Requests, Responses, and Methods
(
labs/sections/computing-foundations/day-018-http-requests-responses-and-methods/, to be written).
Expected output
FIELDS.md
# Required report sections (all platforms)
A correct run of `inspect_ports.sh` prints, in order:
1. `=== Ports and Connections ===`
2. `Generated on: YYYY-MM-DD`
3. `Operating system kernel: Darwin` (macOS) or `Linux`
4. `--- Listening TCP ports ---` followed by a table with columns
`PORT CLASS LIKELY SERVICE PROCESS` (one row per listening TCP port;
the exact ports depend on what your machine is running)
5. `--- Well-known vs ephemeral ---` with the three IANA ranges
6. `--- Loopback connection test ---` with one OPEN and one closed probe
against `127.0.0.1`
7. `=== End of report ===`
`sample-macos.txt` in this directory is a real captured run (macOS, Apple
Silicon, 2026-07-12). **Your ports will differ** — that is the whole point;
the set of listening ports is a property of your machine, not a fixed answer.
## Platform differences
- **macOS** uses `lsof -nP -iTCP -sTCP:LISTEN`. The `PROCESS` column shows
the command name (e.g. `ControlCe`, `node`, `rapportd`). Ports `5000` and
`7000` are commonly held by the macOS AirPlay receiver / Control Center.
- **Linux** uses `ss -tlnp` (from `iproute2`), falling back to `ss -tlnH`
for parsing, then `netstat -tln` (from `net-tools`) if `ss` is absent. The
kernel line reads `Linux`; the `PROCESS` column comes from `ss`'s
`users:(("name",pid=...))` field (only shown for your own processes, or all
processes under `sudo` — this lab never uses `sudo`, so you may see fewer
process names).
- On **either** platform, if a service is bound only to the IPv6 loopback
`[::1]`, the IPv4 `nc -z 127.0.0.1` probe will not reach it; the script
says so honestly and suggests probing `::1` instead. This is expected
behaviour, not a bug.
- **Windows**: use `netstat -an | findstr LISTENING` in PowerShell, or run
the script unchanged inside WSL (which behaves as Linux).
sample-macos.txt
=== Ports and Connections ===
Generated on: 2026-07-12
Operating system kernel: Darwin
--- Listening TCP ports ---
A 'listening' port is a service waiting for incoming connections.
PORT CLASS LIKELY SERVICE PROCESS
4321 registered - node
5000 registered common dev server / AirPlay on macOS ControlCe
7000 registered AirPlay receiver (macOS) ControlCe
21460 registered - Code\x20H
42050 registered - OneDrive
54815 ephemeral - Code\x20H
64322 ephemeral - rapportd
--- Well-known vs ephemeral ---
Ports 0-1023 are WELL-KNOWN: reserved for standard services.
22=SSH 53=DNS 80=HTTP 443=HTTPS 123=NTP 631=IPP
Ports 1024-49151 are REGISTERED (databases, dev servers: 5432, 6379, 8000).
Ports 49152-65535 are EPHEMERAL: short-lived client-side port numbers.
--- Loopback connection test ---
127.0.0.1 is 'localhost' — this machine talking to itself, no network.
Testing 127.0.0.1:5000 ... OPEN (something is listening here)
Testing 127.0.0.1:1 ... closed (connection refused — nothing listening)
=== End of report ===
Source files
examples/inspect_ports.sh (6002 bytes)
#!/usr/bin/env bash
# Day 017 lab — completed reference implementation.
# "See Ports and Connections": list the TCP ports this machine is LISTENING
# on, explain which are well-known, and prove a loopback connection with nc.
#
# This script is strictly READ-ONLY and LOCAL:
# - it only reads the local list of listening sockets (lsof/ss/netstat)
# - the only network action is a loopback (127.0.0.1) connectivity probe
# - it never connects to, or scans, any external host, and needs no sudo.
set -euo pipefail
os="$(uname -s)"
echo "=== Ports and Connections ==="
echo "Generated on: $(date '+%Y-%m-%d')"
echo "Operating system kernel: ${os}"
# ---------------------------------------------------------------------------
# name_for_port PORT — a tiny well-known-port lookup table.
# Returns a human-readable guess for the service usually behind a port.
# ---------------------------------------------------------------------------
name_for_port() {
case "$1" in
22) echo "SSH (secure shell login)" ;;
53) echo "DNS (domain name lookups)" ;;
80) echo "HTTP (unencrypted web)" ;;
123) echo "NTP (clock sync)" ;;
443) echo "HTTPS (encrypted web)" ;;
631) echo "IPP (printing)" ;;
3000) echo "common dev server (Node/React)" ;;
5000) echo "common dev server / AirPlay on macOS" ;;
5432) echo "PostgreSQL database" ;;
6379) echo "Redis database" ;;
7000) echo "AirPlay receiver (macOS)" ;;
8000) echo "common dev / model server" ;;
8080) echo "common HTTP alternative" ;;
*) echo "-" ;;
esac
}
# ---------------------------------------------------------------------------
# class_for_port PORT — which of the three IANA ranges a port falls in.
# 0-1023 : well-known (system) ports
# 1024-49151 : registered ports
# 49152-65535 : ephemeral / dynamic ports
# ---------------------------------------------------------------------------
class_for_port() {
local p="$1"
if [ "$p" -le 1023 ]; then echo "well-known"
elif [ "$p" -le 49151 ]; then echo "registered"
else echo "ephemeral"
fi
}
# ---------------------------------------------------------------------------
# list_listening — print "PORT PROCESS" lines for every listening TCP socket,
# choosing the best available tool for this OS. Read-only.
# ---------------------------------------------------------------------------
list_listening() {
if [ "${os}" = "Darwin" ] && command -v lsof >/dev/null 2>&1; then
# macOS: lsof reports the local address in the NAME column, e.g.
# "*:7000 (LISTEN)" or "127.0.0.1:54815 (LISTEN)". Take the port after
# the LAST colon of the second-to-last field; the command is field 1.
lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | awk '
NR > 1 {
addr = $(NF - 1)
n = split(addr, parts, ":")
port = parts[n]
if (port ~ /^[0-9]+$/) print port, $1
}'
elif command -v ss >/dev/null 2>&1; then
# Linux: ss -tlnH lists listening TCP sockets with no header. The local
# address:port is field 4; the port is after the last colon.
ss -tlnH 2>/dev/null | awk '
{
addr = $4
n = split(addr, parts, ":")
port = parts[n]
proc = $NF
if (port ~ /^[0-9]+$/) print port, proc
}'
elif command -v netstat >/dev/null 2>&1; then
# Fallback: netstat. -tln = TCP, listening, numeric.
netstat -tln 2>/dev/null | awk '
/LISTEN/ {
addr = $4
n = split(addr, parts, ":")
port = parts[n]
if (port ~ /^[0-9]+$/) print port, "(netstat)"
}'
else
echo "NO_TOOL"
fi
}
echo
echo "--- Listening TCP ports ---"
echo "A 'listening' port is a service waiting for incoming connections."
raw="$(list_listening | sort -n -u)"
ports_only=""
if [ "${raw}" = "NO_TOOL" ] || [ -z "${raw}" ]; then
echo "(No listening-port tool found, or no listening TCP ports detected.)"
echo "Install lsof (macOS) or iproute2/net-tools (Linux) to see more."
else
printf '%-7s %-12s %-32s %s\n' "PORT" "CLASS" "LIKELY SERVICE" "PROCESS"
while read -r port proc; do
[ -z "${port}" ] && continue
ports_only="${ports_only} ${port}"
printf '%-7s %-12s %-32s %s\n' \
"${port}" "$(class_for_port "${port}")" "$(name_for_port "${port}")" "${proc}"
done <<EOF
${raw}
EOF
fi
echo
echo "--- Well-known vs ephemeral ---"
echo "Ports 0-1023 are WELL-KNOWN: reserved for standard services."
echo " 22=SSH 53=DNS 80=HTTP 443=HTTPS 123=NTP 631=IPP"
echo "Ports 1024-49151 are REGISTERED (databases, dev servers: 5432, 6379, 8000)."
echo "Ports 49152-65535 are EPHEMERAL: short-lived client-side port numbers."
echo
echo "--- Loopback connection test ---"
echo "127.0.0.1 is 'localhost' — this machine talking to itself, no network."
if command -v nc >/dev/null 2>&1; then
# Find a port that actually answers on the IPv4 loopback, to demonstrate the
# OPEN case. (Some services bind only to the IPv6 loopback [::1] and will
# not answer on 127.0.0.1 — a real subtlety, not an error.)
open_port=""
for port in ${ports_only}; do
if nc -z -w 1 127.0.0.1 "${port}" >/dev/null 2>&1; then
open_port="${port}"
break
fi
done
if [ -n "${open_port}" ]; then
echo "Testing 127.0.0.1:${open_port} ... OPEN (something is listening here)"
else
echo "No listed port answered on the 127.0.0.1 (IPv4) loopback to probe as OPEN."
echo "(Services may be bound only to the IPv6 loopback [::1] — try ::1 instead.)"
fi
# Port 1 is virtually never a listening service on a normal machine, so it
# demonstrates the 'connection refused' case that means "nothing is here".
if nc -z -w 1 127.0.0.1 1 >/dev/null 2>&1; then
echo "Testing 127.0.0.1:1 ... OPEN (unexpected, but honestly reported)"
else
echo "Testing 127.0.0.1:1 ... closed (connection refused — nothing listening)"
fi
else
echo "(nc not found — install it to run the loopback probe.)"
fi
echo "=== End of report ==="
metadata.yml (564 bytes)
lesson_id: D017
day: 17
kind: command-line-inspection
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-017-tcp-udp-and-ports
run_commands:
- bash examples/inspect_ports.sh
- bash starter/inspect_ports.sh
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/inspect_ports.sh # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash tests/run_tests.sh → 9 checks, 0 failure(s)'
requirements/README.md (1024 bytes)
# Dependencies — Day 017 lab
**None that you have to install on a normal machine.** Every tool this lab
uses ships with macOS and mainstream Linux distributions:
- `bash` ≥ 3.2 (preinstalled on macOS and Linux)
- Standard utilities: `uname`, `date`, `awk`, `sort`
- **A listening-socket viewer** — one of:
- `lsof` (preinstalled on macOS; `sudo apt install lsof` on Debian/Ubuntu)
- `ss` (from `iproute2`, preinstalled on most modern Linux)
- `netstat` (from `net-tools`; the last-resort fallback)
- **`nc` / netcat** for the loopback connection test:
- macOS ships BSD `nc` at `/usr/bin/nc`
- Linux: `sudo apt install netcat-openbsd` (Debian/Ubuntu) or
`sudo dnf install nmap-ncat` (Fedora/RHEL) if it is missing
The script degrades gracefully: if no socket viewer is found it says so, and
if `nc` is missing it skips the probe with a clear message. Nothing here
needs `sudo`, a network connection to the outside world, an API key, or a
purchase — it only reads local state and probes `127.0.0.1`.
starter/inspect_ports.sh (4035 bytes)
#!/usr/bin/env bash
# Day 017 lab — See Ports and Connections (STARTER).
#
# Your job: complete the four numbered exercises below. Each one names the
# EXACT command to run. Replace the `EXERCISE N` placeholder lines (and the
# `echo` stand-ins) so the script prints the four required sections, exactly
# like examples/inspect_ports.sh. Try it yourself before peeking at the example.
#
# This script must stay READ-ONLY and LOCAL: only read the local list of
# listening sockets, and only ever probe the 127.0.0.1 loopback. Never
# connect to or scan an external host, and never use sudo.
set -euo pipefail
os="$(uname -s)"
echo "=== Ports and Connections ==="
echo "Generated on: $(date '+%Y-%m-%d')"
echo "Operating system kernel: ${os}"
# ===========================================================================
# EXERCISE 1 — List the listening TCP ports on this machine (READ-ONLY).
#
# On macOS, run: lsof -nP -iTCP -sTCP:LISTEN
# On Linux, run: ss -tlnp (or, if ss is absent: netstat -tln)
#
# Capture the raw listing into the variable `listening` below. The command
# substitution $( ... ) runs the command and stores its text output.
# Replace the placeholder line with the correct branch for your OS.
# ===========================================================================
echo
echo "--- Listening TCP ports ---"
if [ "${os}" = "Darwin" ]; then
listening="$(echo 'EXERCISE 1: replace me with lsof -nP -iTCP -sTCP:LISTEN')"
else
listening="$(echo 'EXERCISE 1: replace me with ss -tlnp (or netstat -tln)')"
fi
echo "${listening}"
# ===========================================================================
# EXERCISE 2 — Pull out just the port numbers, sorted and de-duplicated.
#
# The port is the number after the LAST colon of the local address. On macOS
# lsof, the address is the second-to-last field (e.g. "*:7000"); on Linux ss
# it is field 4 (e.g. "0.0.0.0:22"). Fill in the awk field and finish the
# pipeline with: sort -n -u
#
# Hint (macOS): awk 'NR>1 { n=split($(NF-1),p,":"); print p[n] }'
# Hint (Linux): awk '{ n=split($4,p,":"); print p[n] }'
# ===========================================================================
echo
echo "--- Well-known vs ephemeral ---"
echo "EXERCISE 2: extract the port numbers here, then classify them:"
echo " ports 0-1023 are WELL-KNOWN (22=SSH 53=DNS 80=HTTP 443=HTTPS)."
echo " ports 1024-49151 are REGISTERED; 49152-65535 are EPHEMERAL."
# ports="$(echo "${listening}" | awk 'REPLACE_ME' | sort -n -u)"
# ===========================================================================
# EXERCISE 3 — Prove an OPEN port on the loopback with netcat.
#
# Pick a port number you saw in Exercise 1 that is bound to 127.0.0.1 or *,
# then test it WITHOUT sending data using the -z ("zero-I/O") flag:
#
# nc -z -w 1 127.0.0.1 <PORT>
#
# nc exits 0 if the port is OPEN (something is listening) and non-zero if it
# is closed. Replace <PORT> below with a real port from your own output.
# ===========================================================================
echo
echo "--- Loopback connection test ---"
echo "127.0.0.1 is 'localhost' — this machine talking to itself, no network."
# if nc -z -w 1 127.0.0.1 <PORT> >/dev/null 2>&1; then
# echo "Testing 127.0.0.1:<PORT> ... OPEN"
# else
# echo "Testing 127.0.0.1:<PORT> ... closed"
# fi
echo "EXERCISE 3: run nc -z -w 1 127.0.0.1 <PORT> for a real open port."
# ===========================================================================
# EXERCISE 4 — Prove a CLOSED port ("connection refused").
#
# Probe a port that is almost certainly NOT listening (port 1 is a safe
# choice) and report the refusal:
#
# nc -z -w 1 127.0.0.1 1
#
# "Connection refused" is exactly what a client sees when nothing is
# listening on the other side — the everyday cause of that error.
# ===========================================================================
echo "EXERCISE 4: run nc -z -w 1 127.0.0.1 1 and report 'connection refused'."
echo "=== End of report ==="
starter/ports-worksheet.md (1759 bytes)
# Ports worksheet — Day 017
Fill this in from your own machine using the commands in the lesson and the
lab (`lsof -nP -iTCP -sTCP:LISTEN` on macOS, `ss -tlnp` or `netstat -tln` on
Linux, and `nc -z -w 1 127.0.0.1 <port>` for the connection test). Everything
here is read-only and local — you never touch another machine.
## 1. Two ports your machine is listening on
| Port number | IANA class (well-known / registered / ephemeral) | Which service do you think it is, and why? |
| ----------- | ------------------------------------------------ | ------------------------------------------ |
| | | |
| | | |
Reminder of the ranges: **0–1023** well-known, **1024–49151** registered,
**49152–65535** ephemeral. Use the process name in the last column of your
listing (and the well-known-port table in the lesson) to make your guess.
## 2. Is port 80 open locally?
Run the loopback probe against the standard HTTP port:
```bash
nc -z -w 1 127.0.0.1 80
```
- Command's exit status (`echo $?` right after): __________
- Is anything listening on port 80 of this machine? (open / closed): __________
- If it is closed, what error would a browser-style client report when it
tried to connect there? __________________________________________________
## 3. One sentence in your own words
Explain, in one sentence, the difference between a **port** and an **IP
address**, using the "building with an address, apartments with numbers"
analogy from the lesson:
_______________________________________________________________________________
tests/run_tests.sh (3074 bytes)
#!/usr/bin/env bash
# Tests for the Day 017 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Verifies that the reference script runs read-only, prints the four required
# sections, performs only loopback probes, and exits 0. It must NOT open any
# external connection and must NOT require sudo.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
script="${lab_dir}/examples/inspect_ports.sh"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
echo "Testing ${script} ..."
# 1. It must run and exit 0.
if output="$(bash "${script}" 2>&1)"; then
check "script exits successfully (exit 0)" "yes"
else
check "script exits successfully (exit 0)" "no"
echo "${output}" | sed 's/^/ /'
fi
# 2. Required section headers are all present.
echo "${output}" | grep -q '^=== Ports and Connections ===$' && \
check "prints report header" "yes" || check "prints report header" "no"
echo "${output}" | grep -q '^--- Listening TCP ports ---$' && \
check "prints 'Listening TCP ports' section" "yes" || check "prints 'Listening TCP ports' section" "no"
echo "${output}" | grep -q '^--- Well-known vs ephemeral ---$' && \
check "prints 'Well-known vs ephemeral' section" "yes" || check "prints 'Well-known vs ephemeral' section" "no"
echo "${output}" | grep -q '^--- Loopback connection test ---$' && \
check "prints 'Loopback connection test' section" "yes" || check "prints 'Loopback connection test' section" "no"
echo "${output}" | grep -q '^=== End of report ===$' && \
check "prints report footer" "yes" || check "prints report footer" "no"
# 3. The loopback test must reference 127.0.0.1 and only 127.0.0.1.
echo "${output}" | grep -q '127\.0\.0\.1' && \
check "loopback test targets 127.0.0.1" "yes" || check "loopback test targets 127.0.0.1" "no"
# 4. Read-only / local-only guarantees, checked against the source itself.
if grep -Eq 'sudo|rm -rf|>[^&]|curl |wget ' "${script}"; then
# >/dev/null and 2>&1 redirections are fine; a bare write redirect is not.
if grep -Eq 'sudo |curl |wget ' "${script}"; then
check "script contains no sudo/curl/wget (local & read-only)" "no"
else
check "script contains no sudo/curl/wget (local & read-only)" "yes"
fi
else
check "script contains no sudo/curl/wget (local & read-only)" "yes"
fi
# 5. The only host it ever contacts is the loopback (no external IPs/hostnames
# passed to nc). Every nc invocation must target 127.0.0.1.
if grep -Eq '\bnc\b' "${script}"; then
bad_nc="$(grep -E '\bnc .*(-z|-w)' "${script}" | grep -v '127\.0\.0\.1' || true)"
if [ -z "${bad_nc}" ]; then
check "every nc probe targets the loopback only" "yes"
else
check "every nc probe targets the loopback only" "no"
echo "${bad_nc}" | sed 's/^/ offending: /'
fi
else
check "every nc probe targets the loopback only" "yes"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 017 lab
lsof shows fewer ports than I expected
Without sudo, lsof only reports sockets your own user owns, and macOS may
hide some system services. That is fine — this lab is deliberately
read-only and never asks for elevated privileges. You will still see your own
listening services (editors, dev servers, sync agents). Running with sudo
would reveal more, but the course's rule is: don't sudo a script you are
learning from.
command not found: lsof (or ss, or netstat)
The script tries lsof, then ss, then netstat, and prints a clear
message if none is present. To install one:
- Linux (Debian/Ubuntu):
sudo apt install iproute2(forss) orsudo apt install lsof - Linux (Fedora/RHEL):
sudo dnf install iprouteorsudo dnf install lsof - macOS:
lsofis preinstalled; nothing to do.
command not found: nc
Install netcat: sudo apt install netcat-openbsd (Debian/Ubuntu),
sudo dnf install nmap-ncat (Fedora/RHEL). macOS ships nc already. If you
skip it, you still get the listening-port listing; only the loopback probe is
missing.
A port I can see listening reports "closed" in the loopback test
Some services bind only to the IPv6 loopback [::1], not the IPv4
loopback 127.0.0.1. A nc -z 127.0.0.1 <port> probe cannot reach an
IPv6-only listener, so it reports closed even though the service is up. Probe
the IPv6 loopback instead: nc -z -w 1 ::1 <port>. This is a real networking
subtlety, not a mistake in the lab.
The loopback test says port 1 is "connection refused"
That is the intended demonstration. Port 1 has nothing listening on a normal machine, so the kernel immediately refuses the connection — the exact meaning of the "connection refused" error you will meet again and again.
nc -z 127.0.0.1 5000 succeeds but no dev server is running (macOS)
On macOS, the AirPlay receiver / Control Center often holds ports 5000 and
7000. If you need those ports for your own server, turn off "AirPlay
Receiver" in System Settings → General → AirDrop & Handoff.
Windows: bash is not recognized
Use WSL (wsl --install, then open your Linux distro and follow the Linux
path), or, in PowerShell, list ports with netstat -an | findstr LISTENING
and test a port with Test-NetConnection -ComputerName 127.0.0.1 -Port <n>.
Security notes
Security notes — Day 017 lab
- What the scripts do: read the local list of listening TCP sockets
(
lsof/ss/netstat) and print it, then make one or two connectivity probes to the loopback address127.0.0.1withnc -z(no data is sent). They write no files, change no settings, and make no connection to any external host. - Loopback only — never scan others. The
-zprobes only ever target127.0.0.1(this machine talking to itself). Pointing a port scanner at machines you do not own is, in many jurisdictions, unauthorised access — it can be a crime and will get you banned from most networks. This lab hard-codes the loopback so you cannot do it by accident, and the test suite fails if anyncprobe uses a non-loopback address. - Privileges: everything runs as your normal user. Nothing needs
sudo. Withoutsudo,lsofshows fewer entries — that is a feature, not a problem. If a tutorial ever tells you tosudoa script you have not read, stop and read it first. - Privacy: the list of listening ports and process names is mildly revealing (it shows what software you run and its patch surface). Sharing it in a class forum is normally fine; avoid posting the full listing of an employer-managed machine.
- Reading before running: both scripts are short and commented. Reading a script before you run it is the single best habit for staying safe on the command line, and this course reinforces it every day.