main.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. //! A command-line executable for monitoring a cluster's gossip plane.
  2. #[allow(deprecated)]
  3. use solana_gossip::{contact_info::ContactInfo, gossip_service::discover_peers};
  4. use {
  5. clap::{
  6. crate_description, crate_name, value_t, value_t_or_exit, values_t, App, AppSettings, Arg,
  7. ArgMatches, SubCommand,
  8. },
  9. log::{info, warn},
  10. solana_clap_utils::{
  11. hidden_unless_forced,
  12. input_parsers::{keypair_of, pubkeys_of},
  13. input_validators::{is_keypair_or_ask_keyword, is_port, is_pubkey},
  14. },
  15. solana_net_utils::SocketAddrSpace,
  16. solana_pubkey::Pubkey,
  17. std::{
  18. error,
  19. net::{IpAddr, Ipv4Addr, SocketAddr},
  20. process::exit,
  21. time::Duration,
  22. },
  23. };
  24. fn get_clap_app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> {
  25. let shred_version_arg = Arg::with_name("shred_version")
  26. .long("shred-version")
  27. .value_name("VERSION")
  28. .takes_value(true)
  29. .default_value("0")
  30. .help("Filter gossip nodes by this shred version");
  31. let gossip_port_arg = clap::Arg::with_name("gossip_port")
  32. .long("gossip-port")
  33. .value_name("PORT")
  34. .takes_value(true)
  35. .validator(is_port)
  36. .help("Gossip port number for the node");
  37. let bind_address_arg = clap::Arg::with_name("bind_address")
  38. .long("bind-address")
  39. .value_name("HOST")
  40. .takes_value(true)
  41. .validator(solana_net_utils::is_host)
  42. .help("IP address to bind the node to for gossip");
  43. App::new(name)
  44. .about(about)
  45. .version(version)
  46. .setting(AppSettings::SubcommandRequiredElseHelp)
  47. .arg(
  48. Arg::with_name("allow_private_addr")
  49. .long("allow-private-addr")
  50. .takes_value(false)
  51. .help("Allow contacting private ip addresses")
  52. .hidden(hidden_unless_forced()),
  53. )
  54. .subcommand(
  55. SubCommand::with_name("rpc-url")
  56. .about("Get an RPC URL for the cluster")
  57. .arg(
  58. Arg::with_name("entrypoint")
  59. .short("n")
  60. .long("entrypoint")
  61. .value_name("HOST:PORT")
  62. .takes_value(true)
  63. .required(true)
  64. .validator(solana_net_utils::is_host_port)
  65. .help("Rendezvous with the cluster at this entry point"),
  66. )
  67. .arg(
  68. Arg::with_name("all")
  69. .long("all")
  70. .takes_value(false)
  71. .help("Return all RPC URLs"),
  72. )
  73. .arg(
  74. Arg::with_name("any")
  75. .long("any")
  76. .takes_value(false)
  77. .conflicts_with("all")
  78. .help("Return any RPC URL"),
  79. )
  80. .arg(
  81. Arg::with_name("timeout")
  82. .long("timeout")
  83. .value_name("SECONDS")
  84. .takes_value(true)
  85. .default_value("15")
  86. .help("Timeout in seconds"),
  87. )
  88. .arg(&shred_version_arg)
  89. .arg(&gossip_port_arg)
  90. .arg(&bind_address_arg)
  91. .setting(AppSettings::DisableVersion),
  92. )
  93. .subcommand(
  94. SubCommand::with_name("spy")
  95. .about("Monitor the gossip entrypoint")
  96. .setting(AppSettings::DisableVersion)
  97. .arg(
  98. Arg::with_name("entrypoint")
  99. .short("n")
  100. .long("entrypoint")
  101. .value_name("HOST:PORT")
  102. .takes_value(true)
  103. .multiple(true)
  104. .validator(solana_net_utils::is_host_port)
  105. .help("Rendezvous with the cluster at this entrypoint"),
  106. )
  107. .arg(
  108. Arg::with_name("identity")
  109. .short("i")
  110. .long("identity")
  111. .value_name("PATH")
  112. .takes_value(true)
  113. .validator(is_keypair_or_ask_keyword)
  114. .help("Identity keypair [default: ephemeral keypair]"),
  115. )
  116. .arg(
  117. Arg::with_name("num_nodes")
  118. .short("N")
  119. .long("num-nodes")
  120. .value_name("NUM")
  121. .takes_value(true)
  122. .conflicts_with("num_nodes_exactly")
  123. .help("Wait for at least NUM nodes to be visible"),
  124. )
  125. .arg(
  126. Arg::with_name("num_nodes_exactly")
  127. .short("E")
  128. .long("num-nodes-exactly")
  129. .value_name("NUM")
  130. .takes_value(true)
  131. .help("Wait for exactly NUM nodes to be visible"),
  132. )
  133. .arg(
  134. Arg::with_name("node_pubkey")
  135. .short("p")
  136. .long("pubkey")
  137. .value_name("PUBKEY")
  138. .takes_value(true)
  139. .validator(is_pubkey)
  140. .multiple(true)
  141. .help("Public key of a specific node to wait for"),
  142. )
  143. .arg(&shred_version_arg)
  144. .arg(&gossip_port_arg)
  145. .arg(&bind_address_arg)
  146. .arg(
  147. Arg::with_name("timeout")
  148. .long("timeout")
  149. .value_name("SECONDS")
  150. .takes_value(true)
  151. .help("Maximum time to wait in seconds [default: wait forever]"),
  152. ),
  153. )
  154. }
  155. fn parse_matches() -> ArgMatches<'static> {
  156. get_clap_app(
  157. crate_name!(),
  158. crate_description!(),
  159. solana_version::version!(),
  160. )
  161. .get_matches()
  162. }
  163. /// Determine bind address by checking these sources in order:
  164. /// 1. --bind-address cli arg
  165. /// 2. connect to entrypoints to determine my public IP address
  166. fn parse_bind_address(matches: &ArgMatches, entrypoint_addrs: &[SocketAddr]) -> IpAddr {
  167. if let Some(bind_address) = matches.value_of("bind_address") {
  168. solana_net_utils::parse_host(bind_address).unwrap_or_else(|e| {
  169. eprintln!("failed to parse bind-address: {e}");
  170. exit(1);
  171. })
  172. } else if let Some(bind_addr) = get_bind_address_from_entrypoints(entrypoint_addrs) {
  173. bind_addr
  174. } else {
  175. eprintln!(
  176. "Failed to find a valid bind address. Bind address can be provided directly with \
  177. --bind-address or by the entrypoint functioning as an ip echo server."
  178. );
  179. exit(1);
  180. }
  181. }
  182. /// Find my public IP address by attempting connections to entrypoints until one succeeds.
  183. fn get_bind_address_from_entrypoints(entrypoint_addrs: &[SocketAddr]) -> Option<IpAddr> {
  184. entrypoint_addrs.iter().find_map(|entrypoint_addr| {
  185. solana_net_utils::get_public_ip_addr_with_binding(
  186. entrypoint_addr,
  187. IpAddr::V4(Ipv4Addr::UNSPECIFIED),
  188. )
  189. .ok()
  190. })
  191. }
  192. // allow deprecations here to workaround limitations with dependency specification in
  193. // multi-target crates and agave-unstable-api. `ContactInfo` is deprecated here, but we
  194. // cannot specify deprecation allowances on function arguments. since this function is
  195. // private, we apply the allowance to the entire body as a refactor that would limit it
  196. // to a wrapper is going to be too invasive
  197. //
  198. // this mitigation can be removed once the solana-gossip binary target is moved to its
  199. // own crate and we can correctly depend on the solana-gossip lib crate with
  200. // `agave-unstable-api` enabled
  201. #[allow(deprecated)]
  202. fn process_spy_results(
  203. timeout: Option<u64>,
  204. validators: Vec<ContactInfo>,
  205. num_nodes: Option<usize>,
  206. num_nodes_exactly: Option<usize>,
  207. pubkeys: Option<&[Pubkey]>,
  208. ) {
  209. if timeout.is_some() {
  210. if let Some(num) = num_nodes {
  211. if validators.len() < num {
  212. let add = if num_nodes_exactly.is_some() {
  213. ""
  214. } else {
  215. " or more"
  216. };
  217. eprintln!("Error: Insufficient validators discovered. Expecting {num}{add}",);
  218. exit(1);
  219. }
  220. }
  221. if let Some(nodes) = pubkeys {
  222. for node in nodes {
  223. if !validators.iter().any(|x| {
  224. #[allow(deprecated)]
  225. let pubkey = x.pubkey();
  226. pubkey == node
  227. }) {
  228. eprintln!("Error: Could not find node {node:?}");
  229. exit(1);
  230. }
  231. }
  232. }
  233. }
  234. if let Some(num_nodes_exactly) = num_nodes_exactly {
  235. if validators.len() > num_nodes_exactly {
  236. eprintln!("Error: Extra nodes discovered. Expecting exactly {num_nodes_exactly}");
  237. exit(1);
  238. }
  239. }
  240. }
  241. /// Check entrypoints until one returns a valid non-zero shred version
  242. fn get_entrypoint_shred_version(entrypoint_addrs: &[SocketAddr]) -> Option<u16> {
  243. entrypoint_addrs.iter().find_map(|entrypoint_addr| {
  244. match solana_net_utils::get_cluster_shred_version(entrypoint_addr) {
  245. Err(err) => {
  246. warn!("get_cluster_shred_version failed: {entrypoint_addr}, {err}");
  247. None
  248. }
  249. Ok(0) => {
  250. warn!("entrypoint {entrypoint_addr} returned shred-version zero");
  251. None
  252. }
  253. Ok(shred_version) => {
  254. info!("obtained shred-version {shred_version} from entrypoint: {entrypoint_addr}");
  255. Some(shred_version)
  256. }
  257. }
  258. })
  259. }
  260. fn process_spy(matches: &ArgMatches, socket_addr_space: SocketAddrSpace) -> std::io::Result<()> {
  261. let num_nodes_exactly = matches
  262. .value_of("num_nodes_exactly")
  263. .map(|num| num.to_string().parse().unwrap());
  264. let num_nodes = matches
  265. .value_of("num_nodes")
  266. .map(|num| num.to_string().parse().unwrap())
  267. .or(num_nodes_exactly);
  268. let timeout = matches
  269. .value_of("timeout")
  270. .map(|secs| secs.to_string().parse().unwrap());
  271. let pubkeys = pubkeys_of(matches, "node_pubkey");
  272. let identity_keypair = keypair_of(matches, "identity");
  273. let entrypoint_addrs = parse_entrypoints(matches);
  274. let gossip_addr = get_gossip_address(matches, &entrypoint_addrs);
  275. let mut shred_version = value_t_or_exit!(matches, "shred_version", u16);
  276. if shred_version == 0 {
  277. shred_version = get_entrypoint_shred_version(&entrypoint_addrs)
  278. .expect("need non-zero shred-version to join the cluster");
  279. }
  280. let discover_timeout = Duration::from_secs(timeout.unwrap_or(u64::MAX));
  281. #[allow(deprecated)]
  282. let (_all_peers, validators) = discover_peers(
  283. identity_keypair,
  284. &entrypoint_addrs,
  285. num_nodes,
  286. discover_timeout,
  287. pubkeys.as_deref(),
  288. &[],
  289. Some(&gossip_addr),
  290. shred_version,
  291. socket_addr_space,
  292. )?;
  293. process_spy_results(
  294. timeout,
  295. validators,
  296. num_nodes,
  297. num_nodes_exactly,
  298. pubkeys.as_deref(),
  299. );
  300. Ok(())
  301. }
  302. fn parse_entrypoints(matches: &ArgMatches) -> Vec<SocketAddr> {
  303. values_t!(matches, "entrypoint", String)
  304. .unwrap_or_default()
  305. .into_iter()
  306. .map(|entrypoint| solana_net_utils::parse_host_port(&entrypoint))
  307. .filter_map(Result::ok)
  308. .collect::<Vec<_>>()
  309. }
  310. fn process_rpc_url(
  311. matches: &ArgMatches,
  312. socket_addr_space: SocketAddrSpace,
  313. ) -> std::io::Result<()> {
  314. let any = matches.is_present("any");
  315. let all = matches.is_present("all");
  316. let timeout = value_t_or_exit!(matches, "timeout", u64);
  317. let entrypoint_addrs = parse_entrypoints(matches);
  318. let gossip_addr = get_gossip_address(matches, &entrypoint_addrs);
  319. let mut shred_version = value_t_or_exit!(matches, "shred_version", u16);
  320. if shred_version == 0 {
  321. shred_version = get_entrypoint_shred_version(&entrypoint_addrs)
  322. .expect("need non-zero shred-version to join the cluster");
  323. }
  324. #[allow(deprecated)]
  325. let (_all_peers, validators) = discover_peers(
  326. None,
  327. &entrypoint_addrs,
  328. Some(1),
  329. Duration::from_secs(timeout),
  330. None,
  331. &entrypoint_addrs,
  332. Some(&gossip_addr),
  333. shred_version,
  334. socket_addr_space,
  335. )?;
  336. let rpc_addrs: Vec<_> = validators
  337. .iter()
  338. .filter(|node| {
  339. any || all || {
  340. #[allow(deprecated)]
  341. let addrs = node.gossip();
  342. addrs
  343. .map(|addr| entrypoint_addrs.contains(&addr))
  344. .unwrap_or_default()
  345. }
  346. })
  347. .filter_map(
  348. #[allow(deprecated)]
  349. ContactInfo::rpc,
  350. )
  351. .filter(|addr| socket_addr_space.check(addr))
  352. .collect();
  353. if rpc_addrs.is_empty() {
  354. eprintln!("No RPC URL found");
  355. exit(1);
  356. }
  357. for rpc_addr in rpc_addrs {
  358. println!("http://{rpc_addr}");
  359. if any {
  360. break;
  361. }
  362. }
  363. Ok(())
  364. }
  365. fn get_gossip_address(matches: &ArgMatches, entrypoint_addrs: &[SocketAddr]) -> SocketAddr {
  366. let bind_address = parse_bind_address(matches, entrypoint_addrs);
  367. SocketAddr::new(
  368. bind_address,
  369. value_t!(matches, "gossip_port", u16).unwrap_or_else(|_| {
  370. solana_net_utils::find_available_port_in_range(
  371. IpAddr::V4(Ipv4Addr::UNSPECIFIED),
  372. (0, 1),
  373. )
  374. .expect("unable to find an available gossip port")
  375. }),
  376. )
  377. }
  378. fn main() -> Result<(), Box<dyn error::Error>> {
  379. agave_logger::setup_with_default_filter();
  380. let matches = parse_matches();
  381. let socket_addr_space = SocketAddrSpace::new(matches.is_present("allow_private_addr"));
  382. match matches.subcommand() {
  383. ("spy", Some(matches)) => {
  384. process_spy(matches, socket_addr_space)?;
  385. }
  386. ("rpc-url", Some(matches)) => {
  387. process_rpc_url(matches, socket_addr_space)?;
  388. }
  389. _ => unreachable!(),
  390. }
  391. Ok(())
  392. }