Docker/Container localnet
This commit is contained in:
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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:])
|
||||
Executable
+64
@@ -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
|
||||
Executable
+590
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user