Skip to content
Software Atlas

TCP/IP & the OSI Model

What really happens to your bytes between fetch() and the server: layers, encapsulation, IP addresses and subnets, routing hop by hop, ports and sockets, MTU — and where the neat layer picture leaks in production.

⏱ ~45 min read13 sections13 interactive widgetsGo · JavaScript · shell
FoundationsPractitionerStaff-level
Foundations§01

The 30-second version

In one sentenceThe network is built in layers. Each layer wraps your data in its own small label (a header), and each device along the way reads only the label meant for it.

Here's one line of JavaScript:

const res = await fetch("https://example.com/");

Your code just sent a request and got an answer. But between the two machines there might be 15 boxes, two oceans' worth of cable, and a Wi-Fi radio. None of them knows what HTTP is. So how does it work?

What happens to that one line

Roughly, in order:

  1. Find the address. The name example.com is looked up with DNS and turned into an IP address, a number like 203.0.113.10. (The DNS topic covers this step.)
  2. Open a connection. Your OS opens a TCP connection to that address on port 443, the "door" where web servers wait for HTTPS. (The TCP & UDP topic covers how.)
  3. Wrap and send. Your request is wrapped in a few headers and cut into packets. Each packet travels on its own, hopping from router to router until it reaches the server.
  4. Unwrap and answer. The server's OS strips the headers off again, hands the request to the web server program, and the answer comes back the same way.

This topic is about steps 3 and 4: how bytes actually travel. We'll use plain http:// in most examples, because the encryption that https adds (TLS, see the TLS topic) is one more wrapper and doesn't change the picture.

Letters, envelopes and mailbags

Your request, packed for the trip
mailbag: Ethernet / Wi-Fi“from my laptop to the Wi-Fi box” — re-labelled at every post office
envelope: IP“from 192.168.1.37, to 203.0.113.10”
envelope: TCP“deliver to apartment 443 (the web server program), starting at byte 1 of the stream”
✉ the letter (HTTP)GET / HTTP/1.1 · Host: example.com
Each layer adds its own label on the outside. A post office (router) only reads the label meant for it — it never opens the inner envelopes.

That nesting has a name: encapsulation. It's the single most important idea in networking, and §03 animates it byte by byte.

What you'll be able to do after this topic

  • Explain what "L4" and "L7" mean when someone says "L7 load balancer" (§02).
  • Read an address like 10.0.0.0/16 and know which machines are in it (§04).
  • Debug the classic "works on my laptop, not in Docker" bug in under a minute (§06).
  • Understand how one server port handles 10,000 clients at once (§07).
  • Read a real packet capture (§09), and know where the clean layer picture breaks in production (§10).
Foundations§02

Four layers that run, seven that we name

In one sentenceEach layer solves one problem and trusts the layer below for everything else. The TCP/IP model (4 layers) is what your computer actually runs; the OSI model (7 layers) is mostly the vocabulary engineers use to talk about it.

Why layers at all? Because one giant program that handles Wi-Fi radio and HTTP and lost packets would be impossible to change. With layers, Wi-Fi can be swapped for fibre without touching your browser, and HTTP/2 can replace HTTP/1.1 without touching a single router.

The four layers, bottom to top

LayerIts one jobAddresses things byExample
LinkMove data across one hop (one cable, one Wi-Fi hop)MAC addressEthernet, Wi-Fi
InternetMove a packet from machine to machine, anywhere in the worldIP addressIPv4, IPv6
TransportDeliver data to the right program on that machine (and, for TCP, make it reliable)portTCP, UDP
ApplicationDecide what the bytes meanURL, hostnameHTTP, DNS, gRPC

Read the "job" column from the bottom row up and you have the whole story of §01: an application message, delivered to a program (transport), on a machine (internet), one hop at a time (link).

Same thing, two maps

The OSI model splits the same territory into seven layers. Two of its extra layers (Session, Presentation) never became separate pieces of software on the internet; in practice they're part of the application. The bottom one, Physical, is the actual cable or radio signal.

Two maps of the same territory
Left: the 7-layer OSI model (vocabulary). Right: the 4-layer TCP/IP model (what actually runs). Click any layer.
Transport layerOSI 4
Gets data to the right program on a machine (that's what ports are for). TCP also turns lossy packets into a reliable stream.
Examples: TCP, UDP (QUIC is built on UDP)
Unit of data: segment (TCP) or datagram (UDP)
Addresses by: port numbers
Who reads it: Only the two end machines' operating systems — plus “L4” load balancers and firewalls that look at ports.
✓ Guarantees
TCP: every byte arrives, in order, exactly once — or you get an error. UDP: only a checksum.
✕ Does NOT guarantee
Not speed, not timing, not privacy. TCP can't make a slow link fast, and it doesn't encrypt (TLS does).

Where you'll hear the OSI numbers

You'll almost never need the OSI model to build something, but you'll hear its numbers every week:

  • "L3" = the IP layer. "An L3 problem" means routing or addressing.
  • "L4" = TCP/UDP. An "L4 load balancer" forwards connections by IP address and port, without reading the data inside.
  • "L7" = the application. An "L7 load balancer" or "L7 firewall" reads the HTTP request (path, headers) and decides based on that. The load balancing topic covers the trade-off.
  • "L2" = the link layer: MAC addresses, switches, Wi-Fi.

Nobody says "L5" or "L6" with a straight face.

The most important row: IP is "best effort"

Look again at what the internet layer promises. It's best effort: each router does its best to forward each packet, but a packet may be:

  • dropped, because a router's queue was full or a Wi-Fi frame was damaged,
  • duplicated, which is rare, but it happens,
  • reordered, because packet 2 took a faster path than packet 1,
  • delayed by any amount.

And IP doesn't tell anyone when this happens. That's not a bug. It's the design choice that made the internet cheap and scalable: routers stay simple and forget each packet the moment they've forwarded it.

Foundations§03

Encapsulation: envelopes inside envelopes

In one sentenceGoing down the stack, each layer adds a header in front of what it got from above. Going up on the other side, each layer removes its own header and passes the rest up. Routers in the middle only unwrap as far as the IP header.

Your code only touches the top layer

Here's an HTTP request sent by hand, with no HTTP library (new to Go? see the Go primer). HTTP really is just text written into a TCP connection:

raw.go
conn, err := net.Dial("tcp", "example.com:80") // open a TCP connection
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

// The HTTP request is just text. We write it ourselves:
fmt.Fprint(conn, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")

io.Copy(os.Stdout, conn) // print whatever the server sends back

Notice what's not in that code: no IP addresses in headers, no packet sizes, no MAC addresses, no resending. You write bytes to a socket, and the operating system does every layer below. Now let's watch it do that.

Down, across, and up

Step through it. The colours match the layers from §02: purple application, cyan transport, amber internet, pink link.

Encapsulation, step by step
One small HTTP request going down the stack on your laptop, through a router, and up the stack on the server.
💻 laptop〰 the wire📶 router (×12)🖥 server
on the wire / in memorydata · 74 bytes
HTTP request
74 B
HTTP request
GET / HTTP/1.1
Host: example.com
…
Your program (here, curl http://example.com/) writes 74 bytes of HTTP text to a socket (the OS's handle for a connection). That's all your code ever does. Everything below happens inside the operating system.
1 / 12

The names change as the data goes down: the transport layer makes a segment, the internet layer makes a packet, the link layer makes a frame. People mix these words up all the time ("packet" is often used for all of them), but if someone says "frame" they mean the link layer.

The numbers

HeaderSizeThe fields that matter for this topic
Ethernet14 B header + 4 B trailerdestination MAC, source MAC, "what's inside" (IPv4 or IPv6)
IPv420 B (IPv6: 40 B)source IP, destination IP, TTL (a hop counter, §05), "what's inside" (TCP or UDP)
TCP20 B (usually 32 B with common options)source port, destination port, sequence number (which byte of the stream this is), flags (open / close / "got it" markers)
UDP8 Bsource port, destination port, length, checksum

Two worked examples:

  • A tiny request, the 74 bytes of HTTP that curl http://example.com/ sends: 74 + 20 + 20 + 14 + 4 = 132 bytes on the wire, counting a minimal 20-byte TCP header (with the usual 12 bytes of options it's 144, as in the real capture in §09). 58 of those are headers. That's 44% overhead.
  • A full-size packet in a big download: Ethernet carries at most 1500 bytes of IP packet (that limit is the MTU, more in §08). After 20 B of IP and 20 B of TCP, 1460 B are left for your data. The frame is 1518 B, so headers are only about 4%.

That's why many small messages cost far more than one big one, and why protocols try to batch.

Foundations§04

IP addresses, subnets and /24

In one sentenceAn IP address has two parts: the first bits say which network, the rest say which machine on it. The /24 in 192.168.1.0/24 tells you where the split is.

An IPv4 address is just a 32-bit number

192.168.1.37 looks like four numbers, but it's really one 32-bit number (32 binary digits, each 0 or 1) written in four pieces of 8 bits (one byte each). Each piece goes from 0 to 255. That's 2³² ≈ 4.3 billion possible addresses, which turned out to be far too few for every phone and laptop on Earth. More on that in a moment.

What /24 means

A subnet is a block of addresses that share their network part. We write it in CIDR notation: an address, a slash, and how many leading bits are the network part.

Worked example, 192.168.1.0/24:

  • 24 bits are fixed: the first three numbers, 192.168.1.
  • 8 bits are free: the last number, from 0 to 255. That's 2⁸ = 256 addresses.
  • Two are reserved: .0 names the network itself, and .255 is the broadcast address ("everyone on this subnet").
  • So 254 machines can get an address: 192.168.1.1 to 192.168.1.254.

Every bit you take away from the prefix doubles the block: /23 = 512 addresses, /16 = 65,536, /8 = about 16.7 million. Every bit you add halves it: /30 = 4 addresses (2 usable), and /32 is exactly one address.

You'll also see the same split written as a subnet mask: 255.255.255.0 is the same as /24 (24 one-bits followed by 8 zero-bits). (255 in binary is 11111111: eight one-bits.) Older tools and Windows settings show masks; cloud consoles and Kubernetes use CIDR.

When the cut falls inside a byte

/8, /16 and /24 are easy because the split lands between the dotted numbers. Other prefixes split a number in the middle, so you have to look at its bits. Worked example, 10.0.5.0/26:

  • 26 = 24 + 2: the first three numbers are fixed, plus the first 2 bits of the last number.
  • That leaves 6 host bits: 2⁶ = 64 addresses, from .0 (bits 00|000000) to .63 (bits 00|111111).
  • Network address 10.0.5.0, broadcast 10.0.5.63, so 62 usable hosts.
  • The next address, .64, is 01|000000. A network bit changed, so it belongs to the next block, 10.0.5.64/26.
Inside 10.0.5.0/26
24 + 2 = 26 network bits: the cut falls inside the last byte. Drag (or play) the host part and watch the decimal number follow.
00001010.00000000.00000101.00100101= 10.0.5.37
first 26 bits: network (fixed)last 6 bits: host → 2⁶ = 64 addresses (.0 – .63)
10.0.5.37 — inside 10.0.5.0/26: a normal host address

For any address and prefix, drag the slider and watch the purple network bits grow:

Subnet calculator
Type an IPv4 address and drag the prefix. Purple bits are the network part (fixed); grey bits are free for hosts.
/24
the same address in binary (32 bits)24 network bits · 8 host bits
11000000= 19210101000= 16800000001= 100100101= 37
network address
192.168.1.0/24
subnet mask
255.255.255.0
addresses in block
256
usable host addresses
254 (minus network + broadcast)
first – last host
192.168.1.1 – 192.168.1.254
broadcast
192.168.1.255
● 192.168.1.37 is private (RFC 1918) — most home networks.
Isin 192.168.1.0/24?✕ no — a different subnet, so packets go via the default gateway

Why the split matters: direct, or through a router?

Every machine knows its own address and prefix. Before sending a packet, it asks one question: is the destination in my subnet?

  • Yes → it can reach that machine directly on the local network (same Wi-Fi, same switch).
  • No → it sends the packet to its router, which forwards it onward. That's §05.

This is why a wrong prefix causes weird bugs. If a machine thinks it's in a /16 but is really in a /24, it will try to reach "neighbours" directly that are actually behind a router, and those packets go nowhere.

Addresses with special meanings

Some ranges are reserved. You'll meet these constantly:

RangeMeaning
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16Private: for home, office and cloud networks. Never routed on the internet; NAT translates them (§06).
127.0.0.0/8 (usually 127.0.0.1)Loopback: "this machine". Packets never leave it. localhost is its name.
0.0.0.0As a listen address: "every interface on this machine" (§06). Not a destination you connect to.
169.254.0.0/16Link-local: a machine picks one itself when nobody hands it an address. Clouds put their metadata service (where a cloud VM asks "who am I, and what are my credentials?") at 169.254.169.254.
100.64.0.0/10Carrier-grade NAT: your ISP (internet provider) shares one public address among many customers.
192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24Reserved for documentation. This topic uses them for every "public" address, so no example points at a real machine.

IPv6, briefly

IPv6 fixes the shortage with 128-bit addresses, written as eight groups of four hex digits: 2001:0db8:0000:0000:0000:0000:0000:0001. Leading zeros can be dropped, and one run of all-zero groups can be written as ::, so that's 2001:db8::1.

The ideas carry over unchanged: CIDR prefixes (a normal network is a /64), routers, subnets. A few equivalents to remember: ::1 is loopback, fe80::/10 is link-local, and :: as a listen address means "every interface". With so many addresses, IPv6 doesn't need NAT, but it still needs a firewall.

Checking addresses in code

cidr.go
import (
    "fmt"
    "net/netip"
)

// inside a function:
subnet := netip.MustParsePrefix("192.168.1.0/24")
ip := netip.MustParseAddr("192.168.1.37")

fmt.Println(subnet.Contains(ip))                                  // true
fmt.Println(subnet.Contains(netip.MustParseAddr("192.168.2.10"))) // false
fmt.Println(ip.IsPrivate(), ip.IsLoopback())                      // true false
Practitioner§05

Routing: one hop at a time

In one sentenceNo router knows the whole path to the server. Each one only knows "for addresses like this, the next step is that way", and the packet gets there one hop at a time.

Your laptop's routing table

Every machine has a routing table, even your laptop. On Linux, ip route shows it (macOS: netstat -rn). A typical one at home:

$ ip route
default via 192.168.1.1 dev wlan0
192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.37

Read it line by line:

  • 192.168.1.0/24 dev wlan0: "Addresses in my own subnet: send them directly, out of the Wi-Fi interface wlan0." This is the "same subnet → direct" rule from §04.
  • default via 192.168.1.1: "Everything else: hand it to 192.168.1.1." That router is your default gateway, usually your Wi-Fi box. default is short for 0.0.0.0/0, a prefix with zero fixed bits, which matches every address.

Your laptop got both lines automatically when it joined the Wi-Fi. A service on the router called DHCP handed it an address, a prefix and the gateway.

The rule: most specific match wins

A router on the internet has many more rows. When several rows match, it picks the one with the longest prefix, the most specific one. Worked example: a packet for 203.0.113.10 arrives at a router with this table:

PrefixNext hopMatches 203.0.113.10?
0.0.0.0/0 (default)upstream provider✓ (matches everything)
203.0.0.0/8router A✓ (first 8 bits match)
203.0.113.0/24router B✓ (first 24 bits match), longest, so it wins
198.51.100.0/24router C✕

The packet goes to router B. This is called longest-prefix match, and it's how roughly a million table rows (the approximate size of the full IPv4 internet table today) can cover billions of addresses: broad rows for "roughly that way", specific rows where it matters.

Who writes the tables of the internet's big routers? Networks (ISPs, clouds, universities) announce to their neighbours "I can reach 203.0.113.0/24" using a protocol called BGP, and routers build their tables from those announcements. When someone announces the wrong thing by mistake, large parts of the internet can become unreachable for hours. That really happens.

TTL: the packet's self-destruct counter

What if two routers' tables point at each other by mistake? A packet would bounce between them forever. To prevent that, every IP packet carries a TTL counter. The sender sets it (Linux and macOS use 64, Windows 128), and every router subtracts 1. When it reaches 0, the router drops the packet and sends a small error message back to the sender using ICMP, the internet layer's "error and status" protocol.

That error message is what makes the most useful network tool possible. Watch both:

Hop by hop
Follow one packet from your laptop to a server across the ocean — or run traceroute to discover the path.
TTL on arrival 64 · 0 ms round trip from your laptop
💻
your laptop
0
⬡
home router
1
⬡
ISP edge
2
⬡
ISP core
3
⬡
exchange
4
⬡
backbone NYC
5
⬡
cloud edge
6
🖥
web server
7
your laptop 192.168.1.37
203.0.113.10 isn't in my subnet 192.168.1.0/24 → use the default route 0.0.0.0/0 → send to my gateway 192.168.1.1.

traceroute: seeing the path yourself

traceroute exploits TTL on purpose: it sends a probe with TTL 1 (the first router drops it and replies), then TTL 2, then 3, and so on, until the destination itself answers. Each reply reveals one router and the round-trip time (RTT) to it.

traceroute example.com     # Linux, macOS
tracert example.com        # Windows
mtr example.com            # Linux/macOS: a live, continuously updating traceroute

How to read the output:

  • Each line is cumulative. "88 ms" at hop 5 means the round trip from you to hop 5, not the time between hops 4 and 5.
  • A big jump between two hops usually means distance. Light in fibre covers about 200 km per millisecond, so crossing an ocean adds tens of milliseconds and no software can fix that.
  • * * * in the middle is usually harmless: that router just doesn't reply to probes. Only worry if every line after a point is stars.
  • One slow hop with normal hops after it isn't a problem either. Routers answer probes with low priority, but they forward real traffic at full speed.
Practitioner§06

NAT, localhost and 0.0.0.0

In one sentenceYour home router hides every device behind one public address by rewriting packets as they pass (NAT). Separately, every server chooses which of its machine's addresses to listen on, and choosing 127.0.0.1 means "only this machine may talk to me".

NAT: sharing one public address

§04 said private addresses like 192.168.1.37 are never routed on the internet. Yet your laptop browses the web fine. The trick is NAT: your home router keeps a table and rewrites addresses and ports on every packet that crosses it.

NAT: one public address for the whole house
Watch the home router rewrite addresses and ports in both directions.
home network (private)
laptop 192.168.1.37:51000 → 203.0.113.10:443
router NAT tableinside ⇄ public port
empty
internet (public)
—
Your laptop sends a packet from its private address 192.168.1.37, source port 51000. Private addresses can't travel on the internet — no router out there knows where “192.168.1.37” is, because millions of homes use it.
1 / 6

Three consequences you'll run into:

  • Nobody can connect in by default. NAT only has table rows for conversations started from inside. To run a server at home you add a manual "port forwarding" row, or use a tunnel service (e.g. ngrok or Cloudflare Tunnel, which relay traffic in through a connection your machine opened outward).
  • Idle connections get forgotten. NAT rows expire after a period of silence, often minutes, sometimes less. A database connection that sits idle behind NAT can be silently dead when you next use it. TCP keepalive probes and application heartbeat messages exist partly to keep those rows alive.
  • You might be behind two NATs. Many ISPs, especially mobile ones, put customers behind carrier-grade NAT, so even your router's "public" address is shared.

One machine, several addresses

A machine doesn't have an IP address. Each interface has its own. Your laptop has at least two:

  • lo (loopback), address 127.0.0.1: a fake interface that only leads back to the same machine.
  • wlan0 or en0 (Wi-Fi), address 192.168.1.37: the one other devices can reach.

When a server starts, it binds its listening socket to an address and a port. The address decides which interfaces it hears:

Listen onWho can connect
127.0.0.1:3000 (or localhost:3000)Only programs on this same machine
192.168.1.37:3000Anyone who can reach that one interface
0.0.0.0:3000Any IPv4 address on any interface ("all of them")
[::]:3000Any IPv6 address; on Linux, usually IPv4 too
listen.go
// Pick ONE of these two lines.
// Option A: only this machine can connect.
ln, err := net.Listen("tcp", "127.0.0.1:3000")
// Option B: every interface (no host part = all addresses).
// ln, err := net.Listen("tcp", ":3000")

if err != nil {
    log.Fatal(err)
}
defer ln.Close()

// net/http follows the same rule:
//   http.ListenAndServe(":3000", handler)          → all interfaces
//   http.ListenAndServe("127.0.0.1:3000", handler) → this machine only

"Works on my laptop, not in Docker"

A container gets its own network stack, using a Linux kernel feature called namespaces. Inside, it has its own lo with its own 127.0.0.1, plus its own eth0 with an address like 172.17.0.2. When you run docker run -p 8080:3000, Docker's port publishing forwards host port 8080 to the container's eth0, port 3000.

If your server listens on 127.0.0.1 inside the container, it's listening on the container's private loopback, which nothing from outside can ever reach. Toggle it:

Who can reach my server?
Pick where the server runs and which address it binds to. Each interface has its own address; a server only hears traffic for the addresses it bound.
inside the container's own network namespaceserver bound to 127.0.0.1
lo 127.0.0.1
👂 server listening here
eth0 172.17.0.2
nobody listening on :3000 here
✓curl inside the container→ localhost:3000arrives on lo, where the server is listening
✕curl on the host (docker run -p 8080:3000)→ localhost:8080 → forwarded to 172.17.0.2:3000arrives on the container's eth0, not its lo → nothing answers (reset, empty reply or refused, depending on the path)
✕a colleague's machine→ 192.168.1.37:8080 → forwarded to 172.17.0.2:3000arrives on the container's eth0, not its lo → nothing answers (reset, empty reply or refused, depending on the path)

The fix is one word: listen on 0.0.0.0 (or leave the host out). Many dev tools default to localhost for safety. Vite, for example, needs --host to listen on all interfaces. So this bug shows up the first time you containerize something.

Practitioner§07

Ports, sockets and the 4-tuple

In one sentenceAn IP address gets a packet to the right machine; a port gets it to the right program. A connection is identified by four values together (source IP, source port, destination IP, destination port), and that's how one server port can serve thousands of clients at once.

Ports: apartment numbers

A port is a 16-bit number, 0 to 65535, in the TCP or UDP header. If the IP address is the building, the port is the apartment. Some numbers are conventions everyone knows:

PortUsually
22SSH
53DNS
80 / 443HTTP / HTTPS
5432 / 3306 / 6379Postgres / MySQL / Redis

On Linux, ports below 1024 need extra privileges to listen on, which is why dev servers like to use 3000, 5173 or 8080.

Your side has a port too

When you connect to 203.0.113.10:443, your packets also need a source port, so the replies can find their way back to your program. You don't choose it. The OS picks a free ephemeral port from a range (on Linux, 32768–60999 by default; check with cat /proc/sys/net/ipv4/ip_local_port_range).

So every connection has four addresses, the 4-tuple:

  src IP          src port     dst IP          dst port
  192.168.1.37    51234   →    203.0.113.10    443

Two connections can share three of the four values, but never all four. Open two browser tabs to the same site and the only difference is the source port. (Strictly, the protocol, TCP or UDP, is a fifth value, so you'll also hear "5-tuple".)

One listening socket, many connected sockets

A server has two kinds of sockets:

  • The listening socket, bound to port 443. It never carries any request data. Its only job is to accept new clients.
  • One connected socket per client, created each time a client is accepted. Each is a separate file descriptor with its own 4-tuple.

When a packet arrives, the kernel looks up its 4-tuple and hands it to the matching socket. Try it:

One port, thousands of connections
The server listens on port 443 only. Connect a few clients — then click a row to send a packet and watch the kernel find the right socket.
socketsrc IPsrc portdst IPdst port
fd 3 · LISTEN**203.0.113.10443
Only the listening socket exists. It never carries data; it just waits for new clients. Each accepted client gets a new socket.

So what limits a server to N clients? Not port numbers: the server uses one port for all of them. The real limits are the number of open file descriptors the OS allows (see ulimit) and memory, which on a modern Linux box allows hundreds of thousands of connections.

A complete echo server and client

Here's the whole thing in a few lines. The server repeats back every line it receives; the client sends one line and prints the answer. You can mix them: the Go client works with the Node server and vice versa.

server.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
)

func main() {
    ln, err := net.Listen("tcp", "127.0.0.1:9000") // ← bind + listen
    if err != nil {
        log.Fatal(err)
    }
    for {
        conn, err := ln.Accept() // ← waits for a client; returns a NEW socket
        if err != nil {
            log.Println(err)
            continue
        }
        go handle(conn) // one goroutine per connection
    }
}

func handle(conn net.Conn) {
    defer conn.Close()
    log.Printf("new connection: %s → %s", conn.RemoteAddr(), conn.LocalAddr())
    sc := bufio.NewScanner(conn)
    for sc.Scan() { // one line at a time
        fmt.Fprintf(conn, "echo: %s\n", sc.Text())
    }
}
client.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
)

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:9000") // ← OS picks our source port
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    fmt.Println("me:", conn.LocalAddr(), "server:", conn.RemoteAddr())

    fmt.Fprintf(conn, "hello\n")
    reply, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(reply)
}

Run a client twice (here the Go one, then the Node one) and the output shows the ephemeral port changing:

$ go run client.go
me: 127.0.0.1:44884 server: 127.0.0.1:9000
echo: hello
$ node client.mjs
me: 127.0.0.1:44888 server: 127.0.0.1:9000
echo: hello
One stream, many ways to slice it
The sender wrote "hello\n" and then "world\n". TCP only promises the same bytes in the same order — not the same chunks.
what arrives: one data event per box1 event
"hello\nworld\n"
✕ one data event = one message
message 1: "hello\nworld\n"
Looks fine — but only because nothing was split.
✓ buffer, then split on \n
message 1: "hello"
message 2: "world"
Always the same two messages, however the bytes were cut.
On localhost small writes usually arrive whole. Switch to “real network”.

The errors you'll see

What happenedError you'll see (Go, then Node)
Nobody listening on that address and portdial tcp 127.0.0.1:9000: connect: connection refused
connect ECONNREFUSED 127.0.0.1:9000
Another program already listens therelisten tcp 127.0.0.1:9000: bind: address already in use
listen EADDRINUSE: address already in use 127.0.0.1:9000
No free source port left (port exhaustion, §11)connect: cannot assign requested address
connect EADDRNOTAVAIL …
No answer at all (firewall silently drops, wrong IP)dial tcp …: i/o timeout (when you set a dial timeout)
connect ETIMEDOUT … (after a long OS default)

"Connection refused" is actually good news when debugging: the machine is reachable and answered "nobody's home on that port". A timeout means packets vanished somewhere: a firewall, a wrong route, or a machine that's down.

Practitioner§09

Seeing it: ip, ss and tcpdump

In one sentenceEverything in this topic is visible on your own machine with four commands. When the network misbehaves, look before you guess.

The commands below are Linux (they're also what you'll have inside most containers and servers). macOS equivalents are listed at the end of the section.

ip addr: my interfaces and addresses

$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
    inet6 ::1/128 scope host
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 3c:22:fb:1a:7e:09 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.37/24 brd 192.168.1.255 scope global dynamic wlan0
    inet6 fe80::3e22:fbff:fe1a:7e09/64 scope link

Everything from earlier sections is here: the loopback interface lo with 127.0.0.1/8 and ::1 (§04), the Wi-Fi interface with its MAC (link/ether, §08), its IPv4 address and prefix 192.168.1.37/24 with the broadcast address, an IPv6 link-local address, and each interface's MTU.

ip route: where packets go

You met this in §05. default via … is the gateway; the other lines are directly connected subnets. To ask "which route would a packet to X use?", there's a handy shortcut:

$ ip route get 203.0.113.10
203.0.113.10 via 192.168.1.1 dev wlan0 src 192.168.1.37

ss: sockets

ss lists sockets. Recv-Q/Send-Q are bytes waiting in the kernel; on LISTEN rows they show how many connections have finished the handshake but haven't been accepted by your program yet, and the maximum allowed (the accept backlog). The flags you'll use most: -t TCP, -u UDP, -l listening only, -n show numbers instead of names, -p show the owning process (needs root for other users' processes).

$ ss -tlnp
State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port  Process
LISTEN  0       4096         127.0.0.1:5432        0.0.0.0:*      users:(("postgres",pid=812,fd=6))
LISTEN  0       4096           0.0.0.0:22          0.0.0.0:*      users:(("sshd",pid=640,fd=3))
LISTEN  0       511          127.0.0.1:5173        0.0.0.0:*      users:(("node",pid=4120,fd=20))
LISTEN  0       4096                 *:3000              *:*      users:(("server",pid=5301,fd=3))

This answers "is my server listening, and on which address?" in one line, which is the §06 bug at a glance. Postgres and the Vite dev server (port 5173) are local-only; sshd listens on every IPv4 interface; the Go server on 3000 shows *, every address, IPv4 and IPv6. Drop the -l (ss -tnp) to see established connections with their full 4-tuples (§07).

tcpdump: the packets themselves

tcpdump prints every packet that crosses an interface. Here's a capture of curl http://example.com/ (plain HTTP so we can read it; with HTTPS the payload would be encrypted). -nn means "don't turn addresses and ports into names". The output is trimmed: long options […] fields are shortened, and your numbers will differ.

$ sudo tcpdump -i wlan0 -nn 'host 203.0.113.10 and port 80'
10:32:48.815086 IP 192.168.1.37.51234 > 203.0.113.10.80: Flags [S], seq 4135661311, win 64240, options [mss 1460,…], length 0
10:32:48.906211 IP 203.0.113.10.80 > 192.168.1.37.51234: Flags [S.], seq 3095193394, ack 4135661312, win 65160, options [mss 1460,…], length 0
10:32:48.906290 IP 192.168.1.37.51234 > 203.0.113.10.80: Flags [.], ack 1, win 502, length 0
10:32:48.906402 IP 192.168.1.37.51234 > 203.0.113.10.80: Flags [P.], seq 1:75, ack 1, win 502, length 74: HTTP: GET / HTTP/1.1
10:32:48.997815 IP 203.0.113.10.80 > 192.168.1.37.51234: Flags [.], ack 75, win 509, length 0
10:32:48.998127 IP 203.0.113.10.80 > 192.168.1.37.51234: Flags [P.], seq 1:1257, ack 75, win 509, length 1256: HTTP: HTTP/1.1 200 OK
10:32:48.998190 IP 192.168.1.37.51234 > 203.0.113.10.80: Flags [.], ack 1257, win 501, length 0
10:32:48.998542 IP 192.168.1.37.51234 > 203.0.113.10.80: Flags [F.], seq 75, ack 1257, win 501, length 0
10:32:49.089617 IP 203.0.113.10.80 > 192.168.1.37.51234: Flags [F.], seq 1257, ack 76, win 509, length 0
10:32:49.089680 IP 192.168.1.37.51234 > 203.0.113.10.80: Flags [.], ack 1258, win 501, length 0

How to read one line: time, IP, source IP.port > destination IP.port, then the TCP details. The Flags are TCP's: S = SYN (open), . = ACK (got it), P = push (here's data), F = FIN (I'm done). What the ten packets show:

  1. Lines 1–3: TCP's opening handshake (SYN, SYN-ACK, ACK). Both sides announce mss 1460: a 1500-byte MTU minus 40 bytes of headers (§08). Notice the ~91 ms gap between line 1 and line 2: that's one round trip to the server.
  2. Line 4: your HTTP request, 74 bytes. Port 51234 is curl's ephemeral port (§07).
  3. Lines 5–7: the response, 1256 bytes, and the acknowledgements in both directions.
  4. Lines 8–10: closing, a FIN from each side.

After the handshake, tcpdump shows sequence numbers relative to the start of the connection, which is why seq 4135661311 becomes seq 1:75. What the handshake, seq, ack and win numbers mean is the TCP & UDP topic's job. What matters here: you can see the 4-tuple, the ports, the round-trip time and the data, all in plain text.

Every layer in one packet

Add -e (show the link-layer header) and -v (verbose IP details) and look at just the request packet. Every layer from §03 is visible:

$ sudo tcpdump -i wlan0 -nn -e -v 'host 203.0.113.10 and port 80'
10:32:48.906402 3c:22:fb:1a:7e:09 > a4:91:b1:0c:22:5e, ethertype IPv4 (0x0800), length 140: (tos 0x0, ttl 64, id 23702, offset 0, flags [DF], proto TCP (6), length 126)
    192.168.1.37.51234 > 203.0.113.10.80: Flags [P.], cksum 0x1c3a (incorrect -> 0xc4fa), seq 1:75, ack 1, win 502, options […], length 74: HTTP, length: 74
	GET / HTTP/1.1
	Host: example.com
	User-Agent: curl/8.5.0
	Accept: */*
You seeLayerMeaning
3c:22:… > a4:91:…LinkFrom the laptop's MAC to the router's MAC (not the server's: §08)
length 140 (first one)LinkWhole frame: 14 B Ethernet header + 126 B IP packet (captures don't include the 4-byte FCS trailer)
ttl 64InternetFresh from the sender; each router will subtract 1 (§05)
flags [DF]InternetDon't Fragment is set, so path MTU discovery is on (§08)
proto TCP (6), length 126InternetWhat's inside, and the IP packet's size: 20 B IP + 32 B TCP + 74 B data
192.168.1.37.51234 > 203.0.113.10.80Internet + TransportThe 4-tuple
length 74 + the textApplicationYour HTTP request, byte for byte

The TCP header here is 32 bytes, not 20: 12 bytes of options (timestamps) ride along on every packet.

macOS and Windows equivalents

LinuxmacOSWindows
ip addrifconfigipconfig /all
ip routenetstat -rnroute print
ss -tlnplsof -iTCP -sTCP:LISTEN -n -Pnetstat -ano
ip neigharp -aarp -a
tcpdumptcpdump (built in)Wireshark / pktmon
Staff-level§10

Where the layers leak (staff-level)

In one sentenceThe layer model says each layer can ignore the others. In production that's a leaky abstraction: link-layer quirks show up as TCP slowness, network boxes read headers they shouldn't, and "the network" ends up explaining application bugs.

1. MTU: tunnels, overlays and Kubernetes

Every tunnel wraps packets in more headers, which shrinks the room left for your data. VPNs do it, and so do cloud and Kubernetes overlay networks. VXLAN, a common overlay, adds 50 bytes, so pods get an MTU of 1450 on a 1500-byte network. If any part of the system still assumes 1500, and ICMP "too big" is filtered somewhere, you get the §08 black hole. It tends to show up under rare conditions: only in one cluster, only for large responses, only through one VPN.

What staff engineers do about it:

  • Know the MTU of every hop you own: ip link on nodes, the CNI (Kubernetes network plugin) config, the cloud's documented values (some clouds support 9000-byte jumbo frames inside a VPC but 1500 or less across VPNs and peering).
  • Clamp the MSS at tunnel edges, so TCP never builds segments that won't fit, even if ICMP is lost.
  • Allow ICMP "fragmentation needed" / "packet too big" in security groups and firewalls. Blocking it buys no real security and breaks PMTUD.
  • Linux can also probe for the right size without relying on ICMP (the net.ipv4.tcp_mtu_probing setting), a useful safety net on hosts behind networks you don't control.

2. TCP over lossy Wi-Fi and mobile links

TCP was designed when lost packets almost always meant one thing: a router somewhere was overloaded. So when TCP sees loss, it slows down. On Wi-Fi and mobile links, packets are also lost to radio interference, which slowing down doesn't fix. The link layer partly hides this by retrying frames itself, but every retry adds delay and jitter (varying delay). The result is a familiar mix: low throughput even though the "bandwidth" is high, and latency spikes that look like server slowness. Link-layer behaviour leaks straight into transport-layer performance. How TCP's congestion control decides how fast to send is covered in the TCP & UDP topic.

3. Middleboxes: why protocols "ossify"

In theory only the two endpoints read the TCP header. In practice, middleboxes (firewalls, NAT, corporate proxies, carrier equipment) read it, rewrite it, and sometimes drop packets with options they don't recognise. So adding new features to TCP became nearly impossible, because some box somewhere would break. Engineers call this ossification.

This is a big part of why QUIC (the transport under HTTP/3) runs over UDP and encrypts almost all of its own headers: if middleboxes can't read the headers, they can't depend on them, and the protocol can keep evolving. See the HTTP topic. NAT idle timeouts (§06) are another middlebox leak: long-lived idle connections die silently unless something sends keepalive probes or heartbeat messages.

4. Anycast: one address, many places

Normally one IP address means one machine. With anycast, many sites announce the same prefix through BGP, and each router sends packets to the nearest one. That's how 1.1.1.1 and 8.8.8.8 answer from your own region, how a CDN sends you to a nearby edge, and how DNS root servers survive attacks.

The leak: "nearest" means nearest by routing, which isn't always nearest geographically. Also, a routing change in the middle of a connection can move your packets to a different site that has never heard of your TCP connection. In practice this is rare, and anycast works best for short connections (DNS) or when the sites share connection state.

5. IPv6, dual stack and Happy Eyeballs

On a dual-stack network, example.com may resolve to both an IPv6 and an IPv4 address. If the client tries IPv6 first and IPv6 is broken somewhere on the path, a naive client waits for a full connect timeout, often many seconds, before falling back. Users experience this as "the internet is slow".

The standard fix is Happy Eyeballs: start IPv6, and if it hasn't connected within a short delay, start IPv4 in parallel and keep whichever connects first.

  • Go does this by default in net.Dialer. Its FallbackDelay defaults to 300 ms.
  • Node does it by default since v20 (autoSelectFamily: true), trying each address for 250 ms (net.getDefaultAutoSelectFamilyAttemptTimeout()).

If you write your own dialing logic (custom DNS, connection pools that pin to one address), make sure you didn't throw this behaviour away.

6. Containers and Kubernetes, in one picture

§06 said a container has its own network stack. Here's what Docker builds to make that work:

What docker run actually builds
host (one Linux machine)eth0192.168.1.37NAT + -p forwardingdocker0 bridge172.17.0.1container 1 (own namespace)lo 127.0.0.1eth0 .0.2container 2 (own namespace)lo 127.0.0.1eth0 .0.3veth pairs
Each container has its own network namespace with its own lo and eth0. A veth pair (a virtual cable) connects it to the docker0 bridge (a virtual switch). Outgoing traffic is NAT-ed to the host's address; -p 8080:3000 forwards host port 8080 to the container's eth0 — never to its lo.

The pieces: each container is a network namespace with its own lo and eth0. A veth pair acts as a virtual cable from the container to a bridge on the host. Outgoing traffic is NAT-ed to the host's address, and published ports are forwarded in. Every concept from this topic appears again, just implemented in software inside one kernel.

Kubernetes changes one rule: every pod gets its own routable IP, and any pod can reach any other pod without NAT. The network plugin makes that true across machines, either with plain routes or with an overlay (hence the MTU issue in point 1). Kubernetes Service IPs are stranger: no interface owns them. They're virtual addresses that the kernel rewrites into a real pod's IP as connections are made (kube-proxy programs those rules). That's why, in kube-proxy's default iptables mode, pinging a Service IP usually fails while curl to it works: ICMP isn't covered by the rewrite rules.

7. Where latency hides, layer by layer

When a request is slow, the time went somewhere in the stack. A rough map (all numbers approximate):

LayerWhere time goesTypical size
LinkWi-Fi retries, radio scheduling on mobile, oversized buffers in home routers1–50 ms, very jittery
InternetDistance: light in fibre needs ~1 ms of round trip per 100 km. Plus queues in congested routers.1 ms in a data center, 70–150 ms across an ocean
TransportThe TCP handshake costs 1 round trip; a lost packet must be resent1 RTT for the handshake; a loss costs ~1 RTT if TCP notices quickly, ≥ 200 ms if it has to wait for a timeout
ApplicationDNS lookup, TLS handshake (1 more round trip with TLS 1.3), then the server's own work0–100 ms each

The lesson: with a 90 ms round trip, a new HTTPS connection spends 2 round trips to the server (TCP handshake + TLS 1.3 handshake), about 180 ms, plus a DNS lookup if the name isn't cached, before the first byte of your request is even sent. That's why connection reuse (keep-alive), CDNs close to users and fewer round trips matter more than raw bandwidth. Measure it instead of guessing:

curl -o /dev/null -s -w 'dns %{time_namelookup}  tcp %{time_connect}  tls %{time_appconnect}  first byte %{time_starttransfer}  total %{time_total}\n' https://example.com/
timing.go
start := time.Now()
at := func(what string) { fmt.Printf("%-14s %v\n", what, time.Since(start).Round(time.Millisecond)) }

trace := &httptrace.ClientTrace{ // hooks the HTTP client calls at each stage
    DNSDone:              func(httptrace.DNSDoneInfo) { at("dns done") },
    ConnectDone:          func(network, addr string, err error) { at("tcp connected") },
    TLSHandshakeDone:     func(tls.ConnectionState, error) { at("tls done") },
    GotFirstResponseByte: func() { at("first byte") },
}
req, err := http.NewRequest("GET", "https://example.com/", nil)
if err != nil {
    log.Fatal(err)
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
res, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer res.Body.Close()
io.Copy(io.Discard, res.Body) // read to the end so the connection can be reused
at("done")
Practitioner§11

Pitfalls gallery

#1

Listening on 127.0.0.1 inside a container

The server works when you run it directly and fails behind docker run -p. Inside a container, 127.0.0.1 is the container's own private loopback, and published ports arrive on its eth0. Fix: listen on 0.0.0.0 (or omit the host) inside containers. On your laptop, the opposite habit is safer: keep dev databases on 127.0.0.1. (§06)

#2

Assuming that sent means delivered

IP is best effort, and UDP adds no guarantees on top. Even with TCP, a successful conn.Write / socket.write only means the bytes reached your own kernel's buffer. They may still be in your machine when the server crashes. Fix: if you need to know something was processed, the other side has to say so in your protocol (an HTTP response, an ack message), and you need timeouts and retries for when it doesn't. See worker pools for retry budgets.

#3

Treating TCP reads as messages

One write of 100 bytes can arrive as two reads of 60 and 40, and two writes can arrive as one read. Code that assumes "one data event = one message" works on localhost and breaks over a real network, where packets get split differently. Fix: frame your messages with newlines, a length prefix, or a real protocol (HTTP, gRPC). (§07)

#4

Running out of ephemeral ports

Each outgoing connection to the same server IP and port needs its own source port, and Linux has about 28,000 of them (32768–60999). After a connection closes, its 4-tuple stays reserved in TIME_WAIT for 60 seconds. So a client that opens a new connection per request to one backend can sustain only about 28,000 ÷ 60 ≈ 470 new connections per second. Beyond that, connects fail with cannot assign requested address / EADDRNOTAVAIL.

Fix: reuse connections. In Go, share one http.Client and always read the response body to the end and close it, otherwise the connection can't go back to the pool. In Node 19+, http/https reuse connections by default (keep-alive agent). For databases, use a connection pool.

#5

Blocking all ICMP

It feels secure. It breaks path MTU discovery, so large transfers hang over VPNs and tunnels while small requests work: the black hole from §08. It also makes ping and traceroute useless for debugging. Fix: allow at least ICMP "fragmentation needed" (IPv4 type 3, code 4) and ICMPv6 "packet too big"; rate-limit the rest if you must.

#6

"localhost" isn't always 127.0.0.1

localhost can resolve to ::1 (IPv6) or 127.0.0.1. If your server listens only on 127.0.0.1 and the client picks ::1, you get connect ECONNREFUSED ::1:3000. This was a common surprise when Node 17 began returning addresses in the order the OS gives them, often IPv6 first. Node 20+ usually hides it by trying both families (§10). Fix: be explicit on both sides (127.0.0.1 everywhere), or have the server listen on both.

#7

Gluing host and port with string concatenation

ip + ":" + port produces ::1:8080 for an IPv6 address, which is ambiguous: which colon starts the port? IPv6 addresses must be wrapped in brackets: [::1]:8080.

addr := net.JoinHostPort("::1", "8080") // "[::1]:8080"  ← adds brackets when needed
conn, err := net.Dial("tcp", addr)

In JavaScript, pass host and port as separate arguments (net.connect(port, host)) instead of building an address string. When you do need a URL, write IPv6 hosts in brackets: http://[::1]:8080/.

#8

Trusting the source IP

Behind a reverse proxy, a load balancer or NAT, the source address your server sees is the proxy's, not the user's. Proxies pass the original address along in the X-Forwarded-For header, but any client can send that header too. Fix: only trust X-Forwarded-For entries added by proxies you run, and never use a raw IP alone for authentication.

Practitioner§12

Check yourself

First a warm-up: which layer does each thing belong to? (§02, §03)

Which layer is this?
Sort each item into its TCP/IP layer. You get the explanation right away.
The line GET /users HTTP/1.1
Destination port 443
TTL dropping from 64 to 63
The MAC address 3c:22:fb:1a:7e:09
Resending a lost segment
“Who has 192.168.1.1?” (ARP)
A router choosing the next hop
A load balancer that routes by URL path
A load balancer that only looks at IP and port
A DNS query for example.com
Sorted 0 / 10

Now predict the outcome before you click. Q1 is §06, Q2 is §04, Q3 is §03 and §05, Q4 is §07, Q5 is §08, and Q6 is §05.

QUESTION 1 / 6
Works on my laptop, not in Docker
// server.mjs, inside a container started with: docker run -p 3000:3000 my-app
import http from "node:http";
http.createServer((req, res) => res.end("ok"))
    .listen(3000, "127.0.0.1");

// on the host:  curl http://localhost:3000
QUESTION 2 / 6
How many hosts fit?
You get the subnet 10.0.5.0/26 for a group of servers.
How many of them can get an address?
QUESTION 3 / 6
What does a router change?
A packet goes laptop → router → router → server (no NAT involved).
Which of these is different when it leaves the first router
compared to when it arrived?
QUESTION 4 / 6
Two tabs, one server
Your browser opens two connections to 203.0.113.10:443
from your laptop at 192.168.1.37. What differs between them?
QUESTION 5 / 6
Small pages load, big downloads hang
After the company added a VPN, logging in to an internal site works,
but downloading a large file stalls forever at 0 bytes.
The network team "blocks all ICMP for security".
QUESTION 6 / 6
Stars in the middle
$ traceroute 203.0.113.10
 1  192.168.1.1     3.1 ms
 2  198.51.100.1    9.0 ms
 3  * * *
 4  198.51.100.130 88.2 ms
 5  203.0.113.10   91.0 ms
Answered 0 / 6
Foundations§13

Cheat sheet

📦
Layers wrap layersApp data → TCP segment → IP packet → Ethernet frame. Each layer adds a header going down and removes it going up.
🔢
OSI numbers are vocabularyL2 = MAC/switches, L3 = IP/routing, L4 = TCP/UDP/ports, L7 = HTTP. TCP/IP's 4 layers are what runs.
🎲
IP is best effortPackets can be lost, duplicated, reordered or delayed, silently. Reliability is TCP's job (or yours, with UDP).
🪧
Routing is hop by hopSame subnet → direct. Otherwise → default gateway. Routers pick the longest matching prefix and decrement TTL.
🏷️
IPs stay, MACs changeIP addresses stay the same end to end (except through NAT). Each hop builds a new frame with new MACs.
🔌
A connection = 4-tuplesrc IP, src port, dst IP, dst port. One listening port serves thousands of clients; the client's ephemeral port tells them apart.
🐳
0.0.0.0 in containers127.0.0.1 = this machine only (and a container is its own machine). 0.0.0.0 = every interface.
📏
MTU 1500, MSS 1460Tunnels shrink it. Don't block ICMP "too big", or big transfers hang (PMTUD black hole).
NeedAnswer
Header sizesEthernet 14 + 4 · IPv4 20 (IPv6 40) · TCP 20–60 (often 32) · UDP 8
Private IPv4 ranges10.0.0.0/8 · 172.16.0.0/12 · 192.168.0.0/16 · (CGNAT 100.64.0.0/10)
Special addresses127.0.0.1 / ::1 loopback · 0.0.0.0 / :: "all interfaces" when listening · 169.254.x.x link-local
Size of a /n2^(32 − n) addresses; minus 2 usable for /30 and bigger blocks. /24 = 256 (254 hosts)
Ephemeral ports (Linux)32768–60999 (/proc/sys/net/ipv4/ip_local_port_range)
Default TTL64 (Linux, macOS) · 128 (Windows)
My addresses / routesip addr · ip route · ip route get <ip> (macOS: ifconfig, netstat -rn)
What's listening, on which address?ss -tlnp (macOS: lsof -iTCP -sTCP:LISTEN -n -P)
The path to a hosttraceroute / mtr (Windows: tracert)
See the packetssudo tcpdump -i <iface> -nn 'host <ip> and port <port>' → Wireshark for more
MTU testping -M do -s 1472 <host> (1472 + 28 = 1500)
Gonet.Listen("tcp", ":3000") · net.Dial("tcp", net.JoinHostPort(h, p)) · net/netip for CIDR
Nodenet.createServer(cb).listen(3000) · net.connect(port, host) · net.BlockList for CIDR
Finished reading?Mark it complete to track your progress on a roadmap. Saved in this browser only.
Up next in NetworkingTCP vs UDP
Read →