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>
352 lines
11 KiB
Markdown
352 lines
11 KiB
Markdown
# Without AI — Part 5: Networking
|
||
|
||
**Track:** Advanced (No AI)
|
||
**Time for this section:** 1–2 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 (0–255) 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 (0–65535) that identifies which service on a host should
|
||
handle a connection.
|
||
|
||
```
|
||
IP address = the building
|
||
Port = the apartment number
|
||
```
|
||
|
||
**Well-known ports (0–1023):**
|
||
| 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 (49152–65535):** 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)
|