Build a Network Diagnostic Toolkit

Write a Bash script that diagnoses network issues layer by layer using the OSI model.

Introduction

30 Second Summary

When your internet stops working, most people restart the router and hope for the best. A network engineer diagnoses the problem layer by layer, starting from the physical connection and working up to name resolution.

In this project, you will build a Bash script in WSL that automates this exact layer-by-layer diagnostic using the same Linux networking tools that appear on the CCNA exam.

What You'll Build

When you run ./netdiag.sh, your script probes your actual network stack and prints a pass/fail verdict at each OSI model layer, telling you exactly where a problem lives.

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

  • A single command that checks your network health across Layers 2, 3, 4, and 7, printing a pass/fail result for every check.
  • A DNS resolution test that catches a real failure you intentionally create, proving the script can isolate problems by layer.
  • A live view of your TCP connections, open ports, and the route packets take to reach any destination.
  • Secret Mission: an optional challenge to push your skills further.

Are there any prerequisites?

This project assumes you have WSL with Ubuntu installed and are comfortable running basic commands in a Bash terminal. If you need to set up WSL, follow Microsoft's official installation guide first.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your CCNA prep.

Set Up Your Networking Toolkit

Your diagnostic script will call five Linux networking tools to probe your network stack layer by layer. Three of them (ping, ip, ss) come pre-installed with Ubuntu. The other two need to be added before you can use them.

Without those missing tools, your script would silently skip entire layers of the network stack. In this step, you'll install the missing packages, confirm every tool is ready, and create the project directory where your script will live.

In this step, get ready to:
  • Install the dnsutils and traceroute packages.
  • Verify all five networking tools respond to a test command.
  • Create the ~/netdiag project directory and an empty script file.
Install the missing networking packages

The two packages your script needs are dnsutils (which provides the dig command for DNS lookups) and traceroute (which traces the path packets take across a network).

About the sudo password prompt

The next command uses sudo, which runs commands with administrator privileges. Your terminal may ask for your Ubuntu password. Type it and press Enter. The characters won't appear on screen as you type.

  • Install both packages by running this command:
sudo apt update && sudo apt install -y dnsutils traceroute

What does this command do?

  • sudo apt update refreshes Ubuntu's package index so it knows about the latest available versions.
  • sudo apt install -y dnsutils traceroute installs both packages. The -y flag auto-confirms the installation so you don't have to type "yes" at a prompt.
  • If you already have these packages, apt will simply report that they are already installed. The command is safe to run multiple times.

You should see output scrolling as Ubuntu downloads and installs the packages. The last few lines will confirm both packages are set up.

Seeing 'Unable to locate package'?

  • Make sure you included sudo apt update before the install. The && chains them together so update runs first.
  • Check that your WSL instance has internet access. Try ping -c 1 8.8.8.8 to confirm.

Still stuck? Help me troubleshoot the apt install error in my WSL Ubuntu terminal.

Verify all five tools are available

You now have five networking tools ready to use. Three came pre-installed with Ubuntu (ping, ip, ss) and two were just installed (dig, traceroute). Let's confirm every one of them works.

Before you run these, think about what dig google.com +short might print. What kind of value would a DNS lookup return?

  • Test each tool by running these commands one at a time:
ping -c 1 127.0.0.1
ip -br addr show
dig google.com +short
ss -t -a
traceroute --version

The dig command returns one or more IP addresses for google.com. The traceroute --version command prints a version string confirming it is installed.

What does each tool do?

  • ping sends ICMP echo requests to test whether a host is reachable (Layer 3).
  • ip shows network interface status and routing tables (Layers 2 and 3).
  • dig queries DNS servers to resolve domain names into IP addresses (Layer 7).
  • ss displays TCP connection states and listening ports (Layer 4).
  • traceroute traces the route packets take to reach a destination, showing every hop along the way (Layer 3).

One of the commands not found?

  • If dig or traceroute is not found, the install in the previous substep did not complete. Re-run the install command and check the output for error messages.
  • If ping, ip, or ss is missing, your Ubuntu installation may be a minimal image. Run sudo apt install -y iputils-ping iproute2 to install them.

Need help? Help me figure out why a networking tool is not found in my WSL Ubuntu terminal.

Create the project directory and script file

Every tool is working. Now you need a home for the script that will tie them all together.

  • Create the project directory, move into it, and create an empty script file by running these commands:
mkdir ~/netdiag && cd ~/netdiag
touch netdiag.sh
chmod +x netdiag.sh

What do these commands do?

  • mkdir ~/netdiag creates a new folder called netdiag in your home directory. cd ~/netdiag moves your terminal into it.
  • touch netdiag.sh creates an empty file called netdiag.sh. This is where your diagnostic script will live.
  • chmod +x netdiag.sh marks the file as executable so you can run it directly with ./netdiag.sh later.
  • Confirm the file exists by running:
ls

You should see netdiag.sh listed in the output.

Don't see netdiag.sh?

  • Make sure you ran both lines. The first line creates the directory and moves into it. The second line creates the file inside that directory.
  • Run pwd to confirm you are inside ~/netdiag. If you are somewhere else, run cd ~/netdiag to get there.

Still stuck? Help me set up my project directory and script file in WSL.

Your toolkit is installed, verified, and ready to go. Next up, you'll open netdiag.sh and write your first diagnostic check that pings a real target and reports the result.

Build a Ping-Only Diagnostic

Your networking tools are installed and verified. Now it's time to start building the diagnostic script that ties them all together.

The simplest network test is a ping. It sends a small packet to a target and checks if you get a response, mapping to Layer 3 of the OSI model. In this step, you'll write a Bash function that pings a target and reports whether it is reachable.

In this step, get ready to:
  • Write the diagnostic script skeleton with a header and ping check function.
  • Test both successful and failed ICMP connections.
  • Discover the script's limitation when a hostname is used instead of an IP.
Write the script header

Every diagnostic run should start by telling you what it is testing. The header section sets a default target IP, accepts an optional argument to override it, and prints a banner so you know what the script is doing.

  • Open netdiag.sh in nano by running this command:
nano netdiag.sh
  • Type the following code into the editor:
#!/bin/bash

# Network Diagnostic Toolkit
# Diagnoses network issues layer by layer, mapped to the OSI model

TARGET="${1:-8.8.8.8}"

echo "====================================="
echo "  Network Diagnostic Toolkit"
echo "====================================="
echo "Target IP:   $TARGET"
echo "====================================="
echo ""

What does this code do?

  • #!/bin/bash is the shebang. It tells the system to run this file with Bash.
  • TARGET="${1:-8.8.8.8}" takes the first argument the user passes in. If no argument is given, it defaults to 8.8.8.8 (Google's public DNS server).
  • The echo lines print a banner showing what target the script is diagnosing.
  • Save the file by pressing Ctrl+O, then Enter to confirm.
  • Exit nano by pressing Ctrl+X.
  • Run the script to confirm the banner prints by running:
./netdiag.sh

You should see the banner with Target IP: 8.8.8.8 printed in the terminal.

Seeing permission denied?

Make sure the file is executable. You already ran chmod +x netdiag.sh in Step 1, but if you skipped it or recreated the file, run it again now.

help me debug a permission denied error when running my Bash script

Add the ping check function

The banner confirms the script runs. Now you need the actual diagnostic logic. The check_ping function sends three ICMP packets to the target and reports whether they got through.

  • Reopen the script in nano by running:
nano netdiag.sh
  • Place your cursor at the end of the file, below the last echo "" line.
  • Add the following code:
# ----- Layer 3: ICMP Reachability -----
check_ping() {
    echo "[Layer 3] ICMP Reachability"
    echo "-------------------------------------"
    if ping -c 3 -W 2 "$TARGET" > /dev/null 2>&1; then
        local stats
        stats=$(ping -c 3 -W 2 "$TARGET" 2>/dev/null | tail -1)
        echo "OK: $TARGET is reachable"
        echo "$stats"
        return 0
    else
        echo "FAIL: $TARGET is not reachable"
        return 1
    fi
}

# ===== Main =====
echo ""
check_ping
echo ""

echo "====================================="
echo "  Diagnostic Complete"
echo "====================================="

What does this code do?

  • ping -c 3 -W 2 sends 3 ICMP packets (-c 3) and waits 2 seconds for each reply (-W 2).
  • > /dev/null 2>&1 hides the raw ping output so the script can print its own clean verdict.
  • The if statement checks the exit code of ping. Exit code 0 means packets got through (OK). Anything else means the target is unreachable (FAIL).
  • On success, a second ping call with tail -1 grabs the round-trip time statistics so you can see latency.
  • The # ===== Main ===== section calls check_ping and prints a footer when the diagnostic finishes.
  • Save the file by pressing Ctrl+O, then Enter.
  • Exit nano by pressing Ctrl+X.

✔️ Awesome, I've got everything!

Double check you've saved your file, then continue to the next section where you test it.

ⓧ I'd like to double check the full code

Scroll up and confirm your netdiag.sh contains both of these sections, in order:

  • The script header: shebang (#!/bin/bash), the TARGET variable with a default of 8.8.8.8, and the echo banner block.
  • The check_ping function with the ping -c 3 -W 2 command, OK/FAIL output, and return codes.
  • The Main section that calls check_ping and prints the Diagnostic Complete footer.
Test with a known-good and unreachable IP

A diagnostic tool that only shows success is useless. You need to see what both a pass and a fail look like so you can trust the output.

  • Run the script with the default target (8.8.8.8) by running:
./netdiag.sh

You should see the banner, followed by OK: 8.8.8.8 is reachable and a line of round-trip time statistics.

Before you run the next command, what do you think will happen if you target an IP address that no machine is using?

  • Test the failure path by targeting 192.0.2.1 by running:
./netdiag.sh 192.0.2.1

You should see FAIL: 192.0.2.1 is not reachable. The script waited for replies that never came and reported the failure.

Why does 192.0.2.1 always fail?

192.0.2.1 is part of the TEST-NET-1 range (RFC 5737), reserved for documentation and examples. No router on the public internet will forward traffic to it, making it a reliable target for testing failure paths.

Both tests showing FAIL?

If 8.8.8.8 also fails, your WSL instance may not have internet access. Try running ping -c 1 8.8.8.8 directly (outside the script) to confirm. If that also fails, check your Windows network connection.

help me troubleshoot why ping fails for all targets in WSL

Discover the DNS limitation

Both the OK and FAIL paths work with raw IP addresses. But in the real world, people use hostnames like google.com, not IP addresses. What happens when you pass a hostname to your script?

Before you run this, do you think the ping will succeed or fail? Why?

  • Test the script with a hostname instead of an IP by running:
./netdiag.sh google.com

It works. The script reports OK because DNS resolution happens to be healthy right now, so ping quietly resolves the hostname to an IP before sending packets.

Why is this a problem?

Imagine this ping had failed. Was it because DNS could not resolve the hostname? Or because the resolved IP was unreachable? The script gives you zero information to tell the difference.

This is exactly the kind of ambiguity CCNA troubleshooting questions test you on. A real network engineer needs to separate Layer 7 (DNS) from Layer 3 (connectivity) to isolate the root cause.

Your script can test whether a target is reachable, but it is blind to why a failure happens. In the next step, you will add a dedicated DNS check using dig that separates name resolution from connectivity, closing the gap you just discovered.

Add DNS Resolution Checks

Your ping check works, but it has a blind spot. When you ran ./netdiag.sh google.com in the last step, it succeeded because DNS happened to be healthy. If that ping had failed, you would have no idea whether the network was down or the name just could not resolve.

That inability to separate DNS failures from connectivity failures is exactly the kind of question CCNA troubleshooting scenarios test you on. In this step, you will add a dedicated DNS check using dig that resolves hostnames, inspects your system resolver, and compares it against a public DNS server. This gives the script a Layer 7 check that is completely independent of the Layer 3 ping.

In this step, get ready to:
  • Add DNS target variables to the script header.
  • Write a DNS resolution function using dig with both system and public resolver checks.
  • Test the script against a healthy domain and a non-existent domain to prove it catches DNS failures.
Add DNS target variables to the script header

The DNS check needs to know which hostname to resolve. Right now the script only accepts a target IP. You will add two more arguments so the learner can customize the DNS target and the port to check later.

  • Open netdiag.sh in nano by running:
nano netdiag.sh
  • Find this line near the top of the file:
TARGET="${1:-8.8.8.8}"
  • Add these two lines directly below it, so the three variable lines sit together:
DNS_TARGET="${2:-google.com}"
CHECK_PORT="${3:-443}"

What do these variables do?

DNS_TARGET is the hostname the DNS check will try to resolve. It defaults to google.com if the learner does not pass a second argument.

CHECK_PORT defaults to 443 (HTTPS). You will use it in a later step when you add port checking.

  • Now find the header echo block. Locate this line:
echo "Target IP:   $TARGET"
  • Add these two lines directly below it, before the closing echo "====..." line:
echo "DNS Target:  $DNS_TARGET"
echo "Port:        $CHECK_PORT"
  • Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
  • Confirm the new header shows the DNS target and port by running:
./netdiag.sh

The header now prints DNS Target: google.com and Port: 443 alongside the target IP. The rest of the output is the same ping check from before.

Don't see DNS Target in the header?

  • Make sure the two new echo lines are between echo "Target IP: $TARGET" and the closing echo "====..." line. If they are outside the header block, they will print in the wrong spot.
  • Check that the variable names match exactly: $DNS_TARGET and $CHECK_PORT. A typo here means the variable is empty and the header line appears blank.
  • Help me debug my netdiag.sh header output.
Write the DNS resolution function

The DNS function has two jobs: resolve a hostname using your system's default DNS, and then compare the result against a public DNS server. You will build it in two chunks.

  • Open netdiag.sh in nano again:
nano netdiag.sh
  • Scroll down past the closing } of the check_ping function. Place your cursor on the blank line before the # ===== Main ===== comment.
  • Add the first part of the DNS check function. This handles the system resolver lookup:
# ----- Layer 7: DNS Resolution -----
check_dns() {
    echo "[Layer 7] DNS Resolution"
    echo "-------------------------------------"
    local dns_result
    dns_result=$(dig +short "$DNS_TARGET" 2>/dev/null)
    if [ -z "$dns_result" ]; then
        echo "FAIL: Cannot resolve $DNS_TARGET"
    else
        echo "$DNS_TARGET resolves to:"
        echo "$dns_result"
        echo "OK: DNS resolution working"
    fi

    local resolver
    resolver=$(grep "nameserver" /etc/resolv.conf | head -1 | awk '{print $2}')
    echo "System DNS resolver: $resolver"
    echo ""

What does this code do?

  • dig +short "$DNS_TARGET" queries your system's default DNS server for the target hostname. The +short flag strips the verbose output and returns just the IP addresses.
  • if [ -z "$dns_result" ] checks whether the result string is empty. An empty result means DNS resolution failed.
  • The grep and awk pipeline reads /etc/resolv.conf to find the nameserver your system is using. This tells you WHO is answering DNS queries.
  • Directly below the code you just added (no blank line between), add the public DNS comparison test and the closing brace of the function:
    echo "Testing public DNS (1.1.1.1):"
    local public_result
    public_result=$(dig @1.1.1.1 "$DNS_TARGET" +short 2>/dev/null)
    if [ -z "$public_result" ]; then
        echo "FAIL: Public DNS cannot resolve $DNS_TARGET"
        return 1
    else
        echo "$public_result"
        echo "OK: Public DNS resolution working"
        if [ -z "$dns_result" ]; then
            echo "NOTE: System resolver failed but public DNS works"
            echo "      Check /etc/resolv.conf configuration"
            return 1
        fi
        return 0
    fi
}

Why test both your system resolver and a public DNS server?

dig @1.1.1.1 bypasses your system's configured DNS and queries Cloudflare's public DNS directly. If the system resolver fails but the public DNS succeeds, the problem is your local DNS configuration, not the internet.

This is the same isolation technique CCNA troubleshooting questions use. The exam expects you to distinguish between "DNS is broken everywhere" and "my resolver is misconfigured." Your script now tests for both.

  • Scroll down to the # ===== Main ===== section. Find these lines:
echo ""
check_ping
echo ""
  • Add a call to check_dns after check_ping, so the main section now looks like this:
# ===== Main =====
echo ""
check_ping
echo ""
check_dns
echo ""
  • Save and exit nano (Ctrl+O, Enter, Ctrl+X).
  • Run the script to see the new DNS section in action:
./netdiag.sh

Below the familiar ping results, you should now see a [Layer 7] DNS Resolution section. It shows the IP addresses that google.com resolves to, your system DNS resolver address, and a public DNS test result.

Seeing a syntax error or no DNS output?

  • Check that the closing } of check_dns is on its own line. A missing brace causes a syntax error that prevents the whole script from running.
  • Make sure you pasted both halves of the function in the correct order. The public DNS section must come directly after the system resolver section, inside the same function.
  • Verify that check_dns appears in the Main section. If the function exists but is never called, you will not see its output.
  • Help me fix my check_dns function in netdiag.sh.
Test DNS failure detection

The real value of this DNS check is what happens when resolution fails. You are about to prove the script can catch a DNS problem that the ping check alone would have missed.

Before you run the next command, think about what you expect to happen. If you pass nxdomain.invalid as the DNS target (a hostname that does not exist), will the ping check pass or fail? What about the DNS check?

  • Test the failure case by passing a non-existent hostname as the second argument:
./netdiag.sh 8.8.8.8 nxdomain.invalid

The ping check still reports OK because 8.8.8.8 is reachable. But the DNS section reports FAIL: Cannot resolve nxdomain.invalid. That separation is exactly the point. Your script can now tell you the network is fine but DNS is broken.

Why does this matter for CCNA?

CCNA troubleshooting questions often present a scenario where a user "cannot reach a website" and ask you to identify the failing layer. If you only check ping and it passes, you might wrongly conclude the network is healthy. A separate DNS check at Layer 7 catches the real culprit.

✔️ Awesome, I've got everything!

Make sure you have saved netdiag.sh. You are ready for the next step.

ⓧ I'd like to double check the full code

Here is the full netdiag.sh as it should look at the end of this step. The file has grown past 30 lines, so check each section in order by scrolling through your file in nano:

  • Top of file: shebang, then TARGET, DNS_TARGET, and CHECK_PORT variables.
  • Header echo block: shows Target IP, DNS Target, and Port.
  • check_ping function: unchanged from Step 2.
  • check_dns function: system dig, resolver extraction, public DNS test, closing brace.
  • Main section: calls check_ping then check_dns, then the "Diagnostic Complete" banner.

Your script now covers Layer 3 (ICMP reachability) and Layer 7 (DNS resolution) as separate, independent checks. Next up, you will go deeper into the stack and add interface and routing checks that diagnose what is happening at Layers 2 and 3 before a ping even leaves your machine.

Add Interface and Routing Checks

Your script catches connectivity failures with ping and isolates DNS problems with dig. Those checks handle Layer 3 and Layer 7 of the OSI model.

If a network interface is down or the default gateway is missing, both checks fail without explanation. Adding Layer 2 and Layer 3 foundation checks means the script diagnoses from the bottom of the stack upward, the same troubleshooting methodology CCNA questions test.

In this step, get ready to:
  • Add a network interface check that detects whether your links are active (Layer 2).
  • Add a default gateway check that verifies your next-hop router is reachable (Layer 3).
  • Reorder the script to diagnose bottom-up: interfaces, gateway, ping, then DNS.
Add the interface check function

Before checking if a remote host is reachable, you need to confirm your own network interfaces are up. The ip command's brief mode shows each interface's name, state (UP or DOWN), and IP address in one line.

  • See what your network interfaces look like right now by running this command:
ip -br addr show

You should see at least one interface (likely eth0) with a status of UP and an IP address next to it.

Why does WSL show eth0?

WSL 2 uses a NAT-based virtual network adapter. The eth0 interface you see is the WSL virtual interface, not your Windows host adapter. It gives you a clean Linux networking environment, which is exactly what the diagnostic tools expect.

  • Open netdiag.sh in nano by running:
nano netdiag.sh
  • Find the line that says # ----- Layer 3: ICMP Reachability -----.
  • Add the following code above that line:
# ----- Layer 2/3: Network Interfaces -----
check_interfaces() {
    echo "[Layer 2] Network Interfaces"
    echo "-------------------------------------"
    local up_interfaces
    up_interfaces=$(ip -br addr show | grep UP)
    if [ -z "$up_interfaces" ]; then
        echo "FAIL: No active network interfaces found"
        return 1
    else
        echo "$up_interfaces"
        echo "OK: Active interfaces detected"
        return 0
    fi
}

What does this code do?

  • ip -br addr show | grep UP lists all interfaces and filters to only the active ones. If nothing is UP, your machine has no working network link.
  • This maps to Layer 2 (Data Link) of the OSI model. It checks whether the physical or virtual network link is active before anything else.
  • return 1 signals failure to the caller, and return 0 signals success. This exit-code pattern is the same one check_ping already uses.
  • Save the file with Ctrl+O, press Enter to confirm, then exit with Ctrl+X.

Not sure where to place it?

The new function goes after the closing echo "" line of the header section and before the # ----- Layer 3: ICMP Reachability ----- comment. Leave a blank line between the new function and the existing check_ping function.

Need help placing the code?

Add the gateway check function

Even if interfaces are up, you need a path out of your local network. The default gateway is the router that forwards your traffic to the wider internet. If it is unreachable, nothing beyond your local segment works.

  • Check what your default gateway looks like by running this command:
ip route show default

You should see a line starting with default via followed by an IP address. That IP is your gateway.

  • Open netdiag.sh in nano again by running:
nano netdiag.sh
  • Find the closing } of the check_interfaces function you just added.
  • Add the following code below it, between check_interfaces and check_ping:
# ----- Layer 3: Default Gateway -----
check_gateway() {
    echo "[Layer 3] Default Gateway"
    echo "-------------------------------------"
    local gateway
    gateway=$(ip route show default | awk '{print $3}' | head -1)
    if [ -z "$gateway" ]; then
        echo "FAIL: No default gateway configured"
        return 1
    fi
    echo "Default gateway: $gateway"
    if ping -c 2 -W 2 "$gateway" > /dev/null 2>&1; then
        echo "OK: Gateway $gateway is reachable"
    else
        echo "FAIL: Gateway $gateway is not reachable"
        return 1
    fi
    echo ""
    echo "Route to $TARGET:"
    ip route get "$TARGET" 2>/dev/null
    return 0
}

What does this code do?

  • ip route show default | awk '{print $3}' | head -1 extracts the gateway IP from the routing table. The third field in the default route line is the gateway address.
  • The function pings the gateway with ping -c 2 -W 2 (2 packets, 2-second timeout). If the gateway does not respond, the script reports FAIL because no traffic can leave the local network.
  • ip route get $TARGET asks the kernel how it would route a packet to the target. This shows the exact path the diagnostic packets take.
  • Save the file with Ctrl+O, press Enter to confirm, then exit with Ctrl+X.

Seeing a syntax error when you save?

Check that the awk command uses single quotes around '{print $3}', not double quotes. Double quotes cause Bash to interpret $3 as a shell variable instead of passing it to awk.

Help me debug my check_gateway function.

Why bottom-up troubleshooting?

CCNA troubleshooting questions expect you to start at the lowest layer and work up. If Layer 2 (the link) is down, there is no point checking Layer 3 (routing) or Layer 7 (DNS). Each layer depends on the one below it.

Your script now has checks at Layer 2 (interfaces), Layer 3 (gateway and ping), and Layer 7 (DNS). The next step is to make sure they run in that bottom-up order.

Reorder the main body and run the script

The two new functions exist in the file, but the main body still only calls check_ping and check_dns. You need to add the new calls and put everything in bottom-up order.

  • Open netdiag.sh in nano by running:
nano netdiag.sh
  • Scroll down to the # ===== Main ===== section at the bottom of the file.
  • Find this code:
# ===== Main =====
echo ""
check_ping
echo ""
check_dns
echo ""
  • Replace that entire block with the following, which adds the new function calls and puts them in bottom-up order:
# ===== Main =====
echo ""
check_interfaces
echo ""
check_gateway
echo ""
check_ping
echo ""
check_dns
echo ""

What changed in the main body?

  • check_interfaces runs first (Layer 2). If interfaces are down, nothing else matters.
  • check_gateway runs second (Layer 3 foundation). A missing gateway means traffic cannot leave the local network.
  • check_ping and check_dns follow in the same order as before, now sitting on top of the foundation checks.
  • Save the file with Ctrl+O, press Enter to confirm, then exit with Ctrl+X.

Before you run the script, think about what you expect to see. Which sections should appear first in the output, and what status should each one report?

  • Run the full diagnostic by executing:
./netdiag.sh

You should see four sections in this order: [Layer 2] Network Interfaces showing your active interfaces with IP addresses, then [Layer 3] Default Gateway with a reachability test, then the existing [Layer 3] ICMP Reachability and [Layer 7] DNS Resolution sections.

Not seeing the new sections?

If you only see the ping and DNS checks, the main body is still calling the old functions. Open netdiag.sh and scroll to the # ===== Main ===== section. Confirm that check_interfaces and check_gateway appear there as the first two function calls.

If you see an error like command not found, check that the function names in the main body match the function definitions exactly. Bash function names are case-sensitive.

Help me debug my netdiag.sh script output.

✔️ Awesome, I've got everything!

Great. Double check you have saved netdiag.sh and all four sections appear in bottom-up order when you run the script.

ⓧ I'd like to double check the full code

Your netdiag.sh should have these sections in this order. Scroll through your file and confirm each one is present:

  • The shebang line (#!/bin/bash) and the three variable assignments (TARGET, DNS_TARGET, CHECK_PORT) at the top.
  • The header echo block printing the banner with target info.
  • check_interfaces function (Layer 2/3: Network Interfaces).
  • check_gateway function (Layer 3: Default Gateway).
  • check_ping function (Layer 3: ICMP Reachability).
  • check_dns function (Layer 7: DNS Resolution).
  • The # ===== Main ===== section calling functions in this order: check_interfaces, check_gateway, check_ping, check_dns.
  • The closing Diagnostic Complete banner at the end.

Your script now diagnoses from the foundation up: interfaces, gateway, connectivity, then DNS. In the next step, you will add the missing piece. Layer 4 (Transport) checks for port connectivity and TCP connection states.

Add Port and TCP Connection Analysis

Your script now checks interfaces, gateway, ping, and DNS. It can diagnose from the bottom of the stack up and distinguish a DNS failure from a connectivity failure.

There is still a gap. A host can be pingable and DNS can resolve, yet a specific service on that host might be unreachable because the port is closed or filtered. That is Layer 4 (Transport), and the script does not cover it yet. In this step you will add port checks, TCP connection analysis, and a route trace to complete the diagnostic across all relevant layers.

In this step, get ready to:
  • Add a port connectivity check using Bash /dev/tcp to test whether a specific TCP port is open, closed, or filtered.
  • Add TCP connection state analysis using ss to inspect established connections and listening ports.
  • Add a traceroute check that maps every hop between you and the target.
Add a port connectivity check

Bash has a built-in feature called /dev/tcp that lets you attempt a TCP connection to any host and port using just a redirection. No external tool is needed.

  • Open netdiag.sh in nano by running this command:
nano netdiag.sh
  • Scroll down to the blank area between the closing } of your check_dns function and the # ===== Main ===== comment.
  • Add the check_port function by copying this code into that blank area:
# ----- Layer 4: Port Connectivity -----
check_port() {
    echo "[Layer 4] Port Connectivity"
    echo "-------------------------------------"
    timeout 3 bash -c "</dev/tcp/$TARGET/$CHECK_PORT" 2>/dev/null
    local exit_code=$?
    if [ $exit_code -eq 0 ]; then
        echo "OK: Port $CHECK_PORT on $TARGET is open"
    elif [ $exit_code -eq 1 ]; then
        echo "CLOSED: Port $CHECK_PORT on $TARGET refused connection"
    else
        echo "FILTERED: Port $CHECK_PORT on $TARGET timed out"
    fi
    return $exit_code
}

What does this code do?

timeout 3 bash -c "</dev/tcp/$TARGET/$CHECK_PORT" tries to open a TCP connection to the target on the specified port, with a 3-second time limit.

The function reads the exit code to distinguish three outcomes. Exit code 0 means the port is open. Exit code 1 means the connection was refused (port is closed). Exit code 124 is the timeout command's way of saying the connection hung, which usually means the port is filtered by a firewall.

This open/closed/filtered distinction maps directly to how CCNA questions frame port states.

Now wire the new function into the main body so it actually runs.

  • Scroll down to the # ===== Main ===== section.
  • Find these two lines near the end of the main body:
check_dns
echo ""
  • Place your cursor on the blank line after echo "" (just before the closing echo "==== banner) and add these two lines:
check_port
echo ""
  • Save the file with Ctrl+O, press Enter to confirm, then exit with Ctrl+X.
  • Run the script to see the new port check in action:
./netdiag.sh

You should see a new [Layer 4] Port Connectivity section in the output reporting OK: Port 443 on 8.8.8.8 is open.

Not seeing the port check output?

  • Make sure check_port and echo "" appear AFTER the check_dns call in the Main section, not inside a function definition.
  • If you see a syntax error, check that the function definition has matching curly braces and that local exit_code=$? appears on the line immediately after the timeout line.
  • Still stuck? Help me debug my check_port function. I added it to netdiag.sh but the output does not show a Layer 4 Port Connectivity section.
Inspect your TCP connections

Knowing that a port is open is only half of the Layer 4 picture. The ss command shows you the live TCP connections on your own machine: how many are established, which ports are listening, and what state each socket is in.

  • Open netdiag.sh in nano again:
nano netdiag.sh
  • Scroll down to the closing } of your check_port function.
  • Add the check_tcp_states function right after it, before the # ===== Main ===== comment:
# ----- Layer 4: TCP Connection States -----
check_tcp_states() {
    echo "[Layer 4] TCP Connection States"
    echo "-------------------------------------"
    local conn_count
    conn_count=$(ss -t state established 2>/dev/null | tail -n +2 | wc -l)
    echo "ESTABLISHED connections: $conn_count"
    echo ""
    echo "Listening TCP ports:"
    ss -t -l -n 2>/dev/null | head -10
    return 0
}

What does this code do?

  • ss -t state established filters to only TCP sockets in the ESTABLISHED state. The tail -n +2 strips the header row so wc -l counts only actual connections.
  • ss -t -l -n shows listening TCP sockets with numeric addresses (no DNS resolution). This tells you which services on your machine are accepting connections.
  • Scroll down to the Main section and find the check_port and echo "" lines you just added.
  • Add these two lines right after them, before the closing echo "==== banner:
check_tcp_states
echo ""
  • Save with Ctrl+O, press Enter, then exit with Ctrl+X.
  • Run the script to confirm the TCP states section appears:
./netdiag.sh

You should see a [Layer 4] TCP Connection States section with a count of ESTABLISHED connections and a list of listening TCP ports.

Why ss instead of netstat?

The Ubuntu manpage for netstat explicitly states: "This program is mostly obsolete. Replacement for netstat is ss." ss is faster, supports richer filtering (like ss -t state established), and is the modern standard. The TCP state concepts transfer directly to what CCNA tests.

Seeing 0 ESTABLISHED connections?

This can be normal in WSL if no active network connections are open inside the Linux environment. The count reflects connections from the WSL side, not your Windows host.

If you see an error instead of a count, check that ss is available by running ss --version. It is provided by the iproute2 package, which comes pre-installed on Ubuntu.

Still stuck? Help me troubleshoot the check_tcp_states function in my script.

Trace the route packets take

The traceroute command maps every router hop between you and the target. It works by sending packets with incrementally increasing TTL values. Each router along the path decrements the TTL, and when it reaches zero, that router sends back an ICMP Time Exceeded message revealing itself.

  • Open netdiag.sh in nano one more time:
nano netdiag.sh
  • This time, scroll to the area between the closing } of your check_ping function and the # ----- Layer 7: DNS Resolution ----- comment.
  • Add the check_traceroute function in that gap:
# ----- Layer 3: Traceroute -----
check_traceroute() {
    echo "[Layer 3] Route Trace to $TARGET"
    echo "-------------------------------------"
    traceroute -m 15 -w 2 "$TARGET" 2>/dev/null
    return 0
}

What does this code do?

  • -m 15 caps the trace at 15 hops so it does not run for too long.
  • -w 2 sets a 2-second wait per probe instead of the default 5 seconds, keeping the output snappy.
  • This is a Layer 3 check because it reveals the network path. It is placed after the ping check in the script because it provides deeper detail about the same layer.

What are the asterisks in traceroute output?

You will likely see some hops show * * * instead of an IP address and latency. This means the router at that hop is configured not to send ICMP Time Exceeded replies. It is still forwarding your traffic. The router just stays silent about it.

Asterisks in the middle of an otherwise healthy trace are harmless. Only worry if asterisks continue from one hop all the way to the end, as that can indicate a real routing failure.

  • Now wire check_traceroute into the Main section. Find these lines in the main body:
check_ping
echo ""
  • Add these two lines right after them, so check_traceroute runs between the ping and DNS checks:
check_traceroute
echo ""
  • Save with Ctrl+O, press Enter, then exit with Ctrl+X.

Time for the full test. Before you run the script, think about what you expect to see. The default target is 8.8.8.8 on port 443. Google runs HTTPS on that port. Do you think port 443 will show as open?

  • Run the complete diagnostic by running this command:
./netdiag.sh

You should see all seven sections run in bottom-up order: interfaces, gateway, ICMP ping, traceroute, DNS, port check, and TCP states. Port 443 on 8.8.8.8 should report as open.

Now test the failure case. Port 81 is not a standard service port. What do you think will happen when the script tries to connect to it on 8.8.8.8?

  • Test a known-closed port by running this command:
./netdiag.sh 8.8.8.8 google.com 81

The port check should now report CLOSED or FILTERED for port 81, while all other checks (interfaces, gateway, ping, DNS) still pass. That is exactly the kind of layered isolation this script is built for.

Traceroute showing all asterisks?

Some network configurations (corporate firewalls, restrictive ISPs) block the UDP probes that traceroute sends. If every single hop is * * *, that is a filtering issue, not a script bug. The rest of your diagnostic should still produce results.

If the script throws a command-not-found error for traceroute, reinstall it by running sudo apt install -y traceroute.

Need more help? Help me troubleshoot traceroute in my netdiag.sh script.

✔️ Awesome, I've got everything!

Double check you have saved netdiag.sh. You are all set for the next step.

ⓧ I'd like to double check the full code

The complete file is now too long to show in one block. Scroll through your netdiag.sh file and confirm all seven functions appear in this order:

  • check_interfaces (Layer 2).
  • check_gateway (Layer 3 gateway).
  • check_ping (Layer 3 ICMP).
  • check_traceroute (Layer 3 path trace).
  • check_dns (Layer 7).
  • check_port (Layer 4 port).
  • check_tcp_states (Layer 4 TCP states).

Then confirm the Main section calls them in this order: check_interfaces, check_gateway, check_ping, check_traceroute, check_dns, check_port, check_tcp_states. Each call should have an echo "" after it.

Your diagnostic toolkit is now complete. It covers Layers 2, 3, 4, and 7, and it runs in the bottom-up order that CCNA troubleshooting methodology prescribes. Next up, a Secret Mission that makes the script even smarter by adding an automated verdict.

Secret mission

Add an Automated Verdict

Your diagnostic toolkit prints pass/fail for every layer, but you still have to read the entire output to find the problem. Can you add a one-line verdict that automatically names the failing OSI layer, just like a CCNA troubleshooting question expects?

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 inside WSL, so there are no ongoing costs.

Resources you used:

  • The ~/netdiag project directory containing netdiag.sh.
  • The dnsutils package (provides dig).
  • The traceroute package.

Keep everything running

No action needed. Choose this if you want to keep using the diagnostic script as a real troubleshooting tool.

  • Your netdiag.sh script stays in ~/netdiag and is ready to run anytime with ./netdiag.sh.
  • The dnsutils and traceroute packages remain installed in your WSL environment for general use.

Pause - I'll come back to this later

This project has no running processes or cloud resources to pause. Everything is a file on disk and installed packages, so there is nothing to shut down.

  • Your script and packages stay where they are. When you come back, navigate to ~/netdiag and run ./netdiag.sh to pick up where you left off.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the packages you added.

  • Delete the project directory by running this in your WSL terminal:
rm -rf ~/netdiag
  • Remove the networking packages you installed by running:
sudo apt remove -y dnsutils traceroute
  • Confirm the directory is gone by running:
ls ~/netdiag

You should see a message saying the directory cannot be found. That confirms everything is cleaned up.

Nice Work!

Nice Work!

Nice work! You built a network diagnostic toolkit in Bash that probes your real network stack and maps each check to an OSI model layer.

You now have a reusable script and the mental model for isolating network failures the same way a network engineer would.

You've learned how to:

  • Build a diagnostic script using ping, ip, dig, ss, and traceroute, each mapped to an OSI model layer for structured network troubleshooting.
  • Test ICMP reachability, DNS resolution, TCP port connectivity, and connection states using the same CLI tools referenced in CCNA troubleshooting.
  • Structure diagnostics to run bottom-up from Layer 2 through Layer 7, isolating failures at the lowest broken layer first.
  • Completed an optional Secret Mission to push your diagnostic toolkit even further.

Ready to quiz yourself?