Docker/Container localnet

This commit is contained in:
durch
2025-11-03 12:30:18 +01:00
parent 1a97a53bdb
commit b3d5861244
5 changed files with 1598 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# Single-stage Dockerfile for Nym localnet
# Builds: nym-node, nym-network-requester, nym-socks5-client
# Target: Apple Container Runtime with host networking
FROM rust:latest
WORKDIR /usr/src/nym
COPY ./ ./
ENV CARGO_BUILD_JOBS=8
# Build all required binaries in release mode
RUN cargo build --release --locked \
-p nym-node \
-p nym-network-requester \
-p nym-socks5-client
# Install runtime dependencies
RUN apt update && apt install -y \
python3 \
python3-pip \
netcat-openbsd \
jq \
iproute2 \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies for build_topology.py
RUN pip3 install --break-system-packages base58
# Move binaries to /usr/local/bin for easy access
RUN cp target/release/nym-node /usr/local/bin/ && \
cp target/release/nym-network-requester /usr/local/bin/ && \
cp target/release/nym-socks5-client /usr/local/bin/
# Copy supporting scripts
COPY ./docker/localnet/build_topology.py /usr/local/bin/
WORKDIR /nym
# Default command
CMD ["nym-node", "--help"]
+616
View File
@@ -0,0 +1,616 @@
# Nym Localnet for Apple Container Runtime
A complete Nym mixnet test environment running on Apple's container runtime for macOS.
## Overview
This localnet setup provides a fully functional Nym mixnet for local development and testing:
- **3 mixnodes** (layer 1, 2, 3)
- **1 gateway** (entry + exit mode)
- **1 network-requester** (service provider)
- **1 SOCKS5 client**
All components run in isolated containers with proper networking and dynamic IP resolution.
## Prerequisites
### Required
- **macOS** (tested on macOS Sequoia 15.0+)
- **Apple Container Runtime** - Built into macOS
- **Docker Desktop** (for building images only)
- **Python 3** with `base58` library
### Installation
```bash
# Install Python dependencies
pip3 install --break-system-packages base58
# Verify container runtime is available
container --version
# Verify Docker is installed (for building)
docker --version
```
## Quick Start
```bash
# 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 → Gateway entry
- 10001-10004 → Mixnet ports
- 20001-20004 → Verloc ports
- 30001-30004 → HTTP APIs
### Startup Flow
1. **Container Initialization** (parallel)
- Each container starts and gets a dynamic IP
- Each node runs `nym-node run --init-only` with its container IP
- Bonding JSON files are written to shared volume
2. **Topology Generation** (sequential)
- Wait for all 4 bonding JSON files
- Get container IPs dynamically
- Run `build_topology.py` with container IPs
- Generate `network.json` with correct addresses
3. **Node Startup** (parallel)
- Each container starts its node with `--local` flag
- Nodes read configuration from init phase
- Clients use custom topology file
4. **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:
```json
{
"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
```bash
./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
```bash
./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
```bash
./localnet.sh stop
```
Stops and removes all running containers.
### Clean
```bash
./localnet.sh clean
```
Complete cleanup: removes containers, volumes, network, and temporary files.
### Logs
```bash
# 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
```bash
# List all containers
container list
# Check specific container
container logs nym-gateway
# Inspect network
container network inspect nym-localnet-network
```
## Testing
### Basic SOCKS5 Test
```bash
# Simple HTTP request with redirect following
curl -L --socks5 localhost:1080 http://example.com
# HTTPS request
curl -L --socks5 localhost:1080 https://nymtech.net
# Download a file
curl -L --socks5 localhost:1080 \
https://test-download-files-nym.s3.amazonaws.com/download-files/1MB.zip \
--output /tmp/test.zip
```
### Verify Network Topology
```bash
# View the generated topology
container exec nym-gateway cat /localnet/network.json | jq .
# Check container IPs
container list | grep nym-
# Verify all bonding files exist
container exec nym-gateway ls -la /localnet/
```
### Test Mixnet Routing
```bash
# All traffic flows through: client → mix1 → mix2 → mix3 → gateway → internet
# Watch logs to verify routing:
container logs nym-mixnode1 --follow &
container logs nym-mixnode2 --follow &
container logs nym-mixnode3 --follow &
container logs nym-gateway --follow &
# Make a request
curl -L --socks5 localhost:1080 http://example.com
```
## File Structure
```
docker/localnet/
├── README.md # This file
├── localnet.sh # Main orchestration script
├── Dockerfile.localnet # Docker image definition
└── build_topology.py # Topology generator
```
## How It Works
### Node Initialization
Each node initializes itself at runtime inside its container:
```bash
# 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:
```bash
# 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:
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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 `--local` flag is present in both init and run commands
- Check container can resolve its own IP: `container exec nym-mixnode1 hostname -i`
- Verify `--public-ips` is using `$CONTAINER_IP` variable
**Problem**: "TUN device error"
- The gateway needs TUN device support for exit functionality
- Verify `iproute2` is installed in the image (adds `ip` command)
- 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
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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
```
## Differences from Bash Localnet
The bash localnet (`scripts/localnet_start.sh`) runs everything on localhost:
| Aspect | Bash Localnet | Container Localnet |
|--------|---------------|-------------------|
| **Isolation** | Single host, shared ports | Isolated containers |
| **Networking** | 127.0.0.1 loopback | Bridge network with NAT |
| **IP Addresses** | Hardcoded 127.0.0.1 | Dynamic container IPs |
| **Initialization** | All at once, before start | Per-container at runtime |
| **Topology** | Static 127.0.0.1 addresses | Dynamic container IPs |
| **Process Management** | tmux sessions | Container lifecycle |
| **Cleanup** | Manual process killing | `container rm` |
**Key advantages of container approach**:
- ✅ Complete isolation between components
- ✅ Closer to production deployment
- ✅ Easy cleanup (just remove containers)
- ✅ Reproducible across machines
- ✅ Can test multiple localnets in parallel (with different ports)
## 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`):
```bash
# 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):
```bash
# 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:
```bash
# 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:
1. **Update constants** in `localnet.sh`:
```bash
MIXNODE4_CONTAINER="nym-mixnode4"
```
2. **Add start call** in `start_all()`:
```bash
start_mixnode 4 "$MIXNODE4_CONTAINER"
```
3. **Update topology builder** to include the new node
4. **Rebuild and restart**:
```bash
./localnet.sh clean
./localnet.sh build
./localnet.sh start
```
## Technical Details
### Container Runtime
Apple's container runtime is a native macOS container system:
- Uses Virtualization.framework for isolation
- Lightweight VMs for each container
- Native macOS integration
- Separate image store from Docker
### Image Building
Images are built with Docker then transferred:
1. `docker build` creates the image
2. `docker save` exports to tar file
3. `container image load` imports into container runtime
4. 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:
1. **Shared data** (`/tmp/nym-localnet-*`): Bonding files and topology
2. **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**: Gateway requires `ip` command for network interfaces
## 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.
+287
View File
@@ -0,0 +1,287 @@
import json
import os
import subprocess
import sys
from datetime import datetime
from functools import lru_cache
from pathlib import Path
import base58
DEFAULT_OWNER = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf"
DEFAULT_SUFFIX = os.environ.get("NYM_NODE_SUFFIX", "localnet")
NYM_NODES_ROOT = Path.home() / ".nym" / "nym-nodes"
def debug(msg):
"""Print debug message to stderr"""
print(f"[DEBUG] {msg}", file=sys.stderr, flush=True)
def error(msg):
"""Print error message to stderr"""
print(f"[ERROR] {msg}", file=sys.stderr, flush=True)
def maybe_assign(target, key, value):
if value is not None:
target[key] = value
@lru_cache(maxsize=None)
def get_nym_node_version():
try:
result = subprocess.run(
["nym-node", "--version"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except (subprocess.CalledProcessError, FileNotFoundError):
return None
version_line = result.stdout.strip()
if not version_line:
return None
parts = version_line.split()
for token in reversed(parts):
if token and token[0].isdigit():
return token
return version_line
def node_config_path(prefix, suffix):
path = NYM_NODES_ROOT / f"{prefix}-{suffix}" / "config" / "config.toml"
debug(f"Looking for config at: {path}")
if path.exists():
debug(f" ✓ Config found")
return path
else:
error(f" ✗ Config NOT found at {path}")
return None
def read_node_details(prefix, suffix):
config_path = node_config_path(prefix, suffix)
if config_path is None:
error(f"Cannot read node details for {prefix}-{suffix}: config not found")
return {}
debug(f"Running: nym-node node-details --config-file {config_path}")
try:
result = subprocess.run(
[
"nym-node",
"node-details",
"--config-file",
str(config_path),
"--output=json",
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
debug(f" ✓ node-details command succeeded")
except subprocess.CalledProcessError as e:
error(f"node-details command failed for {prefix}-{suffix}: {e}")
error(f" stdout: {e.stdout}")
error(f" stderr: {e.stderr}")
return {}
except FileNotFoundError:
error("nym-node command not found in PATH")
return {}
try:
details = json.loads(result.stdout)
debug(f" ✓ Parsed node-details JSON")
except json.JSONDecodeError as e:
error(f"Failed to parse node-details JSON: {e}")
error(f" Output was: {result.stdout[:200]}")
return {}
info = {}
# Get sphinx key and decode from Base58 to byte array
sphinx_data = details.get("x25519_primary_sphinx_key")
if isinstance(sphinx_data, dict):
sphinx_key_b58 = sphinx_data.get("public_key")
if sphinx_key_b58:
debug(f" Got sphinx_key (Base58): {sphinx_key_b58[:20]}...")
try:
# Decode Base58 to byte array
sphinx_bytes = base58.b58decode(sphinx_key_b58)
info["sphinx_key"] = list(sphinx_bytes)
debug(f" ✓ Decoded to {len(sphinx_bytes)} bytes")
except Exception as e:
error(f" Failed to decode sphinx_key: {e}")
version = get_nym_node_version()
if version:
info["version"] = version
return info
def resolve_host(data):
# For localnet, always use 127.0.0.1 unless explicitly overridden
env_host = os.environ.get("LOCALNET_PUBLIC_IP") or os.environ.get("NYMNODE_PUBLIC_IP")
if env_host:
return env_host.split(",")[0].strip()
# Default to localhost for localnet (containers can reach each other via published ports)
return "127.0.0.1"
def create_mixnode_entry(base_dir, mix_id, port_delta, suffix, host_ip):
"""Create a node_details entry for a mixnode"""
debug(f"\n=== Creating mixnode{mix_id} entry ===")
mix_file = Path(base_dir) / f"mix{mix_id}.json"
debug(f"Reading bonding JSON from: {mix_file}")
with mix_file.open("r") as json_blob:
mix_data = json.load(json_blob)
node_details = read_node_details(f"mix{mix_id}", suffix)
# Get identity key from bonding JSON (already byte array)
identity = mix_data.get("identity_key")
if not identity:
raise RuntimeError(f"Missing identity_key in {mix_file}")
debug(f" ✓ Got identity_key from bonding JSON: {len(identity)} bytes")
# Get sphinx key from node-details (decoded from Base58)
sphinx_key = node_details.get("sphinx_key")
if not sphinx_key:
raise RuntimeError(f"Missing sphinx_key from node-details for mix{mix_id}")
host = host_ip
port = 10000 + port_delta
debug(f" Using host: {host}:{port}")
entry = {
"node_id": mix_id,
"mix_host": f"{host}:{port}",
"entry": None,
"identity_key": identity,
"sphinx_key": sphinx_key,
"supported_roles": {
"mixnode": True,
"mixnet_entry": False,
"mixnet_exit": False
}
}
maybe_assign(entry, "version", node_details.get("version") or mix_data.get("version"))
return entry
def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip):
"""Create a node_details entry for a gateway"""
debug(f"\n=== Creating gateway entry ===")
gateway_file = Path(base_dir) / "gateway.json"
debug(f"Reading bonding JSON from: {gateway_file}")
with gateway_file.open("r") as json_blob:
gateway_data = json.load(json_blob)
node_details = read_node_details("gateway", suffix)
# Get identity key from bonding JSON (already byte array)
identity = gateway_data.get("identity_key")
if not identity:
raise RuntimeError("Missing identity_key in gateway.json")
debug(f" ✓ Got identity_key from bonding JSON: {len(identity)} bytes")
# Get sphinx key from node-details (decoded from Base58)
sphinx_key = node_details.get("sphinx_key")
if not sphinx_key:
raise RuntimeError("Missing sphinx_key from node-details for gateway")
host = host_ip
mix_port = 10000 + port_delta
clients_port = 9000
debug(f" Using host: {host} (mix:{mix_port}, clients:{clients_port})")
entry = {
"node_id": node_id,
"mix_host": f"{host}:{mix_port}",
"entry": {
"ip_addresses": [host],
"clients_ws_port": clients_port,
"hostname": None,
"clients_wss_port": None
},
"identity_key": identity,
"sphinx_key": sphinx_key,
"supported_roles": {
"mixnode": False,
"mixnet_entry": True,
"mixnet_exit": True
}
}
maybe_assign(entry, "version", node_details.get("version") or gateway_data.get("version"))
return entry
def main(args):
if not args:
raise SystemExit("Usage: build_topology.py <output_dir> [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip]")
base_dir = args[0]
suffix = args[1] if len(args) > 1 and args[1] else DEFAULT_SUFFIX
# Get container IPs from arguments (or use 127.0.0.1 as fallback)
mix1_ip = args[2] if len(args) > 2 else "127.0.0.1"
mix2_ip = args[3] if len(args) > 3 else "127.0.0.1"
mix3_ip = args[4] if len(args) > 4 else "127.0.0.1"
gateway_ip = args[5] if len(args) > 5 else "127.0.0.1"
debug(f"\n=== Starting topology generation ===")
debug(f"Output directory: {base_dir}")
debug(f"Node suffix: {suffix}")
debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}")
# Create node_details entries with integer keys
node_details = {
1: create_mixnode_entry(base_dir, 1, 1, suffix, mix1_ip),
2: create_mixnode_entry(base_dir, 2, 2, suffix, mix2_ip),
3: create_mixnode_entry(base_dir, 3, 3, suffix, mix3_ip),
4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip)
}
# Create the NymTopology structure
topology = {
"metadata": {
"key_rotation_id": 0,
"absolute_epoch_id": 0,
"refreshed_at": datetime.utcnow().isoformat() + "Z"
},
"rewarded_set": {
"epoch_id": 0,
"entry_gateways": [4],
"exit_gateways": [4],
"layer1": [1],
"layer2": [2],
"layer3": [3],
"standby": []
},
"node_details": node_details
}
output_path = Path(base_dir) / "network.json"
debug(f"\nWriting topology to: {output_path}")
with output_path.open("w") as out:
json.dump(topology, out, indent=2)
print(f"✓ Generated topology with {len(node_details)} nodes")
print(f" - 3 mixnodes (layers 1, 2, 3)")
print(f" - 1 gateway (entry + exit)")
debug(f"\n=== Topology generation complete ===\n")
if __name__ == "__main__":
main(sys.argv[1:])
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# Tmux-based log viewer for Nym Localnet containers
# Shows all container logs in a multi-pane layout
SESSION_NAME="nym-localnet-logs"
# Container names
CONTAINERS=(
"nym-mixnode1"
"nym-mixnode2"
"nym-mixnode3"
"nym-gateway"
"nym-network-requester"
"nym-socks5-client"
)
# Check if containers are running
running_containers=()
for container in "${CONTAINERS[@]}"; do
if container inspect "$container" &>/dev/null; then
running_containers+=("$container")
fi
done
if [ ${#running_containers[@]} -eq 0 ]; then
echo "Error: No containers are running"
echo "Start the localnet first: ./localnet.sh start"
exit 1
fi
# Check if we're already in tmux
if [ -n "$TMUX" ]; then
# Inside tmux - create new window
tmux new-window -n "logs" "container logs -f ${running_containers[0]}"
# Split for remaining containers
for ((i=1; i<${#running_containers[@]}; i++)); do
tmux split-window -t logs "container logs -f ${running_containers[$i]}"
tmux select-layout -t logs tiled
done
tmux select-layout -t logs tiled
else
# Not in tmux - check if session exists
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
# Session exists - attach to it
exec tmux attach-session -t "$SESSION_NAME"
else
# Create new session
tmux new-session -d -s "$SESSION_NAME" -n "logs" "container logs -f ${running_containers[0]}"
# Split for remaining containers
for ((i=1; i<${#running_containers[@]}; i++)); do
tmux split-window -t "$SESSION_NAME:logs" "container logs -f ${running_containers[$i]}"
tmux select-layout -t "$SESSION_NAME:logs" tiled
done
tmux select-layout -t "$SESSION_NAME:logs" tiled
# Attach to the session
exec tmux attach-session -t "$SESSION_NAME"
fi
fi
+590
View File
@@ -0,0 +1,590 @@
#!/bin/bash
set -ex
# Nym Localnet Orchestration Script for Apple Container Runtime
# Emulates docker-compose functionality
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IMAGE_NAME="nym-localnet:latest"
VOLUME_NAME="nym-localnet-data"
VOLUME_PATH="/tmp/nym-localnet-$$"
NYM_VOLUME_PATH="/tmp/nym-localnet-home-$$"
SUFFIX=${NYM_NODE_SUFFIX:-localnet}
# Container names
INIT_CONTAINER="nym-localnet-init"
MIXNODE1_CONTAINER="nym-mixnode1"
MIXNODE2_CONTAINER="nym-mixnode2"
MIXNODE3_CONTAINER="nym-mixnode3"
GATEWAY_CONTAINER="nym-gateway"
REQUESTER_CONTAINER="nym-network-requester"
SOCKS5_CONTAINER="nym-socks5-client"
ALL_CONTAINERS=(
"$MIXNODE1_CONTAINER"
"$MIXNODE2_CONTAINER"
"$MIXNODE3_CONTAINER"
"$GATEWAY_CONTAINER"
"$REQUESTER_CONTAINER"
"$SOCKS5_CONTAINER"
)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*"
}
cleanup_host_state() {
log_info "Cleaning local nym-node state for suffix ${SUFFIX}"
for node in mix1 mix2 mix3 gateway; do
rm -rf "$HOME/.nym/nym-nodes/${node}-${SUFFIX}"
done
}
# Check if container command exists
check_prerequisites() {
if ! command -v container &> /dev/null; then
log_error "Apple 'container' command not found"
log_error "Install from: https://github.com/apple/container"
exit 1
fi
}
# Build the Docker image
build_image() {
log_info "Building image: $IMAGE_NAME"
log_warn "This will take 15-30 minutes on first build..."
cd "$PROJECT_ROOT"
# Build with Docker
log_info "Building with Docker..."
if ! docker build \
-f "$SCRIPT_DIR/Dockerfile.localnet" \
-t "$IMAGE_NAME" \
"$PROJECT_ROOT"; then
log_error "Docker build failed"
exit 1
fi
# Transfer image to container runtime
log_info "Transferring image to container runtime..."
# Save to temporary file (container image load doesn't support stdin)
TEMP_IMAGE="/tmp/nym-localnet-image-$$.tar"
if ! docker save -o "$TEMP_IMAGE" "$IMAGE_NAME"; then
log_error "Failed to save Docker image"
exit 1
fi
# Load into container runtime from file
if ! container image load --input "$TEMP_IMAGE"; then
rm -f "$TEMP_IMAGE"
log_error "Failed to load image into container runtime"
exit 1
fi
# Clean up temporary file
rm -f "$TEMP_IMAGE"
# Verify image is available
if ! container image inspect "$IMAGE_NAME" &>/dev/null; then
log_error "Image not found in container runtime after load"
exit 1
fi
log_success "Image built and loaded: $IMAGE_NAME"
}
# Create shared volume directory
create_volume() {
log_info "Creating shared volume at: $VOLUME_PATH"
mkdir -p "$VOLUME_PATH"
chmod 777 "$VOLUME_PATH"
log_success "Volume created"
}
# Create shared nym home directory
create_nym_volume() {
log_info "Creating shared nym home volume at: $NYM_VOLUME_PATH"
mkdir -p "$NYM_VOLUME_PATH"
chmod 777 "$NYM_VOLUME_PATH"
log_success "Nym home volume created"
}
# Remove shared volume directory
remove_volume() {
if [ -d "$VOLUME_PATH" ]; then
log_info "Removing volume: $VOLUME_PATH"
rm -rf "$VOLUME_PATH"
log_success "Volume removed"
fi
if [ -d "$NYM_VOLUME_PATH" ]; then
log_info "Removing nym home volume: $NYM_VOLUME_PATH"
rm -rf "$NYM_VOLUME_PATH"
log_success "Nym home volume removed"
fi
}
# Network name
NETWORK_NAME="nym-localnet-network"
# Create container network
create_network() {
log_info "Creating container network: $NETWORK_NAME"
if container network create "$NETWORK_NAME" 2>/dev/null; then
log_success "Network created: $NETWORK_NAME"
else
log_info "Network $NETWORK_NAME already exists or creation failed"
fi
}
# Remove container network
remove_network() {
if container network list | grep -q "$NETWORK_NAME"; then
log_info "Removing network: $NETWORK_NAME"
container network rm "$NETWORK_NAME" 2>/dev/null || true
log_success "Network removed"
fi
}
# Start a mixnode
start_mixnode() {
local node_id=$1
local container_name=$2
log_info "Starting $container_name..."
# Calculate port numbers based on node_id
local mixnet_port="1000${node_id}"
local verloc_port="2000${node_id}"
local http_port="3000${node_id}"
container run \
--name "$container_name" \
-m 2G \
--network "$NETWORK_NAME" \
-p "${mixnet_port}:${mixnet_port}" \
-p "${verloc_port}:${verloc_port}" \
-p "${http_port}:${http_port}" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing mix'"${node_id}"'...";
nym-node run --id mix'"${node_id}"'-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mixnet-bind-address=0.0.0.0:'"${mixnet_port}"' \
--verloc-bind-address=0.0.0.0:'"${verloc_port}"' \
--http-bind-address=0.0.0.0:'"${http_port}"' \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--output=json \
--bonding-information-output="/localnet/mix'"${node_id}"'.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting mix'"${node_id}"'...";
exec nym-node run --id mix'"${node_id}"'-localnet --unsafe-disable-replay-protection --local
'
log_success "$container_name started"
}
# Start gateway
start_gateway() {
log_info "Starting $GATEWAY_CONTAINER..."
container run \
--name "$GATEWAY_CONTAINER" \
-m 2G \
--network "$NETWORK_NAME" \
-p 9000:9000 \
-p 10004:10004 \
-p 20004:20004 \
-p 30004:30004 \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing gateway...";
nym-node run --id gateway-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mode entry-gateway \
--mode exit-gateway \
--mixnet-bind-address=0.0.0.0:10004 \
--entry-bind-address=0.0.0.0:9000 \
--verloc-bind-address=0.0.0.0:20004 \
--http-bind-address=0.0.0.0:30004 \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--output=json \
--bonding-information-output="/localnet/gateway.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting gateway...";
exec nym-node run --id gateway-localnet --unsafe-disable-replay-protection --local
'
log_success "$GATEWAY_CONTAINER started"
# Wait for gateway to be ready
log_info "Waiting for gateway to listen on port 9000..."
local retries=0
local max_retries=30
while ! nc -z 127.0.0.1 9000 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_error "Gateway failed to start on port 9000"
return 1
fi
done
log_success "Gateway is ready on port 9000"
}
# Start network requester
start_network_requester() {
log_info "Starting $REQUESTER_CONTAINER..."
# Get gateway IP address
log_info "Getting gateway IP address..."
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
log_info "Gateway IP: $GATEWAY_IP"
container run \
--name "$REQUESTER_CONTAINER" \
--network "$NETWORK_NAME" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-e "GATEWAY_IP=$GATEWAY_IP" \
-d \
"$IMAGE_NAME" \
sh -c '
while [ ! -f /localnet/network.json ]; do
echo "Waiting for network.json...";
sleep 2;
done;
while ! nc -z $GATEWAY_IP 9000 2>/dev/null; do
echo "Waiting for gateway on port 9000 ($GATEWAY_IP)...";
sleep 2;
done;
SUFFIX=$(date +%s);
nym-network-requester init \
--id "network-requester-$SUFFIX" \
--open-proxy=true \
--custom-mixnet /localnet/network.json \
--output=json > /localnet/network_requester.json;
exec nym-network-requester run \
--id "network-requester-$SUFFIX" \
--custom-mixnet /localnet/network.json
'
log_success "$REQUESTER_CONTAINER started"
}
# Start SOCKS5 client
start_socks5_client() {
log_info "Starting $SOCKS5_CONTAINER..."
container run \
--name "$SOCKS5_CONTAINER" \
--network "$NETWORK_NAME" \
-p 1080:1080 \
-v "$VOLUME_PATH:/localnet:ro" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
"$IMAGE_NAME" \
sh -c '
while [ ! -f /localnet/network_requester.json ]; do
echo "Waiting for network requester...";
sleep 2;
done;
SUFFIX=$(date +%s);
PROVIDER=$(cat /localnet/network_requester.json | grep -o "\"client_address\":\"[^\"]*\"" | cut -d\" -f4);
if [ -z "$PROVIDER" ]; then
echo "Error: Could not extract provider address";
exit 1;
fi;
nym-socks5-client init \
--id "socks5-client-$SUFFIX" \
--provider "$PROVIDER" \
--custom-mixnet /localnet/network.json \
--no-cover;
exec nym-socks5-client run \
--id "socks5-client-$SUFFIX" \
--custom-mixnet /localnet/network.json \
--host 0.0.0.0
'
log_success "$SOCKS5_CONTAINER started"
# Wait for SOCKS5 to be ready
log_info "Waiting for SOCKS5 proxy on port 1080..."
sleep 5
local retries=0
local max_retries=15
while ! nc -z 127.0.0.1 1080 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_warn "SOCKS5 proxy not responding on port 1080 yet"
return 0
fi
done
log_success "SOCKS5 proxy is ready on port 1080"
}
# Stop all containers
stop_containers() {
log_info "Stopping all containers..."
for container_name in "${ALL_CONTAINERS[@]}"; do
if container inspect "$container_name" &>/dev/null; then
log_info "Stopping $container_name"
container stop "$container_name" 2>/dev/null || true
container rm "$container_name" 2>/dev/null || true
fi
done
# Also clean up init container if it exists
container rm "$INIT_CONTAINER" 2>/dev/null || true
log_success "All containers stopped"
cleanup_host_state
remove_network
}
# Show container logs
show_logs() {
local container_name=${1:-}
if [ -z "$container_name" ]; then
# No container specified - launch tmux log viewer
log_info "Launching tmux log viewer for all containers..."
exec "$SCRIPT_DIR/localnet-logs.sh"
fi
# Show logs for specific container
if container inspect "$container_name" &>/dev/null; then
container logs -f "$container_name"
else
log_error "Container not found: $container_name"
log_info "Available containers:"
for name in "${ALL_CONTAINERS[@]}"; do
echo " - $name"
done
exit 1
fi
}
# Show container status
show_status() {
log_info "Container status:"
echo ""
for container_name in "${ALL_CONTAINERS[@]}"; do
if container inspect "$container_name" &>/dev/null; then
local status=$(container inspect "$container_name" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
echo -e " ${GREEN}${NC} $container_name - $status"
else
echo -e " ${RED}${NC} $container_name - not running"
fi
done
echo ""
log_info "Port status:"
for port in 9000 1080 10001 10002 10003 10004; do
if nc -z 127.0.0.1 $port 2>/dev/null; then
echo -e " ${GREEN}${NC} Port $port - listening"
else
echo -e " ${RED}${NC} Port $port - not listening"
fi
done
}
# Build network topology with container IPs
build_topology() {
log_info "Building network topology with container IPs..."
# Wait for all bonding JSON files to be created
log_info "Waiting for all nodes to complete initialization..."
for file in mix1.json mix2.json mix3.json gateway.json; do
while [ ! -f "$VOLUME_PATH/$file" ]; do
echo " Waiting for $file..."
sleep 1
done
log_success " $file created"
done
# Get container IPs
log_info "Getting container IP addresses..."
MIX1_IP=$(container exec "$MIXNODE1_CONTAINER" hostname -i)
MIX2_IP=$(container exec "$MIXNODE2_CONTAINER" hostname -i)
MIX3_IP=$(container exec "$MIXNODE3_CONTAINER" hostname -i)
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
log_info "Container IPs:"
echo " mix1: $MIX1_IP"
echo " mix2: $MIX2_IP"
echo " mix3: $MIX3_IP"
echo " gateway: $GATEWAY_IP"
# Run build_topology.py in a container with access to the volumes
container run \
--name "nym-localnet-topology-builder" \
--network "$NETWORK_NAME" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
--rm \
"$IMAGE_NAME" \
python3 /usr/local/bin/build_topology.py \
/localnet \
"$SUFFIX" \
"$MIX1_IP" \
"$MIX2_IP" \
"$MIX3_IP" \
"$GATEWAY_IP"
# Verify network.json was created
if [ -f "$VOLUME_PATH/network.json" ]; then
log_success "Network topology created successfully"
else
log_error "Failed to create network topology"
exit 1
fi
}
# Start all services
start_all() {
log_info "Starting Nym Localnet..."
cleanup_host_state
create_network
create_volume
create_nym_volume
start_mixnode 1 "$MIXNODE1_CONTAINER"
start_mixnode 2 "$MIXNODE2_CONTAINER"
start_mixnode 3 "$MIXNODE3_CONTAINER"
start_gateway
build_topology
start_network_requester
start_socks5_client
echo ""
log_success "Nym Localnet is running!"
echo ""
echo "Test with:"
echo " curl -x socks5h://127.0.0.1:1080 https://nymtech.net"
echo ""
echo "View logs:"
echo " $0 logs # All containers in tmux"
echo " $0 logs gateway # Single container"
echo ""
echo "Stop:"
echo " $0 down"
echo ""
}
# Main command handler
main() {
check_prerequisites
local command=${1:-help}
shift || true
case "$command" in
build)
build_image
;;
up)
build_image
start_all
;;
start)
start_all
;;
down|stop)
stop_containers
remove_volume
;;
restart)
stop_containers
start_all
;;
logs)
show_logs "$@"
;;
status|ps)
show_status
;;
help|--help|-h)
cat <<EOF
Nym Localnet Orchestration Script
Usage: $0 <command> [options]
Commands:
build Build the localnet image
up Build image and start all services
start Start all services (requires built image)
down, stop Stop all services and clean up
restart Restart all services
logs [name] Show logs (no args = tmux overlay, with name = single container)
status, ps Show status of all containers and ports
help Show this help message
Examples:
$0 up # Build and start everything
$0 logs # View all logs in tmux overlay
$0 logs gateway # View gateway logs only
$0 status # Check what's running
$0 down # Stop and clean up
EOF
;;
*)
log_error "Unknown command: $command"
echo "Run '$0 help' for usage information"
exit 1
;;
esac
}
main "$@"