# ============================================================ # hash-relay server — remote install template # ============================================================ # Placeholders {{...}} are replaced by install-server.sh. # After install, edit /etc/hash-relay/server.toml as needed. # # Environment overrides (install-server.sh): # LISTEN, TCP_PORT, QUIC_PORT, API_PORT, DATABASE_URL, JWT_SECRET # DATABASE_URL — overrides [server.database].url at runtime # CONFIG_PATH — config file path (set by systemd/procd) # RUST_LOG — log level (overrides [log].level when set) # ------------------------------------------------------------ # Logging # ------------------------------------------------------------ [log] # Enable file logging (false = console only) file_enabled = true # Log directory (absolute path recommended for systemd installs) log_dir = "{{LOG_DIR}}" # Log file name prefix (e.g. hr-server.2026-02-16.log) log_name = "hr-server" # Rotation: "daily" | "hourly" | "never" rotation = "daily" # Max rotated log files to keep max_log_files = 30 # Level: trace | debug | info | warn | error | off level = "info" # Timezone offset (optional). Auto-detects system TZ when omitted. # Recommended on OpenWrt and systems without tzdata, e.g. "+8", "-5", "+05:30" # timezone = "+8" [server] # Bind address (0.0.0.0 = all interfaces) listen = "{{LISTEN}}" # TCP port for client connections tcp_port = {{TCP_PORT}} # QUIC port (UDP) quic_port = {{QUIC_PORT}} # ------------------------------------------------------------ # Encryption / transport security # ------------------------------------------------------------ [server.enc] # TLS certificate source (shared by TCP and QUIC): # "acme" — ACME auto issue/renew (recommended for production) # "file" — manual certificate files # "self_signed" — persistent 99-year self-signed cert (LAN / no domain) tls = "self_signed" # Application-layer frame encryption (can coexist with TLS): # 0 = off, 1 = AES-GCM, 2 = ChaCha20 frame_encrypt_alg = 0 # --- When tls = "acme" --- # domain = "relay.example.com" # contact = "mailto:admin@example.com" # cache_dir = "/var/lib/hash-relay/certs" # # ACME HTTP-01 challenge listen port (default 80). # If port 80 is used by Nginx/Caddy, set a high port and reverse-proxy # /.well-known/acme-challenge/ to it. # # Scenario 1: port 80 free — leave default, no change needed. # # Scenario 2: Nginx on port 80 # 1. Set acme_challenge_port = 8080 # 2. Add to Nginx: # server { # listen 80; # server_name relay.example.com; # location /.well-known/acme-challenge/ { # proxy_pass http://127.0.0.1:8080; # } # } # # Scenario 3: Caddy on port 80 # 1. Set acme_challenge_port = 8080 # 2. Add to Caddyfile: # relay.example.com { # handle /.well-known/acme-challenge/* { # reverse_proxy localhost:8080 # } # } # # acme_challenge_port = 8080 # --- When tls = "file" --- # cert_path = "/etc/ssl/certs/hash-relay/fullchain.pem" # key_path = "/etc/ssl/private/hash-relay/privkey.pem" # # Caddy certificate path examples: # Linux: cert_path = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/your-domain/your-domain.crt" # key_path = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/your-domain/your-domain.key" # # Linux permission note (Caddy certs): # Caddy stores certs as mode 600 (caddy user only). If hash-relay runs as another user: # Option A: add hash-relay user to caddy group, chmod 640 cert/key files # Option B: setfacl -m u::r on cert and key files # # Cert hot-reload: server checks file mtime every 12 hours; no restart needed after renewal. # --- When tls = "self_signed" --- # Self-signed cert storage (default below; creates self-signed.crt/key) cache_dir = "{{CACHE_DIR}}" # Optional SAN for self-signed cert (default: hash-relay) # self_signed_san = "203.0.113.10" # On first start, server logs pin_sha256 — set [client.enc].pin_sha256 on clients for pinning # ------------------------------------------------------------ # Authentication # ------------------------------------------------------------ # Client node auth; access tokens are managed in the client_nodes table. # type: "token" (static token) | "challenge" (challenge-response, requires key pair) [server.auth] type = "token" # Anti-replay: max allowed timestamp drift (seconds) max_timestamp_drift = 300 # ------------------------------------------------------------ # Database # ------------------------------------------------------------ # Supports MySQL, PostgreSQL, and SQLite. Driver is selected from URL scheme. [server.database] # MySQL example: # url = "mysql://user:password@127.0.0.1:3306/hashrelay" # PostgreSQL example: # url = "postgres://user:password@127.0.0.1:5432/hashrelay" # SQLite example (mode=rwc = read/write, create if missing): url = "{{DATABASE_URL}}" # Pool max connections (50+ for MySQL/PostgreSQL; ignored for SQLite) max_connections = 50 # Pool min idle connections (ignored for SQLite) min_connections = 5 # Days to keep worker_events (by event_at; 0 = disable purge) worker_event_retention_days = 90 # Days to keep worker_extra history (by created_at; 0 = disable purge) worker_extra_retention_days = 90 # ------------------------------------------------------------ # Management API # ------------------------------------------------------------ # REST API + Web admin UI; bind 127.0.0.1 and expose via reverse proxy when possible. [server.api] listen = "{{API_LISTEN}}" port = {{API_PORT}} # HTTPS for API/Web (same cert as [server.enc]); keep false when Nginx/Caddy terminates TLS in front tls = false # JWT signing secret for admin login tokens — change to a strong random value jwt_secret = "{{JWT_SECRET}}" # JWT expiry (seconds); default 86400 (24 hours) jwt_expiry = 86400 # Response compression (negotiated via Accept-Encoding: zstd preferred, then gzip) compress = true # ------------------------------------------------------------ # Web admin UI # ------------------------------------------------------------ # UI is served on the API port unless [server.web].port is set. # /api/v1/* → API; other paths → static frontend files. [server.web] # Enable built-in web admin UI (false = API only) enabled = true # Separate UI port (omit or comment out to share [server.api].port) # port = 8081 # Static frontend directory (npm run build output) dir = "{{WEB_DIR}}" # Static asset compression (/proxy/* miner admin proxy is handled separately on the client) compress = true # ------------------------------------------------------------ # Heartbeat & connections # ------------------------------------------------------------ [server.connection] # Heartbeat interval (seconds) heartbeat_interval = 60 # Disconnect after this many unanswered heartbeats heartbeat_timeout_count = 3 # Max client connections per server instance (0 = unlimited) max_client_connections = 0 # ------------------------------------------------------------ # Protocol mirroring / sniffing (global engine settings) # ------------------------------------------------------------ # Per-pool sniffing is controlled by pools.sniffing_enabled in the database. # These are global engine parameters. [sniffing] # Mirror channel capacity (drops oldest when full to avoid blocking relay) channel_capacity = 8192 # Parser worker threads (0 = auto: CPU cores / 4, min 1) parser_workers = 0 # Miner stats DB flush interval (seconds) flush_interval = 30 [sniffing.alerts] # Global alert switch enabled = true # High reject rate threshold (0.10 = 10%) reject_rate_threshold = 0.10 # Reject rate window (last N shares) reject_rate_window = 50 # Miner offline threshold (seconds) offline_threshold = 300 # Hashrate drop vs 15-min average (0.50 = 50% drop) hashrate_drop_threshold = 0.50 # Consecutive zero-hashrate periods before alert hashrate_zero_periods = 3 # Consecutive stale shares before alert consecutive_stale_threshold = 5 # Share latency alert threshold (milliseconds) high_latency_threshold_ms = 5000 # Frequent reconnect detection window (seconds) reconnect_window = 300 # Reconnect count threshold within window reconnect_threshold = 3 # Cooldown for duplicate alerts (seconds) alert_cooldown = 600 [sniffing.alerts.webhook] # Webhook notifications (Slack, Telegram, DingTalk, Feishu, etc.) enabled = false url = "" # HTTP timeout (seconds) timeout = 10 # Custom headers (optional) # [sniffing.alerts.webhook.headers] # Authorization = "Bearer xxx" # ------------------------------------------------------------ # License (optional) # ------------------------------------------------------------ # [license] # Instance ID (optional; overrides auto machine_id; or set HASHRELAY_INSTANCE_ID) # instance_id = "my-server-01" # License key from VS admin # key = "LIC-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" # Token cache when VS is unreachable # token_cache = "/var/lib/hash-relay/license_token.dat" # ------------------------------------------------------------ # Miner admin panel reverse-proxy (Admin Proxy) # ------------------------------------------------------------ # Lets operators open a miner's built-in web UI from the hash-relay dashboard # while the miner stays on a private LAN. Flow depends on access_mode: # path: Browser → /proxy/{token}/… → Client → miner # subdomain: Browser → {token}.{base_domain}/… → Client → miner # (requires wildcard DNS + TLS for *.{base_domain}; operator-managed) # Requires stats.write and a connected client node. [server.admin_proxy] # Master switch for the feature and proxy gateway routes enabled = true # Browser entry mode: # "path" — /proxy/{token}/… (default; path rewrite + Service Worker) # "subdomain" — https://{token}.{base_domain}/… (no path rewrite; needs DNS + wildcard cert) access_mode = "path" # Required when access_mode = "subdomain" (bare hostname, no scheme/port/path): # 2nd-level: base_domain = "example.com" → {token}.example.com # 3rd-level: base_domain = "proxy.example.com" → {token}.proxy.example.com # base_domain = "proxy.example.com" # Absolute URL scheme returned to the UI for subdomain mode ("http" / "https"). # When omitted: https if server.api.tls, else http. Set "https" when TLS terminates # at a reverse proxy and api.tls = false. # public_scheme = "https" # Max seconds to wait for the client to connect to the miner after AdminProxyOpen open_timeout_secs = 10 # After connect, max seconds to wait for the first browser HTTP request (session stays Ready) ready_timeout_secs = 120 # Max seconds with no traffic before an active session is closed idle_timeout_secs = 600 # Hard cap on total session lifetime from creation (seconds) max_lifetime_secs = 7200 # HMAC URL token lifetime returned to the browser (seconds) token_ttl_secs = 1800 # After close, seconds to keep draining in-flight bytes before force cleanup drain_timeout_secs = 30 # Concurrent proxy sessions per administrator account max_per_admin = 3 # Concurrent proxy sessions per connected client node max_per_node = 10 # Concurrent proxy sessions across the whole server max_global = 100 # Concurrent proxy sessions per worker (miner); reuse of an existing session does not count as new max_per_worker = 2 # Max parallel HTTP round-trips per proxy session (browser concurrent asset loads) max_concurrent_http = 6