docs: janitorial cleanup checklist + L4D2 server cvar reference

Two follow-ups bundled into a single commit:

- docs/superpowers/specs/2026-05-15-janitorial-cleanup.md collects
  the "do later" small TODOs that surfaced across the recent idmap
  + consolidation work: dead cake-related artifacts, obsolete
  static systemd units in deploy/files/, the bubblewrap→systemd-run
  doc drift, stale gameserver-side idmap binds on un-checked
  instances, calendar reminder for SM 1.13 stable. Each item is
  small and self-contained.

- docs/l4d2-server-cvar-reference.md captures the research from
  the early-session L4D2 cvar deep-dive: tickrate sweet spots,
  nb_update_frequency cheat-protection + sm_cvar workaround,
  cvars that don't exist in L4D2 (net_maxcleartime,
  z_resolve_zombie_collision_multiplier per RCON probe), recommended
  plugins, MetaMod/SourceMod branch tracking, and the empirically-
  verified idmap-propagation-through-rebind kernel-6.12 quirk.
  Reference material, not a spec — lives at docs/ root.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-15 02:05:12 +02:00
parent a450491a90
commit e38b844978
No known key found for this signature in database
2 changed files with 477 additions and 0 deletions

View file

@ -0,0 +1,259 @@
# L4D2 server cvar reference
Working notes from the 2026-05 research session on best-practice
L4D2 dedicated server settings. Sources cited inline; some findings
verified empirically on the `left4.me` Trixie test server (kernel
6.12.86). This is reference material, not a settled design.
## Quick lookup
| Topic | Recommended |
|---|---|
| Tickrate (stock) | 30 |
| Tickrate (competitive) | 100, requires Tickrate Enabler plugin |
| `sv_pure` | `2` (strict), or `0`/`1` for modded servers |
| `sv_cheats` | `0` (set to 1 only on private practice servers; disables VAC) |
| `sv_consistency` | `0` (allow custom campaigns) or `1` (strict for competitive) |
| `sv_alltalk` | `0` (no cross-team voice), `1` for casual / fun servers |
| `sv_lan` | `0` (internet server) |
| `sv_voiceenable` | `1` |
| `nb_update_frequency` | `0.033` (safe, no plugin), `0.014` with the SM fix plugin. **Cheat-protected — must be set via `sm_cvar`.** |
| `fps_max` | `64` for 30-tick, `0` (uncapped) for higher ticks |
## Network rates
L4D2 default tickrate is **30**. Rates above the corresponding
ceiling are ignored without the
[Tickrate Enabler plugin](https://github.com/SirPlease/Server4Dead-Project/tree/master/Tickrate%20Enabler).
Rule of thumb: `sv_maxrate = tickrate × 1000`.
### 30-tick (stock)
```
sv_minrate 30000
sv_maxrate 30000
sv_mincmdrate 30
sv_maxcmdrate 30
sv_minupdaterate 30
sv_maxupdaterate 30
net_splitpacket_maxrate 30000
fps_max 64
```
### 60-tick (requires Tickrate Enabler)
```
sv_minrate 60000
sv_maxrate 60000
sv_mincmdrate 60
sv_maxcmdrate 60
sv_minupdaterate 60
sv_maxupdaterate 60
net_splitpacket_maxrate 60000
fps_max 128
```
### 100-tick (competitive, requires Tickrate Enabler)
```
sv_minrate 100000
sv_maxrate 100000
sv_mincmdrate 100
sv_maxcmdrate 100
sv_minupdaterate 100
sv_maxupdaterate 100
net_splitpacket_maxrate 100000
fps_max 0
```
### sv_min*rate vs. sv_max*rate
- Locking `min == max` (competitive servers do this) ensures every
client sends at the tickrate exactly. Strict — kicks clients
that dip below.
- Leaving a range (e.g. `min=10, max=30` on a 30-tick public
server) tolerates clients on weak connections or loaded CPUs.
- Setting `sv_mincmdrate=0` means *no enforced minimum* — clients
could send as few as 1-2 cmds/sec. Bad. Pick a floor that's
playable (~10 minimum).
## Cheat-protected cvars and `sm_cvar`
Several gameplay-affecting cvars are flagged as "cheat" in L4D2 and
**cannot be set via `server.cfg` unless `sv_cheats 1`** — which
disables VAC and gates achievements. Trying to set them from cfg
silently fails (the value stays at default).
To set them on a real (VAC-protected) server: install SourceMod and
use `sm_cvar <name> <value>` instead of `<name> <value>`. SourceMod
bypasses the cheat protection for *server-side cvar writes only*
(does not grant cheat commands to players).
Cheat-protected cvars worth knowing:
- `nb_update_frequency` — common-infected pathing/state update
rate (see below).
- `director_*` — most director cvars (AI difficulty, panic events,
pacing).
- `z_*` — most zombie-behavior cvars.
`sm_cvar` writes go in `cfg/sourcemod/sourcemod.cfg` (auto-execed
by SM on map change) or in any cfg under `cfg/sourcemod/`. SM
re-applies these on every map change — important because
cheat-protected cvars *reset to defaults on map change* even
within the same server session.
## `nb_update_frequency`
Controls how often common infected and witches recalculate their
pathfinding and state. Default `0.1` (10 Hz), independent of
server tickrate.
| Value | Effect |
|---|---|
| `0.1` (default) | Common-infected look choppy regardless of tickrate |
| `0.033` | ~30 Hz updates; smooth, safe without plugin |
| `0.024` | Lowest "safe" without plugin per community testing |
| `0.014` | ~71 Hz; clients with `cl_interp 0` see jittery commons unless the [nb_update_frequency fix plugin](https://forums.alliedmods.net/showthread.php?t=344019) is installed |
Set via `sm_cvar nb_update_frequency 0.033` in
`cfg/sourcemod/sourcemod.cfg` (or any sm-auto-execed cfg). Without
SourceMod, you cannot reliably set this on a VAC-protected server.
If commons still look choppy after lowering `nb_update_frequency`,
try `sm_cvar z_resolve_zombie_collision_multiplier 0.6` (default
`0.25`) — helps commons unstick from each other. Cheat-protected,
same `sm_cvar` mechanism.
## Cvars that DO NOT exist in L4D2 (despite some guides claiming otherwise)
These come up in older guides or are inherited from other Source
games but don't actually exist in L4D2's command set. Verified by
RCON `<cmd>` returning "unknown command":
- `net_maxcleartime` — CSGO/CS:S only.
- `z_resolve_zombie_collision_multiplier` — confirmed unknown in
current L4D2 builds (verified via RCON 2026-05-14). Some
community guides list it; it's not in the binary.
If a guide tells you to set one of these in L4D2, the guide is
wrong or out of date.
## Security and integrity
```
sv_cheats 0
sv_pure 2 # force Steam-only files (strictest)
sv_consistency 1 # enforce file hashes for critical files
# (set 0 if hosting custom campaigns)
sv_lan 0 # internet server
```
Launch the server with `-secure` to enable VAC. `sv_cheats 1`
requires `-insecure` (no VAC) — only acceptable on private
practice servers.
`sv_pure 2` breaks many workshop maps/mods. Use `sv_pure 0` or `1`
for modded servers.
## Player limits
```
# Co-op / Scavenge
sv_maxplayers 4
sv_visiblemaxplayers 4
# Versus
sv_maxplayers 8
sv_visiblemaxplayers 8
```
## Voice
```
sv_voiceenable 1
sv_alltalk 0 # 1 = cross-team voice (casual / fun servers)
```
## Recommended plugins (SourceMod ecosystem)
| Plugin | Purpose |
|---|---|
| MetaMod:Source + SourceMod | Required foundation for most of the below |
| [Tickrate Enabler](https://github.com/SirPlease/Server4Dead-Project/tree/master/Tickrate%20Enabler) | Unlock >30 tick servers |
| [Little Anti-Cheat](https://github.com/J-Tanzanite/Little-Anti-Cheat) | Aimbot / angle-cheat detection |
| SMAC | Secondary AC layer (older but still works) |
| [ZoneMod](https://github.com/SirPlease/L4D2-Competitive-Rework) | Competitive Versus ruleset (full bundle: ZoneMod + MatchMode + Confogl-style plugins) |
| `l4d2_TKStopper` | Teamkill / griefing control |
| `l4d_sb_fix` | Survivor bot behavior fixes |
| [nb_update_frequency fix](https://forums.alliedmods.net/showthread.php?t=344019) | Eliminates client-side jitter at very low `nb_update_frequency` values |
## MetaMod:Source / SourceMod versioning
- Stable branches are pinned in URL paths: `1.10`, `1.11`, `1.12`,
etc. There is no "latest stable" alias URL — you pick the
branch.
- Within a branch, the `mmsource-latest-linux` and
`sourcemod-latest-linux` text files contain the current build's
filename, e.g. `mmsource-1.12.0-git1219-linux.tar.gz`. Curl the
pointer file, then curl the actual tarball.
- AM bumps stable every ~2-3 years. When 1.13 (or later) is
declared stable, update the `MM_BRANCH=1.12` / `SM_BRANCH=1.12`
pins in the seeded Sourcemod overlay script.
- L4D2 has no special branch — it uses whatever the current
stable supports. L4D2's engine is so stable that SM 1.11 and
1.12 both work.
Watch for stable announcements at
[Metamod:Source news](https://www.sourcemm.net/) and
[SourceMod releases](https://github.com/alliedmodders/sourcemod/releases).
## Empirically-verified kernel quirk (relevant if you tweak the helpers)
Idmapped bind mounts on kernel 6.12 (Trixie) **do** propagate
through plain `mount --bind` re-binds. Verified end-to-end on
`left4.me` during the 2026-05-15 build-time-idmap refactor: a
sandbox process inside a re-bound idmapped mount can write files,
and those writes land on disk with the idmap-translated uid.
This contradicts some published claims (including a generic
research-agent summary) that idmaps don't propagate through plain
re-bind on this kernel. Our use case is `mount --bind --map-users
src staging` → systemd-run with `BindPaths=staging:/overlay` (a
plain re-bind into the unit's namespace). It works.
The `--map-users <a>:<b>:<count>` direction is **on-disk uid
first**, then in-mount uid. The util-linux man page calls these
`<inner>:<outer>` which is confusing — `<inner>` means "the
filesystem's native uid" (on disk) and `<outer>` means "the uid
exposed outward through the mount." Empirically verified; do not
trust the man page's word choice.
## Launch parameters (reference)
Typical srcds invocation:
```
./srcds_run -console -game left4dead2 -secure -autoupdate \
+maxplayers 8 -port 27015 +exec server.cfg +log on
```
- `-secure` enables VAC. Don't run public servers without it.
- `-autoupdate` keeps the server patched automatically.
- `+exec server.cfg` runs your config on startup.
- `-tickrate <N>` sets the engine tickrate (requires Tickrate
Enabler for `N > 30`).
## Sources
Primary references used for the recommendations above:
- [L4D2-Competitive-Rework server.cfg](https://github.com/SirPlease/L4D2-Competitive-Rework/blob/master/cfg/server.cfg)
- [L4D2 Dedicated Server Guide (Steam Community)](https://steamcommunity.com/sharedfiles/filedetails/?id=276173458)
- [L4D2 Dedicated Server Network Tweaks (Steam Discussions)](https://steamcommunity.com/app/550/discussions/1/1839063537784156851/)
- [SirPlease/Server4Dead-Project — Tickrate Enabler](https://github.com/SirPlease/Server4Dead-Project/tree/master/Tickrate%20Enabler)
- [Valve Developer Community — L4D2 console commands](https://developer.valvesoftware.com/wiki/List_of_Left_4_Dead_2_console_commands_and_variables)
- [AlliedModders — nb_update_frequency fix (Experimental)](https://forums.alliedmods.net/showthread.php?t=344019)
- [Source Multiplayer Networking — Valve Developer Community](https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking)
- [Required Versions (SourceMod wiki)](https://wiki.alliedmods.net/Required_Versions_(SourceMod))
- [MetaMod:Source news](https://www.sourcemm.net/)

View file

@ -0,0 +1,218 @@
# Janitorial cleanup checklist
**Status: TODO list, not a settled design.** Collects the "do later"
items that surfaced across multiple plans and handoffs during the
2026-05-14/15 idmap + consolidation work. Each is small and
self-contained. Knock them out individually or batch them into a
single janitorial PR. None are urgent — the project works fine with
all of these still present.
## Items
### 1. `left4me-apply-cake` — dead code
**What**: `deploy/files/usr/local/libexec/left4me/left4me-apply-cake`
(POSIX sh, ~47 lines) that applies/clears CAKE egress traffic
shaping via `tc`.
**Why dead**: CAKE migrated to systemd-networkd via
`network/<iface>/cake` metadata in ckn-bw's `bundles/network/`. The
service unit that invoked this helper
(`left4me-cake.service`) is also obsolete (see item 2). The script
is currently shipping to `/usr/local/libexec/left4me/` on every
`bw apply` via the install glob, but nothing on the system invokes
it.
**Action**: delete `deploy/files/usr/local/libexec/left4me/left4me-apply-cake`.
The deploy will stop installing it on next apply. Existing
deployed copy at `/usr/local/libexec/left4me/left4me-apply-cake` on
the test server can be `sudo rm`d at the same time.
**Verification**:
```
sudo find /var/lib/left4me /opt/left4me /usr/local -name 'left4me-apply-cake'
# expect: empty after the rm
```
### 2. Obsolete systemd unit files in `deploy/files/`
**What**:
- `deploy/files/usr/local/lib/systemd/system/left4me-cake.service`
- `deploy/files/usr/local/lib/systemd/system/left4me-nft-mark.service`
- `deploy/files/usr/local/lib/systemd/system/{left4me-web.service,left4me-server@.service,left4me-workshop-refresh.service,left4me-workshop-refresh.timer,l4d2-game.slice,l4d2-build.slice}`
**Why dead**: ckn-bw's `systemd_units` reactor in
`bundles/left4me/metadata.py` emits these units (and slices) from
metadata. The static files in `deploy/files/usr/local/lib/systemd/system/`
are not consulted by the deploy at all. They drifted out of sync
with the reactor-emitted versions (e.g. the reactor uses
`Slice=l4d2-game.slice` with current resource caps, the static file
might not). Currently kept as "greppable reference" per the
README's table; that's been the framing since the
historical-reference era.
**Action**: decide policy in concert with the deploy-dir-rethink
handoff. Either:
- **Delete them.** They're not the source of truth; the reactor
is. The README table loses a row but gains accuracy.
- **Keep them but stamp obsolete** somewhere visible (e.g. a
comment header in each file pointing at the reactor).
Recommendation: delete. The reactor output is what actually ships;
the static files are a footgun (someone might edit them thinking
they matter).
**Verification**: `find deploy/files/usr/local/lib/systemd/system -type f`
should match the README's "what's canonical" list.
### 3. `deploy/files/etc/left4me/cake.env`
**What**: env file referenced by the obsolete `left4me-cake.service`.
**Why dead**: bandwidth lives in node metadata under
`network/external/cake/Bandwidth` in ckn-bw. The env file is not
read by anything live.
**Action**: delete `deploy/files/etc/left4me/cake.env`.
### 4. `deploy/files/usr/local/lib/left4me/nft/`
**What**: nftables fragment for `left4me-nft-mark.service`.
**Why dead**: the central `bundles/nftables/` bundle consumes the
rules from `bundles/left4me/`'s defaults in ckn-bw. The static
fragment isn't read.
**Action**: delete `deploy/files/usr/local/lib/left4me/`
recursively.
### 5. `deploy-test-server.sh`'s fate
**What**: `deploy/deploy-test-server.sh`, the historical one-shot
bash deploy.
**Why ambiguous**: the deploy-dir-rethink doc
(`2026-05-15-deploy-dir-rethink-design.md`) calls this out as an
open decision. Three options listed there:
- Delete entirely (git history preserves the content).
- Relocate to `docs/` as a walkthrough, mark non-executable.
- Keep as-is with a louder warning header.
**Action**: pick one as part of the broader deploy-dir-rethink
work, or as an isolated decision now.
### 6. `bubblewrap` references in spec docs
**What**: `docs/superpowers/specs/2026-05-08-l4d2-script-overlays-design.md`
(if it still exists) describes the sandbox as using `bubblewrap`.
The actual implementation uses `systemd-run` with hardening
properties — no `bwrap` binary is invoked.
**Why misleading**: someone reading the spec would go looking for
`bwrap` in the helper and not find it.
**Action**: grep the specs for `bubblewrap` / `bwrap` and either
correct to `systemd-run` or delete the references. Drive-by
correction; no rationale needed beyond "matches reality."
### 7. Empty / unused `_sandbox_script_dir` after build-overlay-unit refactor
**What** (conditional on the build-overlay-unit refactor landing —
see `2026-05-15-build-overlay-unit-design.md`): if Option B in that
doc is chosen (unit fetches script from DB), the
`_sandbox_script_dir()` helper in
`l4d2web/services/overlay_builders.py` and the on-disk
`/var/lib/left4me/sandbox-scripts/` directory become unused.
**Action**: if that refactor lands, remove the helper function and
the dir. ckn-bw can stop creating the directory.
### 8. Legacy idmap binds on un-checked instances
**What**: server@2's stale idmap binds (from the idmap-on-mount
era) were manually cleaned during this session's verification.
Other server instances (`left4me-server@1`, …, if any are running
or have been recently) may still have orphan binds in PID 1's
mount namespace.
**Why**: the old helper had a `_is_mountpoint` bug that left binds
behind on stop. Our fix (`dd918ac`) cleaned the bug, but binds
created by the old version persist until manual cleanup or reboot.
**Action**: on the test server, run:
```bash
sudo findmnt --task 1 -o TARGET | grep '/var/lib/left4me/runtime/.*/idmap/'
```
For each result, `sudo umount` it and remove its parent
`runtime/<n>/idmap/` directory after all binds for that instance
are gone.
Alternative: schedule a host reboot. Reboot wipes the entire mount
table and gets everything clean in one step.
### 9. `Optimized Settings` files-overlay verification
**What**: overlay id 8 (`Optimized Settings`, type `files`) wasn't
included in the rebuild test during the build-time-idmap
verification. We only rebuilt the 5 script overlays.
**Why low-risk**: files overlays are populated by the web app
(uid `left4me`) directly via Python file ops, not through the
sandbox helper, so the idmap refactor doesn't touch them. But it's
worth a 30-second check.
**Action**: in the web UI, open overlay 8's detail page. Confirm
the files list renders correctly. (Or `sudo find
/var/lib/left4me/overlays/8 -type f -uid 981` should be empty.)
### 10. SourceMod 1.13 stable bump (calendar item, not a janitorial fix)
**What**: the example Sourcemod-overlay script in
`examples/script-overlays/Sourcemod.sh` pins `MM_BRANCH=1.12` and
`SM_BRANCH=1.12`. When AlliedModders declares 1.13 stable, the
seeded script needs updating.
**Why not now**: 1.12 is current stable as of 2026-05.
AlliedModders bumps stable every ~2-3 years.
**Action**: set a calendar reminder for late 2026 / early 2027 to
check
[the SourceMod releases page](https://github.com/alliedmodders/sourcemod/releases)
and bump the branch pin if 1.13 has been declared stable. When
bumping, also test the rebuild on a non-production server first;
plugin compat across major SM versions is occasionally non-clean.
## Suggested batching
Items 1, 3, 4 are tiny and self-contained — bundle into a single
"delete dead cake-related artifacts" commit.
Items 2 and 6 are deploy/spec cleanup — bundle with the broader
deploy-dir-rethink decision.
Items 5, 7 are conditional on other decisions — handle when the
prerequisite design choices are made.
Item 8 is an operational check, not a code change — run it once
and forget.
Item 9 is a 30-second verification, not a change.
Item 10 is a calendar reminder.
## Verification (after the bundle of items 1, 2, 3, 4 lands)
```
# nothing references the deleted artifacts
git grep -i 'apply-cake\|cake.env\|left4me-nft-mark\|left4me-cake' deploy/
# the deploy artifacts are pruned
find deploy/files -type f | sort
# bw apply still works
cd ~/Projekte/ckn-bw && bw apply ovh.left4me --interactive=no
```
If `bw apply` errors with a missing source path, the bundle
references one of the deleted files; fix the bundle reference
before pushing the deletion.