|
| 1 | +//! The AccessPass pre-flight shared by `doublezero connect` and the serviceability CLI. |
| 2 | +//! |
| 3 | +//! Both surfaces answer the same question before they send anything: does the caller hold a pass |
| 4 | +//! the program will accept for this `client_ip`? They reach the ledger through different client |
| 5 | +//! abstractions, so the ledger read is a closure and only the decision lives here — previously it |
| 6 | +//! was two copies with a comment on each asking the next person to keep them in sync. |
| 7 | +
|
| 8 | +use doublezero_serviceability::state::accesspass::AccessPass; |
| 9 | +use std::net::Ipv4Addr; |
| 10 | + |
| 11 | +/// Reports whether the caller holds a usable AccessPass for `client_ip`. |
| 12 | +/// |
| 13 | +/// `lookup` resolves the pass stored at a given `client_ip` for the caller's payer; `epoch` reads |
| 14 | +/// the current ledger epoch, and is only called when `enforce_epoch` is set so the common |
| 15 | +/// "no pass at all" answer costs one RPC. |
| 16 | +/// |
| 17 | +/// A pass stored at [`Ipv4Addr::UNSPECIFIED`] (0.0.0.0) is valid for any client IP — that is how |
| 18 | +/// dynamic seats, including the `EdgeSeat` passes issued by the shred oracle, are held. The |
| 19 | +/// program accepts either the exact-IP PDA or the UNSPECIFIED one (see `create_core.rs`), so |
| 20 | +/// probing only the exact IP would report "no valid AccessPass" for a wildcard holder and bail |
| 21 | +/// before ever reaching the program, which would have accepted them. Hence the fallback. |
| 22 | +/// |
| 23 | +/// `Ok(false)` rather than an error when no pass exists: the caller renders its own diagnostic |
| 24 | +/// (the client IP and payer) before bailing, which is more use than a generic "not found". |
| 25 | +pub fn check_accesspass<L, E>( |
| 26 | + client_ip: Ipv4Addr, |
| 27 | + enforce_epoch: bool, |
| 28 | + lookup: L, |
| 29 | + epoch: E, |
| 30 | +) -> eyre::Result<bool> |
| 31 | +where |
| 32 | + L: Fn(Ipv4Addr) -> eyre::Result<Option<AccessPass>>, |
| 33 | + E: FnOnce() -> eyre::Result<u64>, |
| 34 | +{ |
| 35 | + let accesspass = match lookup(client_ip)? { |
| 36 | + Some(accesspass) => Some(accesspass), |
| 37 | + // Already the dynamic PDA — a second identical lookup would tell us nothing. |
| 38 | + None if client_ip == Ipv4Addr::UNSPECIFIED => None, |
| 39 | + None => lookup(Ipv4Addr::UNSPECIFIED)?, |
| 40 | + }; |
| 41 | + |
| 42 | + let Some(accesspass) = accesspass else { |
| 43 | + return Ok(false); |
| 44 | + }; |
| 45 | + |
| 46 | + if !enforce_epoch { |
| 47 | + return Ok(true); |
| 48 | + } |
| 49 | + Ok(accesspass.last_access_epoch >= epoch()?) |
| 50 | +} |
| 51 | + |
| 52 | +#[cfg(test)] |
| 53 | +mod tests { |
| 54 | + use super::*; |
| 55 | + use doublezero_serviceability::state::{ |
| 56 | + accesspass::{AccessPassStatus, AccessPassType}, |
| 57 | + accounttype::AccountType, |
| 58 | + }; |
| 59 | + use solana_sdk::pubkey::Pubkey; |
| 60 | + use std::cell::RefCell; |
| 61 | + |
| 62 | + const CLIENT_IP: Ipv4Addr = Ipv4Addr::new(203, 0, 113, 7); |
| 63 | + const EPOCH: u64 = 100; |
| 64 | + |
| 65 | + fn pass(client_ip: Ipv4Addr, last_access_epoch: u64) -> AccessPass { |
| 66 | + AccessPass { |
| 67 | + account_type: AccountType::AccessPass, |
| 68 | + owner: Pubkey::new_unique(), |
| 69 | + bump_seed: 0, |
| 70 | + accesspass_type: AccessPassType::Prepaid, |
| 71 | + client_ip, |
| 72 | + user_payer: Pubkey::new_unique(), |
| 73 | + last_access_epoch, |
| 74 | + connection_count: 0, |
| 75 | + status: AccessPassStatus::Connected, |
| 76 | + mgroup_pub_allowlist: vec![], |
| 77 | + mgroup_sub_allowlist: vec![], |
| 78 | + flags: 0, |
| 79 | + tenant_allowlist: vec![], |
| 80 | + unicast_user_count: 0, |
| 81 | + max_unicast_users: 1, |
| 82 | + multicast_user_count: 0, |
| 83 | + max_multicast_users: 1, |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /// Records the IPs looked up, so the tests can assert the fallback happened (or did not). |
| 88 | + fn recording_lookup<'a>( |
| 89 | + seen: &'a RefCell<Vec<Ipv4Addr>>, |
| 90 | + answer: impl Fn(Ipv4Addr) -> Option<AccessPass> + 'a, |
| 91 | + ) -> impl Fn(Ipv4Addr) -> eyre::Result<Option<AccessPass>> + 'a { |
| 92 | + move |ip| { |
| 93 | + seen.borrow_mut().push(ip); |
| 94 | + Ok(answer(ip)) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + #[test] |
| 99 | + fn specific_ip_pass_is_found_without_probing_the_dynamic_pda() { |
| 100 | + let seen = RefCell::new(vec![]); |
| 101 | + let found = check_accesspass( |
| 102 | + CLIENT_IP, |
| 103 | + true, |
| 104 | + recording_lookup(&seen, |ip| (ip == CLIENT_IP).then(|| pass(ip, EPOCH))), |
| 105 | + || Ok(EPOCH), |
| 106 | + ) |
| 107 | + .unwrap(); |
| 108 | + |
| 109 | + assert!(found); |
| 110 | + assert_eq!(*seen.borrow(), vec![CLIENT_IP]); |
| 111 | + } |
| 112 | + |
| 113 | + #[test] |
| 114 | + fn falls_back_to_a_valid_dynamic_pass() { |
| 115 | + let seen = RefCell::new(vec![]); |
| 116 | + let found = check_accesspass( |
| 117 | + CLIENT_IP, |
| 118 | + true, |
| 119 | + recording_lookup(&seen, |ip| { |
| 120 | + (ip == Ipv4Addr::UNSPECIFIED).then(|| pass(ip, EPOCH)) |
| 121 | + }), |
| 122 | + || Ok(EPOCH), |
| 123 | + ) |
| 124 | + .unwrap(); |
| 125 | + |
| 126 | + assert!(found); |
| 127 | + assert_eq!(*seen.borrow(), vec![CLIENT_IP, Ipv4Addr::UNSPECIFIED]); |
| 128 | + } |
| 129 | + |
| 130 | + #[test] |
| 131 | + fn no_pass_at_either_pda_is_not_an_error() { |
| 132 | + let seen = RefCell::new(vec![]); |
| 133 | + let found = check_accesspass(CLIENT_IP, true, recording_lookup(&seen, |_| None), || { |
| 134 | + panic!("the epoch is not read when there is no pass") |
| 135 | + }) |
| 136 | + .unwrap(); |
| 137 | + |
| 138 | + assert!(!found); |
| 139 | + assert_eq!(*seen.borrow(), vec![CLIENT_IP, Ipv4Addr::UNSPECIFIED]); |
| 140 | + } |
| 141 | + |
| 142 | + #[test] |
| 143 | + fn an_epoch_expired_dynamic_pass_does_not_count() { |
| 144 | + let seen = RefCell::new(vec![]); |
| 145 | + let found = check_accesspass( |
| 146 | + CLIENT_IP, |
| 147 | + true, |
| 148 | + recording_lookup(&seen, |ip| { |
| 149 | + (ip == Ipv4Addr::UNSPECIFIED).then(|| pass(ip, EPOCH - 1)) |
| 150 | + }), |
| 151 | + || Ok(EPOCH), |
| 152 | + ) |
| 153 | + .unwrap(); |
| 154 | + |
| 155 | + assert!(!found); |
| 156 | + } |
| 157 | + |
| 158 | + #[test] |
| 159 | + fn an_expired_pass_still_counts_when_the_epoch_is_not_enforced() { |
| 160 | + let seen = RefCell::new(vec![]); |
| 161 | + let found = check_accesspass( |
| 162 | + CLIENT_IP, |
| 163 | + false, |
| 164 | + recording_lookup(&seen, |ip| { |
| 165 | + (ip == Ipv4Addr::UNSPECIFIED).then(|| pass(ip, EPOCH - 1)) |
| 166 | + }), |
| 167 | + || panic!("the epoch is not read when it is not enforced"), |
| 168 | + ) |
| 169 | + .unwrap(); |
| 170 | + |
| 171 | + assert!(found); |
| 172 | + } |
| 173 | + |
| 174 | + #[test] |
| 175 | + fn an_unspecified_client_ip_is_looked_up_once() { |
| 176 | + let seen = RefCell::new(vec![]); |
| 177 | + let found = check_accesspass( |
| 178 | + Ipv4Addr::UNSPECIFIED, |
| 179 | + true, |
| 180 | + recording_lookup(&seen, |_| None), |
| 181 | + || Ok(EPOCH), |
| 182 | + ) |
| 183 | + .unwrap(); |
| 184 | + |
| 185 | + assert!(!found); |
| 186 | + assert_eq!(*seen.borrow(), vec![Ipv4Addr::UNSPECIFIED]); |
| 187 | + } |
| 188 | +} |
0 commit comments