Skip to content

cli: obtain an IP ownership proof during connect and attach it - #4227

Draft
elitegreg wants to merge 1 commit into
gm/ip-proof-instruction-sdkfrom
gm/ip-proof-cli-connect
Draft

cli: obtain an IP ownership proof during connect and attach it#4227
elitegreg wants to merge 1 commit into
gm/ip-proof-instruction-sdkfrom
gm/ip-proof-cli-connect

Conversation

@elitegreg

@elitegreg elitegreg commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Resolves #4201. Part of RFC-27 (rfcs/rfc27-ip-verification.md); tracker #4194.

Stacked on #4224 (gm/ip-proof-instruction-sdk), which adds the SDK's ip_proof field this branch sets. Review that one first; the base will move to main once it merges.

Summary of Changes

  • doublezero connect obtains an RFC-27 IpOwnershipProof from the verification service and attaches it to user creation. serviceability-instruction/sdk-rs: carry the IP ownership proof and Ed25519 instruction #4200 gave the SDK somewhere to put a proof; nothing obtained one until now.
  • The service decides the address, not the host. The verifier signs the source address it observes and refuses a caller-supplied one — its request body has no client_ip field at all. So the returned address is authoritative, and the daemon's own discovery (ifconfig.me, inside doublezerod) stays a convenience for display and the AccessPass pre-flight.
  • Multi-homed hosts. The request is bound to the address the tunnel will use, so a machine with several egress paths proves the one it will actually originate from. On a NATed host that address is not on any local interface and the bind fails; that is expected, not an error, so the request falls back to the default egress, where the service observes the NAT's public address — the same one the daemon discovered. Both paths are logged, because a silent fallback on a multi-homed host would prove the wrong address.
  • One hard failure. When the service's observed address disagrees with the one being provisioned, connect stops and names both. Attaching the proof would guarantee an onchain IpProofClientIpMismatch; dropping it would bind an address nobody proved. Guessing between them is worse than stopping.
  • Everything else is reported and non-fatal. Unconfigured, unreachable, and declined (not_globally_routable for a CGNAT source, rate_limited, …) each print their own reason and continue without a proof. The program is the enforcement point, so this succeeds while require-ip-ownership-proof is clear and fails with a named error once it is set — the behavior wanted during rollout. Breaking connect here would take every host in a not-yet-enforcing environment offline the moment a verifier went down.
  • One proof per invocation: a single connect only ever creates users of one user_type, and the proof binds user_type, so the proof is fetched once and threaded to whichever creation path the mode selects.
  • NetworkConfig gains ip_verifier_url, overridable by DZ_IP_VERIFIER_URL and by a new --ip-verifier-url on connect.

Notes for reviewers

--client-ip disagreement. The issue asks to fail when an explicit --client-ip disagrees with what the service observed. That flag is already deprecated and ignored on the CLI — connect warns and reads the address from the daemon instead. So the disagreement that can actually occur is daemon-discovered versus service-observed, and that is what is checked. The remedy the error suggests is --client-ip on the daemon, which is the flag that still does something.

Per-environment URLs. Only localnet gets a default, the verifier's own listen address, which is what dev/dzctl will run (#4204). Mainnet-beta, testnet, and devnet have no verifier deployed yet — that is #4199 — so their ip_verifier_url is None, which is a documented state rather than a placeholder hostname that would be wrong on arrival. --ip-verifier-url and DZ_IP_VERIFIER_URL cover the gap in the meantime. #4199 should fill these in.

A new dependency. doublezero-daemon-cli gains reqwest (blocking, already a workspace dep used by two other crates). Blocking is deliberate: it sits beside LedgerClient's blocking RPC in the same code path, and one short request per invocation does not justify a second async HTTP stack in this crate. local_address is what makes the source binding above possible.

Diff Breakdown

Category Files Lines (+/-) Net
Core logic 4 +816 / -12 +804
Config/build 1 +6 / -0 +6
Generated 1 +5 / -0 +5
Docs 1 +2 / -0 +2
Scaffolding 1 +1 / -0 +1
Total 8 +830 / -12 +818

423 of the core-logic lines are inline #[cfg(test)] tests in those same files, leaving about 390 lines of new logic — one new module and the threading of a single value through connect.

Key files (click to expand)

Testing Verification

  • Five connect tests drive every branch against a mocked verifier: a proof attached and asserted field for field on the command that reaches the SDK; the proof bound to Multicast on the multicast path and to IBRL on the unicast one, so a proof for the wrong user_type would fail the test rather than the chain; an unreachable service continuing without a proof; a not_globally_routable refusal surfacing the service's own reason; and the address disagreement failing with both addresses in the message and no ledger call at all.
  • The fallback test reuses the existing expect_create_user_with_tenant helper, which pins ip_proof: None, so if the warn-and-continue path ever starts attaching something the test fails.
  • The other 45 connect tests run against a verifier that reports "not configured", which is the pre-RFC-27 shape they were written to assert — they are unchanged apart from the new field in their Connect literals.
  • Wire-form tests for the proof response: a round trip, a signature that is not base58, a payer that is not a pubkey, and the unconfigured-client case.
  • 189 tests pass in doublezero-daemon-cli.

Still outstanding from the issue's acceptance list: manual verification against local devnet end to end, which needs #4204 to run a verifier in dev/dzctl. Worth doing before this merges.

Resolves #4201. Part of RFC-27; tracker #4194. Stacked on #4224.

The SDK can carry an RFC-27 proof as of #4200, but nothing obtained one. This
makes `doublezero connect` ask the verification service for a proof and attach it
to user creation.

- config: NetworkConfig gains ip_verifier_url, overridable by DZ_IP_VERIFIER_URL
  and by a new --ip-verifier-url on connect. Only localnet has a built-in
  default, the verifier's own listen address; deployed URLs land with #4199, and
  until then those environments simply have no verifier.
- New ip_proof module in doublezero-daemon-cli: an automock'd IpProofClient trait
  plus a blocking reqwest implementation. The request is bound to the address the
  tunnel will use, so a multi-homed host proves the address it actually
  originates from; on a NATed host that bind fails and the request falls back to
  the default egress, where the service observes the NAT address the daemon
  already discovered.
- The service's observed address is authoritative. Where it disagrees with what
  the daemon discovered, connect stops and names both: attaching the proof would
  guarantee an onchain rejection, and dropping it would bind an address nobody
  proved.
- Every other failure is reported and non-fatal — unconfigured, unreachable, or
  declined (a CGNAT source, a rate limit) each print the specific reason and
  continue without a proof. The program is the enforcement point, so this
  succeeds while require-ip-ownership-proof is clear and fails with a named error
  once it is set, which is the behavior wanted during rollout.
- One proof per invocation: a single connect only ever creates users of one
  user_type, and the proof binds user_type.

The issue asks to fail when an explicit --client-ip disagrees with the service.
That flag is deprecated and ignored on the CLI, so the disagreement that can
actually happen is between the daemon's discovered address and the service's
observed one; that is what is checked.
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.

1 participant