Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ All notable changes to this project will be documented in this file.
- `CreateUser` and `CreateSubscribeUser` validate an optional RFC-27 `IpOwnershipProof`, verified through the native Ed25519 precompile and signed by `globalstate.ip_verifier_authority_pk`, so a caller can no longer bind a `client_ip` it cannot originate traffic from. Enforcement is gated on the new `require-ip-ownership-proof` feature flag: while it is clear a missing proof is accepted, and a supplied proof is validated in full either way. The sentinel authority may omit the proof, because the shred-oracle provisions users owned by validators and has no proof it could obtain; a proof it does supply is still validated (#4215). (#4197)
- IP verifier
- New `doublezero-ip-verifier` service signs the source address it observes as an RFC-27 `IpOwnershipProof`, over `POST /v1/proof`. Forwarded headers count only for connections from a `--trusted-proxy` CIDR, and only the `--forwarded-header` the proxy actually writes is read; the chain is walked from the right so a client-prepended hop is ignored. With no trusted proxies configured the connection peer address is the only address it will sign. Non-routable and IPv6 sources are refused, as is a request the cached ledger epoch is too old to answer. The verifier key is checked against `GlobalState.ip_verifier_authority_pk` at startup and periodically after, so a rotation this service was not redeployed for takes it out of rotation instead of silently failing every user creation onchain. Built on axum, the first HTTP server framework in the Rust workspace. (#4198)
- CLI
- `doublezero connect` obtains an RFC-27 IP ownership proof from the verification service and attaches it to user creation. The service signs the source address it observes, so that address — not the daemon's own discovery — is authoritative; where the two disagree, `connect` stops and names both rather than binding an address nobody proved. The request is bound to the address the tunnel will use where the host allows it, so a multi-homed machine proves the right one. A verifier that is unreachable, unconfigured, or that declines (a CGNAT source, for instance) is reported and the connect continues without a proof, which the program accepts until `require-ip-ownership-proof` is set for the environment. `--ip-verifier-url` or `DZ_IP_VERIFIER_URL` points at a verifier; only localnet has a built-in default until the deployment work lands. (#4201)
- Rust SDK
- `CreateUserCommand` and `CreateSubscribeUserCommand` take an optional RFC-27 `ip_proof`. Supplying one attaches the native `Ed25519SigVerify` instruction the program looks for and sends both as one transaction; the verifier key comes from `GlobalState.ip_verifier_authority_pk`, the same place the program reads it, so a caller cannot pair a proof with the wrong key. A proof naming a different owner, address, or user type is refused before the transaction is paid for. On the `--owner` override path the proof must name that owner, because the program binds it to the user's effective owner. Omitting it produces the pre-RFC-27 transaction unchanged. Nothing sets it yet; the CLI is #4201. (#4200)
- `DoubleZeroClient` gains `send_instructions`, for a transaction that needs more than one instruction. `send_transaction` is unchanged. (#4200)
Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions config/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,15 @@ pub const ENV_LOCAL_INTERNET_LATENCY_COLLECTOR_PUBKEY: Pubkey =
Pubkey::from_str_const("3fXen9LP5JUAkaaDJtyLo1ohPiJ2LdzVqAnmhtGgAmwJ");
pub const ENV_LOCAL_GEOLOCATION_PUBKEY: Pubkey =
Pubkey::from_str_const("36WA9nUCsJaAQL5h44WYoLezDpocy8Q71NZbtrUN8DyC");

// RFC-27 IP ownership verification service (`doublezero-ip-verifier`). The service signs the
// source address it observes a request originate from, so this URL has to be reachable from the
// connecting host — and reachable over the same path the tunnel will use.
//
// Only localnet has a default, the verifier's own default listen address, which is what
// `dev/dzctl` runs (#4204). Deployed URLs for mainnet-beta, testnet, and devnet land with the
// deployment work (#4199); until then those environments have no verifier, and `doublezero
// connect` creates users without a proof — which the program accepts while
// `require-ip-ownership-proof` is clear. `--ip-verifier-url` or `DZ_IP_VERIFIER_URL` points at
// one in the meantime.
pub const ENV_LOCAL_IP_VERIFIER_URL: &str = "http://localhost:8080";
13 changes: 13 additions & 0 deletions config/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl Environment {
telemetry_program_id: ENV_MAINNET_BETA_TELEMETRY_PUBKEY,
internet_latency_collector_pk: ENV_MAINNET_BETA_INTERNET_LATENCY_COLLECTOR_PUBKEY,
geolocation_program_id: ENV_MAINNET_BETA_GEOLOCATION_PUBKEY,
ip_verifier_url: None,
},
Environment::Testnet => NetworkConfig {
ledger_public_rpc_url: ENV_TESTNET_DOUBLEZERO_LEDGER_RPC_URL.to_string(),
Expand All @@ -76,6 +77,7 @@ impl Environment {
telemetry_program_id: ENV_TESTNET_TELEMETRY_PUBKEY,
internet_latency_collector_pk: ENV_TESTNET_INTERNET_LATENCY_COLLECTOR_PUBKEY,
geolocation_program_id: ENV_TESTNET_GEOLOCATION_PUBKEY,
ip_verifier_url: None,
},
Environment::Devnet => NetworkConfig {
ledger_public_rpc_url: ENV_DEVNET_DOUBLEZERO_LEDGER_RPC_URL.to_string(),
Expand All @@ -85,6 +87,7 @@ impl Environment {
telemetry_program_id: ENV_DEVNET_TELEMETRY_PUBKEY,
internet_latency_collector_pk: ENV_DEVNET_INTERNET_LATENCY_COLLECTOR_PUBKEY,
geolocation_program_id: ENV_DEVNET_GEOLOCATION_PUBKEY,
ip_verifier_url: None,
},
Environment::Local => NetworkConfig {
ledger_public_rpc_url: ENV_LOCAL_DOUBLEZERO_LEDGER_RPC_URL.to_string(),
Expand All @@ -94,6 +97,7 @@ impl Environment {
telemetry_program_id: ENV_LOCAL_TELEMETRY_PUBKEY,
internet_latency_collector_pk: ENV_LOCAL_INTERNET_LATENCY_COLLECTOR_PUBKEY,
geolocation_program_id: ENV_LOCAL_GEOLOCATION_PUBKEY,
ip_verifier_url: Some(ENV_LOCAL_IP_VERIFIER_URL.to_string()),
},
};

Expand All @@ -106,6 +110,11 @@ impl Environment {
if std::env::var("DZ_SOLANA_RPC_URL").is_ok() {
config.solana_l1_rpc_url = std::env::var("DZ_SOLANA_RPC_URL").unwrap();
}
// Set this to point at a verifier in an environment that has no deployed one yet, or at
// a local one while developing. `--ip-verifier-url` on `connect` overrides it in turn.
if std::env::var("DZ_IP_VERIFIER_URL").is_ok() {
config.ip_verifier_url = Some(std::env::var("DZ_IP_VERIFIER_URL").unwrap());
}

Ok(config)
}
Expand Down Expand Up @@ -163,6 +172,10 @@ pub struct NetworkConfig {
pub telemetry_program_id: Pubkey,
pub internet_latency_collector_pk: Pubkey,
pub geolocation_program_id: Pubkey,
/// Base URL of the RFC-27 IP ownership verification service, or `None` where none is
/// deployed yet. `None` is not an error: user creation without a proof is accepted until
/// `require-ip-ownership-proof` is set for the environment.
pub ip_verifier_url: Option<String>,
}

#[cfg(test)]
Expand Down
6 changes: 6 additions & 0 deletions crates/doublezero-daemon-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ hyper-util.workspace = true
hyperlocal.workspace = true
indicatif.workspace = true
mockall.workspace = true
# The RFC-27 proof retrieved from the verification service and forwarded to the SDK.
doublezero-ip-proof.workspace = true
# One short blocking HTTP request per `connect`, to the IP verification service. `local_address`
# is what lets a multi-homed host prove the address its tunnel will actually use.
reqwest = { workspace = true, features = ["json", "blocking"] }
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
solana-sdk.workspace = true
Expand Down
Loading
Loading