Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ All notable changes to RigForge are documented here. The format is based on

## [Unreleased]

### Added

- **A pool can be dialled through a SOCKS5 proxy (#400).** XMRig has always supported a per-pool
`socks5`, but RigForge rebuilt each pool from a fixed key set, so the key was dropped and the
operator got a warning that it was ignored — leaving a rig no way to reach a stratum published as
an onion service. `socks5` now passes through as `host:port`, emitted only when set, so no existing
rig's generated config changes shape on its next apply. Its address is checked by the same
validator as the pool `url`, which is now shared between the two rather than copied. RigForge
points the miner at a proxy; running one is still the operator's to arrange.

### Fixed

- **A failed watchdog re-render no longer reports the change as applied (#395).** `install_watchdog`
Expand Down
4 changes: 2 additions & 2 deletions config.reference.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"_docs": "Reference for every config.json key, each shown with its default value. Copy ONLY the keys you actually want to change into config.json; any key you omit keeps the default shown here. The pool target is the native XMRig 'pools' array: each pool needs a 'url' (host:port); every other field falls back to a Pithead-friendly default. A pool's 'user' is the rig's label on the dashboard (defaults to the machine hostname); list multiple entries for failover. An empty ACCESS_TOKEN (the default) leaves the rig's read-only HTTP API open (no token) — which matches Pithead's default no-auth stats probe; set a value only to require a Bearer token (then match it dashboard-side). 'autotune' is one of \"disabled\" (default — no scheduled tuning), \"performance\" (monthly tune for raw hashrate), or \"efficiency\" (monthly tune for hashrate-per-watt). 'watchdog' set to \"enabled\" installs a timer that health-checks the miner every 'watchdog_interval_min' minutes (default 5) and restarts it after two consecutive checks see 0 H/s or a dead API; 'max_temp_c' (empty = off) additionally stops the miner above that °C and starts it again 5°C below — check what your board's thermal_zone0 reports before setting it. 'miner_user' (empty = run as root, the default) runs the miner as that dedicated non-root system user — RigForge applies the MSR preset root-side; on CPU families without a known preset the ~10-15% MSR boost is skipped. 'api_allow_from' (empty = off) scopes the API port(s) — including the writable control port when 'control' is enabled — to one IPv4 or IPv6 address/CIDR source + loopback via an own nftables table (Linux, needs nft; for an IPv6 source the served port must bind a v6 address, so set 'api_bind'/'control_bind' to \"::\"). Set 'api' to enabled for the sister API: a second read-only port (default 8081) serving XMRig's summary enriched with RigForge tune/power/health/provenance data, gated by the same ACCESS_TOKEN; 'api_port'/'api_bind' place it. Set 'control' to \"enabled\" for the WRITABLE control path (#236): a SEPARATE authenticated port (default 8082, 'control_port'/'control_bind' place it) that lets a Pithead stack apply config changes through RigForge so config.json stays authoritative. Fail-closed: enabling it REQUIRES both ACCESS_TOKEN and api_allow_from (a writable API with no token or no pinned source is refused). Only pools, DONATION, autotune, watchdog(+interval), and max_temp_c are writable through it (and the REMOTE path refuses to disable watchdog or to unset/out-of-band max_temp_c — a rig's thermal protection can only be removed by a local rigforge.sh apply, #257); every change is validated, the old config is snapshotted to config-backups/, and a change that doesn't come back live is rolled back. Set 'control_upgrade' to \"enabled\" (a SECOND opt-in, only valid when 'control' is also enabled) to let the stack trigger this rig to upgrade its own RigForge to the latest release (#308, ADR 0002) — i.e. fetch and run new root code on a remote trigger. Default off, and never on just because 'control' is: the rig refuses any target that isn't a real release newer than the one running, verifies the release commit is reachable on main (the release branch), throttles repeat runs, and health-gates the swap with rollback. It rides the control path's own Bearer token + api_allow_from pin. Releases are checksummed, not signed — GitHub is the trust root (see SECURITY.md). For co-locating a miner on a busy box (e.g. a Pithead stack host), 'hugepages_reserve_extra_mb' (default 0) adds that many MB of HugePages to RigForge's computed mining reservation so the pool covers stack + miner — RigForge stays the sole writer of the reservation and, when the box's existing reservation already covers both, changes nothing and needs no reboot; 'hugepages_pool_ceiling_mb' (default 0 = no ceiling) is a HARD CAP on the runtime pool RigForge will grow to, in MB, distinct from hugepages_reserve_extra_mb (which adds declared headroom into the computed requirement rather than bounding the write) — set it on a RAM-constrained co-resident box so the grow-only write can never exceed the box's honest capacity no matter what the requirement/availability math computes (rigforge#398; an odd MB value floors to the 2MB page below, e.g. 5121 -> 5120MB effective, never rounds up past the declared ceiling); 'threads' (empty/null = auto) caps the RandomX thread count (a ceiling — min(auto, threads), e.g. nproc-2) so the miner leaves spare cores, and it sizes the reservation to match. A TLS pool can pin the server certificate with 'tls-fingerprint' (its SHA-256 as 64 hex chars; null = no pin) — XMRig does no CA verification for stratum, so the pin is the only server authentication TLS stratum has. Full descriptions: https://github.com/p2pool-starter-stack/rigforge/blob/main/docs/configuration.md#configuration-reference",
"_docs": "Reference for every config.json key, each shown with its default value. Copy ONLY the keys you actually want to change into config.json; any key you omit keeps the default shown here. The pool target is the native XMRig 'pools' array: each pool needs a 'url' (host:port); every other field falls back to a Pithead-friendly default. A pool's 'user' is the rig's label on the dashboard (defaults to the machine hostname); list multiple entries for failover. A pool's 'socks5' (default null) dials that pool through a SOCKS5 proxy given as host:port \u2014 e.g. \"127.0.0.1:9050\" for a local Tor client, which is how a rig reaches a stratum published as an onion service; XMRig sends the hostname to the proxy, so the proxy resolves it. RigForge points the miner at a proxy, it does not install or run one. An empty ACCESS_TOKEN (the default) leaves the rig's read-only HTTP API open (no token) — which matches Pithead's default no-auth stats probe; set a value only to require a Bearer token (then match it dashboard-side). 'autotune' is one of \"disabled\" (default — no scheduled tuning), \"performance\" (monthly tune for raw hashrate), or \"efficiency\" (monthly tune for hashrate-per-watt). 'watchdog' set to \"enabled\" installs a timer that health-checks the miner every 'watchdog_interval_min' minutes (default 5) and restarts it after two consecutive checks see 0 H/s or a dead API; 'max_temp_c' (empty = off) additionally stops the miner above that °C and starts it again 5°C below — check what your board's thermal_zone0 reports before setting it. 'miner_user' (empty = run as root, the default) runs the miner as that dedicated non-root system user — RigForge applies the MSR preset root-side; on CPU families without a known preset the ~10-15% MSR boost is skipped. 'api_allow_from' (empty = off) scopes the API port(s) — including the writable control port when 'control' is enabled — to one IPv4 or IPv6 address/CIDR source + loopback via an own nftables table (Linux, needs nft; for an IPv6 source the served port must bind a v6 address, so set 'api_bind'/'control_bind' to \"::\"). Set 'api' to enabled for the sister API: a second read-only port (default 8081) serving XMRig's summary enriched with RigForge tune/power/health/provenance data, gated by the same ACCESS_TOKEN; 'api_port'/'api_bind' place it. Set 'control' to \"enabled\" for the WRITABLE control path (#236): a SEPARATE authenticated port (default 8082, 'control_port'/'control_bind' place it) that lets a Pithead stack apply config changes through RigForge so config.json stays authoritative. Fail-closed: enabling it REQUIRES both ACCESS_TOKEN and api_allow_from (a writable API with no token or no pinned source is refused). Only pools, DONATION, autotune, watchdog(+interval), and max_temp_c are writable through it (and the REMOTE path refuses to disable watchdog or to unset/out-of-band max_temp_c — a rig's thermal protection can only be removed by a local rigforge.sh apply, #257); every change is validated, the old config is snapshotted to config-backups/, and a change that doesn't come back live is rolled back. Set 'control_upgrade' to \"enabled\" (a SECOND opt-in, only valid when 'control' is also enabled) to let the stack trigger this rig to upgrade its own RigForge to the latest release (#308, ADR 0002) — i.e. fetch and run new root code on a remote trigger. Default off, and never on just because 'control' is: the rig refuses any target that isn't a real release newer than the one running, verifies the release commit is reachable on main (the release branch), throttles repeat runs, and health-gates the swap with rollback. It rides the control path's own Bearer token + api_allow_from pin. Releases are checksummed, not signed — GitHub is the trust root (see SECURITY.md). For co-locating a miner on a busy box (e.g. a Pithead stack host), 'hugepages_reserve_extra_mb' (default 0) adds that many MB of HugePages to RigForge's computed mining reservation so the pool covers stack + miner — RigForge stays the sole writer of the reservation and, when the box's existing reservation already covers both, changes nothing and needs no reboot; 'hugepages_pool_ceiling_mb' (default 0 = no ceiling) is a HARD CAP on the runtime pool RigForge will grow to, in MB, distinct from hugepages_reserve_extra_mb (which adds declared headroom into the computed requirement rather than bounding the write) — set it on a RAM-constrained co-resident box so the grow-only write can never exceed the box's honest capacity no matter what the requirement/availability math computes (rigforge#398; an odd MB value floors to the 2MB page below, e.g. 5121 -> 5120MB effective, never rounds up past the declared ceiling); 'threads' (empty/null = auto) caps the RandomX thread count (a ceiling — min(auto, threads), e.g. nproc-2) so the miner leaves spare cores, and it sizes the reservation to match. A TLS pool can pin the server certificate with 'tls-fingerprint' (its SHA-256 as 64 hex chars; null = no pin) — XMRig does no CA verification for stratum, so the pin is the only server authentication TLS stratum has. Full descriptions: https://github.com/p2pool-starter-stack/rigforge/blob/main/docs/configuration.md#configuration-reference",

"pools": [
{ "url": "<YOUR_POOL_HOST>:3333", "user": "", "pass": "x", "keepalive": true, "tls": false, "tls-fingerprint": null, "enabled": true }
{ "url": "<YOUR_POOL_HOST>:3333", "user": "", "pass": "x", "keepalive": true, "tls": false, "tls-fingerprint": null, "socks5": null, "enabled": true }
],

"ACCESS_TOKEN": "",
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ you care about:
| `keepalive` | `true` |
| `tls` | `false` — set `true` when you connect on the pool's TLS/SSL port. |
| `tls-fingerprint` | `null` (no pin) — the pool cert's SHA-256 as 64 hex chars. XMRig does no CA validation on stratum TLS, so the pin is the only server authentication; without it, TLS encrypts but doesn't authenticate. Requires `"tls": true`. See [Pithead Integration › Stratum over TLS](pithead-integration.md#stratum-over-tls-optional). |
| `socks5` | `null` (direct connection) — dial this pool through a SOCKS5 proxy at `host:port`, e.g. `"127.0.0.1:9050"` for a local Tor client. Same address rules as `url`. XMRig sends the pool's **hostname** to the proxy rather than resolving it first, so a v3 `.onion` stratum works with no extra setting. RigForge points the miner at a proxy; running one is yours to arrange. |
| `enabled` | `true` |

Two common setups follow; pick the one that matches where you're mining.
Expand Down
58 changes: 44 additions & 14 deletions rigforge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,31 @@ ensure_config_exists() {
fi
}

# #400: ONE host:port validator, shared by a pool's `url` and its `socks5` proxy. The issue asks for
# the socks5 value to get "the same rules the pool URL gets" — a second copy of these four checks
# would satisfy that on the day it was written and drift the first time either side is touched, so
# the rules live in one place instead. <value> <label> <example-port>: the label leads the operator
# -facing message, and the example port makes the hint fit its key (a pool is :3333, a SOCKS5 proxy
# is :9050). Every check and every regex below is verbatim from the pool-url path it replaces.
_validate_host_port() { # <value> <label> <example-port>
local _v="$1" _label="$2" _eg="$3" _h _p
if ! [[ "$_v" =~ :[0-9]+$ ]]; then
error "$_label '$_v' must include a port, e.g. $_v:$_eg."
fi
_h="${_v%:*}"
_p="${_v##*:}"
if [ "$_p" -lt 1 ] || [ "$_p" -gt 65535 ]; then
error "$_label port must be between 1 and 65535 (got '$_p' in '$_v')."
fi
# The host must be a valid hostname / FQDN / IPv4, or a bracketed IPv6 literal. This also
# rejects the unfilled template placeholder (<...>), whitespace, and shell/URL metacharacters.
# A v3 .onion is an ordinary hostname to this pattern, which is what makes #400 work at all.
case "$_h" in
\[*\]) [[ "$_h" =~ ^\[[0-9A-Fa-f:]+\]$ ]] || error "$_label '$_v' has an invalid IPv6 literal (use [addr]:port)." ;;
*) [[ "$_h" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]] || error "$_label host '$_h' is not a valid hostname or IP." ;;
esac
}

parse_config() {
log "Parsing configuration..."
# A missing config (e.g. `apply`/`tune` before `setup`) is a different, clearer error than bad JSON.
Expand Down Expand Up @@ -430,6 +455,14 @@ parse_config() {
# attribute in-string program lines, and the patch-coverage gate needs every new line hittable.
POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | ."tls-fingerprint"]] | transpose | map(.[0] + (if (.[1] // null) != null then {"tls-fingerprint": .[1]} else {} end))' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON."

# socks5 (#400): re-attach the per-pool proxy from the raw config, emitted ONLY when set — the map
# above rebuilds each pool from a fixed key set, which is what dropped this key. Same shape and
# the same reasons as the #115 pass directly above: emitting it unconditionally (null) would
# change the generated config's shape for every existing rig on its next apply, and it is a
# single-line pass rather than lines inside the map because kcov cannot attribute in-string
# program lines and the patch-coverage gate needs every new line hittable.
POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | .socks5]] | transpose | map(.[0] + (if (.[1] // null) != null then {"socks5": .[1]} else {} end))' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON."

# #265: jq's `//` treats an explicit false like null/missing, so the map above rewrites an
# operator's "keepalive": false / "enabled": false to the true default. Restore explicit falses
# from the raw config in another single-line pass (same kcov rationale as #115 above). Only a
Expand All @@ -447,20 +480,17 @@ parse_config() {
_user=$(jq -r '.user' <<<"$_pool")
_pass=$(jq -r '.pass' <<<"$_pool")
[ -n "$_u" ] || error "A pool entry has no url — set 'pools[].url' (host:port) in $CONFIG_JSON."
if ! [[ "$_u" =~ :[0-9]+$ ]]; then
error "Pool url '$_u' must include a port, e.g. $_u:3333."
fi
_host="${_u%:*}"
_port="${_u##*:}"
if [ "$_port" -lt 1 ] || [ "$_port" -gt 65535 ]; then
error "Pool port must be between 1 and 65535 (got '$_port' in '$_u')."
_validate_host_port "$_u" "Pool url" 3333
# SOCKS5 proxy (#400), per pool, emitted only when set. XMRig dials the pool through it and
# sends the HOSTNAME in the CONNECT request (Client::Socks5::connect uses ATYP 0x03 for
# anything that is not an IP literal), so the proxy resolves the name — which is what lets a
# v3 .onion stratum work with no `socks5h` variant to ask for. Same host:port rules as the
# pool url, via the shared validator, so an onion pool and its proxy cannot be judged by two
# different standards.
_s5=$(jq -r '.socks5 // empty' <<<"$_pool")
if [ -n "$_s5" ]; then
_validate_host_port "$_s5" "Pool socks5" 9050
fi
# The host must be a valid hostname / FQDN / IPv4, or a bracketed IPv6 literal. This also
# rejects the unfilled template placeholder (<...>), whitespace, and shell/URL metacharacters.
case "$_host" in
\[*\]) [[ "$_host" =~ ^\[[0-9A-Fa-f:]+\]$ ]] || error "Pool url '$_u' has an invalid IPv6 literal (use [addr]:port)." ;;
*) [[ "$_host" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]] || error "Pool url host '$_host' is not a valid hostname or IP." ;;
esac
if [ -n "$_user" ] && ! [[ "$_user" =~ ^[A-Za-z0-9._:@+-]+$ ]]; then
error "Pool user '$_user' has invalid characters (allowed: letters, digits, . _ - : @ +)."
fi
Expand Down Expand Up @@ -689,7 +719,7 @@ parse_config() {
# #1 image seed. Warn NAMES only, never values — a fat-fingered token must not land in a log.
_warn_unknown_config_keys() {
local known="pools ACCESS_TOKEN DONATION autotune add_to_path HOME_DIR api api_port api_bind api_allow_from miner_user RIG_NAME watchdog watchdog_interval_min max_temp_c control control_port control_bind control_upgrade hugepages_reserve_extra_mb hugepages_pool_ceiling_mb threads"
local known_pool="url user pass keepalive tls enabled tls-fingerprint"
local known_pool="url user pass keepalive tls enabled tls-fingerprint socks5"
local k lk m lm hit hint unknown_seen=0
while IFS= read -r k; do
case "$k" in _*) continue ;; esac
Expand Down
Loading