Skip to content
Closed
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
9 changes: 9 additions & 0 deletions crates/cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ pub struct StartNetworkOptions {
long_help = "Run without a remote RPC datasource. Use this to simulate an offline environment.\n\nExample: surfpool start --offline"
)]
pub offline: bool,
/// Accept the datasource's TLS certificate without verifying it.
#[clap(
long = "allow-insecure-remote-tls",
action=ArgAction::SetTrue,
default_value = "false",
long_help = "Accept the datasource's TLS certificate without verifying it. Use this only for a datasource behind a self-signed cert.\n\nAnyone able to intercept the connection can then impersonate the datasource, and the accounts it serves are written into the local bank, executable program bytecode included.\n\nExample: surfpool start --rpc-url https://my-datasource.internal --allow-insecure-remote-tls"
)]
pub allow_insecure_remote_tls: bool,
}

#[derive(Args, PartialEq, Clone, Debug)]
Expand Down Expand Up @@ -672,6 +680,7 @@ impl StartSimnet {

SimnetConfig {
remote_rpc_url,
allow_insecure_remote_tls: self.network.allow_insecure_remote_tls,
slot_time: self.svm.slot_time,
block_production_mode: self.svm.block_production_mode.clone(),
airdrop_addresses,
Expand Down
9 changes: 7 additions & 2 deletions crates/core/src/runloops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,12 @@ pub async fn start_local_surfnet_runloop(

let remote_rpc_client = match simnet.offline_mode {
true => None,
false => SurfnetRemoteClient::new_unsafe(
false => SurfnetRemoteClient::for_datasource(
simnet
.remote_rpc_url
.as_ref()
.unwrap_or(&DEFAULT_MAINNET_RPC_URL.to_string()),
simnet.allow_insecure_remote_tls,
),
};

Expand Down Expand Up @@ -372,6 +373,7 @@ pub async fn start_block_production_runloop(
expiry_duration_ms.map(|expiry_val| Utc::now().timestamp_millis() as u64 + expiry_val);
let global_skip_sig_verify = simnet_config.skip_signature_verification;
let ix_profiling_initially_enabled = simnet_config.instruction_profiling_enabled;
let allow_insecure_remote_tls = simnet_config.allow_insecure_remote_tls;
loop {
let mut do_produce_block = false;

Expand Down Expand Up @@ -567,7 +569,10 @@ pub async fn start_block_production_runloop(
SimnetCommand::FetchRemoteAccounts(pubkeys, remote_url) => {
// The submitter already marked RemoteAccounts as started;
// StartStartupTask precedes this command on the same channel.
let fetch_result = match SurfnetRemoteClient::new_unsafe(&remote_url) {
let fetch_result = match SurfnetRemoteClient::for_datasource(
&remote_url,
allow_insecure_remote_tls,
) {
Some(remote_client) => match svm_locker
.get_multiple_accounts_with_remote_fallback(
&remote_client,
Expand Down
106 changes: 101 additions & 5 deletions crates/core/src/surfnet/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ impl SurfpoolRpcClient {

/// A variant that accepts invalid TLS certificates, for datasources
/// behind self-signed certs.
fn new_unsafe<U: ToString>(remote_rpc_url: U) -> Option<Self> {
///
/// Reached only where the operator asked for it. Certificate verification
/// is what establishes that the datasource on the other end is the one that
/// was asked for, so a client built here trusts whoever answers.
fn new_insecure<U: ToString>(remote_rpc_url: U) -> Option<Self> {
use reqwest;

// Construction can fail after a fork (the daemonize path), so a
Expand All @@ -148,14 +152,29 @@ impl SurfpoolRpcClient {
}
}

/// Whether the datasource's TLS certificate gets verified.
///
/// Recorded on the client because [`Clone`] has nothing but `&self` to rebuild
/// from: a posture that is not stored is a posture a clone cannot keep.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DatasourceTls {
Verified,
/// The operator opted in, per [`SurfpoolRpcClient::new_insecure`].
Insecure,
}

pub struct SurfnetRemoteClient {
pub client: RpcClient,
tls: DatasourceTls,
}
impl Clone for SurfnetRemoteClient {
fn clone(&self) -> Self {
let remote_rpc_url = self.client.url();
SurfnetRemoteClient::new_unsafe(remote_rpc_url)
.expect("unable to clone SurfnetRemoteClient")
match self.tls {
DatasourceTls::Verified => SurfnetRemoteClient::new(remote_rpc_url),
DatasourceTls::Insecure => SurfnetRemoteClient::new_insecure(remote_rpc_url)
.expect("unable to clone SurfnetRemoteClient"),
}
}
}

Expand All @@ -174,15 +193,33 @@ impl SurfnetRemoteClient {
pub fn new<U: ToString>(remote_rpc_url: U) -> Self {
SurfnetRemoteClient {
client: SurfpoolRpcClient::new(remote_rpc_url).client,
tls: DatasourceTls::Verified,
}
}

pub fn new_unsafe<U: ToString>(remote_rpc_url: U) -> Option<Self> {
SurfpoolRpcClient::new_unsafe(remote_rpc_url).map(|rpc_client| SurfnetRemoteClient {
/// Accepts the datasource's certificate without verifying it. Call only
/// where the operator opted in; [`Self::for_datasource`] is that decision.
pub fn new_insecure<U: ToString>(remote_rpc_url: U) -> Option<Self> {
SurfpoolRpcClient::new_insecure(remote_rpc_url).map(|rpc_client| SurfnetRemoteClient {
client: rpc_client.client,
tls: DatasourceTls::Insecure,
})
}

/// The client a running surfnet reaches its datasource through.
///
/// One place decides the posture, so the two runloop paths that build a
/// datasource client cannot drift apart on it.
pub fn for_datasource<U: ToString>(
remote_rpc_url: U,
allow_insecure_remote_tls: bool,
) -> Option<Self> {
match allow_insecure_remote_tls {
false => Some(Self::new(remote_rpc_url)),
true => Self::new_insecure(remote_rpc_url),
}
}

pub async fn get_epoch_info(&self) -> SurfpoolResult<EpochInfo> {
self.client.get_epoch_info().await.map_err(Into::into)
}
Expand Down Expand Up @@ -656,4 +693,63 @@ mod tests {
"the failure should identify the request: {message}"
);
}

const DATASOURCE: &str = "https://api.mainnet-beta.solana.com";

/// The default datasource, the public mainnet endpoint included, is reached
/// over a connection whose certificate is checked. Accepting an unverified
/// certificate is a decision the operator makes, not the default.
#[test]
fn the_default_datasource_client_verifies_certificates() {
assert_eq!(
SurfnetRemoteClient::new(DATASOURCE).tls,
DatasourceTls::Verified
);
assert_eq!(
SurfnetRemoteClient::for_datasource(DATASOURCE, false)
.expect("the verified client is infallible")
.tls,
DatasourceTls::Verified
);
}

/// A datasource behind a self-signed cert stays reachable, by asking.
#[test]
fn accepting_an_invalid_certificate_remains_available_as_an_opt_in() {
assert_eq!(
SurfnetRemoteClient::for_datasource(DATASOURCE, true)
.expect("the opt-in client should build")
.tls,
DatasourceTls::Insecure
);
}

/// Every datasource read clones the client, so a clone that rebuilt itself
/// through the unverified constructor downgraded every request made after
/// the first regardless of what the operator asked for.
#[test]
fn a_clone_does_not_downgrade_a_verified_client() {
let client = SurfnetRemoteClient::new(DATASOURCE);

assert_eq!(client.clone().tls, DatasourceTls::Verified);
assert_eq!(client.clone().clone().tls, DatasourceTls::Verified);
}

/// The other direction of the same requirement: a clone preserves the
/// posture it was given, so opting in survives being cloned.
#[test]
fn a_clone_preserves_an_opted_in_insecure_client() {
let client = SurfnetRemoteClient::for_datasource(DATASOURCE, true)
.expect("the opt-in client should build");

assert_eq!(client.clone().tls, DatasourceTls::Insecure);
}

/// The URL survives the posture-preserving clone.
#[test]
fn a_clone_still_points_at_the_same_datasource() {
let client = SurfnetRemoteClient::new(DATASOURCE);

assert_eq!(client.clone().client.url(), client.client.url());
}
}
26 changes: 26 additions & 0 deletions crates/types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,14 @@ pub enum StartupPlanner {
pub struct SimnetConfig {
pub offline_mode: bool,
pub remote_rpc_url: Option<String>,
/// Accept the datasource's TLS certificate without verifying it, for a
/// datasource behind a self-signed cert.
///
/// Off unless the operator asks for it. An unverified datasource can be
/// impersonated by anyone on the path to it, and the accounts it serves are
/// written into the local bank, executable bytecode included.
#[serde(default)]
pub allow_insecure_remote_tls: bool,
pub slot_time: u64,
pub block_production_mode: BlockProductionMode,
pub airdrop_addresses: Vec<Pubkey>,
Expand All @@ -869,6 +877,7 @@ impl Default for SimnetConfig {
Self {
offline_mode: false,
remote_rpc_url: Some(DEFAULT_MAINNET_RPC_URL.to_string()),
allow_insecure_remote_tls: false,
slot_time: DEFAULT_SLOT_TIME_MS, // Default to 400ms to match CLI default
block_production_mode: BlockProductionMode::Clock,
airdrop_addresses: vec![],
Expand Down Expand Up @@ -2297,6 +2306,23 @@ mod tests {
assert!(!config.skip_blockhash_check);
}

// The datasource's certificate is verified unless the operator asks
// otherwise, and a config written before the field existed asks for
// nothing.
#[test]
fn test_simnet_config_allow_insecure_remote_tls_defaults_off() {
assert!(!SimnetConfig::default().allow_insecure_remote_tls);

let mut config_json = serde_json::to_value(SimnetConfig::default()).unwrap();
config_json
.as_object_mut()
.unwrap()
.remove("allow_insecure_remote_tls");

let config: SimnetConfig = serde_json::from_value(config_json).unwrap();
assert!(!config.allow_insecure_remote_tls);
}

// Configs written before the startup planner existed must keep working;
// the runloop-seals default is also the correct reading of them, since
// none of their writers seal a plan.
Expand Down
Loading