Computing FoundationsHow the Internet Works › Day 17

Hands-on lab — Day 17: 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-ports when 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) or ss / 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 -z to 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 ss from iproute2, or netstat as 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) or ss / netstat (Linux).
  • nc (netcat) for the loopback probe — preinstalled on macOS; a one-line install on Linux (see requirements/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:LISTEN on macOS; ss -tlnp / netstat -tln on Linux), classifies each port into its IANA range and guesses the service, then runs two nc -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 the EXERCISE N placeholder 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 targets 127.0.0.1, and the script contains no sudo/curl/wget and no nc probe 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

  1. Run bash examples/inspect_ports.sh — it must exit without errors.
  2. Confirm all four sections print: Listening TCP ports, Well-known vs ephemeral, Loopback connection test, and the header/footer.
  3. Confirm the loopback test prints one OPEN line and one closed line, both against 127.0.0.1.
  4. Fill in starter/ports-worksheet.md for your own machine.
  5. 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

  1. 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?
  2. 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.
  3. Extend the well-known-port lookup table in the script with three more ports you care about (for example 3306 MySQL, 27017 MongoDB, 11434 for a local model server) and re-run it.
  • 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 (for ss) or sudo apt install lsof
  • Linux (Fedora/RHEL): sudo dnf install iproute or sudo dnf install lsof
  • macOS: lsof is 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 address 127.0.0.1 with nc -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 -z probes only ever target 127.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 any nc probe uses a non-loopback address.
  • Privileges: everything runs as your normal user. Nothing needs sudo. Without sudo, lsof shows fewer entries — that is a feature, not a problem. If a tutorial ever tells you to sudo a 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.