kitestacks-homelab/homelab-mastery/build-guide/without-ai/05-networking.md
kenpat 1e8319ee75 docs: comprehensive homelab-mastery rewrite with full build guides
Complete documentation suite for KiteStacks covering all 11 services across
2-host active-active architecture. Includes beginner track (with AI, 8 files)
and advanced track (without AI, 7 files) with time estimates, real troubleshooting
cases, and command-by-command explanations. Updates certifications roadmap to
reflect July 7 2026 A+ Core 2 exam goal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:08:43 -05:00

352 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Without AI — Part 5: Networking
**Track:** Advanced (No AI)
**Time for this section:** 12 weeks
Networking is the hardest part to learn and the most important. Every problem in this
homelab ultimately involves a packet trying to get somewhere. If you understand how
packets travel, you can debug anything.
---
## IP Addresses
Every device on a network has an IP address — a number that identifies it.
**IPv4:** Four octets (0255) separated by dots: `192.168.1.205`
**Classes of addresses:**
| Range | Who Owns It | Used For |
|-------|------------|---------|
| `10.0.0.0/8` | Private | Corporate networks, VPNs |
| `172.16.0.0/12` | Private | Docker bridge networks |
| `192.168.0.0/16` | Private | Home networks (your router) |
| `100.64.0.0/10` | Shared | Tailscale uses this range |
| Everything else | Public | Routable on the internet |
Private addresses are not routable on the internet. Your home router uses NAT
(Network Address Translation) to let private-addressed devices reach the internet.
---
## Subnetting and CIDR Notation
CIDR (Classless Inter-Domain Routing) notation describes a range of IP addresses:
```
192.168.1.0/24
└── prefix length: how many bits are fixed
```
An IPv4 address is 32 bits. A `/24` means the first 24 bits are fixed (the network),
leaving 8 bits for hosts. `2^8 = 256` addresses, minus network (`.0`) and broadcast (`.255`)
= 254 usable host addresses.
| CIDR | Addresses | Usable | Example |
|------|-----------|--------|---------|
| `/32` | 1 | 1 | A single IP |
| `/31` | 2 | 2 | Point-to-point link |
| `/30` | 4 | 2 | Small link |
| `/29` | 8 | 6 | Small subnet |
| `/28` | 16 | 14 | |
| `/27` | 32 | 30 | |
| `/26` | 64 | 62 | |
| `/25` | 128 | 126 | |
| `/24` | 256 | 254 | Typical home/office LAN |
| `/16` | 65,536 | 65,534 | Large network |
| `/12` | 1,048,576 | — | Docker range: 172.16.0.0/12 |
| `/8` | 16,777,216 | — | 10.x.x.x range |
**Subnetting practice:** Calculating the host range of `172.17.0.0/16`:
- Fixed: `172.17` (first 16 bits)
- Variable: last 16 bits
- Host range: `172.17.0.1` to `172.17.255.254`
- This covers all of `172.17.x.x`
**Why `/12` covers all Docker networks:**
`172.16.0.0/12` covers `172.16.0.0` through `172.31.255.255`.
Docker creates bridge networks in the `172.17.x.x`, `172.18.x.x`, etc. ranges.
All of those are inside `172.16.0.0/12` — so one ufw rule covers all Docker bridges.
---
## Ports
A port is a 16-bit number (065535) that identifies which service on a host should
handle a connection.
```
IP address = the building
Port = the apartment number
```
**Well-known ports (01023):**
| Port | Protocol | Service |
|------|----------|---------|
| 22 | TCP | SSH |
| 25 | TCP | SMTP (email sending) |
| 53 | UDP/TCP | DNS |
| 80 | TCP | HTTP |
| 443 | TCP | HTTPS |
| 5432 | TCP | PostgreSQL |
| 6379 | TCP | Redis |
**Ephemeral ports (4915265535):** OS assigns these randomly for outbound connections.
**In Docker:**
```yaml
ports:
- "9100:9100" # host:container — both the same number
```
Container port 9100 is mapped to host port 9100.
External systems connect to the host IP on port 9100.
Internally, containers on the Docker network use the container port directly.
---
## DNS (Domain Name System)
DNS is a distributed database that maps names to IP addresses.
**The hierarchy:**
```
. (root)
└── com
└── kitestacks
├── www → Cloudflare anycast IP
├── auth → Cloudflare anycast IP
└── grafana → Cloudflare anycast IP
```
**Resolution process for `grafana.kitestacks.com`:**
1. Browser checks local cache — not found
2. Browser asks OS resolver (usually `127.0.0.53`)
3. OS asks the configured DNS server (your home router, or 8.8.8.8)
4. Resolver asks root nameservers: "who handles `.com`?"
5. Root says: "Ask Verisign's servers"
6. Resolver asks Verisign: "who handles `kitestacks.com`?"
7. Verisign says: "Ask Cloudflare's nameservers (`vera.ns.cloudflare.com`)"
8. Resolver asks Cloudflare: "what is `grafana.kitestacks.com`?"
9. Cloudflare returns: "Cloudflare's anycast IP: 104.x.x.x"
10. Browser connects to 104.x.x.x on port 443
**Internal Docker DNS:**
Inside the `kitestacks` Docker network, Docker runs a DNS server at `127.0.0.11`.
When cloudflared resolves `grafana`, Docker DNS returns the container's bridge IP.
```bash
# Check what an external name resolves to
dig grafana.kitestacks.com
# Check DNS from inside a container
docker exec cloudflared nslookup grafana
docker exec cloudflared cat /etc/resolv.conf # Shows the DNS server: 127.0.0.11
```
---
## HTTP and HTTPS
**HTTP:** Plain text request/response protocol. Anyone who can see the traffic can read it.
```
GET /api/health HTTP/1.1
Host: grafana.kitestacks.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{"ok": true}
```
**HTTPS:** HTTP inside a TLS-encrypted tunnel. The connection is encrypted from client to
Cloudflare's edge. Between Cloudflare and your containers (inside Docker network), it is
plain HTTP — this is fine because that traffic never leaves the host.
**TLS handshake (simplified):**
1. Client says "hello, I support these cipher suites"
2. Server sends its certificate (proves it is `kitestacks.com`)
3. Client verifies certificate against trusted Certificate Authorities
4. Both sides agree on encryption keys (Diffie-Hellman key exchange)
5. Encrypted connection established
6. HTTP requests flow inside this encrypted tunnel
In this homelab, Cloudflare handles TLS entirely. Your containers never see TLS.
---
## Cloudflare Tunnel — Technical Details
**What cloudflared actually does:**
```bash
# Watch cloudflared connect
docker logs cloudflared -f
# You see: "Connection established" connIndex=0 location=ORD
# ORD = Chicago data center (or nearest Cloudflare POP to you)
```
cloudflared establishes persistent multiplexed HTTP/2 connections to Cloudflare's
edge network. When a request comes in:
```
Internet user → Cloudflare edge → tunnel (HTTP/2 multiplexed) → cloudflared
cloudflared reads Ingress rules from Cloudflare API:
grafana.kitestacks.com → http://grafana:3000
cloudflared → Docker DNS → grafana container IP → sends request
```
The tunnel connection uses QUIC (UDP-based) when possible, falls back to HTTPS/TCP.
**Active-Active with two connectors:**
Each connector registers separately. Cloudflare maintains a list of active connectors.
Incoming requests are distributed across connectors by Cloudflare — no configuration
needed on your end. If one connector drops, the others take all traffic within seconds.
---
## Tailscale — WireGuard Under the Hood
Tailscale is a managed WireGuard VPN. Understanding WireGuard explains Tailscale.
**WireGuard:**
- Modern VPN protocol, designed in 2016
- Uses UDP (faster than TCP-based VPNs like OpenVPN)
- Cryptography: Curve25519 key exchange, ChaCha20-Poly1305 encryption
- Each peer has a public/private key pair (like SSH keys)
- Configured via static peer lists with IP allowances
**The NAT problem:** Home machines are behind NAT. Their public IP is the router's IP,
not their own. Two NAT-ed machines cannot easily make direct connections.
**Tailscale's solution — UDP hole punching:**
1. Both machines connect to Tailscale's coordination server (DERP)
2. Tailscale orchestrates a "hole punch": both machines send packets to each other
simultaneously, which opens NAT mappings on both routers
3. Direct WireGuard connection established peer-to-peer
4. Tailscale coordination servers are no longer involved in the data path
```bash
# Check Tailscale status
tailscale status
# See your device's Tailscale IP
tailscale ip -4
# Check connectivity to kscloud1
tailscale ping 100.123.x.x
# See if connection is direct or via relay
tailscale status --json | python3 -m json.tool | grep -A5 "kscloud1"
```
**Why Tailscale IPs are stable:** Each device's `100.x.x.x` IP is tied to its machine
identity in Tailscale's database. It does not change when you move networks or reconnect.
---
## Firewalls (ufw)
ufw (Uncomplicated Firewall) is a frontend for iptables/nftables.
**kscloud1's firewall configuration:**
```bash
# View current rules
sudo ufw status verbose
# Default policies
sudo ufw default deny incoming # Block all inbound by default
sudo ufw default allow outgoing # Allow all outbound
# Allow specific services
sudo ufw allow ssh # Allow SSH (port 22)
sudo ufw allow from 172.16.0.0/12 to any port 8000 proto tcp # Docker → metrics API
# Why 172.16.0.0/12 and not just the specific Docker subnet?
# Docker creates a new bridge network with a random 172.x subnet for each network.
# /12 covers ALL possible Docker subnets so this rule always works.
```
**The ufw/Docker conflict:** Docker modifies iptables rules directly, bypassing ufw.
This means Docker's port mappings (`-p 9100:9100`) are accessible regardless of ufw rules.
Only services running in `network_mode: host` are controlled by ufw.
kscloud1's metrics API uses `network_mode: host`, so it needs an explicit ufw allow rule
for Docker containers to reach it.
---
## Reverse Proxies
A reverse proxy receives requests on behalf of backend services:
```
Client → Reverse Proxy → Backend A
→ Backend B
→ Backend C
```
In this homelab:
- **Cloudflare + cloudflared** — the primary reverse proxy routing by hostname
- **nginx (homepage container)** — secondary proxy forwarding `/api/*` to metrics API
nginx config that proxies API calls:
```nginx
location /api/ {
proxy_pass http://host.docker.internal:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
```
`host.docker.internal` resolves to the host machine's IP from inside a Docker container.
This lets the nginx container reach the metrics API running in `network_mode: host`.
---
## Diagnosing Network Problems
**"I can't reach the service from outside"**
```bash
# Is cloudflared running and connected?
docker logs cloudflared | tail -20
# Is the target container running and on the right network?
docker inspect homepage --format '{{range .NetworkSettings.Networks}}{{println .}}{{end}}'
# Can cloudflared reach the container?
docker exec cloudflared curl -s http://homepage:3000
```
**"Two containers can't talk to each other"**
```bash
# Are they on the same network?
docker network inspect kitestacks | grep -A5 "Containers"
# DNS resolution working?
docker exec service-a nslookup service-b
# Is the target port open inside the container?
docker exec service-b ss -tlnp
```
**"The database won't accept connections"**
```bash
# Is Postgres listening?
docker exec authentik-postgres ss -tlnp | grep 5432
# From another container, can we reach it?
docker exec authentik nc -zv authentik-postgres 5432
# Is it bound to the right interface on kscloud1?
docker exec authentik-postgres ss -tlnp | grep 5432
# Should show: *:5432 or 100.123.x.x:5432, not 127.0.0.1:5432
```
---
**Next:** [Part 6 — Full Build](06-full-build.md)