Wireless, VPNs & Firewalls — Guarding the Edges
What you'll learn
- How Wi-Fi shares the air, and how WPA2 and WPA3 protect it (and why WEP and WPS do not)
- How a VPN carries your packets inside an encrypted tunnel, with WireGuard and IPsec as examples
- How stateless and stateful firewalls decide which packets may pass, and why the rule order matters
- How NAT lets a whole home share one public address, and what a DMZ and network segmentation are for
- You will build three tools: a first-match firewall, a NAT translation table and WireGuard's cryptokey routing
⚠️ Ethics first: configure and test only networks and devices you own or have written permission to test. Connecting to someone else's Wi-Fi without permission, cracking its password or getting around a firewall you do not control are crimes in most countries. This lesson is about building the defences.
The building with a guard 🏢
Think of your network as an office building. Wi-Fi is a meeting room with the windows open: everyone outside can hear what is said, so you must speak in a code only your colleagues know. A VPN is an armoured van that carries your documents through the public streets to another office: people see a van, not the papers. The firewall is the guard at the door with a list of who may enter and leave. NAT is the reception desk that sends all outgoing letters with the company's single street address and knows which employee each reply is for. And the DMZ is the lobby: visitors meet staff there and never walk into the offices.
Wi-Fi: one shared radio channel
Wi-Fi is the IEEE 802.11 family. The Wi-Fi Alliance gives the generations simple names: Wi-Fi 4 (802.11n), Wi-Fi 5 (802.11ac), Wi-Fi 6 (802.11ax), Wi-Fi 6E (802.11ax extended to the 6 GHz band) and Wi-Fi 7 (802.11be). Devices talk to an access point (AP). The network has a name, the SSID, and each AP radio has its own MAC address, the BSSID. A device first associates with an AP, then (on a protected network) proves it knows the key, and only then may it send data.
Radio is a shared medium: only one device can transmit on a channel at a time, and a transmitter cannot hear a collision while it is sending. So Wi-Fi uses CSMA/CA (collision avoidance): listen first, wait a random time, then send, and every unicast frame is acknowledged. That is why a crowded channel feels slow for everyone, and why channel planning matters:
- 2.4 GHz: long range, walls hurt less, but only a few non-overlapping channels and plenty of interference (Bluetooth, microwave ovens, neighbours). Channels are 5 MHz apart while each transmission is about 20 MHz wide, which is why
1,6and11are the classic choice. - 5 GHz and 6 GHz: many more and wider channels, higher speeds, shorter range.
The most important security fact about Wi-Fi follows from the shared medium: anyone in range receives every frame. Security therefore depends entirely on encryption and authentication.
Each channel number moves the centre by only 5 MHz, so neighbouring channels share most of their spectrum and interfere. With the 22 MHz width of the original 802.11b signal you can fit three channels side by side in 1 to 11 (1, 6, 11); with 20 MHz OFDM signals and channels up to 13, as allowed in many countries, four fit (1, 5, 9, 13). Try a 40 MHz width and see why wide channels make little sense at 2.4 GHz.
# 2.4 GHz Wi-Fi channels are only 5 MHz apart, but each transmission is about 20-22 MHz wide.
def centre(channel):
"""Centre frequency in MHz (channel 14 exists only in Japan, for old 802.11b)."""
return 2484 if channel == 14 else 2407 + 5 * channel
def overlap(a, b, width):
return abs(centre(a) - centre(b)) < width
def non_overlapping(width, channels):
chosen = []
for ch in channels:
if all(not overlap(ch, c, width) for c in chosen):
chosen.append(ch)
return chosen
for ch in (1, 6, 11):
print("channel %2d -> %d MHz" % (ch, centre(ch)))
print("do channels 1 and 3 overlap at 22 MHz?", overlap(1, 3, 22))
print("22 MHz wide (802.11b), channels 1-11:", non_overlapping(22, range(1, 12)))
print("20 MHz wide (802.11g/n), channels 1-13:", non_overlapping(20, range(1, 14)))
Securing Wi-Fi: from WEP to WPA3
- Open networks encrypt nothing: anyone nearby can read unencrypted traffic. Newer access points can offer Enhanced Open (OWE, RFC 8110), which encrypts each client's traffic without a password, though it does not prove the AP is genuine.
- WEP (1997) is broken beyond repair: its RC4-based design leaks the key after enough traffic. Never use it.
- WPA with TKIP was a stopgap for old hardware and is deprecated.
- WPA2 (IEEE 802.11i, 2004) uses AES-CCMP. In WPA2-Personal everybody shares one passphrase (a PSK), and a 4-way handshake derives fresh session keys. Its weakness: someone who records a handshake can test password guesses offline, as fast as their hardware allows, so a short or common passphrase falls quickly. (The 2017 KRACK attack on the handshake was fixed by software updates.)
- WPA2/WPA3-Enterprise replaces the shared passphrase with 802.1X: each user or device logs in with its own credentials, checked by a RADIUS server, so a leaving employee loses access without changing everyone's password.
- WPA3 (2018) replaces the PSK handshake with SAE (Simultaneous Authentication of Equals). Every guess now needs a live exchange with the AP, so recorded traffic cannot be attacked offline, and each session gets forward secrecy. WPA3 also requires Protected Management Frames (802.11w), which stops forged "disconnect" frames.
A sound home or office setup: WPA3 (or WPA2/WPA3 transition mode for older devices), a long random passphrase, WPS switched off (its 8-digit PIN can be brute-forced because the protocol checks the two halves separately), a separate guest network in its own VLAN, a changed admin password and current firmware. On someone else's hotspot, assume others can see your traffic and rely on TLS and a VPN.
VPNs: a private tunnel across a public network
A VPN takes each packet, encrypts and authenticates it, and puts it inside a new packet addressed to the other end of the tunnel. On the way, observers see only the outer header: your address, the VPN server's address and an encrypted blob.
inner packet: | IP 10.0.0.2 -> 10.0.0.1 | TCP | data |
on the wire: | IP 192.0.2.10 -> 203.0.113.1 | UDP 51820 | WireGuard header | encrypted inner packet | tag |
outer header (visible) (unreadable and tamper-proof)
There are two common shapes. Remote access: a laptop on a café network connects to the office or to a VPN provider. Site-to-site: two office routers keep a permanent tunnel so the branches act like one network. A full tunnel sends all traffic through the VPN; a split tunnel only sends the company's prefixes and lets the rest go directly to the Internet.
The main protocols:
- IPsec: the long-standing standard (architecture RFC 4301, ESP RFC 4303), with IKEv2 (RFC 7296) to authenticate the peers and agree on keys. Common for site-to-site links between firewalls.
- WireGuard: a small, modern design that runs over UDP (51820 by convention) with a fixed set of modern algorithms (Curve25519, ChaCha20-Poly1305, BLAKE2s). It has been part of the Linux kernel since version 5.6. Each peer is identified by a public key.
- TLS-based VPNs such as OpenVPN run over TLS and work even where only HTTPS-like traffic gets through.
A WireGuard configuration shows the most elegant idea, cryptokey routing. Each peer has a public key and a list of AllowedIPs (the keys below are placeholders):
[Interface]
PrivateKey = <laptop-private-key>
Address = 10.0.0.2/32
[Peer]
PublicKey = <office-public-key>
Endpoint = 203.0.113.1:51820
AllowedIPs = 10.0.0.0/24, 192.168.50.0/24
The AllowedIPs list works in both directions. Sending: a packet for 192.168.50.17 is encrypted for the peer whose AllowedIPs contain it (longest prefix wins). Receiving: after decryption, a packet is accepted only if its inner source address maps back to the peer that sent it. So a peer can never pretend to be another address. Tunnels add headers: over IPv4 WireGuard adds 60 bytes (20 IP + 8 UDP + 16 WireGuard header + 16 authentication tag), over IPv6 80 bytes, which is why tunnel MTUs are smaller than 1500.
Know what a VPN does not do. It protects the path to the VPN server, not what happens after it; the VPN operator can see your traffic; and it does not make you anonymous or stop malware.
Lab exercise 1
GoGraded in Code LabImplement WireGuard's cryptokey routing table.
Input, one item per line:
peer <name> <cidr>[,<cidr>...]: a peer and its allowed IPv4 ranges (a peer may also have none). All ranges are different.send <destination-ip>: the interface wants to send a packet.recv <peer> <source-ip>: a packet arrived from that peer and decrypted correctly; its inner source address is<source-ip>.
Rules. Build one table from all allowed ranges. Looking up an address means finding the range that contains it with the longest prefix, and taking its peer.
send:encrypt for <peer>with the peer found by the lookup, ordrop (no peer).recv: if the peer name was never declared,drop (unknown peer). Otherwise look up the source address: if it gives this same peer,accept; if it gives another peer,drop (source belongs to <that peer>); if it gives nothing,drop (source not allowed).
Output, one line per send or recv: the command as given, a colon, a space and the result.
Example
Input:
peer laptop 10.0.0.2/32
peer office 10.0.0.3/32,192.168.50.0/24
peer gateway 0.0.0.0/0
send 192.168.50.17
send 198.51.100.7
recv office 192.168.50.9
recv laptop 10.0.0.3
Output:
send 192.168.50.17: encrypt for office
send 198.51.100.7: encrypt for gateway
recv office 192.168.50.9: accept
recv laptop 10.0.0.3: drop (source belongs to office)
- The worked example, plus a gateway that claims an office address
- A more specific range inside another peer's range; unknown peers and unknown sources
- Three nested ranges and their exact edges
- A peer without allowed IPs can send nothing and claim nothing
- No peers at all
Example run
- Input
peer laptop 10.0.0.2/32 peer office 10.0.0.3/32,192.168.50.0/24 peer gateway 0.0.0.0/0 send 192.168.50.17 send 10.0.0.2 send 198.51.100.7 recv office 192.168.50.9 recv laptop 10.0.0.3 recv gateway 198.51.100.7 recv gateway 192.168.50.9
- Expected output
send 192.168.50.17: encrypt for office send 10.0.0.2: encrypt for laptop send 198.51.100.7: encrypt for gateway recv office 192.168.50.9: accept recv laptop 10.0.0.3: drop (source belongs to office) recv gateway 198.51.100.7: accept recv gateway 192.168.50.9: drop (source belongs to office)
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
type allowed struct {
peer stringMore lines in Code Lab: 77
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Encapsulation is not free: every tunnel shrinks the largest packet that fits. If the tunnel's MTU is set too high, large packets must be fragmented or are dropped, which produces the 'small requests work, big uploads hang' symptom from the troubleshooting lesson. Change LINK_MTU to 1420 (a tunnel inside a tunnel) and see how much room is left.
// What a WireGuard tunnel adds to every packet, and what that means for the MTU.
const OUTER_IP = { IPv4: 20, IPv6: 40 };
const UDP = 8;
const WG_HEADER = 16; // message type (1) + reserved (3) + receiver index (4) + counter (8)
const TAG = 16; // Poly1305 authentication tag after the encrypted packet
const LINK_MTU = 1500; // an ordinary Ethernet link
for (const family of Object.keys(OUTER_IP)) {
const overhead = OUTER_IP[family] + UDP + WG_HEADER + TAG;
console.log("tunnel over " + family + ": " + overhead + " bytes of overhead, so inner packets up to " + (LINK_MTU - overhead) + " bytes fit");
}
console.log("wg-quick subtracts 80 from the link MTU when none is configured: " + (LINK_MTU - 80));
// WireGuard also pads the inner packet to a multiple of 16 bytes (never past the tunnel MTU),
// so the result below is a lower bound.
const inner = 1500;
const outer = inner + OUTER_IP.IPv4 + UDP + WG_HEADER + TAG;
console.log("a full " + inner + "-byte packet would need at least " + outer + " bytes on a " + LINK_MTU + "-byte link: it does not fit");
Firewalls: stateless and stateful
A firewall applies a policy to traffic between zones. The rules match on fields of each packet: interface and direction, protocol, source and destination address, and port. Two principles apply everywhere:
- First match wins. Rules are read top to bottom and the first rule that matches decides. A broad
allowabove a specificdenysilently disables the deny. - Default deny. Anything that no rule allows is dropped. You list what is needed (an allowlist) instead of trying to list everything bad.
A stateless packet filter (a router's access control list, for example) judges every packet alone. That makes return traffic awkward: to let replies from web servers back in, you must allow inbound packets from port 443 to high ports, and an attacker can send packets that look exactly like such replies.
A stateful firewall keeps a connection tracking table. When an inside host opens a connection that the policy allows, the firewall records its 5-tuple; packets of that connection in either direction are then ESTABLISHED and pass, related ICMP errors are RELATED and pass, and an unsolicited packet from outside matches nothing and is dropped. A minimal Linux host policy in nftables:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
ip saddr 192.168.1.0/24 tcp dport 22 accept
tcp dport { 80, 443 } accept
icmp type echo-request limit rate 5/second accept
}
}
Read it as: drop by default; accept replies to connections this host started; accept loopback; SSH only from the local subnet; web for everyone; a rate-limited ping. Beyond packet filters there are application-level controls (proxies, web application firewalls, "next-generation" firewalls) that look inside the traffic. Remember egress too: limiting what inside machines may send out makes it much harder for malware to call home.
Lab exercise 2
PythonGraded in Code LabWrite a stateless packet filter with first-match rules and a default deny.
Input: rule lines, then a line ---, then packet lines.
- Rule:
<allow|deny> <proto|any> <source-cidr|any> <destination-cidr|any> <port|low-high|any>. Rules are numbered from 1 in input order. - Packet:
<proto> <source-ip> <destination-ip> <destination-port>. ICMP packets have no port and show-; only a rule port ofanymatches-.
A rule matches when the protocol is equal (or the rule says any), both addresses are inside the rule's blocks (or any), and the port is equal to the rule's port, inside its inclusive range, or the rule says any.
Output, one line per packet: <packet as given>: ALLOW (rule <n>) or DENY (rule <n>) for the first matching rule, or DENY (default) if no rule matches.
Example
Input:
allow tcp any 203.0.113.10/32 80-443
allow tcp 198.51.100.0/24 203.0.113.10/32 22
---
tcp 192.0.2.7 203.0.113.10 443
tcp 192.0.2.7 203.0.113.10 22
tcp 198.51.100.40 203.0.113.10 22
Output:
tcp 192.0.2.7 203.0.113.10 443: ALLOW (rule 1)
tcp 192.0.2.7 203.0.113.10 22: DENY (default)
tcp 198.51.100.40 203.0.113.10 22: ALLOW (rule 2)
- The worked example, plus a deny rule that comes too late
- First match wins: a specific deny before a broad allow
- Protocols differ, ranges include both ends, ICMP needs port any
- The edge of a /25 source block
- No rules: everything is denied by default
Example run
- Input
allow tcp any 203.0.113.10/32 80-443 allow tcp 198.51.100.0/24 203.0.113.10/32 22 deny any 192.0.2.66/32 any any allow icmp any 203.0.113.0/24 any --- tcp 192.0.2.7 203.0.113.10 443 tcp 192.0.2.7 203.0.113.10 22 tcp 198.51.100.40 203.0.113.10 22 tcp 192.0.2.66 203.0.113.10 80 udp 192.0.2.66 203.0.113.10 53 icmp 192.0.2.7 203.0.113.99 -
- Expected output
tcp 192.0.2.7 203.0.113.10 443: ALLOW (rule 1) tcp 192.0.2.7 203.0.113.10 22: DENY (default) tcp 198.51.100.40 203.0.113.10 22: ALLOW (rule 2) tcp 192.0.2.66 203.0.113.10 80: ALLOW (rule 1) udp 192.0.2.66 203.0.113.10 53: DENY (rule 3) icmp 192.0.2.7 203.0.113.99 -: ALLOW (rule 4)
import sys
def ip_to_int(text):
a, b, c, d = (int(x) for x in text.split("."))
return (a << 24) | (b << 16) | (c << 8) | d
def in_net(ip, cidr):
"""True if the address is inside the CIDR block ("any" matches everything)."""
if cidr == "any":
return TrueMore lines in Code Lab: 21
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
NAT, DMZs and segmentation
NAT. Your home has many devices with private addresses (RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), which are not routed on the Internet, and one public address from your ISP. The router performs NAPT (network address and port translation, called "traditional NAT" in RFC 3022, and often "PAT" or "masquerading"): it rewrites the source of every outgoing packet to its public address and a public port, and remembers the mapping:
inside NAT table outside
192.168.1.10:51514 -> 198.51.100.1:40000 -> 203.0.113.80:443
192.168.1.11:51514 -> 198.51.100.1:40001 -> 203.0.113.80:443
replies to 198.51.100.1:40001 are sent back to 192.168.1.11:51514
Two inside hosts may use the same source port; the public ports keep them apart. A packet from outside that matches no mapping has nowhere to go and is dropped, which is why NAT looks like a firewall. It is not a security policy, though: use a real stateful firewall as well. To run a server behind NAT you add a port forward (destination NAT), for example public port 8443 to 192.168.1.50:443, which opens that service to the whole Internet. Many ISPs now put customers behind a second layer, carrier-grade NAT, using the shared range 100.64.0.0/10 (RFC 6598). IPv6 has enough addresses for every device, so it does not need NAT, but it still needs a firewall.
DMZ. Servers that the Internet must reach (web, mail, VPN gateway) should not sit next to employees' laptops and the database. A DMZ (screened subnet) is a separate zone between the Internet and the internal network:
Internet --- [ firewall ] --- DMZ: web server, mail relay
|
+---------- internal: staff, database
rules: Internet -> DMZ only 80/443 (and 25 to the mail relay)
DMZ -> internal only the web server to the database port
internal -> Internet allowed, through a proxy
Internet -> internal nothing
If the web server is compromised, the attacker is still in the lobby: the firewall stops them from reaching the rest. Segmentation applies the same idea everywhere: separate zones (VLANs and subnets) for guests, IoT devices, staff, servers and management, with firewall rules that allow only the flows each zone really needs. The next lesson builds on this with the principles of least privilege and defence in depth.
Lab exercise 3
JavaScript (Node.js)Graded in Code LabBuild the translation table of a NAT router (NAPT) that also filters unsolicited inbound traffic.
Input: the first line is public <public-ip> ports <first-port>. Every following line is an event:
out <proto> <inside-ip:port> <remote-ip:port>: an inside host sends a packet out.in <proto> <remote-ip:port> <public-ip:port>: a packet arrives from outside.forward <proto> <public-port> <inside-ip:port>: add a static port forward.
Rules
out: if a mapping already exists for (proto, inside endpoint), reuse its public port, whatever the remote is; otherwise hand out the next public port, starting at<first-port>and counting up by 1 (one counter for all protocols). Remember that this mapping has contacted<remote-ip:port>. Result:as <public-ip>:<public-port>.in: find the mapping or forward for (proto, public port). If the address is not the public address or nothing is found:drop (no mapping). A dynamic mapping only accepts packets from remotes it has contacted: otherwisedrop (not contacted). A port forward accepts every remote. Result:to <inside-ip:port>.forward: resultok.
Output, one line per event: <the event line>: <result>.
Example
Input:
public 198.51.100.1 ports 40000
out tcp 192.168.1.10:51514 203.0.113.80:443
in tcp 203.0.113.80:443 198.51.100.1:40000
out tcp 192.168.1.11:51514 203.0.113.80:443
in tcp 203.0.113.80:443 198.51.100.1:40002
Output:
out tcp 192.168.1.10:51514 203.0.113.80:443: as 198.51.100.1:40000
in tcp 203.0.113.80:443 198.51.100.1:40000: to 192.168.1.10:51514
out tcp 192.168.1.11:51514 203.0.113.80:443: as 198.51.100.1:40001
in tcp 203.0.113.80:443 198.51.100.1:40002: drop (no mapping)
- The worked example, then the reply for the second host
- One mapping for two remotes; a stranger is filtered
- TCP and UDP from the same inside port get separate mappings
- A port forward is open to everyone; other ports and addresses are not
- No events: nothing is printed
Example run
- Input
public 198.51.100.1 ports 40000 out tcp 192.168.1.10:51514 203.0.113.80:443 in tcp 203.0.113.80:443 198.51.100.1:40000 out tcp 192.168.1.11:51514 203.0.113.80:443 in tcp 203.0.113.80:443 198.51.100.1:40001 in tcp 203.0.113.80:443 198.51.100.1:40002
- Expected output
out tcp 192.168.1.10:51514 203.0.113.80:443: as 198.51.100.1:40000 in tcp 203.0.113.80:443 198.51.100.1:40000: to 192.168.1.10:51514 out tcp 192.168.1.11:51514 203.0.113.80:443: as 198.51.100.1:40001 in tcp 203.0.113.80:443 198.51.100.1:40001: to 192.168.1.11:51514 in tcp 203.0.113.80:443 198.51.100.1:40002: drop (no mapping)
const lines = require("fs").readFileSync(0, "utf8").split("\n").map((l) => l.trim()).filter((l) => l !== "");
if (lines.length > 0) {
// public <ip> ports <first port to hand out>
const [, publicIp, , firstPort] = lines[0].split(/\s+/);
let nextPort = Number(firstPort);
const byInside = new Map(); // "proto inside-ip:port" -> mapping
const byPublic = new Map(); // "proto public-port" -> mapping or port forward
for (const line of lines.slice(1)) {
const [kind, proto, a, b] = line.split(/\s+/);
if (kind === "forward") {More lines in Code Lab: 16
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Question 1 of 5
Why is WPA3-Personal better than WPA2-Personal against password guessing?
Question 2 of 5
A firewall's first rule is `allow tcp any any 1-65535` and its second rule is `deny tcp 192.0.2.66/32 any 22`. What happens to SSH from 192.0.2.66?
Question 3 of 5
What does a stateful firewall do that a stateless packet filter cannot?
Question 4 of 5
A WireGuard peer `office` has AllowedIPs `192.168.50.0/24`, and peer `gateway` has `0.0.0.0/0`. A packet decrypted from `gateway` has the inner source address 192.168.50.9. What does WireGuard do?
Question 5 of 5
Where should a company's public web server go, and why?
Recap
- Wi-Fi (802.11) is a shared radio medium using CSMA/CA; everyone in range receives every frame. Use WPA3 (SAE, protected management frames) or at least WPA2 with a long passphrase, Enterprise mode (802.1X) for organisations, and switch WPS off. WEP and TKIP are broken.
- A VPN encrypts and encapsulates packets in a tunnel: IPsec with IKEv2, WireGuard with cryptokey routing, or TLS-based designs. Tunnels add overhead (60 or 80 bytes for WireGuard), so their MTU is smaller.
- Firewalls: first match wins, default deny. Stateless filters judge packets alone; stateful ones track connections and let replies back in.
- NAT maps many private addresses onto one public address and port range; it is not a security policy. Put public servers in a DMZ and segment the rest.
Practice next: log in to your own router, check that it uses WPA3 or WPA2 with AES, that WPS is off and that no port forwards are open that you do not need. On a Linux machine you own, read sudo nft list ruleset and explain each rule. Next lesson: network attacks and the defences against them.
Practice
Reading is not learning until you run something. Pick one.
Graded lab exercises
- Lab exercise 1GoNot passed yet Try in Code Lab
- Lab exercise 2PythonNot passed yet Try in Code Lab
- Lab exercise 3JavaScript (Node.js)Not passed yet Try in Code Lab