Build details, gotchas, and implementation notes that are not obvious from the code. Routes and data models live in remote-monitor.md and ctfd_api.md; this file does not repeat them.
Use the Nix devShell for every cargo command:
nix develop .# --command cargo build --release \
--target x86_64-unknown-linux-musl -p nervctf -p remote-monitor
nix develop .# --command cargo testThe shell provides rustc, cargo, rustfmt, clippy, pkg-config, OpenSSL, and Ansible. Verify with the musl release build, not a native debug build.
Each cross-stdenv sets CC globally, and the last one wins. Pin every target through CC_<triple> and reset CC to native gcc in the shell hook; CC_x86_64_unknown_linux_gnu is mandatory. Without it, ring and cc-rs emit COFF objects for ELF targets.
For Windows, inject libpthread.a via NIX_LDFLAGS_x86_64_w64_mingw32. RUSTFLAGS -L and -C link-arg do not reach the external cross-linker.
Precedence: CLI flags > MONITOR_URL / MONITOR_TOKEN > .nervctf.yml. load_config walks up from the current directory; save_config writes mode 0600 because the file can hold the admin password.
- Challenge visibility must be Public. Private mode blocks even valid API tokens from
GET /api/v1/challenges. - The uploads directory needs
chown -R 1001:1001 <CTFd>/.data/CTFd/uploads, orPOST /api/v1/filesreturns 500. - Upload every file for a challenge in one multipart request (multiple
fileparts). One request per file returns 500. GET /api/v1/challengespaginates at 20.get_challenges()followsmeta.pagination.next; without that, re-deploys duplicate challenges.
- CTFd builds have shipped both
configsandconfigas the settings table.ctfd_db::config_table()checksinformation_schema; the plugin uses the ORM. - Some builds store passwords with passlib
bcrypt_sha256($bcrypt-sha256$...) and hash on assignment through theUsers.passwordsetter._maybe_complete_setup()probes a transientUsersinstance and either passes plaintext or hashes withbcrypt_sha256. Configsin some builds has notypeattribute. The plugin sets onlykeyandvalue.challenges.next_idand the inline scoring columns are absent on some versions;ctfd_db.rsdetects them and substitutes NULL/omits them. See the compatibility matrix inctfd-dependency-audit.md.
- Instance names are
ctf-<sanitized>-<6 random chars>; the random suffix prevents orphan-checker races. pick_free_portsallocates atomically in 40000–60000 from theallocatedset and the DB's used ports, so ports within one instance never collide.extra_portsisNonefor single-port and{"<internal>": <host>}for multi-port.- Compose
up()receives fully resolvedservice_mappings: &HashMap<String, Vec<(u16, u32)>>;compose.rsdoes not parse the config format. CHALLENGES_BASE_DIRmust be identical on the host and in the monitor container. Challenge compose bind mounts use the host path.- The monitor writes lifecycle rows to
events; flag submissions stay inflag_attemptsto avoid a second write on the hot path. - The upgrade playbook recreates the whole Compose stack, not just
ctfd. A restart leaves the old upstream IP in nginx and produces 502s.
setup.rs finds the plugin next to the executable, in the workspace, or from the embedded copy (extracted to $XDG_CACHE_HOME/nervctf/ctfd-plugin). It generates the monitor token from /dev/urandom, resolves CTFd's published HTTP port from Compose, runs the preflight, and writes .nervctf.yml before provisioning. It prompts for the first admin and passes NERVCTF_ADMIN_* / NERVCTF_EVENT_* to the playbooks.
See git log for historical changes.