Homelab Academy / Modules
Guided practice + break/fix + lab gate.
Module: Download Clients & Indexers
Activity type: Lab (Practice)
Lab risk: low
Objective: Explain why an application bound to a VPN address is not automatically protected after that address or interface disappears.
This lab is a deterministic local simulation. It creates and changes files only under /opt/lab-classroom/class51/. It does not modify host routes, interfaces, DNS settings, VPN configuration, or active packet-filter rules.
1
Create the isolated lab directory.
mkdir -p /opt/lab-classroom/class51
2
Create /opt/lab-classroom/class51/policy_lab.py with the exact content supplied in the files section.
3
Run the simulation and write the deterministic report.
python3 /opt/lab-classroom/class51/policy_lab.py
4
Display the generated report without contacting any external service.
python3 -m json.tool /opt/lab-classroom/class51/report.json
5
Review why the healthy scenario passes all checks and why each negative scenario fails at least one specifically named control. Do not interpret this report as evidence about the host's real routes or VPN.
6
Copy the healthy scenario on paper or into notes and add separate fields for VPN endpoint, endpoint transport and port, approved LAN destinations, protected process identity, container scope, and reboot test status. This turns a simplistic model into the beginning of an operational test plan without changing the host.
/opt/lab-classroom/class51/policy_lab.py
import json
from pathlib import Path
base = Path('/opt/lab-classroom/class51')
report_path = base / 'report.json'
scenarios = [
{
'name': 'healthy',
'app_bind': '10.8.0.2',
'vpn_address': '10.8.0.2',
'default_path': 'vpn0',
'allowed_egress': ['vpn0'],
'dns_path': 'vpn0',
'ipv6_path': 'blocked'
},
{
'name': 'wildcard_binding',
'app_bind': '0.0.0.0',
'vpn_address': '10.8.0.2',
'default_path': 'vpn0',
'allowed_egress': ['vpn0'],
'dns_path': 'vpn0',
'ipv6_path': 'blocked'
},
{
'name': 'route_leak',
'app_bind': '10.8.0.2',
'vpn_address': '10.8.0.2',
'default_path': 'wan0',
'allowed_egress': ['vpn0'],
'dns_path': 'vpn0',
'ipv6_path': 'blocked'
},
{
'name': 'kill_switch_failure',
'app_bind': '10.8.0.2',
'vpn_address': '10.8.0.2',
'default_path': 'vpn0',
'allowed_egress': ['vpn0', 'wan0'],
'dns_path': 'vpn0',
'ipv6_path': 'blocked'
},
{
'name': 'dns_leak',
'app_bind': '10.8.0.2',
'vpn_address': '10.8.0.2',
'default_path': 'vpn0',
'allowed_egress': ['vpn0'],
'dns_path': 'wan0',
'ipv6_path': 'blocked'
},
{
'name': 'ipv6_leak',
'app_bind': '10.8.0.2',
'vpn_address': '10.8.0.2',
'default_path': 'vpn0',
'allowed_egress': ['vpn0'],
'dns_path': 'vpn0',
'ipv6_path': 'wan0'
}
]
def evaluate(item):
checks = {
'application_bound_to_vpn_address': item['app_bind'] == item['vpn_address'],
'default_path_uses_vpn': item['default_path'] == 'vpn0',
'protected_egress_excludes_wan': 'wan0' not in item['allowed_egress'],
'dns_uses_vpn': item['dns_path'] == 'vpn0',
'ipv6_is_tunneled_or_blocked': item['ipv6_path'] in ('vpn0', 'blocked')
}
return {
'name': item['name'],
'checks': checks,
'pass': all(checks.values()),
'failed_checks': [name for name, result in checks.items() if not result]
}
results = [evaluate(item) for item in scenarios]
report = {
'notice': 'Simulation only; this is not evidence about the host network.',
'scenario_count': len(results),
'passing_scenarios': [item['name'] for item in results if item['pass']],
'failing_scenarios': [item['name'] for item in results if not item['pass']],
'results': results
}
base.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
print(json.dumps(report, indent=2))
Do not add or remove host routes.
Do not bring interfaces up or down.
Do not change system DNS configuration.
Do not apply active packet-filter rules.
Do not send requests to external IP-check or DNS-leak services from this classroom lab.
All persistent lab mutations must remain under /opt/lab-classroom/class51/.
| Symptom | Likely cause | Fix |
|---|---|---|
| The mkdir command reports permission denied. | The current account cannot create directories beneath /opt/lab-classroom/. | Use the classroom account or approved privilege procedure to create only /opt/lab-classroom/class51/. Do not redirect the lesson into unrelated system directories. |
| python3 is not found. | Python 3 is not installed or is not present in the current PATH. | Install Python 3 through the operating system's trusted package-management process, or run the lab on a compatible classroom host. Do not download and execute an unreviewed installer. |
| The script reports a syntax error. | The file content was copied incompletely or smart quotes replaced ordinary ASCII quotation marks. | Replace /opt/lab-classroom/class51/policy_lab.py with the exact supplied content and run it again. |
| More than one scenario passes. | The scenario data or evaluation expressions were edited. | Restore the supplied script, regenerate report.json, and rerun every verification command. |
| A real application still reaches the Internet after the VPN is disconnected. | Application binding was treated as the only control, an uplink egress allowance is too broad, or traffic is using another protocol family or namespace. | Reconnect through a safe management path, collect route and packet evidence, inspect IPv4 and IPv6 independently, inspect container or virtual-machine networking, and correct the fail-closed host policy before retesting. |
| The VPN cannot reconnect after a kill switch is enabled. | The control-plane exception does not permit the VPN endpoint, transport, port, required bootstrap name resolution, or current endpoint address. | Use console access to restore the known-good policy, validate the endpoint requirements, and add only the narrow exception required for tunnel establishment. |
| DNS fails while direct IP connectivity through the VPN works. | The approved resolver is unreachable through the tunnel, resolver routing differs from application routing, or all resolver traffic was denied without a replacement. | Inspect the active resolver configuration and route to the resolver. Authorize only the intended resolver path and test both UDP and TCP DNS behavior where applicable. |
| IPv4 tests pass but an application still exposes the uplink address. | The application is using IPv6 through the physical interface. | Provide IPv6 through the VPN or explicitly contain IPv6 for the protected scope, then test with an IPv6-capable destination. |
A connected VPN indicator is not proof that all traffic uses the VPN.
Never deploy a new kill switch remotely without console or out-of-band recovery access.
Use an allowlist for the VPN control plane instead of allowing arbitrary uplink traffic.
Account for endpoint address changes when a VPN provider uses hostnames or rotating addresses.
Do not expose VPN credentials, private keys, preshared keys, cookies, or provider tokens in packet captures or lesson artifacts.
Treat IPv4 and IPv6 as separate paths requiring separate evidence.
Verify DNS over both UDP and TCP where relevant, and account for encrypted DNS configured inside individual applications.
Inspect containers, virtual machines, and separate network namespaces independently from the host.
Distinguish inbound listener exposure from outbound egress protection; a safe listener bind does not prove safe outbound behavior.
Preserve a known-good policy and rollback procedure before applying active packet-filter changes.
Review local-network exceptions carefully because broad private-network allowances can expose traffic on untrusted Wi-Fi or overlapping networks.
Use documentation address ranges in examples and never assume an example address belongs to the student's VPN provider.
Stop any editor or Python process using files beneath /opt/lab-classroom/class51/.
Delete /opt/lab-classroom/class51/policy_lab.py if the classroom requires individual artifact removal.
Delete /opt/lab-classroom/class51/report.json if the classroom requires individual artifact removal.
Remove /opt/lab-classroom/class51/ only after confirming it contains no files that must be retained.
No route, interface, DNS, VPN, or active packet-filter rollback is required because the lab never changes those components.
Obtain console or out-of-band access before changing enforcement.
Export and validate the current active policy using the platform's supported mechanism.
Schedule an automatic recovery action where operationally appropriate and cancel it only after validation.
Apply changes in a test environment before the production host.
If management connectivity is lost, restore the previously verified policy from the console rather than adding broad emergency allowances.
After rollback, verify management access, VPN establishment, DNS, IPv4, IPv6, and protected-application behavior.