feat(pools): dial a pool through a SOCKS5 proxy (#400) - #404
Merged
Conversation
XMRig supports a per-pool `socks5`; RigForge dropped it. `parse_config` rebuilds every pool from a fixed key set, so the key was gone before the XMRig config was generated, and `_warn_unknown_config_keys` told the operator it was ignored. That left a rig no way to reach a stratum published as an onion service. - The mapper re-attaches `socks5` in a single-line pass modelled on the #115 `tls-fingerprint` pass above it, for the same two reasons: emitted ONLY when set, so no existing rig's generated config changes shape on its next apply, and a one-line pass because kcov cannot attribute in-string program lines. POOLS_JSON reaches generate_xmrig_config wholesale, so nothing else changed. - `socks5` joins the pool-key allowlist, so the "ignored" warning stops. - config.reference.json gains `"socks5": null` and a `_docs` sentence; docs/configuration.md gains a pool-field row. The issue asks for the proxy to be validated with "the same rules the pool URL gets". A second copy of those checks would satisfy that on the day it was written and drift the first time either side was touched, so the rules now live in one `_validate_host_port <value> <label> <example-port>` that both keys call. Every check and regex inside it is verbatim from the pool-url path it replaces. One operator-facing string changes as a result, and only this one: the port-range error was "Pool port must be between 1 and 65535" and is now "Pool url port must be between 1 and 65535", now that two keys can produce it. The other three are byte-identical for the url path. Why the onion case works: Client::Socks5::connect() uses ATYP 0x03 (DOMAINNAME) for any host that is not an IP literal, so the name goes to the proxy and the proxy resolves it — which is why there is no `socks5h` to ask for. The pool URL host pattern already accepted a v3 .onion. Verified: `bash tests/run.sh` -> 1867 passed, 0 failed (baseline 1853; exactly the 14 new assertions, each confirmed to have executed rather than inferred from a green suite). Three mutants, all killed: dropping the validation call kills the four rejection assertions (the IPv6 acceptance correctly survives); dropping the re-attach pass kills eight; reverting the allowlist kills exactly the unknown-key assertion. Two of the fourteen survive every mutant and should — `no socks5 key when unset` and `null socks5 = absent` assert an absence that also holds without the feature, so they are shape-regression guards, not proof it works. The extraction is guarded by the pre-existing pool-url tests, which pass unchanged. `shfmt -i 4 -d` and `bash -n` clean; config.reference.json re-parsed. NOT run locally: shellcheck (its peak on a file this size has OOM-killed sessions on this box) and the Docker e2e — CI covers both. Also unproven: a real SOCKS5 dial to an onion stratum, which needs bench hardware. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5tRnh79JUNv6q2q4F5qRh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #400.
What was missing
XMRig supports a per-pool
socks5. RigForge did not:parse_configrebuilds every pool from a fixedkey set (
url,user,pass,keepalive,tls,enabled, withtls-fingerprintre-attached in asecond pass), so anything else in the operator's pool object was gone before the XMRig config was
generated.
_warn_unknown_config_keysthen said so out loud —unknown pool field "socks5" is ignored.— which was accurate and unhelpful: a rig had no way to reach a stratum published as anonion service.
What changed
socks5, in a single-line pass modelled directly on thetls-fingerprintpass above it, for the same two stated reasons: emitted only when set, so no existing rig's
generated config changes shape on its next apply; and a one-line pass rather than lines inside the
jqmap, because kcov cannot attribute in-string program lines and the patch-coverage gate needsevery new line hittable.
POOLS_JSONis handed togenerate_xmrig_configwholesale, so nothingelse had to change to get the key into the file the miner actually reads.
socks5joins the pool-key allowlist, so the "ignored" warning stops.config.reference.jsongains"socks5": nullon the reference pool and a sentence in_docs.docs/configuration.mdgains a row in the pool-field table.One refactor, and why it is in scope
The issue asks for the socks5 address to be "validated as
host:portwith the same rules the pool URLgets". A second copy of those four checks would satisfy that on the day it was written and drift the
first time either side was touched — which is the failure mode this repo keeps finding elsewhere. So
the rules now live in one
_validate_host_port <value> <label> <example-port>helper that both thepool
urland the poolsocks5call. Every check and regex inside it is verbatim from the pool-urlpath it replaces.
One operator-facing string changes as a result, and it is the only one: the port-range error was
Pool port must be between 1 and 65535 (got …)and is nowPool url port must be between 1 and 65535 (got …). Nothing pins it, and it is more precise now that two different keys can produce it, but itis a change I made rather than one that was asked for — flagging it rather than burying it. The other
three messages are byte-identical for the pool-url path; the example port is parameterised so the
hint fits its key (
:3333for a pool,:9050for a proxy).Shared file — disclosed
This PR stages
config.reference.json. The fleet's shared-file rule was checked with thecontroller before I touched it: that rule governs the pithead worktrees, which share one
.gitbetween seven lanes, and does not govern rigforge, which has neither that property nor the lane-guard
hook that enforces it. Naming the file here anyway, exactly as the pithead rule would have required,
because there are several rigforge checkouts on this box and nothing mechanical stops a collision.
Why the onion case works at all
Checked against the XMRig source rather than assumed:
Client::Socks5::connect()builds its CONNECTrequest with ATYP
0x03(DOMAINNAME) for any host that is not an IPv4 or IPv6 literal, so thename goes to the proxy and the proxy resolves it. That is what an
.onionneeds, and it is whythere is no
socks5hvariant to ask for. The pool URL validator's existing host pattern alreadyaccepts a v3
.onion, so the dropped key really was the only thing in the way.What was RUN
bash tests/run.sh→ 1867 passed, 0 failed, exit 0 (baseline 1853 — exactly the 14 newassertions, each confirmed to have actually executed rather than inferred from a green suite).
Three mutants, each killed, because a green assertion proves nothing until it is shown to fail
when the code is broken:
bracketed-IPv6 acceptance assertion correctly survives, which is how you can tell the four
deaths are the validator and not collateral.
same four rejections (with no
socks5inPOOLS_JSONthe validator never sees a value toreject). Broader than N1 by construction, and consistent with it.
socks5out of the pool-key allowlist → suite exit 1, exactly the one "no longer warns asunknown" assertion dies. Nothing else moved.
Two of the fourteen survive all three mutants, and should:
no socks5 key when unsetandnull socks5 = absentassert an ABSENCE that also holds when the feature does not exist. They areshape-regression guards — they are what proves a pre-A pool cannot be dialled through a SOCKS5 proxy — the mapper drops the
socks5key XMRig already supports, so a rig can never reach an onion stratum #400 config still produces byte-identicalPOOLS_JSON— not evidence the feature works. Naming that rather than letting a reader countfourteen green ticks as fourteen proofs.
The refactor is guarded by the pre-existing pool-url tests (
bad pool url rejected,blank pool url rejected,url without a port rejected, and the IPv6-literal cases), which still passunchanged — that is what shows the extraction did not quietly alter the url path.
shfmt -i 4 -dclean,bash -nclean on both changed files;config.reference.jsonre-parsed.What was NOT run locally
shellcheck— its historical peak on a file this size has OOM-killed whole sessions on the boxthis was built on. Deferred to CI's lint job deliberately.
stratum; that needs a Tor client and an onion pool, which is bench work. What is proven is that the
key survives parse, validation and config generation, and reaches the file XMRig reads.
Deliberately not in this PR
socks5. It dropspassandtls-fingerprint—credentials. A proxy address is not one, and the feed already exposes each pool's
url, sowithholding the proxy while publishing the destination would buy nothing. Saying so because it was
a decision, not an oversight.
socks5is not added to the writable control allowlist, so it cannot be changed through thecontrol path — the issue asks for the passthrough only.
(
rigforge.sh:335-343). It warns and re-prompts rather than callingerror, so it cannot use theshared validator without restructuring that loop. Left alone; worth its own issue if wanted.
(#1319). This is the passthrough only.
Adversarial pass on this diff (run 2026-08-24, after the checks came back)
The repo's PR gate did not prompt for an over-engineering review on this branch. That is a known
defect in the gate, not a pass — it keys its sentinel to the branch in the session's working
directory and can consume a stale one from another repo entirely. So the pass was run anyway, and
this is what it found.
No regression. Both attacks below land on code that #400 MOVED rather than wrote, and both were
verified to behave identically before the change. They are filed as
#405 and deliberately not fixed here,
because fixing them touches the
urlandtls-fingerprintpaths as well and would turn a scopedpassthrough into a validator rewrite.
[ "$_p" -lt 1 ]returns 2 (not false) on a value bash cannotparse as an integer, so
a.example:99999999999999999999reaches the generated config with a raw[: integer expression expectedon stderr instead of the clear message the check exists to give.Re-derived against the shipped function with both negative controls (
:70000,:0) confirmed tofail first. Since this PR,
socks5inherits it too — that is the one new thing here, and it is whythe finding is disclosed on this PR rather than only on the issue.
"socks5": ""is emitted but not validated. The emit predicate is jq truthiness(
(.[1] // null) != null, where""is truthy) and the validate predicate is[ -n ]on// emptyoutput (where""is not). They disagree on exactly one value.tls-fingerprinthascarried the same asymmetry since Stratum-over-TLS: worker-side wiring for an encrypted miner↔stack link (companion to Pithead #261) #115;
socks5follows the neighbouring key on purpose, so thisis consistency with a pre-existing gap rather than a new one.
Checked and clean:
_s5matches house scoping (parse_configdeclares no locals at all, so theabsence of a
localhere is the convention, not an omission); bracketed-IPv6 and.onionhosts bothstill take the intended branch through the shared validator; the two jq passes compose in the same
order-independent way the #115 and #265 passes already do.
🤖 Generated with Claude Code
https://claude.ai/code/session_01E5tRnh79JUNv6q2q4F5qRh