Networking Integration Guide

Technology: networking · Category: tooling · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/networking

Insight:

Networking is the layer every codeAmani deploy silently depends on and nobody owns: a name resolves, a socket opens, a certificate validates, bytes move. The trade-off is that almost all of it is someone else's infrastructure — DNS caches you cannot flush, CAs you do not run, middleboxes you cannot see — so the practical skill is diagnosis, not construction. Get DNS and TLS right and the Porkbun → Cloudflare → Vercel path is boring; get them wrong and every other layer reports a lie.

███╗   ██╗███████╗████████╗██╗    ██╗ ██████╗ ██████╗ ██╗  ██╗██╗███╗   ██╗ ██████╗
████╗  ██║██╔════╝╚══██╔══╝██║    ██║██╔═══██╗██╔══██╗██║ ██╔╝██║████╗  ██║██╔════╝
██╔██╗ ██║█████╗     ██║   ██║ █╗ ██║██║   ██║██████╔╝█████╔╝ ██║██╔██╗ ██║██║  ███╗
██║╚██╗██║██╔══╝     ██║   ██║███╗██║██║   ██║██╔══██╗██╔═██╗ ██║██║╚██╗██║██║   ██║
██║ ╚████║███████╗   ██║   ╚███╔███╔╝╚██████╔╝██║  ██║██║  ██╗██║██║ ╚████║╚██████╔╝
╚═╝  ╚═══╝╚══════╝   ╚═╝    ╚══╝╚══╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝ ╚═════╝

Networking Integration Guide

Focus: The working model of TCP/IP, addressing, DNS, TLS and HTTP/2–3 that an application developer actually needs — plus the diagnostic commands that prove which layer is lying, and the defensive posture for network-layer risk (open ports, weak TLS, DNS hijacking, SSRF, MITM).

Overview

This is a topic guide, not an SDK. There is nothing to npm install; the deliverable is a mental model plus a toolbox. Reach for it when a deploy "works locally", when a domain resolves for you but not for your users, when a certificate is valid in the browser but rejected by curl, or when a webhook receiver needs to make an outbound call to a URL it did not choose.

Two models describe the same stack. The OSI seven-layer model is the vocabulary (people say "layer 7" and "layer 4"); the TCP/IP four-layer model is what actually ships. Use OSI to talk, TCP/IP to debug.

TCP/IP layer OSI layers What lives here What breaks
Link 1–2 Physical, Data Link Ethernet, Wi-Fi, MAC addresses, ARP Cable/radio, MTU, local LAN only
Internet 3 Network IPv4/IPv6, ICMP, routing, NAT Wrong route, firewall drop, no IPv6
Transport 4 Transport TCP (ordered, reliable), UDP (datagram — carries QUIC) Port closed, RST, SYN timeout
Application 5–7 Session, Presentation, Application DNS, TLS, HTTP/1.1, HTTP/2, HTTP/3 Bad record, expired cert, 4xx/5xx
flowchart LR
  A["Browser<br/>https://app.example.com"] --> B["DNS resolve<br/>A / AAAA / CNAME"]
  B --> C["TCP :443<br/>or QUIC over UDP :443"]
  C --> D["TLS handshake<br/>SNI · ALPN · cert chain"]
  D --> E{"ALPN negotiated"}
  E -->|"h2"| F["HTTP/2<br/>multiplexed over one TCP conn"]
  E -->|"h3"| G["HTTP/3<br/>multiplexed over QUIC streams"]
  E -->|"http/1.1"| H["HTTP/1.1<br/>one request per connection"]
  F --> I["CDN / edge PoP"]
  G --> I
  H --> I
  I -->|"cache HIT"| J["Response from edge"]
  I -->|"cache MISS"| K["Origin<br/>Vercel function · Supabase · Neon"]

Every arrow in that diagram is a place a request can die, and each one has a distinct symptom. The rest of this guide walks them in order.

Official Documentation

Resource URL
MDN — HTTP overview https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview
MDN — Transport Layer Security https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security
MDN — Connection management in HTTP/1.x https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x
RFC 9293 — TCP https://www.rfc-editor.org/rfc/rfc9293
RFC 1035 — DNS implementation and specification https://www.rfc-editor.org/rfc/rfc1035
RFC 8499 — DNS terminology https://www.rfc-editor.org/rfc/rfc8499
RFC 8446 — TLS 1.3 https://www.rfc-editor.org/rfc/rfc8446
RFC 9110 — HTTP semantics https://www.rfc-editor.org/rfc/rfc9110
RFC 9113 — HTTP/2 https://www.rfc-editor.org/rfc/rfc9113
RFC 9000 — QUIC transport https://www.rfc-editor.org/rfc/rfc9000
RFC 9114 — HTTP/3 https://www.rfc-editor.org/rfc/rfc9114
RFC 1918 — Private IPv4 address space https://www.rfc-editor.org/rfc/rfc1918
RFC 4632 — CIDR https://www.rfc-editor.org/rfc/rfc4632
RFC 8659 — CAA records https://www.rfc-editor.org/rfc/rfc8659
IANA — Service names and port numbers https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
OWASP — SSRF prevention cheat sheet https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
OWASP — Transport layer security cheat sheet https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html
Mozilla SSL Configuration Generator https://ssl-config.mozilla.org/
BIND 9 manpages (dig, nslookup) https://bind9.readthedocs.io/en/latest/manpages.html
curl manual https://curl.se/docs/manpage.html
Nmap — legal issues https://nmap.org/book/legal-issues.html

Setup — the diagnostics toolbox

Install the tools first; every section below assumes them.

# Debian / Ubuntu / WSL
sudo apt update && sudo apt install -y dnsutils curl iproute2 net-tools traceroute nmap openssl

# macOS (Homebrew) — dig/host/nslookup come from the bind formula
brew install bind curl nmap openssl@3 mtr
# Windows — nslookup, tracert and netstat ship with the OS.
# The PowerShell equivalents are richer and script better:
Resolve-DnsName tech-stack.codeamanilabs.org -Type A
Test-NetConnection tech-stack.codeamanilabs.org -Port 443
Get-NetTCPConnection -State Listen | Sort-Object LocalPort

Optional environment variables the examples use:

# .env.local — used by the SSRF-guard example below. Server-side only.
OUTBOUND_URL_ALLOWLIST=api.stripe.com,sandbox.safaricom.co.ke,api.safaricom.co.ke

apt install nmap is fine; running nmap is not automatically fine. See Scanning, authorized only.


IP addressing, CIDR, subnets, NAT

An IPv4 address is 32 bits (203.0.113.10); IPv6 is 128 bits (2001:db8::1). CIDR notation (RFC 4632) appends a prefix length: the first N bits are the network, the rest identify hosts inside it.

CIDR Netmask Usable host addresses (IPv4) Typical use
/32 255.255.255.255 1 A single host — firewall rules, allowlists
/24 255.255.255.0 254 One small subnet / VPC subnet
/16 255.255.0.0 65,534 A VPC
/8 255.0.0.0 16,777,214 10.0.0.0/8 private space
/0 0.0.0.0 everything Default route; "any source" in a security group

Two addresses in every IPv4 subnet are not usable hosts — the all-zeros network address and the all-ones broadcast address — which is why a /24 gives 254, not 256.

Ranges that must never be reachable from user input

These are the ranges an SSRF guard denies. Memorise them; they show up again in the security section.

Range RFC Meaning
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 RFC 1918 Private IPv4
127.0.0.0/8, ::1/128 — Loopback
169.254.0.0/16, fe80::/10 RFC 3927 Link-local — includes 169.254.169.254, the cloud metadata endpoint
100.64.0.0/10 RFC 6598 Carrier-grade NAT shared space
fc00::/7 RFC 4193 IPv6 unique local addresses
0.0.0.0/8 — "This network"; 0.0.0.0 often resolves to localhost

NAT

Network Address Translation lets many private addresses share one public address by rewriting source IP + port and keeping a translation table. Consequences that matter to application code:


Ports and sockets

A socket is the five-tuple {protocol, source IP, source port, destination IP, destination port}. A server binds and listens on a well-known port; a client connects from an ephemeral port. IANA divides the space into System (0–1023), User/Registered (1024–49151) and Dynamic/ephemeral (49152–65535, never assigned).

Port Service Note
22 SSH Never expose with password auth; keys only
25 / 465 / 587 SMTP / SMTPS / submission 587 is the modern submission port; many hosts block 25 outbound
53 DNS UDP first, TCP fallback for large responses (RFC 7766)
80 HTTP Keep it open only to 301 to HTTPS and to serve ACME HTTP-01
443 HTTPS TCP for HTTP/1.1 + HTTP/2, UDP for HTTP/3/QUIC
853 DNS-over-TLS RFC 7858
3000 / 3001 Next.js dev Local only
5432 Postgres Supabase / Neon — never open to 0.0.0.0/0
6379 Redis Historically unauthenticated by default; treat as internal-only

Two rules that prevent most self-inflicted outages:

  1. Bind to 127.0.0.1 for anything that does not need to be public. A service bound to 0.0.0.0 on a cloud VM is on the internet the moment the security group allows it.
  2. Open ports are inventory. You cannot defend a listener you did not know existed — see the ss recipe below.

DNS

DNS turns a name into an address (RFC 1035; terminology in RFC 8499). Four roles are involved, and confusing them is the root of most "it works for me" reports.

flowchart TD
  A["Application<br/>getaddrinfo / fetch"] --> B["Stub resolver<br/>OS cache"]
  B --> C["Recursive resolver<br/>ISP · 1.1.1.1 · 8.8.8.8<br/>owns the TTL cache"]
  C -->|"cache miss"| D["Root servers<br/>. → 'ask .org'"]
  D --> E["TLD servers<br/>.org → 'ask ns1.porkbun.com'"]
  E --> F["Authoritative NS<br/>your zone — the only source of truth"]
  F -->|"answer + TTL"| C
  C -->|"cached answer"| B
  B --> A

The authoritative nameserver is the only place a record actually changes. Everything between it and the user is a cache. "DNS propagation" is not a push — it is the world's recursive resolvers expiring their cached copy after the record's TTL elapses. That is the whole mechanism, and it dictates the operational rule below.

Record types

Type Points at Notes
A IPv4 address Apex domains on Vercel use an A record
AAAA IPv6 address Add it — a growing share of mobile networks are IPv6-only with NAT64
CNAME Another name Cannot coexist with other records on the same name, and cannot legally sit at the zone apex. Providers work around this with CNAME flattening / ALIAS / ANAME
MX Mail exchanger + priority Lower priority number wins (RFC 5321)
TXT Arbitrary text Domain-ownership proofs, SPF (RFC 7208), DKIM (RFC 6376), DMARC (RFC 7489)
CAA Which CAs may issue for this name RFC 8659 — issue / issuewild / iodef
NS Delegation to authoritative servers Changing these at the registrar moves the whole zone
SOA Zone metadata Serial, refresh, and the negative-cache TTL
SRV Service host and port Used by protocols that need port discovery
HTTPS / SVCB Connection hints before the first request RFC 9460 — lets a client learn ALPN (h3), IP hints and port up front
PTR Reverse: address → name Lives in the provider's zone, not yours

Two TXT records everyone gets wrong: SPF must be one TXT record per domain (multiple v=spf1 records is a permanent error), and DMARC lives at _dmarc.example.com, not the apex.

The TTL rule

Lower the TTL before you change anything.

# 1. Days before a cutover: drop TTL to 300s on the records you will change,
#    then wait for the OLD TTL to fully elapse.
# 2. Make the change.
# 3. Verify from multiple resolvers.
# 4. Days later, once stable, raise TTL back to 3600+.

You cannot shorten a TTL retroactively: a resolver that cached the old record at 86400s will hold it for up to a day no matter what you do afterwards. This is why a rushed DNS cutover is a multi-hour outage and a planned one is invisible.

Diagnosing DNS

# What does the authoritative server say? (bypasses every cache — the ground truth)
dig +short NS codeamanilabs.org
dig @ns1.porkbun.com tech-stack.codeamanilabs.org A

# What does the world see? Query specific public resolvers.
dig @1.1.1.1 tech-stack.codeamanilabs.org A +short
dig @8.8.8.8 tech-stack.codeamanilabs.org A +short

# Full delegation path, root → TLD → authoritative
dig +trace tech-stack.codeamanilabs.org

# Remaining TTL on the cached answer (run twice — it counts down)
dig tech-stack.codeamanilabs.org A | grep -A1 "ANSWER SECTION"

# Other record types
dig codeamanilabs.org MX +short
dig codeamanilabs.org CAA +short
dig _dmarc.codeamanilabs.org TXT +short

# nslookup — everywhere, including bare Windows
nslookup -type=A tech-stack.codeamanilabs.org 1.1.1.1
# Windows PowerShell equivalents
Resolve-DnsName tech-stack.codeamanilabs.org -Type A -Server 1.1.1.1
Resolve-DnsName codeamanilabs.org -Type MX
Clear-DnsClientCache   # flushes the local stub cache only, not the recursive resolver

If dig @<authoritative-ns> is right but dig @1.1.1.1 is wrong, you are waiting on TTL — do nothing. If the authoritative answer itself is wrong, fix the record. That single test separates "be patient" from "act", and it is the most useful thing in this guide.


TLS and HTTPS

TLS 1.3 (RFC 8446) authenticates the server, negotiates keys, and encrypts everything after. The handshake is one round trip before application data in the full case; a resumed session can send 0-RTT early data (with replay caveats — never put a non-idempotent request in 0-RTT).

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: TCP SYN → SYN/ACK → ACK
  C->>S: ClientHello (SNI=app.example.com, ALPN=[h2,http/1.1], key_share)
  S->>C: ServerHello (key_share) + {Certificate, CertificateVerify, Finished}
  Note over C,S: Client validates chain to a trusted root,<br/>checks hostname against SAN, checks validity dates
  C->>S: {Finished}
  C->>S: {HTTP request} — encrypted
  S->>C: {HTTP response} — encrypted

Three fields in ClientHello do a lot of work:

Certificates: what validation actually checks

A certificate is trusted only if all of these hold. Any one failing is a hard error, and the error message rarely says which.

  1. Chain of trust — leaf → intermediate(s) → a root in the client's trust store. The single most common production TLS bug is a server that serves the leaf but omits the intermediate: browsers often paper over it with AIA fetching, curl and mobile SDKs do not. Symptom: "works in Chrome, fails in the app".
  2. Hostname match — the requested name must appear in the certificate's Subject Alternative Name list (RFC 9525). The legacy Common Name field is no longer used for matching.
  3. Validity window — notBefore ≤ now ≤ notAfter. Expiry is an outage, so renewal must be automated.
  4. Revocation / CT — modern clients also expect Certificate Transparency signatures.
# Full handshake detail: chain, SANs, protocol, cipher
openssl s_client -connect tech-stack.codeamanilabs.org:443 \
  -servername tech-stack.codeamanilabs.org -showcerts </dev/null

# Just the dates and names
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# Does the chain validate standalone? (no browser AIA rescue)
curl -vI https://example.com 2>&1 | grep -Ei "SSL|subject|issuer|ALPN|HTTP/"

# Prove a specific TLS version is or is not accepted
curl --tlsv1.3 --tls-max 1.3 -sI https://example.com -o /dev/null -w "%{http_version} %{ssl_verify_result}\n"

Hardening checklist


HTTP/1.1, HTTP/2, HTTP/3

HTTP/1.1 HTTP/2 (RFC 9113) HTTP/3 (RFC 9114)
Transport TCP TCP QUIC over UDP (RFC 9000)
Framing Text Binary frames Binary frames on QUIC streams
Concurrency One request in flight per connection; browsers open ~6 Multiplexed streams on one connection Multiplexed streams, independent
Head-of-line blocking At the request level Removed at HTTP level, remains at TCP level — one lost segment stalls every stream Removed: a lost packet stalls only its own stream
Header compression None HPACK QPACK
Handshake TCP + TLS separately TCP + TLS separately TLS 1.3 folded into the QUIC handshake
Connection migration No No Yes — connection ID survives an IP change
Discovery default ALPN h2 Alt-Svc: h3=":443" header, or a DNS HTTPS record

HTTP/3 cannot be negotiated in-band on the first connection the way h2 can, because it is a different transport. A client learns about it from an Alt-Svc response header (RFC 7838) or, increasingly, from an HTTPS/SVCB DNS record (RFC 9460) that advertises alpn=h3 before the first packet — saving the initial TCP round trip entirely.

# Which protocol did you actually get?
curl -sI --http2 https://example.com -o /dev/null -w "%{http_version}\n"
curl -sI --http3 https://example.com -o /dev/null -w "%{http_version}\n"   # needs an HTTP/3-capable curl

# Is HTTP/3 advertised?
curl -sI https://example.com | grep -i alt-svc
dig example.com HTTPS +short        # SVCB/HTTPS record, if published

# Timing breakdown — where the milliseconds go
curl -s -o /dev/null -w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://example.com

That last command is the highest-value one-liner in this document: it splits a "slow site" complaint into a DNS problem, a TCP-RTT problem, a TLS problem, or an origin problem, in one request.


CDNs and the edge

A CDN terminates TLS at a point of presence close to the user, serves cacheable responses from there, and reuses long-lived warm connections back to origin. The win is not only bytes — it is round trips. On a 3G link with a 200–400 ms RTT, moving the TLS handshake from a US origin to a nearby PoP saves more wall-clock time than any payload optimisation you can make.

# Which PoP answered, and did it cache?
curl -sI https://tech-stack.codeamanilabs.org | grep -Ei "cf-ray|cf-cache-status|x-vercel-cache|age|cache-control"
Header Meaning
x-vercel-cache: HIT / MISS / STALE Vercel edge cache outcome
cf-cache-status: HIT / MISS / DYNAMIC / BYPASS Cloudflare cache outcome
cf-ray: …-NBO Cloudflare ray id; the suffix is the PoP IATA code (NBO = Nairobi)
age Seconds the response has sat in cache

When a name is proxied through Cloudflare (orange cloud), dig returns Cloudflare's anycast address, not your origin — expected, not a misconfiguration. See cloudflare for proxy status and the origin SSL modes; use Full (strict) so the Cloudflare↔origin hop is verified too, otherwise the padlock the user sees covers only half the path.


Diagnostics: which layer is lying?

Work bottom-up. Each command clears one layer, so the first failure localises the fault.

# L3 — is the host routable at all?
ping -c 4 1.1.1.1                 # raw IP: no DNS involved
traceroute -n example.com          # hop-by-hop path; * * * means filtered ICMP, not always a fault
mtr -rw example.com                # traceroute + loss stats over time (best for flaky links)

# L4 — is the port open?
nc -vz example.com 443
# PowerShell: Test-NetConnection example.com -Port 443

# What is listening locally, and which process owns it?
ss -tlnp                           # TCP, listening, numeric, process (needs sudo for other users' procs)
ss -tunap | grep :5432             # every socket touching Postgres
netstat -tulpn                     # older systems; ss is the modern replacement

# L7 — the full story of one request
curl -v https://example.com
curl -sSL -o /dev/null -w "%{http_code} %{num_redirects} %{redirect_url}\n" http://example.com   # follow the redirect chain
Symptom Most likely layer Next command
Could not resolve host DNS dig @1.1.1.1 <name> then dig @<authoritative-ns> <name>
Connection refused Transport — nothing listening ss -tlnp on the host
Connection timed out Firewall / security group silently dropping traceroute, then check the security group
certificate verify failed TLS chain or hostname openssl s_client -showcerts
Works in browser, fails in curl/app Missing intermediate certificate Serve the full chain
Right in dig @ns, wrong for users DNS TTL still counting down Wait; do not re-edit the record
Fast locally, slow in production RTT / cache miss curl -w timing breakdown

Scanning, authorized only

nmap is a legitimate inventory tool for systems you own or have explicit written permission to test. Unauthorized port scanning is unlawful in many jurisdictions and violates most providers' terms of service — read https://nmap.org/book/legal-issues.html before pointing it anywhere.

# Legitimate use: audit YOUR OWN host's exposed surface from outside it.
nmap -Pn -p 1-1024 <your-own-host>          # what does the internet see?
nmap --script ssl-enum-ciphers -p 443 <your-own-host>   # TLS versions/ciphers your server offers

Prefer the alternatives when they exist: ss -tlnp on the box itself is faster, more accurate and needs no permission conversation; your cloud provider's security-group listing is the authoritative answer to "what is exposed".


Network-layer risks and their mitigations

Defensive framing only — each row is a risk you close on your own infrastructure.

Risk Why it happens Mitigation
Open ports / exposed services Service bound to 0.0.0.0; security group left at 0.0.0.0/0 from a debugging session Bind internal services to 127.0.0.1; default-deny inbound; audit with ss -tlnp and the provider's security-group list; put admin surfaces behind a VPN or identity proxy, never behind "an unguessable port"
Weak / misconfigured TLS Legacy protocol versions left enabled; missing intermediate; expired cert TLS 1.2 minimum; config from Mozilla's generator; serve the full chain; automate renewal and alert on notAfter; enable HSTS
DNS hijacking (registrar/zone takeover) Registrar account compromise, or a stale CNAME to a deprovisioned host that an attacker re-claims 2FA + registrar lock on the registrar account (porkbun); publish CAA; delete DNS records when you tear down the resource they point at — dangling CNAMEs are subdomain takeover
DNS spoofing / cache poisoning Plaintext UDP:53 answers are forgeable on a hostile network DNSSEC (RFC 4033) on your zone; DNS-over-HTTPS (RFC 8484) or DNS-over-TLS (RFC 7858) on clients you control; never make a security decision from an unauthenticated DNS answer
SSRF — server fetches an attacker-chosen URL Any endpoint that takes a URL: webhook-target config, image import, link unfurling, "test my callback" buttons Allowlist destination hosts; resolve DNS yourself and reject private/link-local IPs before connecting; refuse redirects or re-validate every hop; block non-http(s) schemes; enforce egress rules at the network. Detail below
MITM on hostile networks Public Wi-Fi, transparent proxies, captive portals HTTPS everywhere + HSTS; never disable certificate verification; treat any request arriving over plain HTTP as untrusted
Credentials on the wire Postgres/Redis exposed publicly; API keys in query strings (they land in every access log) TLS on database connections (sslmode=require or stricter); keys in Authorization headers, never in URLs; rotate on exposure
Amplification / volumetric abuse of your endpoints Any unauthenticated endpoint that does real work Rate limit at the edge; keep expensive routes behind auth; let the CDN absorb L3/L4 volume

SSRF: the pattern that matters for webhook and callback endpoints

The dangerous shape is outbound traffic to a URL a user supplied. From inside a cloud network, http://169.254.169.254/ and http://10.x.x.x/ are reachable and often unauthenticated, so a naïve fetch(userUrl) hands an attacker your internal network.

// lib/safe-fetch.ts — server-side only.
import { lookup } from "node:dns/promises";
import { isIP } from "node:net";

const ALLOWED_HOSTS = new Set(
  (process.env.OUTBOUND_URL_ALLOWLIST ?? "").split(",").map((h) => h.trim()).filter(Boolean),
);

/** RFC 1918 / 3927 / 6598 / 4193 + loopback. Reject, do not "sanitize". */
function isPrivateAddress(ip: string): boolean {
  if (isIP(ip) === 6) {
    const v6 = ip.toLowerCase();
    return v6 === "::1" || v6.startsWith("fe80:") || v6.startsWith("fc") || v6.startsWith("fd");
  }
  const [a, b] = ip.split(".").map(Number);
  if (a === 10 || a === 127 || a === 0) return true;
  if (a === 172 && b >= 16 && b <= 31) return true;
  if (a === 192 && b === 168) return true;
  if (a === 169 && b === 254) return true;          // cloud metadata lives here
  if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
  return false;
}

export async function safeFetch(rawUrl: string, init?: RequestInit): Promise<Response> {
  const url = new URL(rawUrl);

  // 1. Scheme allowlist — blocks file:, gopher:, ftp:, data:
  if (url.protocol !== "https:") throw new Error("only https is allowed");

  // 2. Host allowlist — the strongest control. Prefer it whenever the set is known.
  if (ALLOWED_HOSTS.size > 0 && !ALLOWED_HOSTS.has(url.hostname)) {
    throw new Error(`host not allowed: ${url.hostname}`);
  }

  // 3. Resolve and reject internal addresses (all records, not just the first).
  const resolved = await lookup(url.hostname, { all: true });
  if (resolved.some((r) => isPrivateAddress(r.address))) {
    throw new Error("resolved to a private address");
  }

  // 4. Never follow redirects blindly — a 302 to 169.254.169.254 defeats steps 1-3.
  return fetch(url, { ...init, redirect: "error", signal: AbortSignal.timeout(5_000) });
}

Two honest caveats. First, a DNS rebinding attacker can return a public IP to your lookup() and a private one to the connection that follows; closing that gap requires pinning the validated IP into the connection itself (an undici Agent with a custom connect.lookup). Second, application-level checks are defence in depth — the durable control is network egress policy, so the internal address is unreachable from the request-handling process no matter what the code does. OWASP's cheat sheet is the reference: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html

Inbound webhooks are the mirror-image problem — signature verification, not URL validation. That belongs to webhooks; the broader application-security posture is in security.


codeAmani notes

The deploy path: Porkbun → Cloudflare → Vercel

This is the sequence that actually wires a codeAmani property, and the order matters.

  1. Registrar — the name lives at Porkbun; the porkbun-dns skill edits records programmatically. Registrar lock + 2FA on that account is the root of trust for the entire domain: whoever controls it controls your DNS, and therefore your certificates.
  2. DNS — either Porkbun's nameservers or Cloudflare's. Apex → A record (Vercel shows the value in the project's Domains tab; historically 76.76.21.21); subdomain → CNAME to the per-project Vercel target. CNAME cannot sit at the apex, which is exactly why the apex gets an A.
  3. TLS — issued and renewed automatically by the platform. Publish a CAA record for whichever CA the platform uses, or issuance fails; verify with dig <domain> CAA +short.
  4. Verify before declaring done — dig @<authoritative-ns> for truth, dig @1.1.1.1 for reach, curl -vI for the chain. A green dashboard and a wrong CAA record look identical until the first renewal.

If a name is proxied through Cloudflare in front of Vercel, set SSL mode to Full (strict) — anything less leaves the Cloudflare↔origin hop unverified.

Security

Kenya-targeted projects: the bandwidth angle

On a 2G/3G link, latency and packet loss — not throughput — decide whether an app feels usable. Round trips are the budget.


Troubleshooting

Issue Fix
Record changed but users still see the old value TTL has not expired. dig @<authoritative-ns> to confirm the record is correct, then wait — re-editing resets nothing
CNAME rejected at the apex Not legal in DNS. Use an A record, or the provider's ALIAS/ANAME/CNAME-flattening feature
Certificate issuance fails on a new domain Check dig <domain> CAA +short — a CAA record that omits your platform's CA blocks issuance
curl says certificate verify failed, browser is fine Server omits the intermediate; the browser fetched it via AIA and curl did not. Serve the full chain
ERR_SSL_PROTOCOL_ERROR on a proxied domain Cloudflare SSL mode vs origin mismatch — Flexible in front of an HTTPS origin loops; use Full (strict)
Webhook works in production, never fires locally Your dev box is behind NAT and unroutable. Tunnel it: ngrok http 3000, and register the HTTPS URL
Port shows open with nc but the app 502s Transport is fine, application is not — move up a layer: curl -v and the origin's logs
traceroute shows * * * for the last hops ICMP filtered — normal for cloud hosts, not evidence of a fault. Test the port with nc -vz instead
Two v=spf1 TXT records on one domain Permanent SPF error. Merge into a single record
ss shows nothing but the service "is running" It bound to 127.0.0.1 (or inside a container's namespace) — check the bind address and, for Docker, the port publish flags

Official docs: