Configuration Reference

Server and client read TOML configs from /etc/hash-relay/server.toml and /etc/hash-relay/client.toml. The install scripts generate these from the annotated templates shipped with each release:

The snippets below mirror the current templates (v0.2.2). Placeholders like {{JWT_SECRET}} are substituted by the installer.

server.toml

Logging — [log]

[log]
file_enabled  = true
log_dir       = "/var/log/hash-relay"
log_name      = "hr-server"
rotation      = "daily"    # daily | hourly | never
max_log_files = 30
level         = "info"     # trace | debug | info | warn | error | off
# timezone = "+8"          # optional; recommended on OpenWrt without tzdata

Relay listener — [server]

[server]
listen    = "0.0.0.0"
tcp_port  = 8555
quic_port = 9555

Encryption / TLS — [server.enc]

[server.enc]
# TLS cert source (shared by TCP and QUIC):
#   acme        — ACME auto issue/renew (production)
#   file        — manual cert files
#   self_signed — persistent self-signed cert (LAN / no domain)
tls               = "self_signed"
frame_encrypt_alg = 0      # 0=off  1=AES-GCM  2=ChaCha20

# --- tls = "acme" ---
# domain  = "relay.example.com"
# contact = "mailto:admin@example.com"
# cache_dir = "/var/lib/hash-relay/certs"
# acme_challenge_port = 8080   # when port 80 is taken by Nginx/Caddy

# --- tls = "file" ---
# cert_path = "/etc/ssl/certs/hash-relay/fullchain.pem"
# key_path  = "/etc/ssl/private/hash-relay/privkey.pem"
# Cert hot-reload: server checks mtime every 12h; no restart after renewal.

# --- tls = "self_signed" ---
cache_dir = "/var/lib/hash-relay/certs"
# self_signed_san = "203.0.113.10"
# On first start, server logs pin_sha256 — set on clients for cert pinning

Authentication — [server.auth]

[server.auth]
# type: "token" (static token) | "challenge" (challenge-response)
type                = "token"
max_timestamp_drift = 300

Client access tokens are managed in the client_nodes table via the admin UI or API.

Database — [server.database]

[server.database]
# Driver is selected from URL scheme (MySQL / PostgreSQL / SQLite)
url = "sqlite:///var/lib/hash-relay/hashrelay.db?mode=rwc"
# url = "mysql://user:password@127.0.0.1:3306/hashrelay"
# url = "postgres://user:password@127.0.0.1:5432/hashrelay"
max_connections              = 50
min_connections              = 5
worker_event_retention_days  = 90   # 0 = disable purge
worker_extra_retention_days  = 90

Management API — [server.api]

[server.api]
listen     = "0.0.0.0"   # use 127.0.0.1 + reverse proxy when possible
port       = 8080
tls        = false       # true = HTTPS on API port (same cert as [server.enc])
jwt_secret = "CHANGE_ME_TO_A_LONG_RANDOM_STRING"
jwt_expiry = 86400        # seconds (24 h)
compress   = true         # zstd preferred, then gzip

Web admin UI — [server.web]

[server.web]
enabled = true
# port = 8081            # omit to share [server.api].port
dir     = "/var/lib/hash-relay/web"
compress = true
# /api/v1/* → API; other paths → static frontend

Connections — [server.connection]

[server.connection]
heartbeat_interval      = 60
heartbeat_timeout_count = 3
max_client_connections  = 0   # 0 = unlimited

Protocol mirroring & alerts — [sniffing]

Per-pool sniffing is toggled via pools.sniffing_enabled in the database. These are global engine settings:

[sniffing]
channel_capacity = 8192
parser_workers   = 0      # 0 = auto (CPU cores / 4, min 1)
flush_interval   = 30     # miner stats DB flush interval (seconds)

[sniffing.alerts]
enabled                    = true
reject_rate_threshold      = 0.10   # 10%
reject_rate_window         = 50     # last N shares
offline_threshold          = 300    # seconds
hashrate_drop_threshold    = 0.50   # vs 15-min average
hashrate_zero_periods      = 3
consecutive_stale_threshold = 5
high_latency_threshold_ms  = 5000
reconnect_window           = 300
reconnect_threshold        = 3
alert_cooldown             = 600

[sniffing.alerts.webhook]
enabled = false
url     = ""
timeout = 10
# [sniffing.alerts.webhook.headers]
# Authorization = "Bearer xxx"

Miner admin proxy — [server.admin_proxy]

Lets operators open a miner's built-in web UI from the dashboard while the miner stays on a private LAN. Flow: Browser → server /proxy/{token}/… → client → miner.

[server.admin_proxy]
enabled            = true
open_timeout_secs  = 10
ready_timeout_secs = 120
idle_timeout_secs  = 600
max_lifetime_secs  = 7200
token_ttl_secs     = 1800
drain_timeout_secs = 30
max_per_admin      = 3
max_per_node       = 10
max_global         = 100
max_per_worker     = 2
max_concurrent_http = 6

License (optional) — [license]

# [license]
# instance_id = "my-server-01"   # or HASHRELAY_INSTANCE_ID
# key         = "LIC-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
# token_cache = "/var/lib/hash-relay/license_token.dat"

client.toml

Logging — [log]

[log]
file_enabled  = true
log_dir       = "/var/log/hash-relay"
log_name      = "hr-client"
rotation      = "daily"
max_log_files = 30
level         = "info"

Server endpoint — [client]

[client]
server_addr = "relay.example.com"
tcp_port    = 8555    # must match server [server].tcp_port
quic_port   = 9555    # must match server [server].quic_port
client_id   = "farm-a"
token       = "CHANGE_ME_ACCESS_TOKEN"

Encryption — [client.enc]

[client.enc]
frame_encrypt_alg = 0       # must match server
insecure          = true    # skip cert validation (testing / self-signed)
# pin_sha256 = "..."        # from server startup logs (recommended)

Connection management — [client.connection]

[client.connection]
reconnect_interval      = 5
max_reconnect_attempts  = 0   # 0 = unlimited
heartbeat_interval      = 60
heartbeat_timeout_count = 3
Local listen ports are not configured on the client. After handshake, the server pushes port mappings via ConfigSync. The client opens listeners dynamically; transport (TCP or QUIC) follows each mapping's protocol field.

Hardware telemetry — [client.telemetry]

Periodically polls miner admin APIs (asic-rs) for connected miners and reports stats to the server.

[client.telemetry]
enabled             = true
collect_interval_secs = 300

Miner admin proxy — [client.admin_proxy]

When the server opens a proxy tunnel, the client dials the miner's web admin port on the LAN and relays HTTP/WebSocket bytes over the control channel.

[client.admin_proxy]
enabled          = true
max_sessions     = 10
idle_timeout_secs = 600

Environment Variable Overrides

Install scripts accept these overrides. At runtime, DATABASE_URL and RUST_LOG can also be set in the systemd/procd unit:

VariableApplies ToEffect
LISTENserver installRelay bind address ([server].listen)
TCP_PORT / QUIC_PORT / API_PORTserver installRelay and API ports
API_LISTENserver installAPI/Web bind address ([server.api].listen)
DATABASE_URLserverOverride [server.database].url at runtime
JWT_SECRETserver installSet [server.api].jwt_secret
DATA_DIR / BIN_DIR / WEB_DIR / LOG_DIRserver installOverride install paths
SERVER_ADDRclient installRelay hostname or IP
TCP_PORT / QUIC_PORTclient installMust match server ports
CLIENT_ID / TOKENclient installNode identity and access token
DATA_DIR / BIN_DIRclient installOverride install paths
RUST_LOGbothOverride [log].level
CONFIG_PATHserverConfig file path (set by service unit)

TLS Modes

  • self_signed (default) — persistent self-signed cert for LAN or no domain; copy pin_sha256 from server logs to clients
  • acme— automatic Let's Encrypt issuance and renewal; set domain, contact, and acme_challenge_port when port 80 is occupied
  • file — supply cert_path / key_path; hot-reloads every 12 hours without restart
Keep [server.api].tls = false when Nginx or Caddy terminates TLS in front of the management port. Enable it only when the API/Web UI should speak HTTPS directly.