Nym Localnet
A complete Nym mixnet test environment with OpenTelemetry instrumentation. Supports both Docker Desktop and Apple Container Runtime on macOS.
Overview
This localnet setup provides a fully functional Nym mixnet for local development and testing:
- 3 mixnodes (layer 1, 2, 3)
- 2 gateways (entry + exit mode)
- 1 network-requester (service provider)
- 1 SOCKS5 client
- OpenTelemetry tracing via OTLP/gRPC to SigNoz (or any OTLP collector)
All components run in isolated containers with proper networking and dynamic IP resolution.
When the otel feature is enabled (default), every nym-node exports traces covering
the full packet lifecycle: ingress, Sphinx processing, forwarding, and final-hop delivery.
Prerequisites
Required
- macOS (tested on macOS Sequoia 15.0+)
- Docker Desktop (recommended) or Apple Container Runtime
- Python 3 with
base58library
SigNoz (for trace viewing)
SigNoz is an open-source APM that receives and visualises OpenTelemetry data. Install it locally with Docker Compose -- this takes about 2 minutes:
# Clone the SigNoz repository
git clone -b main https://github.com/SigNoz/signoz.git ~/signoz
cd ~/signoz/deploy
# Start SigNoz (runs ClickHouse, otel-collector, query-service, frontend)
docker compose up -d
# Verify it is running
docker ps --filter "name=signoz" --format "table {{.Names}}\t{{.Status}}"
Once running:
- SigNoz UI: http://localhost:8080
- OTLP gRPC collector: localhost:4317 (used by nym-nodes)
- OTLP HTTP collector: localhost:4318
The localnet script auto-detects the SigNoz Docker network (signoz-net) and
routes OTel traffic directly to the collector container -- no manual endpoint
configuration needed.
To stop SigNoz later:
cd ~/signoz/deploy && docker compose down
Installation
# Install Python dependencies
pip3 install --break-system-packages base58
# Verify Docker is installed
docker --version
If using Apple Container Runtime instead of Docker:
container --version
Quick Start
# Navigate to the localnet directory
cd docker/localnet
# Build the container image
./localnet.sh build
# Start the localnet
./localnet.sh start
# Test the SOCKS5 proxy
curl -L --socks5 localhost:1080 https://nymtech.net
# View logs
./localnet.sh logs gateway
./localnet.sh logs socks5
# Stop the localnet
./localnet.sh stop
# Clean up everything
./localnet.sh clean
Architecture
Container Network
All containers run on a custom bridge network (nym-localnet-network) with dynamic IP assignment:
Host Machine (macOS)
├── nym-localnet-network (bridge)
│ ├── nym-mixnode1 (192.168.66.3)
│ ├── nym-mixnode2 (192.168.66.4)
│ ├── nym-mixnode3 (192.168.66.5)
│ ├── nym-gateway (192.168.66.6)
│ ├── nym-network-requester (192.168.66.7)
│ └── nym-socks5-client (192.168.66.8)
Ports published to host:
- 1080 → SOCKS5 proxy
- 9000/9001 → Gateway entry ports
- 10001-10005 → Mixnet ports
- 20001-20005 → Verloc ports
- 30001-30005 → HTTP APIs
- 41264/41265 → LP control ports (registration)
- 51822/51823 → WireGuard tunnel ports (gateway/gateway2; only used when WireGuard is enabled)
WireGuard and privileges
By default, gateways run with WireGuard disabled (--wireguard-enabled false). No elevated capabilities are required: the script does not use --cap-add=NET_ADMIN or --device /dev/net/tun, so localnet runs without net admin privileges and is suitable for mixnet packet testing and SOCKS5 over the mixnet.
To enable WireGuard VPN routing in localnet (e.g. for two-hop VPN tests), set WIREGUARD_ENABLED=1 before starting. The script will then add --cap-add=NET_ADMIN and --device /dev/net/tun to the gateway containers and configure IP forwarding and NAT. This may not work in all Docker environments (e.g. some hosted runners restrict capabilities).
WIREGUARD_ENABLED=1 ./localnet.sh start
Startup Flow
-
Container Initialization (parallel)
- Each container starts and gets a dynamic IP
- Each node runs
nym-node run --init-onlywith its container IP - Bonding JSON files are written to shared volume
-
Topology Generation (sequential)
- Wait for all 4 bonding JSON files
- Get container IPs dynamically
- Run
build_topology.pywith container IPs - Generate
network.jsonwith correct addresses
-
Node Startup (parallel)
- Each container starts its node with
--localflag - Nodes read configuration from init phase
- Clients use custom topology file
- Each container starts its node with
-
Service Providers (sequential)
- Network requester initializes and starts
- SOCKS5 client initializes with requester address
Network Topology
The network.json file contains the complete network topology:
{
"metadata": {
"key_rotation_id": 0,
"absolute_epoch_id": 0,
"refreshed_at": "2025-11-03T..."
},
"rewarded_set": {
"epoch_id": 0,
"entry_gateways": [4],
"exit_gateways": [4],
"layer1": [1],
"layer2": [2],
"layer3": [3],
"standby": []
},
"node_details": {
"1": { "mix_host": "192.168.66.3:10001", ... },
"2": { "mix_host": "192.168.66.4:10002", ... },
"3": { "mix_host": "192.168.66.5:10003", ... },
"4": { "mix_host": "192.168.66.6:10004", ... }
}
}
Commands
Build
./localnet.sh build
Builds the Docker image and loads it into Apple container runtime.
Note: First build takes ~5-10 minutes to compile all components.
Start
./localnet.sh start
Starts all containers, generates topology, and launches the complete network.
Expected output:
[INFO] Starting Nym Localnet...
[SUCCESS] Network created: nym-localnet-network
[INFO] Starting nym-mixnode1...
[SUCCESS] nym-mixnode1 started
...
[INFO] Building network topology with container IPs...
[SUCCESS] Network topology created successfully
[SUCCESS] Nym Localnet is running!
Test with:
curl -x socks5h://127.0.0.1:1080 https://nymtech.net
Stop
./localnet.sh stop
Stops and removes all running containers.
Clean
./localnet.sh clean
Complete cleanup: removes containers, volumes, network, and temporary files.
Logs
# View logs for a specific container
./localnet.sh logs <container-name>
# Container names:
# - mix1, mix2, mix3
# - gateway
# - requester
# - socks5
# Examples:
./localnet.sh logs gateway
./localnet.sh logs socks5
container logs nym-gateway --follow
Status
# List all containers
docker ps --filter "name=nym-" --format "table {{.Names}}\t{{.Status}}"
# Check specific container
docker logs nym-gateway
# Inspect network
docker network inspect nym-localnet-network
Testing
Basic SOCKS5 Test
# Simple HTTP request through the mixnet
curl -x socks5h://127.0.0.1:1080 https://httpbin.org/get
# HTTPS request
curl -x socks5h://127.0.0.1:1080 https://nymtech.net
# Download a file
curl -x socks5h://127.0.0.1:1080 \
https://test-download-files-nym.s3.amazonaws.com/download-files/1MB.zip \
--output /tmp/test.zip
Load Testing
A load test script is included to generate sustained traffic and populate SigNoz with meaningful trace data:
# Default: 10 concurrent workers, 60 seconds
./loadtest.sh
# Heavier load: 20 workers for 2 minutes
./loadtest.sh -c 20 -d 120
# Light single-threaded test
./loadtest.sh -c 1 -d 10
# Target a specific URL
./loadtest.sh -c 5 -d 30 -u https://httpbin.org/bytes/4096
The script reports live progress, then prints a summary with request counts, throughput, and latency percentiles (p50/p95/p99).
Verify Network Topology
# View the generated topology
docker exec nym-gateway cat /localnet/network.json | jq .
# Check container status
docker ps --filter "name=nym-" --format "table {{.Names}}\t{{.Status}}"
# Verify all bonding files exist
docker exec nym-gateway ls -la /localnet/
Test Mixnet Routing
# All traffic flows through: client -> gateway -> mix1 -> mix2 -> mix3 -> gateway -> internet
# Watch logs to verify routing:
docker logs nym-mixnode1 --follow &
docker logs nym-mixnode2 --follow &
docker logs nym-mixnode3 --follow &
docker logs nym-gateway --follow &
# Make a request
curl -x socks5h://127.0.0.1:1080 https://nymtech.net
OpenTelemetry
OTel is enabled by default. Each nym-node exports traces via OTLP/gRPC covering packet ingress, Sphinx processing, forwarding, and final-hop delivery.
Viewing Traces
- SigNoz UI: http://localhost:8080 -- filter by
serviceName = nym-node - Terminal report (queries ClickHouse directly, no login needed):
./otel-report.sh # last 15 minutes
./otel-report.sh 60 # last 60 minutes
./otel-report.sh live # auto-refresh every 10s
Disabling OTel
OTEL_ENABLE=0 ./localnet.sh start # disable
OTEL_ENDPOINT=http://my-collector:4317 ./localnet.sh start # custom collector
LP (Lewes Protocol) Testing
The gateway is configured with LP listener enabled and mock ecash verification for testing:
# LP listener ports (exposed on host):
# - 41264: LP control port (TCP registration)
# - 51264: LP data port
# Check LP ports are listening
nc -zv localhost 41264
nc -zv localhost 51264
# Test LP registration with nym-gateway-probe
cargo run -p nym-gateway-probe run-local \
--mnemonic "test mnemonic here" \
--gateway-ip 'localhost:41264' \
--only-lp-registration
Mock Ecash Mode:
- Gateway uses
--lp.use-mock-ecash trueflag - Accepts ANY bandwidth credential without blockchain verification
- Perfect for testing LP protocol implementation
- WARNING: Never use mock ecash in production!
Testing without blockchain: The mock ecash manager allows testing the complete LP registration flow without requiring:
- Running nyxd blockchain
- Deploying smart contracts
- Acquiring real bandwidth credentials
- Setting up coconut signers
This makes localnet perfect for rapid LP protocol development and testing.
File Structure
docker/localnet/
├── README.md # This file
├── localnet.sh # Main orchestration script
├── loadtest.sh # Load test / traffic generator
├── otel-report.sh # Terminal-based OTel metrics report
├── Dockerfile.localnet # Multi-stage Docker image (builder + slim runtime)
├── build_topology.py # Topology generator
└── localnet-logs.sh # Tmux-based multi-container log viewer
How It Works
Node Initialization
Each node initializes itself at runtime inside its container:
# Get container IP
CONTAINER_IP=$(hostname -i)
# Initialize with container IP
nym-node run --id mix1-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mixnet-bind-address=0.0.0.0:10001 \
--verloc-bind-address=0.0.0.0:20001 \
--http-bind-address=0.0.0.0:30001 \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--output=json \
--bonding-information-output="/localnet/mix1.json"
Key flags:
--local: Accept private IPs for local development--public-ips: Announce the container's IP address--unsafe-disable-replay-protection: Disable bloomfilter to save memory
Dynamic Topology
The topology is built after containers start:
# Get container IPs
MIX1_IP=$(container exec nym-mixnode1 hostname -i)
MIX2_IP=$(container exec nym-mixnode2 hostname -i)
MIX3_IP=$(container exec nym-mixnode3 hostname -i)
GATEWAY_IP=$(container exec nym-gateway hostname -i)
# Build topology with actual IPs
python3 build_topology.py /localnet localnet \
$MIX1_IP $MIX2_IP $MIX3_IP $GATEWAY_IP
This ensures the topology contains reachable container addresses.
Client Configuration
Clients use --custom-mixnet to read the local topology:
# Network requester
nym-network-requester init \
--id "network-requester-$SUFFIX" \
--open-proxy=true \
--custom-mixnet /localnet/network.json
# SOCKS5 client
nym-socks5-client init \
--id "socks5-client-$SUFFIX" \
--provider "$REQUESTER_ADDRESS" \
--custom-mixnet /localnet/network.json \
--host 0.0.0.0
The --custom-mixnet flag tells clients to use our local topology instead of fetching from nym-api.
Troubleshooting
Container Build Issues
Problem: Docker build fails
# Check Docker is running
docker info
# Clean Docker cache
docker system prune -a
# Rebuild with no cache
./localnet.sh build
Problem: Container image load fails
# Verify temp file was created
ls -lh /tmp/nym-localnet-image-*
# Check container runtime
container image list
# Manually load if needed
docker save -o /tmp/nym-image.tar nym-localnet:latest
container image load --input /tmp/nym-image.tar
Network Issues
Problem: Containers can't communicate
# Check network exists
container network list | grep nym-localnet
# Inspect network
container network inspect nym-localnet-network
# Verify containers are on the network
container list | grep nym-
Problem: SOCKS5 connection refused
# Check SOCKS5 is listening
container logs nym-socks5-client | grep "Listening on"
# Verify port mapping
container list | grep socks5
# Test from host
nc -zv localhost 1080
Node Issues
Problem: "No valid public addresses" error
- Ensure
--localflag is present in both init and run commands - Check container can resolve its own IP:
container exec nym-mixnode1 hostname -i - Verify
--public-ipsis using$CONTAINER_IPvariable
Problem: "TUN device error"
- The gateway needs TUN device support for exit functionality
- Verify
iproute2is installed in the image (addsipcommand) - Check gateway logs:
container logs nym-gateway - The gateway should show: "Created TUN device: nymtun0"
Problem: "Noise handshake" warnings
- These are warnings, not errors - nodes fall back to TCP
- Does not affect functionality in local development
- Safe to ignore for testing purposes
Topology Issues
Problem: Network.json not created
# Check all bonding files exist
container exec nym-gateway ls -la /localnet/
# Verify build_topology.py ran
container logs nym-gateway | grep "Building network topology"
# Check Python dependencies
container exec nym-gateway python3 -c "import base58"
Problem: Clients can't connect to nodes
# Verify IPs in topology match container IPs
container exec nym-gateway cat /localnet/network.json | jq '.node_details'
container list | grep nym-
# Check containers can reach each other
container exec nym-socks5-client ping -c 1 192.168.66.6
Startup Issues
Problem: Containers exit immediately
# Check logs for errors
container logs nym-mixnode1
# Common issues:
# - Missing network.json: Wait for topology to be built
# - Port already in use: Check for conflicting services
# - Init failed: Check for correct container IP
Problem: Topology build times out
# Verify all containers initialized
container exec nym-gateway ls -la /localnet/*.json
# Check for init errors
container logs nym-mixnode1 | grep -i error
# Manual cleanup and restart
./localnet.sh clean
./localnet.sh start
Performance Notes
Memory Usage
- Each mixnode: ~200MB
- Gateway: ~300MB (includes TUN device)
- Network requester: ~150MB
- SOCKS5 client: ~150MB
- Total: ~1.2GB + overhead
Recommended: 4GB+ system memory
Startup Time
- Image build: ~5-10 minutes (first time)
- Network start: ~20-30 seconds
- Node initialization: ~5-10 seconds per node (parallel)
Latency
Mixnet adds latency by design for privacy:
- ~1-3 seconds for SOCKS5 requests
- Cover traffic adds random delays
- Local testing may show variable timing
This is expected behavior - the mixnet provides privacy through traffic mixing.
Advanced Configuration
Custom Node Configuration
Edit node init commands in localnet.sh (search for nym-node run --init-only):
# Example: Change mixnode ports
--mixnet-bind-address=0.0.0.0:11001 \
--verloc-bind-address=0.0.0.0:21001 \
--http-bind-address=0.0.0.0:31001 \
Remember to update port mappings in the container run command as well.
Enable Replay Protection
Remove --unsafe-disable-replay-protection flags (requires more memory):
# In start_mixnode() and start_gateway() functions
nym-node run --id mix1-localnet --init-only \
--local \
--mixnet-bind-address=0.0.0.0:10001 \
# ... other flags (without --unsafe-disable-replay-protection)
Note: Each node will require an additional ~1.5GB memory for bloomfilter.
API Access
Each node exposes an HTTP API:
# Get gateway info
curl -H "Authorization: Bearer lala" http://localhost:30004/api/v1/gateway
# Get mixnode stats
curl -H "Authorization: Bearer lala" http://localhost:30001/api/v1/stats
# Get node description
curl -H "Authorization: Bearer lala" http://localhost:30001/api/v1/description
Access token is lala (configured with --http-access-token=lala).
Add More Mixnodes
To add a 4th mixnode:
- Update constants in
localnet.sh:
MIXNODE4_CONTAINER="nym-mixnode4"
- Add start call in
start_all():
start_mixnode 4 "$MIXNODE4_CONTAINER"
-
Update topology builder to include the new node
-
Rebuild and restart:
./localnet.sh clean
./localnet.sh build
./localnet.sh start
Technical Details
Container Runtime
Docker Desktop is the default and recommended runtime; no extra setup is required for mixnet testing.
Apple Container Runtime is an optional alternative on macOS. It natively uses Kata Containers images and is only required if you use container instead of Docker (e.g. for consistency with other Apple tooling). Kata is also the path that provides a kernel with CONFIG_TUN=y if you need TUN/WireGuard inside containers under the Apple runtime.
Initial setup for Container Runtime (optional)
- MUST have MacOS Tahoe for inter-container networking
brew install --cask container- Download Kata Containers 3.20, this one can be loaded by
containerand hasCONFIG_TUN=ykernel flaghttps://github.com/kata-containers/kata-containers/releases/download/3.20.0/kata-static-3.20.0-arm64.tar.xz
- Load new kernel
container system kernel set --tar kata-static-3.20.0-arm64.tar.xz --binary opt/kata/share/kata-containers/vmlinux-6.12.42-162
- Validate kernel version once you have container running
uname -rshould return6.12.42cat /proc/config.gz | grep CONFIG_TUNshould returnCONFIG_TUN=y
Image Building
Images are built with Docker then transferred:
docker buildcreates the imagedocker saveexports to tar filecontainer image loadimports into container runtime- Temporary file is cleaned up
This approach allows using Docker's build cache while running on Apple's runtime.
Network Architecture
The custom bridge network (nym-localnet-network):
- Provides container-to-container communication
- Assigns dynamic IPs from 192.168.66.0/24
- NAT for outbound internet access
- Port publishing for host access
Volumes
Two types of volumes:
- Shared data (
/tmp/nym-localnet-*): Bonding files and topology - Node configs (
/tmp/nym-localnet-home-*): Node configurations
Both are ephemeral by default (cleaned up on stop).
Known Limitations
- macOS only: Apple container runtime requires macOS
- No Docker Compose: Uses custom orchestration script
- Dynamic IPs: Container IPs may change between restarts
- Port conflicts: Cannot run alongside services using same ports
- TUN device: Only required when
WIREGUARD_ENABLED=1; otherwise gateways run without it
Support
For issues and questions:
- GitHub Issues: https://github.com/nymtech/nym/issues
- Documentation: https://nymtech.net/docs
- Discord: https://discord.gg/nym
License
This localnet setup is part of the Nym project and follows the same license.