Reference
Technical Appendix
Commands only. What each step does, why it is built this way, and the judgment behind it are in the book.
What's here
Setting up WSL2 and Docker on Windows
Step 1, Open PowerShell as Administrator
> $id = [Security.Principal.WindowsIdentity]::GetCurrent() > $me = New-Object Security.Principal.WindowsPrincipal($id) > $me.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Expected
True
A simpler check
> whoami /groups | Select-String 'Mandatory Level'
Elevated
Mandatory Label\High Mandatory Level Label S-1-16-12288
Not elevated
Mandatory Label\Medium Mandatory Level Label S-1-16-8192
Step 2, Confirm virtualization is available
> $cs = Get-CimInstance Win32_ComputerSystem > $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 > [PSCustomObject]@{ HypervisorPresent = $cs.HypervisorPresent FirmwareVirt = $cpu.VirtualizationFirmwareEnabled SLAT = $cpu.SecondLevelAddressTranslationExtensions } | Format-List
Result A, a hypervisor is already running (most common)
HypervisorPresent : True FirmwareVirt : False SLAT : False
Result B, no hypervisor yet, hardware ready
HypervisorPresent : False FirmwareVirt : True SLAT : True
Result C, needs fixing in firmware
HypervisorPresent : False FirmwareVirt : False SLAT : True
Step 2b, Check the Windows features
> $id = [Security.Principal.WindowsIdentity]::GetCurrent() > $me = New-Object Security.Principal.WindowsPrincipal($id) > $admin = $me.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) > if (-not $admin) { Write-Warning 'Not elevated. Reopen as Administrator, then retry.' } else { 'VirtualMachinePlatform','Microsoft-Windows-Subsystem-Linux' | ForEach-Object { Get-WindowsOptionalFeature -Online -FeatureName $_ | Select-Object FeatureName, State } }
Expected, Disabled is normal at this point
FeatureName State ----------- ----- VirtualMachinePlatform Enabled Microsoft-Windows-Subsystem-Linux Disabled
Step 3, Install WSL2 and Ubuntu
> wsl --install -d Ubuntu-24.04 > wsl --set-default-version 2
Step 4, Verify you're on version 2
> wsl --update > wsl --list --verbose > wsl --version
Expected
NAME STATE VERSION * Ubuntu-24.04 Running 2 WSL version: 2.3.26.0 Kernel version: 5.15.167.4-1 Windows version: 10.0.26100.2605
Step 4b, Name the machine and the user
One file controls all of it
$ sudo tee /etc/wsl.conf > /dev/null <<'EOF' [boot] systemd = true [network] hostname = darkweb-lab [user] default = darkweb EOF $ cat /etc/wsl.conf
> wsl --shutdown # wait ~10 seconds, then reopen Ubuntu
$ hostname $ whoami
Expected
darkweb-lab darkweb
Changing the user account
$ sudo adduser darkweb $ sudo usermod -aG sudo darkweb # then set default = darkweb in /etc/wsl.conf and shut down
> wsl --shutdown > wsl -d Ubuntu-24.04 -u root
# usermod -l darkweb -d /home/darkweb -m oldname # groupmod -n darkweb oldname # id darkweb
Step 5, Allocate resources
> notepad "$env:USERPROFILE\.wslconfig"
[wsl2] memory=12GB processors=4 swap=4GB localhostForwarding=true # Docker pulls fail without this — see the note below kernelCommandLine = ipv6.disable=1
> wsl --shutdown # wait ~10 seconds, then reopen Ubuntu
Step 6, Install Docker
# 1. Download and install Docker Desktop for Windows: # https://www.docker.com/products/docker-desktop/ # or, from this elevated PowerShell window: > winget install --id Docker.DockerDesktop --accept-source-agreements # 2. Settings → General → "Use the WSL 2 based engine" [checked] # 3. Settings → Resources → WSL Integration → Ubuntu-24.04 [enabled] # 4. Apply & Restart, then verify from inside Ubuntu: $ docker run --rm hello-world
$ sudo apt-get update $ sudo apt-get install -y ca-certificates curl $ sudo install -m 0755 -d /etc/apt/keyrings $ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc $ sudo chmod a+r /etc/apt/keyrings/docker.asc $ . /etc/os-release $ ARCH=$(dpkg --print-architecture) $ KEY=/etc/apt/keyrings/docker.asc $ REPO=https://download.docker.com/linux/ubuntu $ echo "deb [arch=$ARCH signed-by=$KEY] $REPO $VERSION_CODENAME stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null $ sudo apt-get update $ sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin # run docker without sudo — log out and back in afterwards $ sudo usermod -aG docker $USER
# you set this in Step 4b — verify rather than rewrite $ grep -A1 '\[boot\]' /etc/wsl.conf $ systemctl is-active docker
Step 7, Where the repository lives
# correct — the Linux filesystem $ cd ~ && pwd /home/darkweb # wrong — this is your Windows drive, mounted over a network protocol # /mnt/c/Users/you/darkweblabs
# one helper, so both tests are identical $ bench() { time (mkdir -p "$1" && for i in $(seq 1 2000); do echo x > "$1/$i"; done); } $ echo "--- Linux home ---" ; bench ~/fstest $ echo "--- Windows /mnt/c ---" ; bench /mnt/c/fstest $ rm -rf ~/fstest /mnt/c/fstest
Example run; your numbers will differ
--- Linux home --- real 0m0.098s user 0m0.011s sys 0m0.073s --- Windows /mnt/c --- real 0m4.275s user 0m0.031s sys 0m0.353s
A, Moving the lab to another drive
> Get-PSDrive -PSProvider FileSystem |
Select-Object Name,
@{n='FreeGB';e={[math]::Round($_.Free/1GB,1)}},
@{n='UsedGB';e={[math]::Round($_.Used/1GB,1)}}
Example
Name FreeGB UsedGB ---- ------ ------ C 42.3 431.2 D 856.7 102.4
Method 1, move it in place
> wsl --shutdown > mkdir D:\wsl -Force > wsl --manage Ubuntu-24.04 --move D:\wsl\Ubuntu-24.04
Expected
The operation completed successfully.
> wsl --shutdown > wsl --list --running
What you need to see
There are no running distributions.
Method 2, export and re-import
> wsl --shutdown > mkdir D:\wsl -Force # 1. export the whole distribution to a tar archive > wsl --export Ubuntu-24.04 D:\wsl\ubuntu-backup.tar # 2. remove it from C: — the tar is your only copy at this point > wsl --unregister Ubuntu-24.04 # 3. re-import it onto the new drive > wsl --import Ubuntu-24.04 D:\wsl\Ubuntu-24.04 ` D:\wsl\ubuntu-backup.tar --version 2
After Method 2, check your user
> wsl --shutdown > wsl -d Ubuntu-24.04 whoami
Expected; your username, not root
yourusername
Docker's storage
Verify the move worked
> wsl --list --verbose > Get-ChildItem D:\wsl -Recurse -Filter *.vhdx | Select-Object FullName, @{n='SizeGB';e={[math]::Round($_.Length/1GB,2)}}
Expected
NAME STATE VERSION * Ubuntu-24.04 Stopped 2 FullName SizeGB -------- ------ D:\wsl\Ubuntu-24.04\ext4.vhdx 2.14
Exclude the lab from real-time scanning
> Add-MpPreference -ExclusionPath 'D:\wsl' > Add-MpPreference -ExclusionProcess 'wsl.exe' > Add-MpPreference -ExclusionProcess 'wslservice.exe' > Get-MpPreference | Select-Object -ExpandProperty ExclusionPath
> wsl -d Ubuntu-24.04 -- hostname > wsl --list --verbose
Expected
darkweb-lab NAME STATE VERSION * Ubuntu-24.04 Running 2
Running the lab from a USB drive
> wsl --shutdown > wsl --list --running > mkdir E:\wsl -Force > wsl --manage Ubuntu-24.04 --move E:\wsl\Ubuntu-24.04
Using the drive on a second machine
> wsl --import-in-place Ubuntu-24.04 ` E:\wsl\Ubuntu-24.04\ext4.vhdx > wsl -d Ubuntu-24.04
B, Git and GitHub configuration
$ git config --global user.name "Your Name" $ git config --global user.email "you@example.com" $ git config --global init.defaultBranch main $ git config --global core.autocrlf input $ git config --global pull.rebase false # confirm $ git config --global --list
Case A, HTTPS with a personal access token
# Option 1 — hold it in memory for an hour at a time $ git config --global credential.helper 'cache --timeout=3600' # Option 2 — reuse Windows Credential Manager from inside WSL (recommended) $ git config --global credential.helper \ "/mnt/c/Program Files/Git/mingw64/bin/git-credential-manager.exe"
# Username: your GitHub username # Password: paste the token — NOT your account password
Case B, SSH key
$ ssh-keygen -t ed25519 -C "darkweb-lab $(hostname)" # accept the default path: ~/.ssh/id_ed25519 # set a passphrase — leaving it empty means anyone with the file has the key $ chmod 700 ~/.ssh $ chmod 600 ~/.ssh/id_ed25519 $ chmod 644 ~/.ssh/id_ed25519.pub
$ eval "$(ssh-agent -s)" $ ssh-add ~/.ssh/id_ed25519 $ ssh-add -l
$ cat >> ~/.bashrc <<'EOF' # start ssh-agent once per WSL session and reuse it if [ -z "$SSH_AUTH_SOCK" ]; then eval "$(ssh-agent -s)" > /dev/null ssh-add ~/.ssh/id_ed25519 2> /dev/null fi EOF $ source ~/.bashrc
$ cat ~/.ssh/id_ed25519.pub
Expected, one line, starts with ssh-ed25519
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... darkweb-lab DESKTOP-4K2L9
$ ssh -T git@github.com
Expected
Hi yourusername! You've successfully authenticated, but GitHub does not provide shell access.
If your network blocks SSH
$ cat >> ~/.ssh/config <<'EOF' Host github.com Hostname ssh.github.com Port 443 User git IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes EOF $ chmod 600 ~/.ssh/config $ ssh -T git@github.com
Cloning, pushing, and switching remotes
$ cd ~ # Case A — HTTPS $ git clone https://github.com/toniall/darkweblabs.git # Case B — SSH $ git clone git@github.com:toniall/darkweblabs.git $ cd darkweblabs $ git remote -v
$ cd ~/my-new-project $ git init $ git add . $ git commit -m "Initial commit" $ git branch -M main $ git remote add origin git@github.com:toniall/my-new-project.git $ git push -u origin main
# HTTPS → SSH $ git remote set-url origin git@github.com:toniall/darkweblabs.git # SSH → HTTPS $ git remote set-url origin https://github.com/toniall/darkweblabs.git $ git remote -v
C, Running this site locally
Put it in place
$ cd ~ $ git clone https://github.com/toniall/darkweblabs.git # unpack the site package that came with the book into the clone $ unzip ~/Downloads/darkweb-public-site.zip -d darkweblabs/site $ cd darkweblabs/site && ls
Expected
assets chapters index.html labs.html LICENSE
Serve it, recommended
$ python3 -m http.server 8080
Expected
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
$ echo "alias site='python3 -m http.server 8080 \ --directory ~/darkweblabs/site'" >> ~/.bashrc $ source ~/.bashrc $ site
Or open the file directly
$ explorer.exe . # then double-click index.html (or labs.html to go straight to the labs)
D, Troubleshooting
Jump to
Windows and PowerShell
Confirming an elevated terminal
Checking the Windows features
WSL version and hostname
$ sudo sed -i "1i 127.0.1.1 $(hostname)" /etc/hosts $ head -3 /etc/hosts
Editing .wslconfig
Docker can't pull images
$ ./lab fix-ipv6 # follow the printed steps, then reopen Ubuntu $ ./lab up base
$ sudo systemctl restart docker $ ./lab pin-registry $ ./lab up base # once the base images are downloaded, undo it: $ ./lab unpin-registry
$ BASES="debian:bookworm-slim nginx:1.27-alpine \ kasmweb/core-ubuntu-jammy:1.16.0" $ docker pull $BASES $ docker save $BASES -o darkweb-bases.tar
$ ./lab load-images darkweb-bases.tar $ ./lab doctor # base images all present
Why the obvious fixes don't work
> notepad "$env:USERPROFILE\.wslconfig"
kernelCommandLine = ipv6.disable=1
> wsl --shutdown # wait ten seconds, then reopen Ubuntu
$ ip -6 addr $ ./lab doctor
Expected
# ip -6 addr prints nothing at all ipv6 stack absent — Docker will use IPv4 only
Image builds fail
# use the default noble base $ unset LAB_WS_BASE $ ./lab up base # or name a base explicitly $ LAB_WS_BASE=kasmweb/core-ubuntu-noble:1.16.0 ./lab up base
$ grep BASE_IMAGE compose.yml $ ./lab selftest # checks this specifically
$ curl -s https://dist.torproject.org/torbrowser/16.0a9/ \ | grep -o 'tor-browser-linux-[a-z0-9_]*-[^"]*\.tar\.xz' | sort -u # then build with a version that has your architecture $ LAB_TB_VERSION=16.0a9 ./lab up base
A container never becomes healthy
$ ./lab logs tor # the container name works too $ ./lab logs darkweb-tor
What to look for
Bootstrapped 0% (starting) Bootstrapped 5% (conn) ... Bootstrapped 100% (done) <- this is what health waits for
The desktop rejects your credentials (401)
$ ./lab creds # or read it directly $ docker exec darkweb-workstation \ sh -c 'cut -d: -f1 "$HOME/.kasmpasswd"'
Changes appear to do nothing
$ git pull $ ./lab selftest
$ ./lab rebuild
$ ./lab rebuild 2>&1 | grep '\[branding\]'
What a working run looks like
[branding] wallpaper source: /tmp/branding/background.png [branding] found xfce config: /home/.../xfce4-desktop.xml [branding] it points at: /usr/share/backgrounds/... [branding] wrote /usr/share/backgrounds/... [branding] logo files replaced: 2 [branding] stylesheets patched: 3 [branding] files with vendor URLs rewritten: 1 -> https://abrandao.net [branding] summary: wallpaper=3 logo=2 urls=1
$ ./lab rotate-creds $ ./lab creds
The lab script won't run
$ git pull $ chmod +x lab labs/checks/*.sh $ ./lab doctor
$ sed -i 's/\r$//' lab labs/checks/*.sh $ git config --global core.autocrlf input
An overlay testnet won't converge (I2P or Hyphanet)
# each overlay is its own profile — they do not come up with the base lab $ ./lab up i2p # or: ./lab up hyphanet $ ./lab ps # every stack, base + overlays, in one view # convergence signals $ ./lab i2p netdb ff1 # I2P: routerInfo count should climb past zero $ ./lab fn peers n1 # Hyphanet: darknet peers should reach "connected" # the real gate is the on-host check $ ./lab check 6.1 # or: ./lab check 7.1
# pause an overlay — containers kept; ./lab up resumes it in seconds $ ./lab stop i2p # or: ./lab stop hyphanet | overlays # delete one overlay's containers (keeps its seed volume) $ ./lab down i2p # or: ./lab down hyphanet | overlays # start it over from a clean bootstrap (also drops the seed volume + network) $ ./lab down hyphanet --volumes $ ./lab up hyphanet
The range won't come up, or onions won't publish ( Chapter 8 )
# bring the range up (generates watermarked content, then publishes onions) $ ./lab up range $ ./lab range list # the published services + their (ephemeral) onions # the answer key and the scorer work even before the services are reachable $ ./lab range truth $ ./lab range score sample-crawl.json # the real gate is the on-host check $ ./lab check 8.1 # confirms publication + watermarking
The crawler won't fetch, or won't score ( Chapter 9 )
# the engine's brain, offline — the fastest way to prove the crawler itself is fine $ ./lab crawl selftest # a live crawl needs the range up; scope + seed come from ./lab range list $ ./lab up range $ ./lab crawl range # the full engine; writes crawl-output.json to the evidence volume $ ./lab crawl range --naive # a first crawl with sessions, dedup, clone-detection off $ ./lab crawl score # crawl, then grade on the Lab 8.7 harness
The dedup detector's numbers look wrong, or ./lab dedup won't run ( Chapter 10 )
# the whole detector, offline — the fastest way to prove it is fine $ ./lab dedup selftest # cluster the clone-lab corpus into mirrors and clones, and print the result $ ./lab dedup run $ ./lab dedup run --naive # the Chapter 9 detector only (exact-hash + keyed-structural) # grade the clustering against the shipped ground truth $ ./lab dedup score # full engine: mirror/clone recall 1.00, no false merges $ ./lab dedup score --naive # baseline: mirror recall 0.33, clone recall 0.50
The market extractor's numbers look wrong, or ./lab market won't run ( Chapter 11 )
# the whole extractor, offline — the fastest way to prove it is fine $ ./lab market selftest # ingest the corpus into the content-addressed store (mirror collapses at storage) $ ./lab market store # parse listings/vendors and flag the market's lies; --naive is the brittle scraper $ ./lab market extract $ ./lab market graph # resale rings, borrowed keys, gamed reputation, bait prices # grade extraction against the shipped ground truth $ ./lab market score # full: field recall 1.00, all flags, poison refused $ ./lab market score --naive # baseline: 0.92, no flags, poison extracted
The leak/negotiation extractor's numbers look wrong, or ./lab leak won't run (Chapter 12)
# the whole extractor, offline — the fastest way to prove it is fine $ ./lab leak selftest # reuse the Chapter 11 store on the leak corpus (unchanged victim collapses across snapshots) $ ./lab leak store # recover both surfaces; --naive is the brittle reader that believes the leak site $ ./lab leak extract $ ./lab leak lifecycle # slid deadlines (theatre), quiet withdrawals $ ./lab leak reposts # mirror = affiliate movement, clone = recycled claim $ ./lab leak negotiate # the private arc + operator tactics $ ./lab leak correlate # the bluff is in the gap: deadline, volume, sold, deletion # grade both surfaces against the shipped ground truth $ ./lab leak score # full: field 1.00, lifecycle/repost/tactic/bluff recall 1.00 $ ./lab leak score --naive # baseline: 0.88 fields, 0.00 on everything two-surface
RansomChat won't start, the model won't load, or the chat is unreachable (Chapter 12, Lab 12.8 )
A real-data lab (11.8, 12.9, 14.8) won't run or shows nothing (Chapters 11, 12, 14)
./lab report produced nothing, or a page is blank in the workstation (Chapter 15)
The persona linker over-links or under-links, or ./lab link won't run (Chapter 13)
# the whole linkage engine, offline — the fastest way to prove it is fine $ ./lab link selftest # the identifier ledger: what each persona signs vs merely shows $ ./lab link ledger $ ./lab link hard # shared signed key/wallet links; displayed-only key = framing flag # the soft signals — corroborate, never prove $ ./lab link style # writing voice; threshold set from the measured gap $ ./lab link behavior # rhythm (a timezone hint), handle transform, tactic sequence # fuse into operators with confidence; --naive is the merge-on-any-string baseline $ ./lab link fuse $ ./lab link fuse --naive # over-merges the frame + look-alike, splits the rotated operator # grade against the shipped ground truth $ ./lab link score # full: recall 7/7, precision 7/7, false merges 0 $ ./lab link score --naive # baseline: precision 0.40, nine false merges
./lab detect floods with alerts, buries the criticals, or won't run (Chapter 14)
# the whole detection engine, offline — the fastest way to prove it is fine $ ./lab detect selftest # what the monitor watches — built from Chapter 13's high-confidence operators $ ./lab detect watchlist # the raw change feed, then each change typed into the taxonomy $ ./lab detect feed # 22 raw events — mostly churn and mirror duplicates $ ./lab detect classify # new_victim, publication, new_clone, operator_resurface, cosmetic_churn… # score by severity (watched operators boosted), then collapse duplicates $ ./lab detect score # criticals to the top; churn to suppress $ ./lab detect correlate # a mirrored victim is one alert, not two # the full watch loop, then the naive baseline that floods $ ./lab detect monitor # 8 ranked alerts, both criticals on top $ ./lab detect monitor --naive # 22 flat alerts, criticals buried mid-stream # grade against the shipped ground truth $ ./lab detect grade # full: recall 8/8, precision 1.00, 0 false alerts, 2/2 criticals $ ./lab detect grade --naive # baseline: precision 0.36, 12 false alerts, 0/2 criticals
./lab capstone won't run, overclaims, or the report looks thin (Chapter 15)
# the whole capstone, offline — runs the Ch11–14 engines and grades the report $ ./lab capstone selftest # chain the four engines into one evidence graph for the target operator $ ./lab capstone evidence # keyed by the reused signed key F19B7A0C… $ ./lab capstone claims # each claim: provenance + type + a calibrated confidence # assemble the brief, then the careless baseline that overclaims $ ./lab capstone report # BLUF, findings by confidence, what-would-change, boundary $ ./lab capstone report --naive # flattens every finding to sourceless high-confidence fact # grade against the ground-truth claim set $ ./lab capstone grade # full: coverage 8/8, provenance 1.00, calibration 1.00, 0 overclaims $ ./lab capstone grade --naive # baseline: same coverage 8/8, provenance 0.00, calibration 0.25, 6 overclaims
E, Running the labs on a VPS
What to skip
$ sudo apt-get update $ sudo apt-get install -y ca-certificates curl $ sudo install -m 0755 -d /etc/apt/keyrings $ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc $ sudo chmod a+r /etc/apt/keyrings/docker.asc $ . /etc/os-release $ ARCH=$(dpkg --print-architecture) $ echo "deb [arch=$ARCH signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null $ sudo apt-get update $ sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin $ sudo usermod -aG docker $USER # log out and back in, then continue with Lab 1.1
Reaching the desktop
$ ssh -L 6901:127.0.0.1:6901 you@your-vps # leave that open, then browse locally to: # https://127.0.0.1:6901
What gets better, and what to watch
F, Clipboard, and why the lab prefers Chrome
What works where
Why Chrome, specifically
$ LAB_CLIPBOARD_AUTO=0 ./lab rebuild
If it stops working
$ docker exec darkweb-workstation pgrep -a vncconfig $ docker exec darkweb-workstation pgrep -a autocutsel
$ docker exec -e DISPLAY=:1 darkweb-workstation xclip -o -selection clipboard