Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pwn.college Dojo — Stress Test Suite

A standalone stress tester for single-node pwn.college dojo deployments. Measures concurrent challenge container capacity, infrastructure limits, and CTFd database performance under load.


Files

File Purpose
dojo_stress.py Main stress tester
register_users.py Bulk-register test accounts via the web form
README.md This file

Requirements

pip install aiohttp

That is the only dependency. Everything else is Python stdlib.


Quick Start

# Get your dojo IP
export DOJO_CONTAINER=dojo
export DOJO_IP=$(docker inspect dojo | jq -r '.[0].NetworkSettings.Networks.bridge.IPAddress')

# Run smoke test first (1 user, ~10 seconds)
python dojo_stress.py \
  --url "http://localhost.pwn.college" \
  --dojo YOUR_DOJO_SLUG \
  --module YOUR_MODULE_SLUG \
  --challenge YOUR_CHALLENGE_SLUG \
  --admin-user admin \
  --admin-pass admin \
  --scenarios smoke

Setup Checklist

Before running any stress test, verify all of these pass.

1. Dojo is fully up

docker exec dojo dojo logs
docker exec dojo dojo compose ps

All services (db, cache, ctfd, nginx, sshd, homefs, workspacefs, prometheus, grafana) should show as Up.

2. HTTP is reachable

curl -s -o /dev/null -w "%{http_code}" http://localhost.pwn.college/
# Should return 200 or 302

3. Infra probes work

# PostgreSQL
docker exec dojo docker exec db psql -U ctfd ctfd -c \
  "SELECT count(*) FROM pg_stat_activity WHERE state='active';"

# Redis
docker exec dojo docker exec cache redis-cli INFO memory | grep used_memory_human

# btrfs
docker exec dojo btrfs filesystem usage /data/homes

4. Find your dojo/module/challenge slugs

Open http://localhost.pwn.college in a browser, navigate to a challenge, and read the URL:

http://localhost.pwn.college/DOJO_SLUG/MODULE_SLUG/CHALLENGE_SLUG

Or query the database directly:

docker exec dojo docker exec db psql -U ctfd ctfd -c \
  "SELECT id, name FROM dojos;"

5. Make the dojo accessible to test users

The dojo must be marked official OR users must be joined to it. The easiest fix for a test environment:

# Mark official (all users can access without joining)
docker exec dojo docker exec db psql -U ctfd ctfd -c \
  "UPDATE dojos SET official=true WHERE id='YOUR_DOJO_SLUG';"

6. Register test users

The web registration form has a CTFd rate limit of 10 requests per 60 seconds per IP. Use the curl loop below with a 2-second delay between registrations:

COMMITMENT="I have read the ground rules and commit to not publish pwn.college writeups on the internet."

for i in $(seq 1 20); do
  NONCE=$(curl -s -c /tmp/reg_$i.txt http://localhost.pwn.college/register \
    | grep -oP "'csrfNonce': \"\K[^\"]+")
  curl -s -c /tmp/reg_$i.txt -b /tmp/reg_$i.txt \
    -d "name=testuser${i}&email=testuser${i}@stress.test&password=testpass${i}&_submit=Submit&nonce=${NONCE}&commitment_verified=I+have+read+the+ground+rules+and+commit+to+not+publish+pwn.college+writeups+on+the+internet." \
    -L http://localhost.pwn.college/register -o /dev/null
  echo "Registered testuser${i}"
  sleep 2
done

Then promote to admin and join the dojo:

docker exec dojo docker exec db psql -U ctfd ctfd -c \
  "UPDATE users SET type='admin' WHERE name LIKE 'testuser%';"

docker exec dojo docker exec db psql -U ctfd ctfd -c "
INSERT INTO dojo_users (dojo_id, user_id, type)
SELECT (SELECT dojo_id FROM dojos WHERE id='YOUR_DOJO_SLUG'), id, 'student'
FROM users WHERE name LIKE 'testuser%'
ON CONFLICT DO NOTHING;"

Verify:

docker exec dojo docker exec db psql -U ctfd ctfd -c \
  "SELECT count(*) FROM users WHERE name LIKE 'testuser%';"

Running the Stress Test

Standard run (recommended starting point)

python dojo_stress.py \
  --url "http://localhost.pwn.college" \
  --dojo example \
  --module world \
  --challenge earth \
  --admin-user admin \
  --admin-pass admin \
  --user-prefix testuser \
  --pass-prefix testpass \
  --scenarios smoke baseline ramp_10 spike_10 repeat_10

All flags

Flag Default Description
--url http://localhost.pwn.college Dojo base URL
--dojo example Dojo slug
--module world Module slug
--challenge earth Challenge slug
--admin-user admin Admin username (used for DB load phase)
--admin-pass admin Admin password
--user-prefix If set, workers log in as prefix1, prefix2, ...
--pass-prefix Password prefix matched to user index
--scenarios all Space-separated list of scenarios to run
--hold-secs 3.0 How long each worker holds its container open
--cooldown-secs 15 Seconds between scenarios
--db-workers auto Concurrent workers for DB load phase
--db-reqs 20 Requests per DB load worker
--dojo-container dojo Name of the outer Docker container
--output ./results Output directory for reports

Scenarios

Scenario Users Mode What it tests
smoke 1 single Sanity check — does one container start?
baseline 5 simultaneous Light concurrent load, reference numbers
ramp_10 10 0.5s apart Gradual load, tests staggered arrival
spike_10 10 simultaneous Hard spike, tests peak concurrent capacity
repeat_10 10 simultaneous Recovery test — does system bounce back?

How it works

Each scenario runs three phases:

Phase 1 — Pre-authentication

All N workers log in simultaneously but staggered by 0.15 seconds to stay under CTFd's login rate limiter (10 requests per 60 seconds per IP, discovered during testing). Sessions and CSRF tokens are cached. Workers that fail login are recorded as login_failed and excluded from Phase 2.

Phase 2 — Container lifecycle

All authenticated workers fire container starts simultaneously. Each worker:

  1. POSTs to /pwncollege_api/v1/docker to start a challenge container
  2. Holds the container open for --hold-secs seconds
  3. DELETEs /pwncollege_api/v1/docker to stop and remove it

Measures spin-up latency (POST → 200 OK) and teardown latency per user.

Phase 3 — CTFd DB load

After all containers close, N concurrent sessions hammer CTFd's read-only API endpoints to measure database and cache performance under load:

  • /api/v1/scoreboard
  • /api/v1/challenges
  • /api/v1/users?page=1
  • /api/v1/statistics/users
  • /api/v1/statistics/submissions

Background — Infrastructure probes

Every 4 seconds throughout Phase 2, the test probes the inner infrastructure:

What Command used
PostgreSQL active connections dojo dbpg_stat_activity
PostgreSQL db size dojo dbpg_database_size
Redis memory + ops/sec docker exec cache redis-cli INFO
btrfs free space btrfs filesystem usage /data/homes
btrfs subvolume count btrfs subvolume list /data/homes
Workspace container count docker ps --filter label=pwn.college.user
Prometheus health curl http://prometheus:9090/-/ready

Output

After each run, ./results/ contains:

results/
├── stress_TIMESTAMP.json    # full per-user timings + infra timeline
└── stress_TIMESTAMP.txt     # detailed report + summary table

The txt report includes per-user spin-up and teardown times, infra timeline sampled every 4 seconds, DB latency broken down by endpoint, and a summary table across all scenarios.


Failure taxonomy

Error tag Meaning
login_failed CTFd rejected the login — rate limited or wrong credentials
forbidden 403 from the docker API — user not joined to dojo
rate_limited 429 — CTFd rate limit on container start endpoint
server_500:<msg> Inner Docker daemon error — OOM, OverlayFS, btrfs issue
http_<N> Unexpected HTTP status from the API
timeout_90s Container didn't start within 90 seconds
conn_refused nginx or CTFd process died

Known bottlenecks (single-node)

CTFd login rate limiter — first wall at scale

Hard cap of 10 logins per 60 seconds per IP. At 20+ concurrent users, ~half get rate-limited before any container starts. This is a CTFd configuration issue, not a container capacity issue. To disable for testing:

# Find the rate limit decorator
docker exec dojo docker exec ctfd grep -n "ratelimit" \
  /opt/CTFd/CTFd/views/auth.py | head -5

# Comment it out, then restart CTFd
docker exec dojo dojo compose restart ctfd

btrfs subvolume contention — visible at 10+ simultaneous starts

Each container start creates a btrfs subvolume under /data/homes/ for the user's home directory. At 10 simultaneous starts, the btrfs driver serializes some operations causing spin-up to climb from ~3s (idle) to ~10s (10 users). This is the primary container-layer bottleneck. Staggering starts (ramp mode) reduces this significantly.

PostgreSQL connection pool — headroom at current scale

pgbouncer handles connection pooling. At 10 concurrent users the pool shows 1-2 active connections — nowhere near saturation. Would become a bottleneck at 50+ concurrent users making simultaneous DB-heavy requests.

Redis — not a bottleneck at current scale

Memory stays flat at ~2.7MB across all scenarios. The rate limiter itself is Redis-backed (ops/sec spikes when login rate limiting fires) but the cache layer has enormous headroom.

Host memory — not yet tested

Each workspace container runs code-server (~150MB RSS) plus the challenge binary. At 10 concurrent containers that's ~1.5GB just for workspaces. Becomes relevant at 20-30+ concurrent containers on an 8GB host.


Results from live testing (single node, WSL2, example dojo)

Scenario Users OK% p50 spin p50 teardown DB p50
smoke 1 100% 3.05s 3.78s 0.035s
baseline 5 100% 6.33s 4.43s 0.158s
ramp_10 10 100% 5.40s 3.96s 0.285s
spike_10 10 100% 10.52s 4.12s 0.296s
repeat_10 10 100% 6.61s 3.83s 0.342s

Key takeaway: This single-node deployment handles 10 concurrent challenge starts with 100% success rate. Spin-up degrades from 3s (idle) to 10.5s (10 simultaneous) due to btrfs contention, but the system is self-healing — after a 15s cooldown, performance recovers to 6.6s p50.


Useful dojo commands during testing

# Watch CTFd logs live
docker exec dojo docker logs ctfd -f 2>&1 | grep -v "GET /"

# Check all inner services
docker exec dojo dojo compose ps

# See running workspace containers
docker exec dojo docker ps --format "table {{.Names}}\t{{.Status}}"

# Query DB directly
docker exec dojo docker exec db psql -U ctfd ctfd -c \
  "SELECT name, type FROM users LIMIT 10;"

# Check btrfs subvolume count
docker exec dojo btrfs subvolume list /data/homes | wc -l

# Check inner docker stats
docker exec dojo docker stats --no-stream

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages