Skip to content

feat(pools): dial a pool through a SOCKS5 proxy (#400) - #404

Merged
VijitSingh97 merged 1 commit into
developfrom
feat/400-pool-socks5-passthrough
Aug 24, 2026
Merged

feat(pools): dial a pool through a SOCKS5 proxy (#400)#404
VijitSingh97 merged 1 commit into
developfrom
feat/400-pool-socks5-passthrough

Conversation

@VijitSingh97

@VijitSingh97 VijitSingh97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #400.

What was missing

XMRig supports a per-pool socks5. RigForge did not: parse_config rebuilds every pool from a fixed
key set (url, user, pass, keepalive, tls, enabled, with tls-fingerprint re-attached in a
second pass), so anything else in the operator's pool object was gone before the XMRig config was
generated. _warn_unknown_config_keys then 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 an
onion service.

What changed

  • The mapper re-attaches socks5, in a single-line pass modelled directly on the tls-fingerprint
    pass 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
    jq map, because kcov cannot attribute in-string program lines and the patch-coverage gate needs
    every new line hittable. POOLS_JSON is handed to generate_xmrig_config wholesale, so nothing
    else had to change to get the key into the file the miner actually reads.
  • socks5 joins the pool-key allowlist, so the "ignored" warning stops.
  • config.reference.json gains "socks5": null on the reference pool and a sentence in _docs.
  • docs/configuration.md gains 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:port with the same rules the pool URL
gets". 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 the
pool url and the pool socks5 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 it is the only one: the port-range error was
Pool port must be between 1 and 65535 (got …) and is now Pool 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 it
is 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 (:3333 for a pool, :9050 for a proxy).

Shared file — disclosed

This PR stages config.reference.json. The fleet's shared-file rule was checked with the
controller before I touched it: that rule governs the pithead worktrees, which share one .git
between 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 CONNECT
request with ATYP 0x03 (DOMAINNAME) for any host that is not an IPv4 or IPv6 literal, so the
name goes to the proxy and the proxy resolves it. That is what an .onion needs, and it is why
there is no socks5h variant to ask for. The pool URL validator's existing host pattern already
accepts a v3 .onion, so the dropped key really was the only thing in the way.

What was RUN

  • bash tests/run.sh1867 passed, 0 failed, exit 0 (baseline 1853 — exactly the 14 new
    assertions, 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:

    • remove the socks5 validation call → suite exit 1, the four rejection assertions die. The
      bracketed-IPv6 acceptance assertion correctly survives, which is how you can tell the four
      deaths are the validator and not collateral.
    • remove the re-attach pass → suite exit 1, eight die: the four passthrough assertions plus the
      same four rejections (with no socks5 in POOLS_JSON the validator never sees a value to
      reject). Broader than N1 by construction, and consistent with it.
    • revert socks5 out of the pool-key allowlist → suite exit 1, exactly the one "no longer warns as
      unknown" assertion dies. Nothing else moved.

    Two of the fourteen survive all three mutants, and should: no socks5 key when unset and
    null socks5 = absent assert an ABSENCE that also holds when the feature does not exist. They are
    shape-regression guards — they are what proves a pre-A pool cannot be dialled through a SOCKS5 proxy — the mapper drops the socks5 key XMRig already supports, so a rig can never reach an onion stratum #400 config still produces byte-identical
    POOLS_JSON — not evidence the feature works. Naming that rather than letting a reader count
    fourteen 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 pass
    unchanged — that is what shows the extraction did not quietly alter the url path.

  • shfmt -i 4 -d clean, bash -n clean on both changed files; config.reference.json re-parsed.

What was NOT run locally

  • shellcheck — its historical peak on a file this size has OOM-killed whole sessions on the box
    this was built on. Deferred to CI's lint job deliberately.
  • The Docker e2e — deferred to CI.
  • A real SOCKS5 dial. Nothing here proves a rig actually mines through a proxy to an onion
    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

  • The masked open feed (Enriched feed: expose the effective writable config (rigforge.config) so the stack can prefill/round-trip Worker Inspect #253) does not drop socks5. It drops pass and tls-fingerprint
    credentials. A proxy address is not one, and the feed already exposes each pool's url, so
    withholding the proxy while publishing the destination would buy nothing. Saying so because it was
    a decision, not an oversight.
  • socks5 is not added to the writable control allowlist, so it cannot be changed through the
    control path — the issue asks for the passthrough only.
  • A third copy of the host check still exists in the interactive setup prompt
    (rigforge.sh:335-343). It warns and re-prompts rather than calling error, so it cannot use the
    shared validator without restructuring that loop. Left alone; worth its own issue if wanted.
  • Who runs the Tor client — per the issue, the appliance end is tracked in the Pithead repo
    (#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 url and tls-fingerprint paths as well and would turn a scoped
passthrough into a validator rewrite.

  1. An absurd-magnitude port passes. [ "$_p" -lt 1 ] returns 2 (not false) on a value bash cannot
    parse as an integer, so a.example:99999999999999999999 reaches the generated config with a raw
    [: integer expression expected on stderr instead of the clear message the check exists to give.
    Re-derived against the shipped function with both negative controls (:70000, :0) confirmed to
    fail first. Since this PR, socks5 inherits it too — that is the one new thing here, and it is why
    the finding is disclosed on this PR rather than only on the issue.
  2. "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
    // empty output (where "" is not). They disagree on exactly one value. tls-fingerprint has
    carried the same asymmetry since Stratum-over-TLS: worker-side wiring for an encrypted miner↔stack link (companion to Pithead #261) #115; socks5 follows the neighbouring key on purpose, so this
    is consistency with a pre-existing gap rather than a new one.

Checked and clean: _s5 matches house scoping (parse_config declares no locals at all, so the
absence of a local here is the convention, not an omission); bracketed-IPv6 and .onion hosts both
still 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

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
@VijitSingh97
VijitSingh97 merged commit 16b6fed into develop Aug 24, 2026
9 checks passed
@VijitSingh97
VijitSingh97 deleted the feat/400-pool-socks5-passthrough branch August 24, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A pool cannot be dialled through a SOCKS5 proxy — the mapper drops the socks5 key XMRig already supports, so a rig can never reach an onion stratum

1 participant