Network Diagnostics Toolkit with Bash

Build a modular bash script that diagnoses network health across ICMP, DNS, and TCP layers.

Introduction

30 Second Summary

You know that moment when the internet "stops working" and you have no idea if it's your Wi-Fi, a DNS issue, or the website itself being down? Most people just restart their router and hope for the best.

In this project, you will build a Bash script that diagnoses network problems across multiple layers. Instead of guessing, you'll have a single command that checks connectivity, resolves domain names, scans ports, and reports on your machine's active connections.

What You'll Build

Run ./netcheck.sh in your terminal and watch a color-coded health report appear in seconds, telling you exactly which network layers are healthy and which ones need attention.

By the end of this project, you'll have:

  • A color-coded diagnostics tool that pings hosts, resolves DNS, and scans TCP ports, showing green pass or red fail results for each check.
  • A live connection dashboard that shows every active TCP connection and listening service on your machine, so you can spot abnormal states at a glance.
  • A menu system that lets you run individual diagnostic checks (ping only, DNS only, ports only) or the full suite with a single argument.
  • Secret Mission: Add a --report flag that generates a clean, timestamped log file you can share with teammates or attach to incident timelines.

Are there any prerequisites?

You should be comfortable with basic bash scripting (variables, loops, and conditionals) and have a theoretical understanding of IP addresses. You'll also need a Lubuntu VM running on Oracle VirtualBox with nano available.

Before We Start

Network troubleshooting is one of those skills every DevOps engineer uses daily. When a service goes down, the first question is always: is it the network?

Before you can automate diagnostics, you need the right tools available on your system. In this step, you will install the missing networking utilities, verify everything works, and create your project script with safe defaults.

In this step, get ready to:
  • Install the dnsutils package and verify your networking tools respond correctly.
  • Create your project directory and script file with safe scripting defaults.
  • Run your script for the first time and see output in the terminal.
Install dnsutils and verify networking tools

Your Lubuntu VM comes with most networking tools pre-installed. The one missing piece is dig, a DNS lookup utility.

  • Update your package lists and install dnsutils by running this command:
sudo apt update && sudo apt install dnsutils -y

What does this command do?

This runs two commands in sequence. sudo apt update refreshes your package list so your system knows what is available.

sudo apt install dnsutils -y installs the dnsutils package, which provides the dig command for DNS lookups. The -y flag automatically confirms the installation.

  • Verify your pre-installed tools work by running each of these commands one at a time:
ping -c 1 8.8.8.8

You should see a response showing bytes received from 8.8.8.8 with a round-trip time.

ss --version

You should see a version string from the ss utility (part of iproute2).

ip addr show

You should see your network interfaces listed with IP addresses assigned.

Seeing 'command not found' for ping or ss?

These tools come pre-installed on Lubuntu via the ubuntu-minimal package. If any are missing, run sudo apt install iputils-ping iproute2 to install them.

help me troubleshoot missing networking tools on my Lubuntu VM

  • Confirm dig installed successfully by running:
dig -v

You should see a version number printed, confirming dnsutils is ready to use.

Create your project directory and script file

With all your tools confirmed, you need a home for your script. You will create a dedicated project folder and set up your script file with safe defaults that catch errors early.

  • Create the ~/netcheck/ directory and move into it by running these commands:
mkdir ~/netcheck
cd ~/netcheck
  • Confirm you are in the right directory by running:
pwd

You should see /home/your-username/netcheck printed.

  • Create an empty script file and set it as executable by running:
touch netcheck.sh
chmod +x netcheck.sh

What does chmod +x do?

chmod +x adds execute permission to the file. Without this, the system would refuse to run your script directly. You would get a "Permission denied" error.

  • Verify the file was created by running:
ls -l netcheck.sh

You should see netcheck.sh listed with an x in its permissions (something like -rwxr-xr-x).

  • Open the file in nano by running:
nano netcheck.sh
  • Add the shebang line and safe scripting defaults by typing this into the file:
#!/bin/bash
set -euo pipefail

What do these lines do?

#!/bin/bash is the shebang line. It tells your system to use Bash as the interpreter for this script.

set -euo pipefail enables strict error handling. -e exits on any error. -u treats unset variables as errors. -o pipefail catches errors in piped commands. Together they prevent silent failures in your script.

  • Save the file by pressing Ctrl+O then Enter, and exit nano by pressing Ctrl+X.
Add a header and run your script

Your script has safe defaults but does not produce any output yet. Adding a simple header line gives you visible proof that the script executes correctly.

  • Open the script in nano again by running:
nano netcheck.sh
  • Add a blank line after set -euo pipefail, then add this header print statement:
echo "netcheck.sh - Network Diagnostics Toolkit"

What does this line do?

This prints a title banner when the script runs. It is your first visible output, confirming the shebang, permissions, and strict mode are all working correctly.

  • Save the file with Ctrl+O then Enter, and exit with Ctrl+X.

Before you run this, what do you think you will see printed in the terminal?

  • Run your script by executing:
./netcheck.sh

You should see netcheck.sh - Network Diagnostics Toolkit printed to your terminal.

Seeing 'Permission denied' or no output?

If you get "Permission denied", the execute bit was not set. Run chmod +x netcheck.sh and try again.

If you see no output at all, make sure you saved the file in nano (Ctrl+O confirms the save). You can check the file contents with cat netcheck.sh to verify your edits are there.

help me troubleshoot why my netcheck.sh script is not producing output

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Your netcheck.sh file should look exactly like this:

#!/bin/bash
set -euo pipefail

echo "netcheck.sh - Network Diagnostics Toolkit"

Your toolkit is alive. Next up, you will teach it to ping real hosts and report whether they are reachable with color-coded output.

Build the Connectivity Checker

The most basic network question any engineer asks is: "can I reach anything at all?" Before you diagnose DNS, firewalls, or application-layer problems, you need to know whether packets leave your machine and come back.

In this step, you will build a connectivity checker that pings real hosts and reports the results in color-coded output. You will also see what ping cannot tell you, which motivates the deeper checks you will build in later steps.

In this step, get ready to:
  • Define ANSI color codes and reusable output helpers for your script.
  • Write a connectivity check function that pings multiple hosts and prints pass/fail results.
  • Run the script and observe the limitation of ping-only diagnostics.
Set up color codes and output helpers

Terminal output becomes unreadable fast when every line looks the same. ANSI escape codes let you print colored text so pass/fail results stand out at a glance.

  • Open netcheck.sh in nano by running:
nano ~/netcheck/netcheck.sh
  • Find the existing line that says:
echo "netcheck.sh - Network Diagnostics Toolkit"
  • Delete that line. Position your cursor on it and press Ctrl+K to cut it.
  • In its place, add the following color variable definitions and helper functions:
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'

# --- Helper Functions ---
print_section_header() {
    echo ""
    echo -e "${CYAN}=== $1 ===${NC}"
    echo ""
}

print_pass() {
    echo -e "  ${GREEN}[PASS]${NC} $1"
}

print_fail() {
    echo -e "  ${RED}[FAIL]${NC} $1"
}

print_warn() {
    echo -e "  ${YELLOW}[WARN]${NC} $1"
}

What does this code do?

Each color variable stores an ANSI escape sequence that tells your terminal to switch text color. NC (No Color) resets back to default so color does not bleed into subsequent output.

The three helper functions give you reusable building blocks. print_pass() prints a green [PASS] label, print_fail() prints a red [FAIL] label, and print_section_header() prints a cyan divider line so each check section is visually distinct.

  • Save the file with Ctrl+O, press Enter, then exit nano with Ctrl+X.
  • Run the script to confirm there are no syntax errors:
./netcheck.sh

The script should return to your command prompt with no output and no error messages. This confirms your function definitions and color codes are syntactically correct.

Seeing a syntax error?

  • Check that each function ends with a closing } on its own line.
  • Make sure the quotes around color codes are single quotes, not backticks or double quotes.
  • Verify you kept #!/bin/bash and set -euo pipefail on the first two lines.

help me fix a syntax error in my bash script with function definitions and ANSI color variables

Write the connectivity check and main entry point

Now you need the function that actually tests network reachability. The ping command sends an ICMP echo request to a host and waits for a reply. If the reply comes back, the host is reachable.

  • Open the script again in nano:
nano ~/netcheck/netcheck.sh
  • Below the print_warn() function, add the connectivity check function, a banner printer, and the main entry point:
# --- Connectivity Check ---
check_connectivity() {
    print_section_header "Connectivity Check (ICMP)"
    local hosts=("8.8.8.8" "1.1.1.1" "192.168.1.1")
    for host in "${hosts[@]}"; do
        if ping -c 1 -W 2 "$host" > /dev/null 2>&1; then
            print_pass "$host is reachable"
        else
            print_fail "$host is unreachable"
        fi
    done
}

# --- Main ---
print_banner() {
    echo -e "${CYAN}"
    echo "  netcheck.sh - Network Diagnostics Toolkit"
    echo "  $(date '+%Y-%m-%d %H:%M:%S')"
    echo -e "${NC}"
}

main() {
    print_banner
    check_connectivity
}

main "$@"

What does this code do?

  • check_connectivity() loops through an array of three IP addresses. For each one, it runs ping -c 1 -W 2 (send 1 packet, wait 2 seconds max). If ping exits with code 0, the host is reachable. Any other exit code means failure.
  • 8.8.8.8 is Google's public DNS, 1.1.1.1 is Cloudflare's, and 192.168.1.1 is a common home router gateway (which may or may not exist in your VM network).
  • print_banner() shows the script name and a timestamp. main() orchestrates the run, and main "$@" at the bottom calls it, passing any command-line arguments through.
  • Save with Ctrl+O then Enter, and exit with Ctrl+X.

Before you run the script, think about what you expect to see. Which of the three hosts do you think will respond, and which might not?

  • Run the script:
./netcheck.sh

You should see a cyan banner with the timestamp, followed by a "Connectivity Check (ICMP)" section. Each host shows a green [PASS] if reachable or a red [FAIL] if not.

Why might 192.168.1.1 fail?

Your VM's virtual network adapter may use a different gateway address (like 10.0.2.2 for VirtualBox NAT mode). A red [FAIL] for 192.168.1.1 is perfectly normal and actually proves your error handling works correctly.

Script hangs or takes a long time?

  • Each ping waits up to 2 seconds (-W 2). If multiple hosts are unreachable, expect up to 6 seconds total.
  • If it hangs beyond 10 seconds, press Ctrl+C to cancel, then check your VM has network connectivity with ping -c 1 8.8.8.8 directly.

help me troubleshoot my bash ping script that hangs or takes too long

Observe what ping cannot tell you

Look at your output. You know which hosts are reachable and which are not. That is useful, but consider what questions remain unanswered.

  • Ping cannot tell you why a host failed. Was it a DNS problem? A firewall blocking the port? The service being down?
  • Ping cannot tell you if a specific service (like a web server on port 443) is running.
  • Ping cannot tell you what connections your own machine has open right now.

These are the gaps you will close in the next steps by adding DNS resolution, port scanning, and connection analysis.

✔️ Awesome, I've got everything!

Great. Double check you have saved netcheck.sh and that running it produces color-coded output with at least one green [PASS] line.

ⓧ I'd like to double check the full code

Your netcheck.sh should look exactly like this:

#!/bin/bash
set -euo pipefail

# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'

# --- Helper Functions ---
print_section_header() {
    echo ""
    echo -e "${CYAN}=== $1 ===${NC}"
    echo ""
}
print_pass() {
    echo -e "  ${GREEN}[PASS]${NC} $1"
}

print_fail() {
    echo -e "  ${RED}[FAIL]${NC} $1"
}

print_warn() {
    echo -e "  ${YELLOW}[WARN]${NC} $1"
}

# --- Connectivity Check ---
check_connectivity() {
    print_section_header "Connectivity Check (ICMP)"
    local hosts=("8.8.8.8" "1.1.1.1" "192.168.1.1")
    for host in "${hosts[@]}"; do
        if ping -c 1 -W 2 "$host" > /dev/null 2>&1; then
            print_pass "$host is reachable"
        else
            print_fail "$host is unreachable"
        fi
    done
}
# --- Main ---
print_banner() {
    echo -e "${CYAN}"
    echo "  netcheck.sh - Network Diagnostics Toolkit"
    echo "  $(date '+%Y-%m-%d %H:%M:%S')"
    echo -e "${NC}"
}

main() {
    print_banner
    check_connectivity
}

main "$@"

Your connectivity checker works. You can see which hosts are reachable at a glance. Next up, you will add DNS resolution to reveal whether name lookup failures are hiding behind a simple "unreachable" result.

Add DNS Resolution Checks

Your connectivity check proves whether hosts are reachable, but it has a blind spot. When ping fails for a domain name, it cannot tell you whether the network is down or the name simply failed to resolve to an IP address.

DNS is the translation layer between human-readable domain names and IP addresses. If DNS breaks, everything that uses domain names appears unreachable even though the network itself is fine. This step adds a DNS resolution layer that exposes that hidden failure point.

In this step, get ready to:
  • Write a check_dns() function that resolves domains and measures response time.
  • Integrate the DNS check into your script and observe passing and failing lookups.
Write the DNS resolution function

The dig command queries DNS servers and returns the resolved IP address for a domain. Combined with a timing measurement, you can see both whether resolution succeeds AND how long it takes.

  • Open netcheck.sh in nano by running:
nano ~/netcheck/netcheck.sh
  • Find the # --- Main --- comment line.
  • Add the following function ABOVE that comment, leaving one blank line between the new function and the # --- Main --- line:
# --- DNS Resolution ---
check_dns() {
    print_section_header "DNS Resolution"
    local domains=("google.com" "github.com" "fakdomain.invalid")
    for domain in "${domains[@]}"; do
        local start_ms
        start_ms=$(date +%s%3N)
        local result
        result=$(dig +short "$domain" 2>/dev/null | head -1)
        local end_ms
        end_ms=$(date +%s%3N)
        local elapsed=$(( end_ms - start_ms ))
        if [[ -n "$result" ]]; then
            print_pass "$domain -> $result (${elapsed}ms)"
        else
            print_fail "$domain -> resolution failed (${elapsed}ms)"
        fi
    done
}

What does this code do?

This function performs DNS lookups and times each one:

  • domains is an array of test targets. fakdomain.invalid is deliberately bogus so you can see what a DNS failure looks like.
  • date +%s%3N captures the current time in milliseconds. By recording it before and after the dig call, you get the lookup duration.
  • dig +short queries DNS and returns only the resolved IP address. If the domain does not exist, it outputs nothing.
  • The -n test checks whether the result is non-empty. A non-empty result means resolution succeeded.
Integrate the DNS check and run the script

The function exists but nothing calls it yet. You need to add a call in main() so the DNS check runs after the connectivity check.

  • Scroll down to the main() function and find this line:
    check_connectivity
  • Add check_dns on the line directly below it, so main() now looks like this:
main() {
    print_banner
    check_connectivity
    check_dns
}
  • Save the file with Ctrl+O, press Enter, then exit nano with Ctrl+X.

Before you run the script, think about what you expect to see. Which domains do you think will resolve successfully, and which will fail?

  • Run your script by executing:
./netcheck.sh

You should see your existing connectivity section followed by a new DNS Resolution section. Green PASS lines appear for google.com and github.com with their resolved IPs and millisecond timings. A red FAIL line appears for fakdomain.invalid because that domain does not exist.

Why does timing matter?

A DNS lookup that takes 200ms or more can make your network feel sluggish even when connectivity is perfect. The millisecond value next to each result tells you whether DNS itself is a bottleneck.

If you see fast connectivity results (ping passes) but slow DNS times, that points directly at your DNS server as the culprit. Without timing, you would never know.

Seeing 'dig: command not found'?

This means the dnsutils package was not installed successfully in Step 1. Run sudo apt install dnsutils and try the script again.

If you see a syntax error instead, double-check that the $(date +%s%3N) lines use backtick-free syntax and that every quote is matched.

Help me troubleshoot my netcheck.sh script

✔️ Awesome, I've got everything!

Great. Double check you saved netcheck.sh and that both the connectivity and DNS sections appear when you run it.

ⓧ I'd like to double check the full code

Your netcheck.sh should contain these sections in order. Scroll through your file and confirm each is present:

  • The shebang line (#!/bin/bash) and set -euo pipefail.
  • The # --- Colors --- section with RED, GREEN, YELLOW, CYAN, NC variables.
  • The # --- Helper Functions --- section with print_section_header(), print_pass(), print_fail(), and print_warn().
  • The # --- Connectivity Check --- section with check_connectivity() containing the hosts array and ping loop.
  • The # --- DNS Resolution --- section with check_dns() containing the domains array, dig +short calls, and millisecond timing.
  • The # --- Main --- section with print_banner() and main() calling check_connectivity then check_dns.

Your toolkit now diagnoses two network layers. Next up, you will add port scanning to check whether specific services are actually running on remote hosts.

Scan TCP Ports

Your script can now tell you whether a host is reachable and whether DNS is resolving. But there is a third layer that neither ping nor dig can see: is the actual service you need running on the port you expect?

A web server might be reachable at the IP level, but if nothing is listening on port 443, your HTTPS request will fail. In this step, you will add a TCP port scanner that uses bash's built-in /dev/tcp pseudo-device. No extra tools required.

In this step, get ready to:
  • Write a function that tests whether a specific port is open, closed, or filtered.
  • Build a port scanner that checks multiple host:port targets in a loop.
  • Run the script and see service-level details that ping and DNS could never reveal.
Write the port checking function

The /dev/tcp pseudo-device is a bash built-in that opens a TCP connection to a host and port. If the connection succeeds, exit code 0. If the remote host actively refuses, exit code 1. If a firewall silently drops the packet and the connection times out, the timeout command returns exit code 124.

  • In netcheck.sh, find the # --- Main --- comment near the bottom of the file.
  • Add the following code above that comment to create the port scanning section:
# --- Port Scanning ---
check_port() {
    local host="$1"
    local port="$2"
    local exit_code=0
    timeout 2 bash -c "</dev/tcp/$host/$port" 2>/dev/null || exit_code=$?
    if [[ $exit_code -eq 0 ]]; then
        print_pass "$host:$port OPEN"
    elif [[ $exit_code -eq 1 ]]; then
        print_fail "$host:$port CLOSED"
    elif [[ $exit_code -eq 124 ]]; then
        print_warn "$host:$port FILTERED (timeout)"
    else
        print_fail "$host:$port ERROR (exit code: $exit_code)"
    fi
}

What does this code do?

The || exit_code=$? pattern is essential here. Your script uses set -e which normally exits on any non-zero return. This pattern captures the exit code into a variable instead of letting bash abort.

The timeout 2 wrapper kills the connection attempt after 2 seconds. Without it, a filtered port would hang your script for the system's default TCP timeout (often 60+ seconds).

  • Save netcheck.sh in nano with Ctrl+O, press Enter, but stay in the editor for the next chunk.
Build the port scanner and wire it into main

The check_port() function handles a single host:port pair. Now you need a wrapper that defines your targets and loops through them.

  • Directly below the check_port() function you just wrote, add the scan_ports() function:
scan_ports() {
    print_section_header "Port Scan (TCP)"
    local targets=("google.com:443" "google.com:80" "localhost:22" "localhost:9999")
    for target in "${targets[@]}"; do
        local host="${target%%:*}"
        local port="${target##*:}"
        check_port "$host" "$port"
    done
}

What does this code do?

The targets array holds host:port pairs as single strings. The ${target%%:*} syntax strips everything from the first colon onward (giving you the host). The ${target##*:} syntax strips everything up to the last colon (giving you the port).

The targets deliberately include ports you expect to be open (443, 80 on google.com) and ports you expect to be closed (9999 on localhost) so you can see both outcomes.

  • Now scroll down to the main() function and find this line:
    check_dns
  • Add scan_ports on a new line directly below it, so main() now looks like this:
main() {
    print_banner
    check_connectivity
    check_dns
    scan_ports
}
  • Save the file with Ctrl+O, press Enter, then exit nano with Ctrl+X.

Seeing a syntax error when you save?

Check that the scan_ports() function's closing brace } is on its own line. Also verify the targets array uses parentheses () not curly braces.

Make sure the scan_ports call inside main() has no parentheses after it. In bash, you call functions by name alone.

Help me debug a syntax error in my netcheck.sh port scanning function.

Run the script and observe port states

Before you run the script, think about what you expect to see. Google's servers should have ports 443 and 80 open. Port 9999 on localhost almost certainly has nothing listening. What about port 22 on localhost?

  • Run your script by executing:
./netcheck.sh

You should see a new Port Scan (TCP) section at the bottom of your output. google.com:443 should show green OPEN. localhost:9999 should show red CLOSED.

What do these port states mean?

OPEN means a service is actively listening and accepted your TCP connection. CLOSED means the host is reachable but nothing is listening on that port (the OS sent back a RST packet refusing the connection).

FILTERED means your connection attempt disappeared into a void. A firewall silently dropped the packet and you got no response at all. This is the hardest state to debug in real life because you cannot tell if the host is down or just ignoring you.

Why /dev/tcp instead of netcat?

Your VM has netcat installed, and many tutorials use nc -zv for port scanning. The /dev/tcp approach works on any Linux machine with bash, even ones where netcat is not available. On a locked-down production server, you might not be able to install packages but bash is always there.

✔️ Awesome, I've got everything!

Great. Double check you have saved netcheck.sh and your output shows all three sections (Connectivity, DNS, and Port Scan).

ⓧ I'd like to double check the full code

Your netcheck.sh should contain these sections in order. Scroll up through the step to confirm each one matches:

  • Shebang line and set -euo pipefail at the top.
  • Color variables (RED, GREEN, YELLOW, CYAN, NC).
  • Helper functions: print_section_header(), print_pass(), print_fail(), print_warn().
  • Connectivity Check section with check_connectivity().
  • DNS Resolution section with check_dns().
  • Port Scanning section with check_port() and scan_ports() (added in this step).
  • Main section with print_banner() and main() calling check_connectivity, check_dns, and scan_ports.

Your toolkit now covers three network layers: ICMP reachability, DNS resolution, and TCP port states. Next up, you will turn the spotlight on your own machine and inspect what connections are active and what services are listening right now.

Analyze Local Connection State

Your script can now reach outward to test remote hosts, resolve domain names, and scan ports on distant machines. But what about your own machine?

A DevOps engineer needs to see what connections are active locally, what services are listening, and whether anything looks abnormal (too many TIME-WAIT sockets, unexpected ESTABLISHED connections). In this step, you will use ss to inspect your local TCP state and add a menu system so you can run individual checks on demand.

In this step, get ready to:
  • Write a connection state summary that counts active TCP sockets by state.
  • List all listening TCP ports on your machine.
  • Add a menu system so each diagnostic can run independently.
Write the connection state summary

Every TCP connection on your machine is in a specific state: ESTABLISHED means actively communicating, LISTEN means waiting for incoming connections, TIME-WAIT means a recently closed connection still occupying a slot. Counting these states gives you a snapshot of what your machine is doing right now.

  • Open netcheck.sh in nano by running:
nano ~/netcheck/netcheck.sh
  • Find the # --- Main --- comment line.
  • Add the following code directly ABOVE that line to create the connection analysis section:
# --- Connection State Analysis ---
check_connections() {
    print_section_header "Connection Summary"
    echo "  TCP connection states:"
    ss -tan 2>/dev/null | awk 'NR>1 {state[$1]++} END {for (s in state) printf "    %-14s %d\n", s, state[s]}' | sort
    echo ""
}

What does this code do?

  • ss -tan lists all TCP sockets (-t for TCP, -a for all states, -n for numeric ports instead of service names).
  • The awk command skips the header row (NR>1), counts how many connections share each state, then prints a formatted summary table.
  • Piping through sort puts the states in alphabetical order so the output is consistent every time you run it.

You also need to call this new function so it actually runs.

  • Scroll down to the main() function and find the line that says scan_ports.
  • Add check_connections on a new line directly below scan_ports:
    check_connectivity
    check_dns
    scan_ports
    check_connections
  • Save the file with Ctrl+O then Enter, and exit nano with Ctrl+X.
  • Run the script to see your new connection summary by running:
./netcheck.sh

You should see a new "Connection Summary" section at the bottom showing counts like ESTAB, LISTEN, or TIME-WAIT next to a number.

Not seeing the Connection Summary section?

  • Make sure you placed check_connections() above the # --- Main --- comment. Bash reads top to bottom, so the function must be defined before it is called.
  • Confirm you added check_connections (without parentheses) inside the main() function body.
  • help me debug my check_connections function in netcheck.sh
Add a listening ports check

Knowing the connection states is useful, but you also want to see which ports your machine is actively listening on. A port in LISTEN state means a service is running and waiting for incoming connections.

  • Open the script again by running:
nano ~/netcheck/netcheck.sh
  • Find the closing brace } of check_connections() (the one right after echo "").
  • Add the following function directly below it:
check_listening() {
    echo "  Listening TCP ports:"
    ss -tln 2>/dev/null | awk 'NR>1 {printf "    %s\n", $4}'
    echo ""
}

What does this code do?

  • ss -tln shows only TCP sockets in LISTEN state with numeric port numbers.
  • The awk command prints the fourth column, which contains the local address and port each service is bound to (like 0.0.0.0:22 or 127.0.0.1:631).
  • The -p flag (show process names) is omitted because it requires sudo privileges. Without it, the listing still works and shows you what is listening.
  • Scroll down to main() and add check_listening on a new line directly below check_connections:
    check_connectivity
    check_dns
    scan_ports
    check_connections
    check_listening
  • Save with Ctrl+O then Enter, exit with Ctrl+X.
  • Run the script to confirm the listening ports appear by running:
./netcheck.sh

Below the Connection Summary counts, you should now see a "Listening TCP ports:" list showing addresses like 0.0.0.0:22 or *:68.

Listening ports list is empty?

  • Run ss -tln directly in your terminal to check if your VM actually has listening services. If that shows results but the script does not, double-check your awk formatting matches the code above exactly.
  • help me debug why check_listening is not printing any ports
Add a menu system for selective diagnostics

Right now the script always runs every check. In practice, you often want to run just one diagnostic quickly. A case statement lets you accept a command-line argument and dispatch to a single function.

  • Open the script by running:
nano ~/netcheck/netcheck.sh
  • Find the main() function. It currently looks like this:
main() {
    print_banner
    check_connectivity
    check_dns
    scan_ports
    check_connections
    check_listening
}

main "$@"
  • Delete that entire main() function and the main "$@" call below it.
  • First, add a run_all() helper function that groups all checks together:
run_all() {
    check_connectivity
    check_dns
    scan_ports
    check_connections
    check_listening
}

Why extract run_all()?

Pulling the five check calls into their own function keeps main() focused on argument parsing. This separation makes it easy to add new checks later without touching the menu logic.

  • Now add the new main() function with a case statement directly below run_all():
main() {
    print_banner
    local mode="${1:-all}"
    case "$mode" in
        all)
            run_all
            ;;
        ping)
            check_connectivity
            ;;
        dns)
            check_dns
            ;;
        ports)
            scan_ports
            ;;
        connections)
            check_connections
            check_listening
            ;;
        *)
            echo "Usage: $0 {all|ping|dns|ports|connections}"
            exit 1
            ;;
    esac
}

main "$@"

What does this code do?

  • ${1:-all} reads the first command-line argument. If none is provided, it defaults to all.
  • Each case branch maps a keyword (ping, dns, ports, connections) to the matching function.
  • The * wildcard catches any unrecognized argument and prints a usage message.
  • main "$@" at the bottom passes all command-line arguments into the function.
  • Save with Ctrl+O then Enter, exit with Ctrl+X.

Before you run this, what do you think will happen if you pass dns as the argument? Will you see just DNS results, or the full report?

  • Test the menu by running just the DNS check:
./netcheck.sh dns

You should see only the DNS Resolution section with the three domain lookups. No connectivity check, no port scan.

  • Now test the connections mode by running:
./netcheck.sh connections

You should see just the Connection Summary and Listening TCP ports sections.

  • Finally, run the full suite to confirm all mode still works:
./netcheck.sh

With no argument, the script defaults to all and you should see all five sections: Connectivity Check, DNS Resolution, Port Scan, Connection Summary, and Listening TCP ports.

Seeing a usage error or unexpected output?

  • If you see "Usage: ./netcheck.sh {all|ping|dns|ports|connections}" when running without arguments, check that the default value uses the correct syntax: ${1:-all} (note the colon and dash).
  • If the script errors on the case statement, make sure each branch ends with ;; (double semicolons) and the whole statement ends with esac.
  • help me fix the case statement in my netcheck.sh menu system

✔️ Awesome, I've got everything!

Great. Double check you have saved netcheck.sh and that running ./netcheck.sh with no arguments shows all five diagnostic sections.

ⓧ I'd like to double check the full code

Your netcheck.sh file should contain these sections in order. Scroll through your file in nano and confirm each is present:

  • Shebang line (#!/bin/bash) and set -euo pipefail.
  • Color variables block (RED, GREEN, YELLOW, CYAN, NC).
  • Helper functions: print_section_header(), print_pass(), print_fail(), print_warn().
  • check_connectivity() with the three-host ping loop.
  • check_dns() with three domains and timing.
  • check_port() and scan_ports() with four host:port targets.
  • check_connections() using ss -tan piped to awk and sort.
  • check_listening() using ss -tln piped to awk.
  • print_banner() function.
  • run_all() calling all five check functions.
  • main() with the case statement dispatching to all, ping, dns, ports, or connections.
  • main "$@" as the final line.

Your toolkit now covers every layer a DevOps engineer checks during an outage: ICMP reachability, DNS resolution, TCP port states, and local connection health. Next up, you will take on a Secret Mission to add report generation so you can save these diagnostics to a timestamped log file.

Secret mission

Generate a Diagnostic Report File

A real DevOps engineer does not just read output on screen. They save diagnostic reports for incident timelines, before/after comparisons, and sharing with teammates. In this secret mission, you will add a --report flag that writes the full output to a timestamped log file.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally on your VM, so there are no ongoing costs.

Resources you used:

  • Project directory: ~/netcheck/ (contains your script and any generated report log files).
  • Installed package: dnsutils (provides the dig command).

Keep everything running

No action needed. Choose this if you plan to keep using the script or want to extend it further.

  • Your netcheck.sh script stays in ~/netcheck/, ready to run anytime you need to diagnose network issues.
  • The dnsutils package remains installed for DNS lookups with dig.
  • Any report log files generated by --report remain in ~/netcheck/ for future reference.

Pause - I'll come back to this later

There are no running processes to pause. The script only executes when you run it manually, so it consumes no resources while idle.

  • Your project directory and installed packages stay in place.
  • If you want to free disk space from old report files, delete them by running:
rm -f ~/netcheck/netcheck-*.log

This removes generated report logs while keeping your script intact.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your script, any generated report files, and the installed package.

  • Delete the project directory by running:
rm -rf ~/netcheck/
  • Remove the dnsutils package by running:
sudo apt remove dnsutils -y
  • Confirm the directory is gone by running:
ls ~/netcheck/

You should see a message saying the directory cannot be found. Your VM is now clean.

Nice Work!

Nice Work!

You just built a production-style network diagnostics toolkit entirely in bash. Your netcheck.sh script can assess network health across multiple layers in seconds, the same way DevOps engineers troubleshoot real infrastructure.

You've learned how to:

  • Test connectivity with ping, resolve domains with dig, and scan TCP ports using bash's built-in /dev/tcp pseudo-device with no external tools.
  • Analyze live connection states with ss to see exactly what your machine is doing on the network, from ESTABLISHED connections to LISTENing services.
  • Structure a modular script with bash functions, arrays, exit codes, and a case menu so each diagnostic runs independently or together.
  • Secret Mission: Implemented a --report flag that generates clean, timestamped log files for incident documentation.

Ready to quiz yourself?