Build a Network Explorer Toolkit

Use Python to explore IP addresses, DNS, TCP ports, and HTTP from scratch.

Introduction

30 Second Summary

Every time you type a website name and press Enter, your computer performs a chain of invisible steps: translating the name to a number, finding the right door to knock on, and exchanging messages in a precise format. These steps happen in milliseconds, completely hidden from view.

In this project, you will build a toolkit of Python scripts that make each of those invisible networking layers visible and interactive. You will see your own IP address, calculate subnet ranges, resolve domain names, scan for open ports, and speak raw HTTP over a TCP connection.

What You'll Build

By the end of this project, you can open a terminal, run one of your scripts against any domain name, and watch the full networking stack unfold in real time: the name resolving to an IP, ports responding to your handshake, and the raw HTTP conversation printing line by line.

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

  • A network identity inspector you can run to instantly see your machine's IP address, hostname, and which private network you belong to.
  • A subnet calculator that takes any CIDR block (like 192.168.1.0/24) and shows you every usable host address, so you can see exactly how cloud VPC sizing works.
  • A DNS resolver and TCP port scanner that reveal how domain names translate to IPs and how your scanner detects running services by completing the three-way handshake.
  • A raw HTTP client built on a bare TCP socket that fetches a real web page by manually speaking the HTTP protocol, proving that web requests are just structured text over a connection.
  • Secret Mission: Combine all your tools into a single network reconnaissance script that produces a full report for any domain.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, loops, if/else, dictionaries). No networking experience is required. Every networking concept is explained as you build the tool that reveals it.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. You are about to create a Python networking toolkit where each script you write reveals one invisible layer of how computers actually communicate.

Discover Your Network Identity

Before you can explore networking concepts like DNS and ports, you need to see your own machine's identity on the network.

Every device connected to a network has a name and an address. In this step, you'll write a Python script that reveals your machine's hostname, IP address, and fully qualified domain name. These are the building blocks of how computers find each other.

In this step, get ready to:
  • Create net_identity.py in your project folder.
  • Write a script that retrieves your machine's network identity using Python's socket module.
  • Run the script and observe your private IP address.
Write your network identity script

Python's socket module gives you direct access to your operating system's networking functions. You'll use it to look up three things: your machine's name, its IP address, and its fully qualified domain name.

  • In VS Code, click File in the top menu bar.
  • Click New File.
  • Name the file net_identity.py and save it inside your network-toolkit folder.
  • Paste the following code into net_identity.py:
import socket


def main():
    # Get your machine's network identity
    hostname = socket.gethostname()
    local_ip = socket.gethostbyname(hostname)
    fqdn = socket.getfqdn()

    # Display the results in a formatted box
    print("=" * 50)
    print("  NETWORK IDENTITY")
    print("=" * 50)
    print(f"  Hostname:     {hostname}")
    print(f"  Local IP:     {local_ip}")
    print(f"  FQDN:         {fqdn}")
    print("=" * 50)
    print()
    # Explain what the IP address means
    print("Your IP starts with 192.168.x.x or 10.x.x.x?")
    print("That means you are on a private network (RFC 1918).")
    print("Cloud VPCs use the same private address ranges.")


if __name__ == '__main__':
    main()

What does this code do?

This script queries three pieces of your machine's network identity:

  • socket.gethostname() returns the name your computer uses to identify itself on the network.
  • socket.gethostbyname(hostname) translates that hostname into an IPv4 address. This is your machine's actual address on the local network.
  • socket.getfqdn() returns the fully qualified domain name, which is the complete name that includes the domain your machine belongs to.
Run your network identity script

Before you run this, what do you think your machine's IP address will look like? Will it be a public address reachable from the internet, or something else?

  • Save net_identity.py.
  • Open a terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Run your script by entering this command:
python net_identity.py

You should see your machine's hostname, local IP address, and FQDN printed inside a formatted box. Your IP address likely starts with 192.168.x.x or 10.x.x.x.

Why does my IP start with 192.168 or 10?

These address ranges are reserved for private networks (defined in RFC 1918). Your router assigns one to each device on your home or office network.

Cloud VPCs use the exact same private address ranges. When you later create an AWS VPC with a CIDR block like 10.0.0.0/16, you are doing the same thing your home router does: carving out a private address space for your resources.

Not seeing any output?

  • Make sure you saved net_identity.py before running the command.
  • Check that your terminal is in the network-toolkit folder. Run ls and confirm you see net_identity.py listed.
  • If you see a syntax error, double check that the f" prefix is present on each formatted print line.

Help me debug why my net_identity.py script isn't producing output.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete net_identity.py file. Make sure yours matches exactly:

import socket


def main():
    hostname = socket.gethostname()
    local_ip = socket.gethostbyname(hostname)
    fqdn = socket.getfqdn()

    print("=" * 50)
    print("  NETWORK IDENTITY")
    print("=" * 50)
    print(f"  Hostname:     {hostname}")
    print(f"  Local IP:     {local_ip}")
    print(f"  FQDN:         {fqdn}")
    print("=" * 50)
    print()
    print("Your IP starts with 192.168.x.x or 10.x.x.x?")
    print("That means you are on a private network (RFC 1918).")
    print("Cloud VPCs use the same private address ranges.")


if __name__ == '__main__':
    main()

You've just revealed your machine's identity on the network. Next up, you'll build a subnet calculator that takes a CIDR block and computes every usable host address in a network range.

Build a Subnet Calculator

Every cloud network is defined by a CIDR block like 10.0.0.0/16. If you don't understand what /16 means, you can't design networks or size subnets in AWS VPCs or Azure VNets.

In this step, you'll build a subnet calculator that takes a CIDR block and computes every address in the range. Python's built-in ipaddress module does the heavy lifting, and you'll see exactly how prefix length controls the number of hosts available.

In this step, get ready to:
  • Parse a CIDR block from the command line using Python's ipaddress module.
  • Calculate and display network properties including usable host addresses.
  • Test with different CIDR blocks to see how prefix length controls available hosts.
Create the subnet calculator and parse CIDR input

A CIDR block like 192.168.1.0/24 defines a range of IP addresses. The number after the slash tells you how many bits are reserved for the network portion. Your calculator will decode this notation into human-readable output.

  • Create a new file called subnet_calc.py in your network-toolkit folder.
  • Add the following code to handle command-line input and parse the CIDR block:
import ipaddress
import sys


def main():
    # Check that a CIDR block was passed as argument
    if len(sys.argv) != 2:
        print("Usage: python subnet_calc.py <CIDR>")
        print("Example: python subnet_calc.py 192.168.1.0/24")
        sys.exit(1)

    cidr = sys.argv[1]

    # Parse the CIDR block into a network object
    try:
        network = ipaddress.ip_network(cidr, strict=False)
    except ValueError as e:
        print(f"Invalid CIDR notation: {e}")
        sys.exit(1)

    # Get all usable host addresses
    hosts = list(network.hosts())

What does this code do?

  • sys.argv[1] grabs the CIDR block you type after the script name (e.g. 192.168.1.0/24).
  • ipaddress.ip_network(cidr, strict=False) parses that string into a network object with properties like network address and host count. The strict=False flag allows host bits to be set (so 192.168.1.5/24 won't raise an error).
  • network.hosts() returns an iterator of all usable host addresses, excluding the network and broadcast addresses.

Now add the output section below the hosts line, plus the entry point at the bottom of the file.

  • Add this code directly below hosts = list(network.hosts()) to display the subnet properties:
    # Display subnet information
    print("=" * 50)
    print("  SUBNET CALCULATOR")
    print("=" * 50)
    print(f"  Input:             {cidr}")
    print(f"  Network Address:   {network.network_address}")
    print(f"  Broadcast Address: {network.broadcast_address}")
    print(f"  Subnet Mask:       {network.netmask}")
    print(f"  Prefix Length:     /{network.prefixlen}")
    print(f"  Total Addresses:   {network.num_addresses}")
    print(f"  Usable Hosts:      {len(hosts)}")


if __name__ == '__main__':
    main()

What do these properties mean?

  • network_address is the first address in the range, reserved to identify the network itself.
  • broadcast_address is the last address, reserved for sending messages to all hosts on the network.
  • netmask (like 255.255.255.0) is an older way of expressing the same information as the prefix length.
  • num_addresses is the total count, while usable hosts is always two fewer (network and broadcast addresses are reserved).
  • Save subnet_calc.py.
  • Run your calculator with a test CIDR block by running this command:
python subnet_calc.py 192.168.1.0/24

You should see the network address as 192.168.1.0, broadcast address as 192.168.1.255, subnet mask as 255.255.255.0, total addresses as 256, and usable hosts as 254.

Seeing a syntax error or no output?

  • Make sure the if __name__ == '__main__': block is at the bottom of the file with no indentation. If it's indented inside main(), the script won't run.
  • Check that your print lines are indented with 4 spaces inside the main() function.
  • If you see Usage: python subnet_calc.py <CIDR>, make sure you included the CIDR block argument after the filename.

Help me debug my subnet calculator script.

Add usable host details and explanation

The calculator shows total addresses and usable hosts, but doesn't yet reveal which addresses are usable. Showing the first and last usable host makes it clear exactly which IPs you can assign to devices.

  • In subnet_calc.py, find the line that says print(f" Usable Hosts: {len(hosts)}").
  • Add this code between that line and the if __name__ block:
    # Show first and last usable addresses
    if hosts:
        print(f"  First Usable Host: {hosts[0]}")
        print(f"  Last Usable Host:  {hosts[-1]}")

    print("=" * 50)
    print()
    # Explain why usable hosts differ from total
    print("Note: Network address and broadcast address are")
    print("reserved. Usable hosts = total addresses - 2.")
    print()
    print("In cloud VPCs, you choose a CIDR block to define")
    print("how many hosts your subnet can hold.")

What does this code do?

  • hosts[0] and hosts[-1] grab the first and last elements of the usable hosts list. For a /24 network, that's .1 through .254.
  • The if hosts: guard prevents an error on tiny networks (like /31 or /32) where no usable hosts exist.
  • The explanatory text at the bottom reminds you why usable hosts = total - 2. The network address and broadcast address are always reserved.
  • Save subnet_calc.py.
  • Run the calculator again by running:
python subnet_calc.py 192.168.1.0/24

You should now see two new lines: First Usable Host as 192.168.1.1 and Last Usable Host as 192.168.1.254, followed by the explanation note.

Not seeing the first/last usable host lines?

  • Check that the if hosts: block is indented at the same level as the other print statements inside main().
  • Make sure the new code is placed before the if __name__ == '__main__': block, not after it.

Help me figure out why my usable host output isn't appearing.

✔️ Awesome, I've got everything!

Great. Double check you've saved subnet_calc.py before moving on.

ⓧ I'd like to double check the full code

Here's the complete subnet_calc.py file. Scroll up and confirm each section is present in this order:

  • Imports (ipaddress and sys).
  • Argument check with usage message.
  • CIDR parsing with try/except error handling.
  • Print statements for all subnet properties.
  • The if hosts: block showing first/last usable host.
  • Explanatory note print statements.
  • The if __name__ == '__main__': entry point at the bottom.
Test with different CIDR blocks

The real power of subnetting becomes clear when you change the prefix length. A larger prefix (like /28) gives you fewer hosts. A smaller prefix (like /16) gives you thousands.

Before you run the next command, predict: how many usable hosts do you think a /16 network has compared to the 254 you just saw with /24?

  • Try a larger network by running:
python subnet_calc.py 10.0.0.0/16

You should see 65534 usable hosts. That's a massive jump from 254.

  • Now try a small network by running:
python subnet_calc.py 172.16.0.0/28

You should see only 14 usable hosts. This is exactly the decision you make when sizing a VPC subnet. A /28 fits a small cluster of servers. A /16 fits an entire enterprise network.

Why does this matter for cloud networking?

When you create a VPC in AWS or Azure, the first thing you choose is a CIDR block. Pick too small a prefix and you'll run out of IP addresses for your servers. Pick too large and you waste address space that could be used by other subnets.

The numbers you just computed (254 hosts for /24, 65534 for /16, 14 for /28) are the same numbers cloud consoles show when you configure subnets.

Your subnet calculator can now decode any CIDR block into usable information. Next up, you'll tackle the layer that makes the internet human-friendly: DNS resolution.

Build a DNS Resolver

Your subnet calculator can compute every address in a network range. But there is a gap: it cannot reach anything by name.

When you type "google.com" into a browser, your computer has no idea what that means. Computers only understand IP addresses. DNS is the hidden translation layer that converts human-friendly names into routable IP addresses. In this step, you will build a tool that reveals this translation in action.

In this step, get ready to:
  • Create a DNS resolver script that translates domain names into IP addresses.
  • Compare the modern dual-stack resolution method with the simpler IPv4-only approach.
  • Test with real domains and observe how load balancing distributes traffic across multiple IPs.
Create the DNS resolver file

Every DNS resolver needs the same foundation as your previous tools: accept input from the command line, validate it, and print a clear header.

  • In VS Code, create a new file called dns_resolver.py inside your network-toolkit folder.
  • Add the file scaffold by pasting this code:
import socket
import sys


def main():
    if len(sys.argv) != 2:
        print("Usage: python dns_resolver.py <hostname>")
        print("Example: python dns_resolver.py google.com")
        sys.exit(1)

    hostname = sys.argv[1]

    print("=" * 50)
    print("  DNS RESOLVER")
    print("=" * 50)
    print(f"  Resolving: {hostname}")
    print("-" * 50)


if __name__ == '__main__':
    main()

What does this code do?

This is the same pattern you used in subnet_calc.py. The script checks that the learner provided a hostname argument, stores it in a variable, and prints a formatted header.

The socket module provides all the networking functions you need. The sys module gives access to command-line arguments.

  • Save dns_resolver.py.
  • Test the scaffold by running this command in your terminal:
python dns_resolver.py google.com

You should see the formatted header with "Resolving: google.com" printed. No IP addresses yet.

Now add the actual DNS resolution logic. This is the core of the tool: translating a domain name into IP addresses using socket.getaddrinfo().

  • In dns_resolver.py, find the line that says print("-" * 50). Copy the following code below it (before the if __name__ block):
    # Modern approach: getaddrinfo (supports IPv4 and IPv6)
    print("\n  [getaddrinfo - Modern, dual-stack]")
    try:
        results = socket.getaddrinfo(
            hostname, 80, proto=socket.IPPROTO_TCP
        )
        seen = set()
        for family, socktype, proto, canonname, sockaddr in results:
            ip = sockaddr[0]
            if ip not in seen:
                seen.add(ip)
                family_name = "IPv4" if family == socket.AF_INET else "IPv6"
                print(f"    {family_name}: {ip}")
    except socket.gaierror as e:
        print(f"    DNS resolution failed: {e}")
        sys.exit(1)

What does this code do?

The socket.getaddrinfo() function is the modern way to resolve a domain name. It returns a list of 5-tuples containing the address family, socket type, protocol, canonical name, and socket address.

  • The seen set prevents duplicate IPs from appearing (DNS can return the same address multiple times in different tuples).
  • The family value tells you whether an address is IPv4 (AF_INET) or IPv6 (AF_INET6).
  • The IP address lives inside the sockaddr tuple at index 0.
  • Save dns_resolver.py and run it again:
python dns_resolver.py google.com

You should now see one or more IP addresses printed under the "getaddrinfo" section, each labeled as IPv4 or IPv6.

Seeing "DNS resolution failed"?

  • Make sure you are connected to the internet. DNS resolution requires a network connection.
  • Check that you typed the domain correctly with no typos (e.g. google.com not gogle.com).
  • If you are behind a corporate firewall or VPN, try a different domain like example.com.

Why is my DNS resolution failing?

Add the gethostbyname comparison

Python offers a simpler DNS function called socket.gethostbyname() that only returns IPv4 addresses. Adding it side by side shows why the modern approach is preferred.

  • In dns_resolver.py, find the except socket.gaierror block from the getaddrinfo section. Copy the following code below it (still inside the main() function, before if __name__):
    # Simple approach: gethostbyname (IPv4 only)
    print("\n  [gethostbyname - Simple, IPv4 only]")
    try:
        ipv4 = socket.gethostbyname(hostname)
        print(f"    IPv4: {ipv4}")
    except socket.gaierror as e:
        print(f"    Failed: {e}")

    print("\n" + "=" * 50)
    print()
    print("Multiple IPs = load balancing (traffic distributed")
    print("across servers). This is what cloud load balancers do.")

What does this code do?

  • The socket.gethostbyname() function translates a hostname to a single IPv4 address. It cannot return IPv6 results.
  • By showing both methods, you can see that getaddrinfo returns the full picture (both address families) while gethostbyname gives only one IPv4 address.
  • The closing print statements explain that multiple IPs for one domain means load balancing. This is the same concept behind cloud load balancers.
  • Save dns_resolver.py and run it:
python dns_resolver.py google.com

You should now see two sections: the getaddrinfo results (potentially showing both IPv4 and IPv6) and the gethostbyname result (one IPv4 address only).

Only seeing one section?

  • Make sure you pasted the new code below the except block from the getaddrinfo section, not inside it.
  • Check your indentation. All the new code should be at the same level as the getaddrinfo section (one level inside main()).

Help me fix my dns_resolver.py indentation.

Test with multiple domains

A single test with google.com proves the script works. But different domains reveal different DNS behaviors that teach you about real-world networking.

  • Try resolving github.com by running:
python dns_resolver.py github.com
  • Now try example.com:
python dns_resolver.py example.com

Why do some domains return more IPs than others?

When a domain returns multiple IP addresses, that is load balancing in action. Traffic gets distributed across several servers so no single machine is overwhelmed.

Some domains return both IPv4 and IPv6 addresses, showing that the internet is gradually transitioning to the larger IPv6 address space. This is exactly what happens when a cloud load balancer distributes traffic across multiple servers.

Before you run the final verification, predict: will google.com return more IP addresses than example.com? Why might that be?

  • Run the resolver one more time to confirm everything works:
python dns_resolver.py google.com

You should see the full output: the domain resolved to one or more IP addresses with address family labels (IPv4/IPv6) under the getaddrinfo section, plus a single IPv4 result under gethostbyname. Google typically returns more IPs because it operates massive global infrastructure with many servers.

✔️ Awesome, I've got everything!

Great. Double check you have saved dns_resolver.py.

ⓧ I'd like to double check the full code

Scroll up and confirm your dns_resolver.py contains these sections in order:

  • The imports (socket and sys) and the main() function definition.
  • The argument check (exits with usage message if no hostname provided).
  • The header print statements (DNS RESOLVER banner and "Resolving:" line).
  • The getaddrinfo section with the seen set and the loop printing each IP with its family name.
  • The gethostbyname section printing a single IPv4 result.
  • The closing print statements about load balancing.
  • The if __name__ == '__main__': block at the bottom.

You now have a tool that reveals the hidden DNS translation layer. When you type a domain name, your computer silently performs this exact lookup to find the server's real address. Next up, you will discover what services are actually running at those addresses by building a TCP port scanner.

Build a TCP Port Scanner

Your DNS resolver can find where a server lives (its IP address), but it cannot tell you what services that server is running.

A port scanner solves this by trying to establish TCP connections to discover which ports are open. This is the exact logic behind firewall rules and security groups in the cloud. In this step, you will build a scanner that reveals which services are listening on a host.

In this step, get ready to:
  • Build a TCP port scanner that probes a range of ports on any host.
  • Add timing to measure how long each scan takes.
  • Prove that a running service opens a port by detecting it with your scanner.
Create the scan_port helper function

When your computer connects to a remote service, it performs a TCP three-way handshake: SYN, SYN-ACK, ACK. If the handshake completes, the port is open. If it times out or is refused, the port is closed.

  • In VS Code, create a new file called port_scanner.py inside your network-toolkit folder.
  • Add the imports and the scan_port function by copying this code:
import socket
import sys
import time


def scan_port(host, port, timeout=0.5):
    # Create a TCP socket for this connection attempt
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Stop waiting after 0.5 seconds if no response
    sock.settimeout(timeout)
    # connect_ex returns 0 if handshake succeeded
    result = sock.connect_ex((host, port))
    sock.close()
    return result == 0

What does this code do?

The scan_port function attempts a single TCP connection to one port on the target host.

  • socket.AF_INET means IPv4, and socket.SOCK_STREAM means TCP (a reliable, connection-based protocol).
  • connect_ex returns 0 if the TCP handshake succeeded (port is open). Any other value means closed or filtered.
  • settimeout(0.5) gives each connection attempt half a second before giving up. This prevents the scanner from hanging forever on unresponsive ports.

What is the TCP three-way handshake?

Every TCP connection starts with three messages: your computer sends SYN ("I want to connect"), the server responds SYN-ACK ("OK, I'm listening"), and your computer replies ACK ("Great, let's talk"). If connect_ex returns 0, all three messages completed successfully.

Cloud security groups work by allowing or denying these handshakes on specific ports. Your scanner performs the same handshake attempt that a security group evaluates.

Build the main function with timing

The main function needs to accept command-line arguments (host, start port, end port), loop through each port in the range, and track how long the scan takes.

  • In port_scanner.py, add the main() function below your scan_port function:
def main():
    if len(sys.argv) != 4:
        print("Usage: python port_scanner.py <host> <start_port> <end_port>")
        print("Example: python port_scanner.py 127.0.0.1 70 90")
        sys.exit(1)

    host = sys.argv[1]
    start_port = int(sys.argv[2])
    end_port = int(sys.argv[3])

    # Print scan header
    print("=" * 50)
    print("  TCP PORT SCANNER")
    print("=" * 50)
    print(f"  Target: {host}")
    print(f"  Range:  {start_port} - {end_port}")
    print("-" * 50)

    # Track open ports and measure scan duration
    open_ports = []
    start_time = time.time()

    for port in range(start_port, end_port + 1):
        if scan_port(host, port):
            print(f"    Port {port}: OPEN")
            open_ports.append(port)
        else:
            print(f"    Port {port}: closed")

    elapsed = time.time() - start_time

What does this code do?

  • The argument check ensures you provide a host and port range. Without all three, the script prints usage instructions and exits.
  • time.time() records the timestamp before and after the loop. The difference shows how long the entire scan took.
  • The loop calls scan_port on every port in the range and prints whether each one is OPEN or closed.
  • Add the results output and entry point directly below the code you just wrote:
    # Print scan results summary
    print("-" * 50)
    print(f"  Scan complete in {elapsed:.2f} seconds")
    print(f"  Open ports found: {len(open_ports)}")

    if open_ports:
        print(f"  Open: {open_ports}")

    print("=" * 50)
    print()
    print("Each open port = a running service accepting TCP")
    print("connections. Security groups control which ports")
    print("are reachable from outside your network.")


if __name__ == '__main__':
    main()

What does this code do?

  • The summary prints how many seconds the scan took and lists any open ports found.
  • if __name__ == '__main__' ensures main() only runs when you execute the script directly (not when importing it as a module).
  • Save port_scanner.py.

✔️ 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 port_scanner.py file is longer than 30 lines, so scroll up and confirm each section is present in this order:

  • Chunk 1: Imports (socket, sys, time) and the scan_port function.
  • Chunk 2: main() with argument parsing, header output, and the timed scan loop through elapsed = time.time() - start_time.
  • Chunk 3: Results summary, footer messages, and the if __name__ == '__main__' entry point.
Test the scanner and discover open ports

Time to see your scanner in action. You will scan your own machine (localhost) to see what happens when no services are listening on a port range.

  • Before you run the next command, predict: do you think any ports between 70 and 90 will be open on your machine right now?
  • Run your port scanner against localhost by running this command:
python port_scanner.py 127.0.0.1 70 90

Every port shows as closed. That is correct and expected. No services are listening on ports 70-90 on your machine, so the TCP handshake fails for each one.

Why is the scan slow?

Notice the scan took several seconds for just 21 ports. Each closed port waits the full 0.5-second timeout before giving up. This is why real port scanners use asynchronous techniques to check many ports simultaneously.

All ports are closed because nothing is running. But what happens when you start a service? A running service opens a port to accept connections, and your scanner should detect it.

  • Open a second terminal in VS Code by clicking **Terminal** in the top menu, then **New Terminal**.
  • Start a simple HTTP server on port 8080 by running this command in the new terminal:
python -m http.server 8080

What does this command do?

python -m http.server 8080 starts Python's built-in HTTP server listening on port 8080. It will keep running and hold this terminal until you stop it with Ctrl+C. Leave it running for now.

  • Switch back to your first terminal (click the terminal tab that is not running the HTTP server).
  • Before you run the next scan, predict: will port 8080 show as OPEN this time?
  • Scan ports 8075-8085 to detect your running server by running this command:
python port_scanner.py 127.0.0.1 8075 8085

Port 8080 now shows as OPEN. Your scanner detected the running HTTP server. Every other port in the range is still closed.

Why does this matter for cloud security?

You just proved that a running service opens a port, and your scanner detects it. This is exactly what a security group rule controls: allowing or denying TCP connections to specific ports.

When you write a security group rule that says "allow inbound TCP on port 443", you are allowing exactly the kind of handshake your scanner just performed.

Don't see port 8080 as OPEN?

  • Make sure the HTTP server is still running in the other terminal. You should see a message like "Serving HTTP on 0.0.0.0 port 8080" in that terminal.
  • Confirm you are scanning the right port range. The command should be python port_scanner.py 127.0.0.1 8075 8085 (8080 must be within the range).
  • Check that you are running the scan in a different terminal from the server. The server terminal is occupied.

Why isn't my port scanner detecting the open port?

  • Stop the HTTP server by pressing Ctrl+C in the terminal where it is running.

Your port scanner is working. You can now detect which services are running on any host by probing their ports. Next up, you will build a raw HTTP client that actually talks to an open port using the HTTP protocol, proving that HTTP is just text over a TCP connection.

Build a Raw HTTP Client

Your toolkit can now resolve domain names to IP addresses and discover which ports are open on a host. But what actually travels over a TCP connection once it is established?

HTTP is just text sent over TCP. In this step, you will build a raw HTTP client from a bare TCP socket, proving that web requests are not magic. They are a protocol, which is just agreed-upon text.

In this step, get ready to:
  • Resolve a hostname and open a TCP connection to port 80.
  • Build and send a raw HTTP/1.1 GET request as bytes over the socket.
  • Receive and print the full HTTP response including status line, headers, and HTML body.
Create the file and resolve the hostname

Your raw HTTP client needs to do three things in sequence: resolve the domain name to an IP (DNS), open a TCP connection to that IP on port 80, then speak HTTP over that connection.

  • In VS Code, create a new file called http_client.py inside your network-toolkit folder.
  • Add the imports, argument check, and header output by pasting this code:
import socket
import sys


def main():
    if len(sys.argv) != 2:
        print("Usage: python http_client.py <hostname>")
        print("Example: python http_client.py example.com")
        sys.exit(1)

    hostname = sys.argv[1]

    print("=" * 50)
    print("  RAW HTTP CLIENT")
    print("=" * 50)
    print(f"  Target: {hostname}")
    print("-" * 50)

What does this code do?

This is the same argument-checking pattern you used in your other tools. It ensures the learner passes a hostname on the command line and prints a usage message if they forget.

  • Now add the DNS resolution and TCP connection code directly below. This is the core of substep 1. Paste the following after the header output:
    # Step 1: DNS resolution
    print("\n  [1] Resolving DNS...")
    try:
        results = socket.getaddrinfo(
            hostname, 80, type=socket.SOCK_STREAM
        )
        addr_info = results[0]
        family = addr_info[0]
        sockaddr = addr_info[4]
        ip = sockaddr[0]
        print(f"      Resolved to: {ip}")
    except socket.gaierror as e:
        print(f"      DNS failed: {e}")
        sys.exit(1)

    # Step 2: TCP connection
    print(f"\n  [2] Connecting to {ip}:80...")
    sock = socket.socket(family, socket.SOCK_STREAM)
    sock.settimeout(5)

    try:
        sock.connect(sockaddr)
        print("      Connected!")
    except (socket.timeout, OSError) as e:
        print(f"      Connection failed: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()

What does this code do?

  • socket.getaddrinfo(hostname, 80, type=socket.SOCK_STREAM) resolves the domain name to an IP address, just like your DNS resolver. The type=socket.SOCK_STREAM filters for TCP connections.
  • The result is a list of 5-tuples. You grab the first one and extract the address family, and the socket address (IP + port).
  • sock.connect(sockaddr) performs the TCP three-way handshake you learned about in the port scanner. If it succeeds, you have an open connection.
  • The if __name__ block at the bottom makes the script runnable. You will add more code above it in the next substep.
  • Save http_client.py.
  • Run the script to confirm DNS resolution and TCP connection work by running this command:
python http_client.py example.com

You should see the DNS resolution succeed and a "Connected!" message confirming the TCP handshake completed.

Seeing "Connection failed" or "DNS failed"?

  • Check that you have an active internet connection. This script connects to a real server.
  • If DNS fails, try a different domain like google.com. Your network might be blocking DNS for certain hosts.
  • If the connection times out, a firewall may be blocking outbound traffic on port 80.

Why can't my script connect to example.com?

Send a raw HTTP request and receive the response

You have an open TCP connection. Now you need to speak HTTP over it. An HTTP request is just a formatted text string that follows a specific pattern: a request line, headers, and a blank line to signal the end of headers.

  • In http_client.py, find the line that says if __name__ == '__main__': and add the following code above it. This builds and sends the HTTP request:
    # Step 3: Send HTTP request
    request = (
        f"GET / HTTP/1.1\r\n"
        f"Host: {hostname}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
    )
    print(f"\n  [3] Sending HTTP request...")
    print(f"      > GET / HTTP/1.1")
    print(f"      > Host: {hostname}")
    print(f"      > Connection: close")
    sock.sendall(request.encode('utf-8'))

What does this HTTP request mean?

  • GET / HTTP/1.1 asks the server for its root page using HTTP version 1.1.
  • Host: {hostname} tells the server which site you want. HTTP/1.1 requires this header because one IP can host many domains.
  • Connection: close tells the server to close the connection after responding, making the receive loop simple.
  • The \r\n\r\n at the end is a blank line that signals "headers are done, send the response now."
  • Now add the response-receiving code directly below the send block (still above if __name__). Paste this:
    # Step 4: Receive response
    print(f"\n  [4] Receiving response...")
    response = b""
    while True:
        chunk = sock.recv(4096)
        if not chunk:
            break
        response += chunk

    sock.close()

    decoded = response.decode('utf-8', errors='replace')
    print("\n" + "-" * 50)
    print("  RESPONSE:")
    print("-" * 50)
    print(decoded[:2000])

    if len(decoded) > 2000:
        print(f"\n  ... ({len(decoded)} total bytes, truncated)")

    print("\n" + "=" * 50)
    print()
    print("HTTP is just text over a TCP connection.")
    print("This is exactly what a browser does, and what")
    print("a load balancer health check performs.")

What does this code do?

  • sock.recv(4096) reads up to 4096 bytes at a time from the connection. The loop keeps reading until the server closes the connection (returns empty bytes).
  • Because you sent Connection: close, the server will close its end after sending the full response. That is what makes the empty-bytes check work.
  • The response is truncated to 2000 characters to keep terminal output readable. The full HTML page would be much longer.
  • Save http_client.py.
Run the HTTP client and observe the full response

Before you run this, what do you think the response will contain? Think about what a server sends back when you visit a website in your browser.

  • Run the complete HTTP client by running this command:
python http_client.py example.com

You will see the full HTTP response printed step by step: the DNS resolution, the TCP connection, the request being sent, and then the raw response from the server.

The response starts with a status line like HTTP/1.1 200 OK, followed by headers (Content-Type, Content-Length, etc.), a blank line, and then the HTML body of the page.

Why does this matter for cloud and DevOps?

This is exactly what your browser does behind the scenes. It is also exactly what a load balancer health check performs: connect to port, send an HTTP request, check that the response status is 200.

When you configure an AWS Application Load Balancer health check path, it runs this same sequence repeatedly to confirm your service is alive.

Seeing garbled output or a timeout?

  • Make sure the request string uses \r\n (carriage return + newline) line endings, not just \n. HTTP requires both characters.
  • If you see a timeout, the server may not be responding. Try python http_client.py google.com as an alternative.
  • Check that Connection: close is in your request. Without it, the server keeps the connection open and your receive loop hangs.

Why is my HTTP client timing out or returning garbled text?

  • Try running it against a different domain to see how responses vary:
python http_client.py google.com

You may notice that Google returns a redirect (status 301) instead of the page directly. That is real-world HTTP in action.

✔️ Awesome, I've got everything!

Great. Double check you have saved http_client.py and confirmed the full HTTP response prints when you run it.

ⓧ I'd like to double check the full code

Your http_client.py should contain these sections in order. Scroll up and confirm each is present:

  • Imports (socket and sys).
  • The main() function starting with argument check and header output.
  • Step 1: DNS resolution using socket.getaddrinfo().
  • Step 2: TCP connection using sock.connect(sockaddr).
  • Step 3: HTTP request built as a formatted string and sent with sendall().
  • Step 4: Response received in a recv(4096) loop, decoded, and printed (truncated to 2000 chars).
  • The if __name__ == '__main__': main() entry point at the bottom.

You have now built a complete networking toolkit that reveals every layer of the stack. From your machine's IP address through DNS resolution to TCP connections to raw HTTP requests, you can see exactly what happens when a computer talks to another computer over the internet.

Secret mission

Build a Network Reconnaissance Report

You have five individual tools that each reveal one layer of the networking stack. Real network troubleshooting requires combining them all. Can you build a single reconnaissance script that takes a domain name and produces a complete network analysis report?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using Python's standard library, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your files for future reference or delete them entirely.

Resources you used:

  • The network-toolkit folder on your Desktop containing all six Python scripts (net_identity.py, subnet_calc.py, dns_resolver.py, port_scanner.py, http_client.py, recon.py).

Keep everything running

No action needed. Choose this if you want to keep your networking toolkit available to reference, extend, or run again later.

  • Your network-toolkit folder stays on your Desktop, ready to use anytime.
  • All scripts use only Python's standard library, so there are no packages or dependencies to maintain.

Pause - I'll come back to this later

Since this project has no running processes or cloud services, there is nothing to pause. Your files sit idle on disk and consume no memory or CPU.

  • If you started a Python HTTP server during Step 4 (python -m http.server 8080), make sure you stopped it with Ctrl+C.
  • Your files will be exactly where you left them when you come back.

Delete - I don't want to use this again

Remove the project folder entirely to clean up your Desktop.

  • Delete the network-toolkit folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/network-toolkit

Windows

rmdir /s /q %USERPROFILE%\Desktop\network-toolkit
  • Confirm the folder is gone by checking your Desktop. You should no longer see network-toolkit listed.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete networking toolkit from scratch using nothing but Python's standard library.

You've learned how to:

  • Built a subnet calculator that computes usable hosts from CIDR notation, the exact skill used when designing VPC subnets and planning IP allocation.
  • Built a DNS resolver showing how domain names translate to IP addresses, the same process behind Route 53, Cloud DNS, and every other DNS service.
  • Built a raw HTTP client from a bare TCP socket, proving that HTTP is just text over a connection. This demystifies web requests, health checks, and load balancer probes.
  • Secret Mission: Combined all tools into a network reconnaissance report, the same workflow used in real network troubleshooting and security assessments.

Ready to quiz yourself?