Homelab Academy / Modules
Guided practice + break/fix + lab gate.
Module: Download Clients & Indexers
Activity type: Lab (Practice)
Lab risk: low
Objective: Differentiate a Usenet indexer, Usenet provider, torrent indexer, BitTorrent tracker, download client, and automation application.
Classify synthetic discovery and transfer services
Create a local service catalog, classify each component, and generate a policy report without making any network requests.
Every file created, modified, or removed by this lab is under /opt/lab-classroom/class53/. The supplied evaluator uses only Python standard-library modules and does not contact external systems.
1
Create the isolated lab and output directories.
mkdir -p /opt/lab-classroom/class53/output
2
Create a synthetic catalog. The names describe fictional lab components and are not provider recommendations.
cat > /opt/lab-classroom/class53/catalog.json <<'EOF'
[
{
"name": "Synthetic Usenet Catalog",
"role": "usenet_indexer",
"transport": "https",
"authentication": "api_key",
"returns": "nzb",
"authorized_content_only": true
},
{
"name": "Synthetic NNTP Access",
"role": "usenet_provider",
"transport": "nntps",
"authentication": "account_secret",
"returns": "article_bodies",
"authorized_content_only": true
},
{
"name": "Synthetic Torrent Catalog",
"role": "torrent_indexer",
"transport": "https",
"authentication": "api_key",
"returns": "torrent_metadata",
"authorized_content_only": true
},
{
"name": "Synthetic Peer Coordinator",
"role": "torrent_tracker",
"transport": "https",
"authentication": "none",
"returns": "peer_information",
"authorized_content_only": true
}
]
EOF
3
Create the offline evaluator. It validates allowed roles, checks transport and policy fields, and records the dependency associated with each role.
cat > /opt/lab-classroom/class53/evaluate.py <<'PY'
import json
from pathlib import Path
root = Path('/opt/lab-classroom/class53')
source = root / 'catalog.json'
destination = root / 'output' / 'recommendations.json'
rules = {
'usenet_indexer': {
'expected_return': 'nzb',
'dependency': 'usenet_provider and usenet_download_client',
'purpose': 'discovery'
},
'usenet_provider': {
'expected_return': 'article_bodies',
'dependency': 'usenet_download_client',
'purpose': 'transfer infrastructure'
},
'torrent_indexer': {
'expected_return': 'torrent_metadata',
'dependency': 'bittorrent_client',
'purpose': 'discovery'
},
'torrent_tracker': {
'expected_return': 'peer_information',
'dependency': 'bittorrent_client and participating peers',
'purpose': 'peer coordination'
}
}
records = json.loads(source.read_text(encoding='utf-8'))
report = []
for record in records:
role = record.get('role')
findings = []
rule = rules.get(role)
if rule is None:
findings.append('unknown role')
else:
if record.get('returns') != rule['expected_return']:
findings.append('returned artifact does not match role')
if record.get('transport') not in {'https', 'nntps'}:
findings.append('encrypted transport not declared')
if record.get('authorized_content_only') is not True:
findings.append('authorization policy not affirmed')
report.append({
'name': record.get('name'),
'role': role,
'purpose': rule['purpose'] if rule else 'unknown',
'dependency': rule['dependency'] if rule else 'manual review',
'status': 'pass' if not findings else 'review',
'findings': findings
})
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
print(f'Wrote {len(report)} classified records to {destination}')
PY
4
Run the evaluator. It reads and writes only within the class directory.
python3 /opt/lab-classroom/class53/evaluate.py
5
Inspect the generated policy report.
python3 -m json.tool /opt/lab-classroom/class53/output/recommendations.json
6
Confirm that each synthetic component passed and that discovery roles are distinct from transfer or coordination roles.
python3 -c "import json; from pathlib import Path; p=Path('/opt/lab-classroom/class53/output/recommendations.json'); data=json.loads(p.read_text()); assert len(data)==4; assert all(x['status']=='pass' for x in data); assert {x['purpose'] for x in data}=={'discovery','transfer infrastructure','peer coordination'}; print('Class 53 verification passed')"
Make one reversible edit inside catalog.json, such as changing the synthetic tracker transport from https to http, rerun the evaluator, and observe that its status becomes review. Restore https and rerun the verification command. Do not add real service addresses or credentials.
| Symptom | Likely cause | Fix |
|---|---|---|
| mkdir reports permission denied. | The current lab account is not allowed to create directories under /opt/lab-classroom. | Use the classroom account or environment that has been delegated access to /opt/lab-classroom. Do not redirect the exercise to an unrelated production path. |
| python3 is not found. | Python 3 is absent from the lab image or is not available through the current command search path. | Use a compatible lab image with Python 3 preinstalled. Do not install system packages as part of this exercise because that would mutate locations outside the permitted class directory. |
| The evaluator reports invalid JSON. | The catalog heredoc was copied incompletely, contains a missing comma, or contains typographic quotation marks. | Recreate catalog.json exactly as shown and validate it with python3 -m json.tool before rerunning the evaluator. |
| A record has status review and the finding says encrypted transport not declared. | The transport field was changed to a value other than https or nntps. | Restore the synthetic transport appropriate to the role, then rerun evaluate.py. |
| A record has status review and the finding says returned artifact does not match role. | The service role and returns fields describe incompatible responsibilities. | Use nzb for usenet_indexer, article_bodies for usenet_provider, torrent_metadata for torrent_indexer, or peer_information for torrent_tracker. |
| A real automation application can search but cannot download. | Discovery succeeded, but the download client, provider authentication, peer discovery, category mapping, or transfer path failed. | Test each boundary separately. Confirm the client API, then provider or peer connectivity, then category and path mapping. Do not treat a successful search as proof of transfer availability. |
| Usenet jobs fail with missing articles. | The selected provider may not have every referenced article, or the NZB may describe an incomplete posting. | Confirm that the metadata is valid and that the content is authorized. Review provider availability and the client's completion logs without exposing credentials. |
| A torrent remains idle even though metadata was accepted. | No reachable peers are available, tracker communication failed, DHT is unavailable or disallowed, or the torrent is no longer active. | Inspect client status and peer-discovery messages. Metadata acceptance proves only that the client understood the job; it does not guarantee available peers. |
Store indexer, provider, and client API secrets in the application's secret mechanism rather than in shared examples or source control.
Use different credentials for different trust boundaries when the service supports that model.
Rotate any credential exposed in logs, screenshots, shell history, or support bundles.
Redact query strings and headers because some APIs place tokens in URLs or authorization headers.
Prefer encrypted HTTPS for indexer and client APIs.
Prefer NNTP protected by TLS when communicating with a Usenet provider.
Validate certificates rather than disabling certificate verification to conceal a trust problem.
Understand that BitTorrent peers can observe participating peer addresses even when control-plane services use HTTPS.
Do not publish download-client administration interfaces directly to untrusted networks.
Require authentication on local service APIs and restrict which automation applications may reach them.
Separate untrusted download staging from curated libraries.
Treat imported archives, media containers, subtitles, scripts, and metadata as untrusted input.
Use indexers, providers, trackers, and peers only for material you are authorized to access or distribute. Encryption, private membership, or a paid subscription does not replace authorization and does not override service terms or applicable law.
The lab uses fictional records, performs no network operations, contains no real credentials, and confines all mutations to /opt/lab-classroom/class53/.
Remove only the files and directories created by this class.
Inspect the target with: find /opt/lab-classroom/class53 -maxdepth 2 -print
python3 -c "from pathlib import Path; root=Path('/opt/lab-classroom/class53').resolve(); expected=Path('/opt/lab-classroom/class53'); assert root==expected; files=[root/'output/recommendations.json',root/'evaluate.py',root/'catalog.json']; [p.unlink() for p in files if p.is_file()]; out=root/'output'; out.rmdir() if out.exists() and not any(out.iterdir()) else None; root.rmdir() if root.exists() and not any(root.iterdir()) else None"
Run: test ! -e /opt/lab-classroom/class53 && echo 'Class 53 artifacts removed' || find /opt/lab-classroom/class53 -maxdepth 2 -print
The cleanup intentionally stops if unexpected content prevents a directory from being removed. Inspect unexpected content rather than broadening the deletion scope.