Module 5: Remote Machines, Networking, and Scripting

2. How machines talk: IP, ports, and DNS

Description

By the end of this lesson you will be able to tell a public IP apart from a private one, explain what actually distinguishes localhost, 127.0.0.1, and 0.0.0.0 — and why confusing them breaks real deployments, not just theory exams —, diagnose from memory why a port is "already in use," and explain step by step what happens between typing a domain and your machine getting back an IP address, including why a DNS change "still is not showing" even though you already made it.

This is not certification trivia. The day you deploy an API inside a container and it works perfectly from the inside but nobody can connect from outside, the cause is almost always one of the first two addresses in this lesson picked wrong. The day you move your domain to a new provider and a coworker swears "the site is still down" while it works perfectly for you, the cause is TTL, not a broken server. Without this model, each of those moments feels like unpredictable black magic; with it, it becomes a question with an answer.

Connection to the module: the previous lesson mapped out the module and promised you would diagnose a connection layer by layer — does the name resolve?, is there reachability?, does the port respond? — before touching SSH. This lesson is exactly the vocabulary for those layers: addresses, ports, and DNS. You are not going to use ping, dig, or curl -v to diagnose anything yet — that is the full method in the next lesson. Today the goal is understanding what each piece is, so tomorrow you know which command points at which.

The client-server model: who asks and who answers

Imagine you order food for delivery. You dial a number (you initiate contact), someone at the restaurant who is waiting for the call picks up, and that person responds to your specific order. The restaurant does not call you first — it just sits there, phone ready, waiting for someone to dial. If nobody picks up, your order never arrives, no matter how many times you dial.

That is essentially how any communication on the internet works. One machine takes on the role of client: it initiates the connection, makes a specific request, and waits for a response. The other takes on the role of server: it keeps a process running that patiently "listens," ready to accept incoming connections and respond to each one. Your browser is the client when you visit a page; the process running on that page's server is, precisely, the server. And the same model applies on your own laptop when you run a local API and query it from another terminal: two processes, two roles, one waiting and one asking.

Worked example

You are going to set up both sides of this conversation on your own machine, without installing anything new — Python ships with a minimal HTTP server ready to use.

Terminal 1 — the server, waiting for connections:

python3 -m http.server 8000

What to expect:

$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

The process stays running in the foreground — it does not hand you back the prompt. It is in the state we call "listening": open, attentive, doing nothing until someone connects.

Terminal 2 — the client, asking:

curl http://localhost:8000/

What to expect: a block of HTML with the file listing of the folder you launched the server from, and in Terminal 1 a new log line confirming it handled your request:

127.0.0.1 - - [21/Jul/2026 09:20:11] "GET / HTTP/1.1" 200 -

Notice that 127.0.0.1 in the log: it is the address the connection came from — in this case, your own machine. That address is exactly the topic of the next section.

IP addresses: the machine's postal address

Before a letter can reach your house, the postal system needs a unique address: street, number, city. Without it, there is no way to route anything. An IP address plays the same role for a machine on a network: a numeric identifier that says, unambiguously, which device every data packet is addressed to. The most common version still today, IPv4, looks like this: four numbers from 0 to 255 separated by dots — 192.168.1.23, 8.8.8.8.

Just like a postal address, there are two different scopes:

  • Public IP: unique across the entire internet, like a globally recognized zip code. It is the address your home router — or a company's server — presents to the rest of the world.
  • Private IP: valid only within a local network, like an apartment number that only makes sense once you have already reached the right building. RFC 1918 reserves three entire blocks for this purpose, which are never assigned as public addresses: 10.0.0.0 through 10.255.255.255, 172.16.0.0 through 172.31.255.255, and 192.168.0.0 through 192.168.255.255. That is why thousands of different networks — your house, your office, your neighbor's — can all reuse exactly 192.168.1.1 with no conflict whatsoever: that address never leaves its own network.

You can see the private IP your own machine has assigned right now:

# macOS (adjust en0 if your network interface has a different name)
ipconfig getifaddr en0

# Linux
hostname -I

What to expect: a single line with a number from the private range, for example 192.168.1.23. A router does the translation between that private IP and the public IP the rest of the internet sees — that mechanism is called NAT (Network Address Translation), and it is exactly why you can have ten devices at home sharing a single public IP without stepping on each other.

localhost, 127.0.0.1, and 0.0.0.0: three addresses that are not interchangeable

All three show up constantly in development configurations, and all three look similar enough that mixing them up feels harmless — until it breaks an entire deployment.

Think of a building with an internal intercom system. 127.0.0.1 is like talking to yourself over that intercom without the signal ever leaving your own apartment: it is the loopback address, a networking trick that makes the packet never leave your own machine. localhost is simply the readable name that almost always points to that same address — an alias, not a different address. 0.0.0.0, on the other hand, is not a destination you can talk to: it is an instruction for whoever is listening, something like telling the intercom doorman "accept calls from any apartment in the building, not just your own." When a server binds to 0.0.0.0, it is saying "accept connections arriving through any network interface on this machine" — not just the internal one.

The difference stops being theoretical the moment you put your application inside a container or a virtual machine. If your server binds to 127.0.0.1, it only accepts connections that originate inside that same container — not even the host machine containing it can talk to it, no matter how well the port is mapped outward, because the incoming connection arrives through an interface other than loopback. If it binds to 0.0.0.0, it accepts connections through any interface, including the one that connects to the container's outside world. This is, by far, cause number one behind "it works in my container but nobody else can connect."

You can verify the restricted behavior yourself. Stop the server in Terminal 1 with Ctrl+C and bring it back up, this time forcing the bind to loopback:

python3 -m http.server 8000 --bind 127.0.0.1

What to expect:

$ python3 -m http.server 8000 --bind 127.0.0.1
Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...

Notice the difference from the first startup: there it said 0.0.0.0, now it says 127.0.0.1. From your own terminal, curl http://localhost:8000/ still works exactly the same — because you are on the same machine too — but any other device on your local network trying to reach that same IP and port would get no response. It is the same difference, in miniature, that breaks a misconfigured container.

Ports: the specific door inside the machine, and why it is "already in use"

An IP gets you to the right building, but a building has many doors. A port is a number from 0 to 65535 that identifies, within a single machine, which of the listening processes a given connection is meant for. Without a port, an IP only tells you "to this machine" — with a port, it tells you "to this machine, to this specific service." By convention, HTTP listens on port 80, HTTPS on 443, SSH on 22 (you will see this up close in lesson 4 of this module).

The port space is divided into three ranges, defined by IANA:

  • 0 to 1023, "well-known" ports, reserved for standard services. On Unix systems, binding a process to one of these ports requires administrator privileges — the same least-privilege logic you saw in the previous lesson from module 4 applies here: nobody wants any regular user to be able to impersonate the system's DNS service just by starting their own process.
  • 1024 to 49151, registered ports for specific applications — this is where informal conventions like 3000 or 8000, which you use when developing locally, fall.
  • 49152 to 65535, dynamic or ephemeral ports, which the operating system automatically assigns to temporary outbound connections.

"Listening" means a process reserved that port number for itself and is now waiting for connections. A port can only have one process listening on it at a time, which is why the message "the port is already in use" is not a whim of the system: it is the same identity and ownership protection you already saw with files, applied here to a network resource.

Verify it by trying to start a second server on the same port as the first one, without having closed the original:

python3 -m http.server 8000

What to expect, if Terminal 1 still has its own server running on port 8000:

$ python3 -m http.server 8000
Traceback (most recent call last):
  ...
OSError: [Errno 48] Address already in use

(On Linux you will see Errno 98 instead of Errno 48 — the number changes depending on the operating system, the Address already in use message is the same). The operating system is not blocking the port arbitrarily: there is literally already another process — the one in your Terminal 1 — bound to it, and only one can be bound at a time. In the next lesson you are going to learn to ask the system exactly which process holds a port, with ss and lsof; for now, the practical fix is to close the previous process with Ctrl+C or choose a different port.

DNS: the directory that translates names into addresses

Nobody memorizes their contacts' phone numbers — you tap a name in your contacts and the phone does the translation for you. DNS (Domain Name System) plays that role for the internet: it translates readable names like example.com into the numeric IP address the network actually needs in order to route traffic. Without DNS, you would have to memorize IPs to visit any site.

The translation follows, in its simplified form, this sequence:

  1. Your machine first checks whether it already has that answer saved from a recent query (local cache).
  2. If it does not, it asks a recursive resolver — usually your internet provider's, or a public one like 8.8.8.8 — which does the heavy lifting on your behalf.
  3. That resolver, if it does not have the answer cached either, walks a hierarchy: it asks a root server which server knows about .com, then it asks that .com server who the authoritative server for example.com is, and finally it asks that authoritative server for the exact record.
  4. The authoritative server responds with the real data, the resolver caches it for a set amount of time, and hands it back to you.

The "real data" that gets returned is usually one of two record types:

  • A record: maps a name directly to an IPv4 address. It is the simplest translation — "this name is this IP," period.
  • CNAME record (canonical name): maps a name to another name, not to an IP. It is an alias — if blog.example.com has a CNAME pointing to example.com, resolving the first one means also resolving the second, one more step in the chain.

Every record comes with a TTL (time to live) attached, a number in seconds that tells every resolver how long it can trust that answer before asking again. Here is the detail that surprises almost everyone the first time: the TTL clock does not start when you change the record — it starts the moment each individual resolver cached its own copy. Imagine your team updates the A record for app.company.com to point at a new server, and that record had a TTL of 3600 seconds (one hour). A resolver that cached the old IP 5 minutes ago is going to keep returning the old IP for 55 more minutes. Another resolver, in another city, that cached it 58 minutes ago, is going to update in 2 minutes. Nobody lied, nothing broke — every cached copy, everywhere in the world, simply expires at a different moment. That is exactly why, the next time you move a domain between providers, it is worth lowering the TTL to a small value (say, 300 seconds) a day or two before the change: so that, when the real moment comes, no cache has to wait a full hour to find out.

/etc/hosts: your own local mini-DNS, no permission needed from anyone

Before your machine asks any external resolver, it checks a plain text file that lives on your own disk: /etc/hosts. It is, literally, a sticky note pasted on top of the full address book — if your note already has the answer, the system does not even bother checking the real directory. Each line in the file associates an IP address with one or more names, in exactly the same format you already saw resolved in the server example.

cat /etc/hosts

What to expect at minimum (the exact contents vary by system):

127.0.0.1       localhost
::1             localhost

That first line is, in fact, the reason localhost always resolves to 127.0.0.1 without needing any real DNS: it is already written there, locally, since you installed the operating system.

You can add your own entries to give a memorable name to a service you run locally, without touching any real DNS or depending on the internet. With administrator privileges — you are going to need sudo because this file belongs to root, just as you saw in the previous lesson from module 4 — add a line at the end:

127.0.0.1       myapp.local

With the Terminal 1 server running again on port 8000 (remember to relaunch it without --bind 127.0.0.1 if you want it to accept the connection), try:

curl http://myapp.local:8000/

What to expect: exactly the same response you got earlier with localhost — because, as far as your system is concerned, myapp.local now means 127.0.0.1, without any real DNS server having gotten involved at all. It is a genuinely useful shortcut when you are developing against several local services at once and want to name them instead of remembering loose port numbers.

Why this layer is still yours, even when a platform "handles it all"

It is tempting to think that, if you deploy on a modern platform that promises to manage DNS, certificates, and load balancing automatically, this model stopped mattering to you. It has not. The platform automates the paperwork, it does not eliminate the physics of the network: when you connect your own domain, you are still waiting on a TTL to propagate. When your container does not respond from outside, the cause is still a bind to 127.0.0.1 instead of 0.0.0.0. When two services in your project compete for the same port on your development machine, the message is still Address already in use. The platform abstracts the how to configure it, but the what is happening when something fails is still exactly this model — and it is the only way to diagnose a problem the platform's nice interface does not explain to you.

Common mistakes

Believing a DNS change is visible instantly everywhere in the world (conceptual). What happens: the student updates an A or CNAME record, checks it from their own machine five minutes later, sees the new result, and concludes "it is already propagated for everyone." Then they get a report from a coworker or a client who is still seeing the old version, and assume something broke. Why it happens: they confuse their own local cache — which may have expired or never held the old value at all — with the state of every cache in the world, each running its own TTL clock starting from a different moment. How to spot it: if your response to a "I am still seeing the old version" report is "that cannot be, it already works for me," that is the signal. How to fix it: remember the TTL counts from when each resolver cached its copy, not from when you changed the record — a change is not fully propagated until, at minimum, the full original TTL has passed since the moment of the change, and lower the TTL ahead of time before any planned migration.

Thinking 0.0.0.0 is an address you can connect to (conceptual). What happens: the student sees 0.0.0.0 in a server's output and then tries to use that same string as a destination — for example, typing it into the browser or passing it to curl — expecting to reach the service. Why it happens: 0.0.0.0 shows up in the same visual spot where they previously saw 127.0.0.1, and both look like "just another IP," so the student treats them as interchangeable. How to spot it: if you have ever literally copied 0.0.0.0 from a server's output to paste it as a destination URL, that is the symptom. How to fix it: remember that 0.0.0.0 is an instruction meaning "accept through any interface," aimed at the listening process — never a valid destination to connect to from the client side. To connect, use localhost, 127.0.0.1, or the machine's real IP.

Reading "Address already in use" as meaning the port is broken or permanently blocked. What happens: the student sees the error, tries the same port several more times, keeps failing, and concludes that specific port number "does not work" on their machine, switching ports without understanding the real cause. Why it happens: the message does not explicitly say "you have ANOTHER process of yours running right now" — it just says the address is already in use, and without that context it sounds like a permanent failure of the number itself. How to spot it: if you restarted your entire machine just to "free up" a port instead of finding out which process held it, that is the sign you did not identify the real cause. How to fix it: the error almost always means a previous process — yours, from an earlier terminal you forgot to close — is still listening on that port. Close it with Ctrl+C in the terminal where it is running, or wait for the next lesson to learn how to identify exactly which process holds it without guessing.

Exercises

1. Classify each of these addresses as public IP or private IP, and justify each with the matching range: 10.0.0.5, 8.8.8.8, 192.168.1.1, 172.20.4.4, 203.0.113.10.

See solution
  • 10.0.0.5 — private (within 10.0.0.0/8).
  • 8.8.8.8 — public (outside the three ranges reserved by RFC 1918).
  • 192.168.1.1 — private (within 192.168.0.0/16, the typical range for home routers).
  • 172.20.4.4 — private (within 172.16.0.0/12, which covers 172.16.x.x through 172.31.x.x).
  • 203.0.113.10 — public (outside the three private ranges; in fact, this specific block is reserved by convention only for documentation and examples, exactly the use you just gave it).

This works because the three private ranges are exactly the ones RFC 1918 reserved and no internet router routes — any address outside those three blocks is, by definition, potentially routable on the public network.

2. You try to bring up your API on port 5000 and see this error:

OSError: [Errno 48] Address already in use

What exactly is happening, and what two immediate solutions do you have without needing yet to identify which specific process holds the port?

See solution

The operating system is telling you another process — almost certainly yours, from an earlier attempt you did not close — is already bound to that same port on that same machine, and a port only allows one listening process at a time. Without yet identifying which process it is, you have two immediate options: check your open terminals and close with Ctrl+C any previous server you left running, or simply bring up your API on a different port (for example 5001) while you figure out which process holds 5000. This works because the error does not describe a damaged port — ports do not get damaged —, it describes a resource momentarily occupied by another live process.

3. Your team updates the A record for app.company.com to point at a new server. The record had a TTL of 3600 seconds. Forty minutes after the change, a coworker in another city reports they are still seeing the old site. Is the change broken? At most, when should they see it updated?

See solution

No, the change is not broken. The DNS resolver your coworker uses probably cached the old record shortly before your team made the change, and that 3600-second (one hour) TTL has not yet expired from that specific cache's point of view — the clock counts from when that cache stored the value, not from when your team changed it. In the worst case, your coworker will see the updated value at most 3600 seconds (a full hour) after the moment of the change, if their resolver cached the old value just an instant before the update. This works because every resolver in the world keeps its own independent copy with its own expiration — there is no mechanism that pushes the change to every cache immediately.

4. You add 127.0.0.1 api.local to your /etc/hosts and bring up a server with python3 -m http.server 4000 --bind 127.0.0.1. Will curl http://api.local:4000/ work from your own machine? Would it work from another computer on your same local network? Explain both answers.

See solution

From your own machine, yes it will work: /etc/hosts translates api.local to 127.0.0.1 before any real DNS gets queried, and the server — even though strictly bound to 127.0.0.1 — does accept connections that originate on the same machine, because that is exactly what the loopback address allows. From another computer on the network, it will not work, for two independent reasons that reinforce each other: first, that other machine does not have the api.local entry in its own /etc/hosts, so it would not even know which IP to point at; and second, even if it connected directly to your machine's real IP on the network, the server bound to 127.0.0.1 would reject that connection because it does not arrive through the loopback interface. This works because /etc/hosts is local to each machine — it is not shared automatically — and the bind to 127.0.0.1 is a restriction of the server process itself, independent of how the name got resolved.

Summary and next step

Today you saw the full vocabulary you need before diagnosing anything: the client-server model as the basic conversation between two processes, public and private IPs as addresses with different scopes, the real difference between localhost, 127.0.0.1 (a valid destination, restricted to the machine itself), and 0.0.0.0 (a binding instruction, never a destination), ports as the specific door inside a machine and why only one process can hold it at a time, and DNS as the hierarchical directory that translates names into addresses, with TTL determining how long each individual cache takes to find out about a change. You also saw /etc/hosts as your own local shortcut, checked before any real DNS query.

Before moving on you should be able to: classify any IP as public or private from memory; explain without hesitation why a server bound to 127.0.0.1 inside a container is invisible from outside even if the port is mapped; explain what "Address already in use" means without thinking the port is broken; and explain, in your own words, why a DNS change can take up to the full TTL to show up everywhere.

What you learned today is the map; the next lesson is the compass. With the vocabulary of IP, ports, and DNS already in your head, in the next lesson you are going to learn the method for ruling things out layer by layer — does the name resolve?, is there reachability?, does the port respond?, is the response correct? — with concrete tools: ping, dig, curl -v, and ss. Each of those tools is going to point exactly at one of the concepts you saw today.

Resources