cli: obtain an IP ownership proof during connect and attach it - #4227
Draft
elitegreg wants to merge 1 commit into
Draft
cli: obtain an IP ownership proof during connect and attach it#4227elitegreg wants to merge 1 commit into
elitegreg wants to merge 1 commit into
Conversation
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.
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.
Resolves #4201. Part of RFC-27 (rfcs/rfc27-ip-verification.md); tracker #4194.
Summary of Changes
doublezero connectobtains an RFC-27IpOwnershipProoffrom 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.client_ipfield at all. So the returned address is authoritative, and the daemon's own discovery (ifconfig.me, insidedoublezerod) stays a convenience for display and the AccessPass pre-flight.connectstops and names both. Attaching the proof would guarantee an onchainIpProofClientIpMismatch; dropping it would bind an address nobody proved. Guessing between them is worse than stopping.not_globally_routablefor a CGNAT source,rate_limited, …) each print their own reason and continue without a proof. The program is the enforcement point, so this succeeds whilerequire-ip-ownership-proofis clear and fails with a named error once it is set — the behavior wanted during rollout. Breakingconnecthere would take every host in a not-yet-enforcing environment offline the moment a verifier went down.connectonly ever creates users of oneuser_type, and the proof bindsuser_type, so the proof is fetched once and threaded to whichever creation path the mode selects.NetworkConfiggainsip_verifier_url, overridable byDZ_IP_VERIFIER_URLand by a new--ip-verifier-urlonconnect.Notes for reviewers
--client-ipdisagreement. The issue asks to fail when an explicit--client-ipdisagrees with what the service observed. That flag is already deprecated and ignored on the CLI —connectwarns 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-ipon 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/dzctlwill run (#4204). Mainnet-beta, testnet, and devnet have no verifier deployed yet — that is #4199 — so theirip_verifier_urlisNone, which is a documented state rather than a placeholder hostname that would be wrong on arrival.--ip-verifier-urlandDZ_IP_VERIFIER_URLcover the gap in the meantime. #4199 should fill these in.A new dependency.
doublezero-daemon-cligainsreqwest(blocking, already a workspace dep used by two other crates). Blocking is deliberate: it sits besideLedgerClient'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_addressis what makes the source binding above possible.Diff Breakdown
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 throughconnect.Key files (click to expand)
crates/doublezero-daemon-cli/src/ip_proof.rs— new module: theIpProofClienttrait, its blocking HTTP implementation with source-address binding, and the classified refusal reasonscrates/doublezero-daemon-cli/src/connect.rs—obtain_ip_proof(the fetch, the address-agreement check, the warn-and-continue paths),--ip-verifier-url, and the proof threaded to all four creation call sitesconfig/src/env.rs—NetworkConfig::ip_verifier_urland itsDZ_IP_VERIFIER_URLoverrideconfig/src/constants.rs— the localnet verifier URL, and why the other environments have none yetTesting Verification
connecttests 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 toMulticaston the multicast path and toIBRLon the unicast one, so a proof for the wronguser_typewould fail the test rather than the chain; an unreachable service continuing without a proof; anot_globally_routablerefusal surfacing the service's own reason; and the address disagreement failing with both addresses in the message and no ledger call at all.expect_create_user_with_tenanthelper, which pinsip_proof: None, so if the warn-and-continue path ever starts attaching something the test fails.connecttests 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 theirConnectliterals.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.