| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | |
|
| 12 | |
|
| 13 | |
|
| 14 | |
|
| 15 | |
|
| 16 | |
|
| 17 | | use crate::home::{Home, RunnerConfig}; |
| 18 | | use crate::images::{cache_path, chunk_cache, manifest_path, save_manifest, write_image, Client, Moved}; |
| 19 | | use anyhow::{bail, Context, Result}; |
| 20 | | use flow_core::api::{SwarmAddr, SwarmPeer}; |
| 21 | | use jet::images::{is_hash, Manifest}; |
| 22 | | use jet::swarm::{Bits, Pick, Picker, Source, Ticket}; |
| 23 | | use oxy_core::identity::Identity; |
| 24 | | use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; |
| 25 | | use rustls::crypto::CryptoProvider; |
| 26 | | use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime}; |
| 27 | | use rustls::{DigitallySignedStruct, SignatureScheme}; |
| 28 | | use std::collections::HashMap; |
| 29 | | use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; |
| 30 | | use std::net::{TcpListener, TcpStream}; |
| 31 | | use std::path::{Path, PathBuf}; |
| 32 | | use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; |
| 33 | | use std::sync::{Arc, Mutex}; |
| 34 | | use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; |
| 35 | |
|
| 36 | |
|
| 37 | | const MAX_CONNS: usize = 16; |
| 38 | |
|
| 39 | | const MAX_OPEN: usize = 4 * MAX_CONNS; |
| 40 | |
|
| 41 | | const PEER_WORKERS: usize = 4; |
| 42 | | const HUB_WORKERS: usize = 8; |
| 43 | |
|
| 44 | | const REFRESH: Duration = Duration::from_millis(500); |
| 45 | |
|
| 46 | | const HUB_FAILURES: usize = 5; |
| 47 | |
|
| 48 | | const MAX_BODY: usize = 64 * 1024; |
| 49 | |
|
| 50 | | static PART: AtomicU64 = AtomicU64::new(0); |
| 51 | |
|
| 52 | 94× | fn now_ms() -> u64 { |
| 53 | 94× | SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) |
| 54 | 94× | } |
| 55 | |
|
| 56 | |
|
| 57 | |
|
| 58 | | struct Offer { |
| 59 | | bits: Option<Bits>, |
| 60 | | fetching: Bits, |
| 61 | | } |
| 62 | |
|
| 63 | | struct State { |
| 64 | | root: PathBuf, |
| 65 | | site: String, |
| 66 | | client: Client, |
| 67 | | |
| 68 | | hub: Mutex<Option<Identity>>, |
| 69 | | offers: Mutex<HashMap<String, Offer>>, |
| 70 | | |
| 71 | | located: Mutex<HashMap<String, (PathBuf, u64, u32)>>, |
| 72 | | |
| 73 | | sessions: Mutex<HashMap<String, (String, u64)>>, |
| 74 | | conns: AtomicUsize, |
| 75 | | |
| 76 | | |
| 77 | | bucket: Mutex<(f64, Instant)>, |
| 78 | | rate: f64, |
| 79 | | tls: Arc<rustls::ServerConfig>, |
| 80 | | } |
| 81 | |
|
| 82 | |
|
| 83 | | pub struct Swarm { |
| 84 | | state: Arc<State>, |
| 85 | | pub addr: SwarmAddr, |
| 86 | | } |
| 87 | |
|
| 88 | | impl Swarm { |
| 89 | | |
| 90 | 9× | pub fn start(home: &Home, cfg: &RunnerConfig) -> Result<Option<Swarm>> { |
| 91 | 9× | let Some(sc) = &cfg.swarm else { return Ok(None) }; |
| 92 | 9× | let listener = TcpListener::bind(&sc.listen).with_context(|| format!("swarm: listen on {}", sc.listen))?; |
| 93 | 9× | let client = Client::new(&cfg.hub, &cfg.token, cfg.ca.as_deref())?; |
| 94 | 9× | let hub = client.hub_identity().map_err(|e| eprintln!("swarm: the hub's key: {e:#}; trying again when a peer comes")).ok(); |
| 95 | 9× | let rate = sc.upload_mib_s.unwrap_or(50).max(1) as f64 * (1 << 20) as f64; |
| 96 | 9× | let (cert, key) = certificate(&home.root)?; |
| 97 | 9× | let fingerprint = blake3::hash(&cert).to_hex().to_string(); |
| 98 | 9× | let tls = server_tls(cert, key)?; |
| 99 | 9× | let state = Arc::new(State { |
| 100 | 9× | root: home.root.clone(), |
| 101 | 9× | site: sc.site.clone(), |
| 102 | 9× | client, |
| 103 | 9× | hub: Mutex::new(hub), |
| 104 | 9× | offers: Mutex::new(HashMap::new()), |
| 105 | 9× | located: Mutex::new(HashMap::new()), |
| 106 | 9× | sessions: Mutex::new(HashMap::new()), |
| 107 | 9× | conns: AtomicUsize::new(0), |
| 108 | 9× | bucket: Mutex::new((rate, Instant::now())), |
| 109 | 9× | rate, |
| 110 | 9× | tls, |
| 111 | 9× | }); |
| 112 | | |
| 113 | | |
| 114 | | |
| 115 | 9× | let mut unscanned = Vec::new(); |
| 116 | 9× | for h in home.images() { |
| 117 | 3× | match std::fs::read(manifest_path(&home.root, &h)).ok().and_then(|b| serde_json::from_slice::<Manifest>(&b).ok()) { |
| 118 | 1× | Some(m) if m.hash == h => offer_whole(&state, &m, &home.rootfs(&h)), |
| 119 | 2× | _ => unscanned.push((h.clone(), home.rootfs(&h), home.image_arch(&h))), |
| 120 | | } |
| 121 | | } |
| 122 | 9× | let scanning = state.clone(); |
| 123 | 9× | std::thread::spawn(move || { |
| 124 | 9× | for (h, path, arch) in unscanned { |
| 125 | 76× | match jet::images::scan_file(&path, arch, |_, _| Ok(())) { |
| 126 | 2× | Ok(m) if m.hash == h => { |
| 127 | 2× | save_manifest(&scanning.root, &m).ok(); |
| 128 | 2× | offer_whole(&scanning, &m, &path); |
| 129 | 2× | } |
| 130 | 0× | Ok(_) => eprintln!("swarm: {} does not hold the image its name says; not offering it", path.display()), |
| 131 | 0× | Err(e) => eprintln!("swarm: scan {}: {e}", path.display()), |
| 132 | | } |
| 133 | | } |
| 134 | 9× | }); |
| 135 | 9× | let serving = state.clone(); |
| 136 | 9× | std::thread::spawn(move || accept(listener, serving)); |
| 137 | 9× | let addr = SwarmAddr { addr: sc.advertise.clone().unwrap_or_else(|| sc.listen.clone()), site: sc.site.clone(), cert: fingerprint }; |
| 138 | 9× | eprintln!( |
| 139 | | "swarm: serving image chunks on {} over TLS (certificate {}, site {:?}, up to {} MiB/s)", |
| 140 | | sc.listen, |
| 141 | 9× | &addr.cert[..12], |
| 142 | | sc.site, |
| 143 | 9× | sc.upload_mib_s.unwrap_or(50) |
| 144 | | ); |
| 145 | 9× | Ok(Some(Swarm { state, addr })) |
| 146 | 9× | } |
| 147 | |
|
| 148 | | |
| 149 | | |
| 150 | | |
| 151 | 5× | pub fn fetch(&self, home: &Home, hash: &str) -> Result<Option<Moved>> { |
| 152 | 5× | let start = Instant::now(); |
| 153 | 5× | let client = &self.state.client; |
| 154 | 5× | let Some(m) = client.manifest(hash)? else { return Ok(None) }; |
| 155 | 5× | if m.hash != hash { |
| 156 | 0× | bail!("the hub's manifest for {} names {}", &hash[..12], &m.hash[..12]); |
| 157 | 5× | } |
| 158 | 5× | save_manifest(&home.root, &m)?; |
| 159 | 5× | let needed: Vec<String> = m.needed().into_iter().map(String::from).collect(); |
| 160 | 5× | let cache = chunk_cache(home); |
| 161 | 5× | let mut have = Bits::new(needed.len()); |
| 162 | 190× | for (i, h) in needed.iter().enumerate() { |
| 163 | 190× | if cache_path(&cache, h).is_file() { |
| 164 | 0× | have.set(i); |
| 165 | 190× | } |
| 166 | | } |
| 167 | 5× | self.state.offers.lock().unwrap().insert(hash.into(), Offer { bits: Some(have.clone()), fetching: Bits::new(needed.len()) }); |
| 168 | 5× | let announced = client |
| 169 | 5× | .announce(hash, "fetching") |
| 170 | 5× | .map_err(|e| eprintln!("swarm: announce {}: {e:#}; fetching from the hub alone", &hash[..12])) |
| 171 | 5× | .ok(); |
| 172 | 5× | let seed = now_ms() ^ (std::process::id() as u64) << 32; |
| 173 | 5× | let picker = Mutex::new(Picker::new(needed.len(), have, seed)); |
| 174 | 5× | let peers: Mutex<Vec<Arc<Peer>>> = Mutex::new(Vec::new()); |
| 175 | 5× | if let Some(a) = &announced { |
| 176 | 5× | self.join(&a.peers, &a.ticket, hash, needed.len(), &peers, &picker); |
| 177 | 5× | } |
| 178 | 5× | let (bytes, peer_bytes, got) = (AtomicU64::new(0), AtomicU64::new(0), AtomicUsize::new(0)); |
| 179 | 5× | let (failed, hub_failures) = (Mutex::new(None::<anyhow::Error>), AtomicUsize::new(0)); |
| 180 | | { |
| 181 | 5× | let (picker, peers, needed, cache) = (&picker, &peers, &needed, &cache); |
| 182 | 5× | let (bytes, peer_bytes, got, failed, hub_failures) = (&bytes, &peer_bytes, &got, &failed, &hub_failures); |
| 183 | 5× | let (state, m) = (&self.state, &m); |
| 184 | 5× | let worker = move |src: Source| loop { |
| 185 | 798× | if failed.lock().unwrap().is_some() { |
| 186 | 0× | break; |
| 187 | 798× | } |
| 188 | 798× | let peer = match src { |
| 189 | 558× | Source::Peer(p) => Some(peers.lock().unwrap()[p].clone()), |
| 190 | 240× | Source::Hub => None, |
| 191 | | }; |
| 192 | 798× | if peer.as_ref().is_some_and(|p| !p.alive.load(Ordering::Relaxed)) { |
| 193 | 0× | break; |
| 194 | 798× | } |
| 195 | | |
| 196 | | |
| 197 | 798× | let pick = picker.lock().unwrap().pick(src); |
| 198 | 798× | let i = match pick { |
| 199 | 52× | Pick::Done => break, |
| 200 | | Pick::Wait => { |
| 201 | 556× | std::thread::sleep(Duration::from_millis(20)); |
| 202 | 556× | continue; |
| 203 | | } |
| 204 | 190× | Pick::Chunk(i) => i, |
| 205 | | }; |
| 206 | 190× | if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) { |
| 207 | 190× | o.fetching.set(i); |
| 208 | 190× | } |
| 209 | 190× | let h = &needed[i]; |
| 210 | 190× | let fetched = match &peer { |
| 211 | 66× | Some(p) => self.peer_chunk(p, h), |
| 212 | 124× | None => client.chunk(h), |
| 213 | | }; |
| 214 | 190× | let kept = fetched.and_then(|b| { |
| 215 | 190× | if blake3::hash(&b).to_hex().as_str() != h { |
| 216 | 0× | bail!("chunk {} came back with other bytes", &h[..12]); |
| 217 | 190× | } |
| 218 | 190× | let p = cache_path(cache, h); |
| 219 | 190× | std::fs::create_dir_all(p.parent().expect("a chunk directory"))?; |
| 220 | 190× | let tmp = p.with_extension(format!("part{}", PART.fetch_add(1, Ordering::Relaxed))); |
| 221 | 190× | std::fs::write(&tmp, &b)?; |
| 222 | 190× | std::fs::rename(&tmp, &p)?; |
| 223 | 190× | Ok(b.len() as u64) |
| 224 | 190× | }); |
| 225 | 190× | match kept { |
| 226 | 190× | Ok(n) => { |
| 227 | 190× | if let Some(Offer { bits: Some(b), fetching }) = state.offers.lock().unwrap().get_mut(&m.hash) { |
| 228 | 190× | b.set(i); |
| 229 | 190× | fetching.clear(i); |
| 230 | 190× | } |
| 231 | 190× | picker.lock().unwrap().got(i); |
| 232 | 190× | bytes.fetch_add(n, Ordering::Relaxed); |
| 233 | 190× | got.fetch_add(1, Ordering::Relaxed); |
| 234 | 190× | if peer.is_some() { |
| 235 | 66× | peer_bytes.fetch_add(n, Ordering::Relaxed); |
| 236 | 124× | } |
| 237 | | } |
| 238 | 0× | Err(e) => { |
| 239 | 0× | picker.lock().unwrap().failed(i); |
| 240 | 0× | if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) { |
| 241 | 0× | o.fetching.clear(i); |
| 242 | 0× | } |
| 243 | 0× | match (&peer, src) { |
| 244 | | |
| 245 | 0× | (Some(_), _) if e.is::<Busy>() => std::thread::sleep(Duration::from_millis(200)), |
| 246 | 0× | (Some(p), Source::Peer(idx)) => { |
| 247 | 0× | eprintln!("swarm: peer {} failed ({e:#}); carrying on without it", p.name); |
| 248 | 0× | p.alive.store(false, Ordering::Relaxed); |
| 249 | 0× | picker.lock().unwrap().drop_peer(idx); |
| 250 | 0× | break; |
| 251 | | } |
| 252 | | _ => { |
| 253 | 0× | if hub_failures.fetch_add(1, Ordering::Relaxed) + 1 >= HUB_FAILURES { |
| 254 | 0× | *failed.lock().unwrap() = Some(e); |
| 255 | 0× | break; |
| 256 | 0× | } |
| 257 | 0× | std::thread::sleep(Duration::from_millis(500)); |
| 258 | | } |
| 259 | | } |
| 260 | | } |
| 261 | | } |
| 262 | 52× | }; |
| 263 | 5× | std::thread::scope(|s| { |
| 264 | 5× | for _ in 0..HUB_WORKERS { |
| 265 | 40× | s.spawn(move || worker(Source::Hub)); |
| 266 | | } |
| 267 | 5× | let mut spawned = 0; |
| 268 | 5× | let mut spawn_new = || { |
| 269 | 5× | let n = peers.lock().unwrap().len(); |
| 270 | 5× | for p in spawned..n { |
| 271 | 3× | for _ in 0..PEER_WORKERS { |
| 272 | 12× | s.spawn(move || worker(Source::Peer(p))); |
| 273 | | } |
| 274 | | } |
| 275 | 5× | spawned = n; |
| 276 | 5× | }; |
| 277 | 5× | spawn_new(); |
| 278 | | |
| 279 | | |
| 280 | 5× | let mut asked_hub = Instant::now(); |
| 281 | 5× | let interval = Duration::from_secs(announced.as_ref().map(|a| a.interval_s).unwrap_or(30).max(5)); |
| 282 | 5× | let mut last = Instant::now(); |
| 283 | 33× | while picker.lock().unwrap().missing() > 0 && failed.lock().unwrap().is_none() { |
| 284 | 28× | std::thread::sleep(Duration::from_millis(100)); |
| 285 | 28× | if last.elapsed() < REFRESH { |
| 286 | 24× | continue; |
| 287 | 4× | } |
| 288 | 4× | last = Instant::now(); |
| 289 | 4× | let now: Vec<(usize, Arc<Peer>)> = |
| 290 | 4× | peers.lock().unwrap().iter().cloned().enumerate().filter(|(_, p)| p.alive.load(Ordering::Relaxed)).collect(); |
| 291 | 4× | for (idx, p) in now { |
| 292 | 4× | match self.peer_have(&p, hash, needed.len()) { |
| 293 | 4× | Ok((bits, fetching)) => picker.lock().unwrap().peer_fetching(idx, bits, fetching), |
| 294 | 0× | Err(e) if e.is::<Busy>() => {} |
| 295 | 0× | Err(_) => { |
| 296 | 0× | p.alive.store(false, Ordering::Relaxed); |
| 297 | 0× | picker.lock().unwrap().drop_peer(idx); |
| 298 | 0× | } |
| 299 | | } |
| 300 | | } |
| 301 | 4× | if announced.is_some() && asked_hub.elapsed() >= interval { |
| 302 | 0× | asked_hub = Instant::now(); |
| 303 | 0× | if let Ok(a) = client.announce(hash, "fetching") { |
| 304 | 0× | self.join(&a.peers, &a.ticket, hash, needed.len(), peers, picker); |
| 305 | 0× | spawn_new(); |
| 306 | 0× | } |
| 307 | 4× | } |
| 308 | | } |
| 309 | 5× | }); |
| 310 | | } |
| 311 | 5× | if let Some(e) = failed.into_inner().unwrap() { |
| 312 | 0× | client.announce(hash, "stopped").ok(); |
| 313 | 0× | return Err(e); |
| 314 | 5× | } |
| 315 | 5× | write_image(home, &m)?; |
| 316 | 5× | offer_whole(&self.state, &m, &home.rootfs(hash)); |
| 317 | 5× | client.announce(hash, "done").ok(); |
| 318 | 5× | Ok(Some(Moved { |
| 319 | 5× | chunks: got.into_inner(), |
| 320 | 5× | bytes: bytes.into_inner(), |
| 321 | 5× | peer_bytes: peer_bytes.into_inner(), |
| 322 | 5× | elapsed: start.elapsed(), |
| 323 | 5× | })) |
| 324 | 5× | } |
| 325 | |
|
| 326 | | |
| 327 | | |
| 328 | 5× | fn join(&self, found: &[SwarmPeer], ticket: &str, hash: &str, len: usize, peers: &Mutex<Vec<Arc<Peer>>>, picker: &Mutex<Picker>) { |
| 329 | 5× | for sp in found { |
| 330 | 4× | if peers.lock().unwrap().iter().any(|p| p.runner == sp.runner) { |
| 331 | 0× | continue; |
| 332 | 4× | } |
| 333 | 4× | if !is_hash(&sp.cert) { |
| 334 | 0× | eprintln!("swarm: peer {} gave no certificate (an older flow); not asking it", sp.name); |
| 335 | 0× | continue; |
| 336 | 4× | } |
| 337 | 4× | let joined = hello(&sp.addr, &sp.cert, ticket).and_then(|(agent, session)| { |
| 338 | 3× | let p = Peer { |
| 339 | 3× | runner: sp.runner, |
| 340 | 3× | name: sp.name.clone(), |
| 341 | 3× | base: format!("https://{}/swarm/v1", sp.addr), |
| 342 | 3× | agent, |
| 343 | 3× | session, |
| 344 | 3× | alive: AtomicBool::new(true), |
| 345 | 3× | }; |
| 346 | 3× | let (bits, fetching) = self.peer_have(&p, hash, len)?; |
| 347 | 3× | Ok((p, bits, fetching)) |
| 348 | 3× | }); |
| 349 | 4× | match joined { |
| 350 | 3× | Ok((p, bits, fetching)) => { |
| 351 | 3× | let mut list = peers.lock().unwrap(); |
| 352 | 3× | picker.lock().unwrap().peer_fetching(list.len(), bits, fetching); |
| 353 | 3× | list.push(Arc::new(p)); |
| 354 | 3× | } |
| 355 | 1× | Err(e) => eprintln!("swarm: peer {} ({}): {e:#}", sp.name, sp.addr), |
| 356 | | } |
| 357 | | } |
| 358 | 5× | } |
| 359 | |
|
| 360 | | |
| 361 | | |
| 362 | | |
| 363 | 7× | fn peer_have(&self, p: &Peer, hash: &str, len: usize) -> Result<(Bits, Bits)> { |
| 364 | 7× | let url = format!("{}/images/{hash}/have", p.base); |
| 365 | 7× | let r = match p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call() { |
| 366 | 7× | Ok(r) => r, |
| 367 | 0× | Err(ureq::Error::Status(404, _)) => return Ok((Bits::new(len), Bits::new(len))), |
| 368 | 0× | Err(e) => return Err(peer_error(&url, e)), |
| 369 | | }; |
| 370 | 7× | let mut body = Vec::new(); |
| 371 | 7× | r.into_reader().take(len as u64 / 4 + 64).read_to_end(&mut body)?; |
| 372 | 7× | if body == b"complete" { |
| 373 | 1× | return Ok((Bits::full(len), Bits::new(len))); |
| 374 | 6× | } |
| 375 | 6× | let (have, fetching) = body.split_at(len.div_ceil(8).min(body.len())); |
| 376 | 6× | Ok((Bits::from_bytes(len, have), Bits::from_bytes(len, fetching))) |
| 377 | 7× | } |
| 378 | |
|
| 379 | 66× | fn peer_chunk(&self, p: &Peer, hash: &str) -> Result<Vec<u8>> { |
| 380 | 66× | let url = format!("{}/chunks/{hash}", p.base); |
| 381 | 66× | let r = p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call().map_err(|e| peer_error(&url, e))?; |
| 382 | 66× | let mut out = Vec::new(); |
| 383 | 66× | r.into_reader().take(oxy_core::chunk::MAX_SIZE as u64 + 1).read_to_end(&mut out)?; |
| 384 | 66× | Ok(out) |
| 385 | 66× | } |
| 386 | | } |
| 387 | |
|
| 388 | |
|
| 389 | |
|
| 390 | | struct Peer { |
| 391 | | runner: i64, |
| 392 | | name: String, |
| 393 | | base: String, |
| 394 | | agent: ureq::Agent, |
| 395 | | session: String, |
| 396 | | alive: AtomicBool, |
| 397 | | } |
| 398 | |
|
| 399 | |
|
| 400 | | #[derive(Debug)] |
| 401 | | struct Busy; |
| 402 | |
|
| 403 | | impl std::fmt::Display for Busy { |
| 404 | 0× | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 405 | 0× | f.write_str("the peer is busy") |
| 406 | 0× | } |
| 407 | | } |
| 408 | |
|
| 409 | | impl std::error::Error for Busy {} |
| 410 | |
|
| 411 | 2× | fn peer_error(url: &str, e: ureq::Error) -> anyhow::Error { |
| 412 | 2× | match e { |
| 413 | 0× | ureq::Error::Status(503, _) => anyhow::Error::new(Busy), |
| 414 | 1× | ureq::Error::Status(code, r) => anyhow::anyhow!("{url}: HTTP {code}: {}", r.into_string().unwrap_or_default().trim()), |
| 415 | 1× | e => anyhow::anyhow!("{url}: {e}"), |
| 416 | | } |
| 417 | 2× | } |
| 418 | |
|
| 419 | |
|
| 420 | |
|
| 421 | 5× | fn hello(addr: &str, cert: &str, ticket: &str) -> Result<(ureq::Agent, String)> { |
| 422 | 5× | let agent = peer_agent(cert)?; |
| 423 | 5× | let url = format!("https://{addr}/swarm/v1/hello"); |
| 424 | 5× | let r: serde_json::Value = agent.post(&url).send_string(ticket).map_err(|e| peer_error(&url, e))?.into_json()?; |
| 425 | 3× | let session = r["session"].as_str().map(String::from).context("the peer gave no session")?; |
| 426 | 3× | Ok((agent, session)) |
| 427 | 5× | } |
| 428 | |
|
| 429 | 16× | fn provider() -> Arc<CryptoProvider> { |
| 430 | 16× | Arc::new(rustls::crypto::ring::default_provider()) |
| 431 | 16× | } |
| 432 | |
|
| 433 | |
|
| 434 | |
|
| 435 | 6× | fn peer_agent(cert: &str) -> Result<ureq::Agent> { |
| 436 | 6× | let provider = provider(); |
| 437 | 6× | let cfg = rustls::ClientConfig::builder_with_provider(provider.clone()) |
| 438 | 6× | .with_safe_default_protocol_versions()? |
| 439 | 6× | .dangerous() |
| 440 | 6× | .with_custom_certificate_verifier(Arc::new(Pinned { cert: cert.to_string(), provider })) |
| 441 | 6× | .with_no_client_auth(); |
| 442 | 6× | Ok(ureq::AgentBuilder::new().tls_config(Arc::new(cfg)).timeout_connect(Duration::from_secs(2)).timeout(Duration::from_secs(20)).build()) |
| 443 | 6× | } |
| 444 | |
|
| 445 | |
|
| 446 | |
|
| 447 | |
|
| 448 | | #[derive(Debug)] |
| 449 | | struct Pinned { |
| 450 | | cert: String, |
| 451 | | provider: Arc<CryptoProvider>, |
| 452 | | } |
| 453 | |
|
| 454 | | impl ServerCertVerifier for Pinned { |
| 455 | 7× | fn verify_server_cert( |
| 456 | 7× | &self, |
| 457 | 7× | end_entity: &CertificateDer<'_>, |
| 458 | 7× | _intermediates: &[CertificateDer<'_>], |
| 459 | 7× | _name: &ServerName<'_>, |
| 460 | 7× | _ocsp: &[u8], |
| 461 | 7× | _now: UnixTime, |
| 462 | 7× | ) -> std::result::Result<ServerCertVerified, rustls::Error> { |
| 463 | 7× | if blake3::hash(end_entity.as_ref()).to_hex().as_str() == self.cert { |
| 464 | 6× | Ok(ServerCertVerified::assertion()) |
| 465 | | } else { |
| 466 | 1× | Err(rustls::Error::General("not the peer the hub named: its certificate is another".into())) |
| 467 | | } |
| 468 | 7× | } |
| 469 | |
|
| 470 | 0× | fn verify_tls12_signature( |
| 471 | 0× | &self, |
| 472 | 0× | message: &[u8], |
| 473 | 0× | cert: &CertificateDer<'_>, |
| 474 | 0× | dss: &DigitallySignedStruct, |
| 475 | 0× | ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> { |
| 476 | 0× | rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms) |
| 477 | 0× | } |
| 478 | |
|
| 479 | 6× | fn verify_tls13_signature( |
| 480 | 6× | &self, |
| 481 | 6× | message: &[u8], |
| 482 | 6× | cert: &CertificateDer<'_>, |
| 483 | 6× | dss: &DigitallySignedStruct, |
| 484 | 6× | ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> { |
| 485 | 6× | rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms) |
| 486 | 6× | } |
| 487 | |
|
| 488 | 19× | fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { |
| 489 | 19× | self.provider.signature_verification_algorithms.supported_schemes() |
| 490 | 19× | } |
| 491 | | } |
| 492 | |
|
| 493 | |
|
| 494 | |
|
| 495 | |
|
| 496 | 10× | fn certificate(root: &Path) -> Result<(Vec<u8>, Vec<u8>)> { |
| 497 | 10× | let dir = root.join("swarm"); |
| 498 | 10× | let (cert_path, key_path) = (dir.join("cert.der"), dir.join("key.der")); |
| 499 | 10× | if let (Ok(c), Ok(k)) = (std::fs::read(&cert_path), std::fs::read(&key_path)) { |
| 500 | 1× | return Ok((c, k)); |
| 501 | 9× | } |
| 502 | 9× | std::fs::create_dir_all(&dir)?; |
| 503 | 9× | let made = rcgen::generate_simple_self_signed(vec!["flow-swarm".to_string()]).context("make the swarm's certificate")?; |
| 504 | 9× | let (cert, key) = (made.cert.der().to_vec(), made.signing_key.serialize_der()); |
| 505 | | { |
| 506 | 9× | let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&key_path)?; |
| 507 | 9× | f.write_all(&key)?; |
| 508 | | } |
| 509 | 9× | std::fs::write(&cert_path, &cert)?; |
| 510 | 9× | Ok((cert, key)) |
| 511 | 10× | } |
| 512 | |
|
| 513 | 10× | fn server_tls(cert: Vec<u8>, key: Vec<u8>) -> Result<Arc<rustls::ServerConfig>> { |
| 514 | 10× | let cfg = rustls::ServerConfig::builder_with_provider(provider()) |
| 515 | 10× | .with_safe_default_protocol_versions()? |
| 516 | 10× | .with_no_client_auth() |
| 517 | 10× | .with_single_cert(vec![CertificateDer::from(cert)], PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key))) |
| 518 | 10× | .context("the swarm's certificate")?; |
| 519 | 10× | Ok(Arc::new(cfg)) |
| 520 | 10× | } |
| 521 | |
|
| 522 | |
|
| 523 | 8× | fn offer_whole(state: &State, m: &Manifest, path: &Path) { |
| 524 | 8× | let mut located = state.located.lock().unwrap(); |
| 525 | 8× | let mut at = 0u64; |
| 526 | 304× | for c in &m.chunks { |
| 527 | 304× | if !c.is_zeros() { |
| 528 | 304× | located.entry(c.hash().to_string()).or_insert_with(|| (path.to_path_buf(), at, c.1)); |
| 529 | 0× | } |
| 530 | 304× | at += c.1 as u64; |
| 531 | | } |
| 532 | 8× | state.offers.lock().unwrap().insert(m.hash.clone(), Offer { bits: None, fetching: Bits::new(0) }); |
| 533 | 8× | } |
| 534 | |
|
| 535 | 9× | fn accept(listener: TcpListener, state: Arc<State>) { |
| 536 | 20× | for conn in listener.incoming() { |
| 537 | 20× | let Ok(conn) = conn else { continue }; |
| 538 | 20× | if state.conns.load(Ordering::Relaxed) >= MAX_OPEN { |
| 539 | 0× | continue; |
| 540 | 20× | } |
| 541 | 20× | let state = state.clone(); |
| 542 | 20× | std::thread::spawn(move || { |
| 543 | | |
| 544 | | |
| 545 | 20× | let busy = state.conns.fetch_add(1, Ordering::Relaxed) >= MAX_CONNS; |
| 546 | 20× | serve_tls(conn, &state, busy).ok(); |
| 547 | 20× | state.conns.fetch_sub(1, Ordering::Relaxed); |
| 548 | 20× | }); |
| 549 | | } |
| 550 | 9× | } |
| 551 | |
|
| 552 | 20× | fn serve_tls(conn: TcpStream, state: &State, busy: bool) -> std::io::Result<()> { |
| 553 | 20× | conn.set_read_timeout(Some(Duration::from_secs(30)))?; |
| 554 | 20× | conn.set_write_timeout(Some(Duration::from_secs(30)))?; |
| 555 | 20× | conn.set_nodelay(true)?; |
| 556 | 20× | let tls = rustls::ServerConnection::new(state.tls.clone()).map_err(std::io::Error::other)?; |
| 557 | 20× | serve_conn(rustls::StreamOwned::new(tls, conn), state, busy) |
| 558 | 20× | } |
| 559 | |
|
| 560 | 78× | fn respond(conn: &mut impl Write, code: u16, kind: &str, body: &[u8]) -> std::io::Result<()> { |
| 561 | 78× | let reason = match code { |
| 562 | 76× | 200 => "OK", |
| 563 | 0× | 400 => "Bad Request", |
| 564 | 1× | 401 => "Unauthorized", |
| 565 | 1× | 403 => "Forbidden", |
| 566 | 0× | 404 => "Not Found", |
| 567 | 0× | 503 => "Service Unavailable", |
| 568 | 0× | _ => "Error", |
| 569 | | }; |
| 570 | 78× | write!(conn, "HTTP/1.1 {code} {reason}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\n\r\n", body.len())?; |
| 571 | 78× | conn.write_all(body)?; |
| 572 | 78× | conn.flush() |
| 573 | 78× | } |
| 574 | |
|
| 575 | |
|
| 576 | 20× | fn serve_conn<S: Read + Write>(conn: S, state: &State, busy: bool) -> std::io::Result<()> { |
| 577 | 20× | let mut r = BufReader::new(conn); |
| 578 | | loop { |
| 579 | 98× | let mut line = String::new(); |
| 580 | 98× | if r.read_line(&mut line)? == 0 { |
| 581 | 0× | return Ok(()); |
| 582 | 78× | } |
| 583 | 78× | let mut parts = line.split_whitespace(); |
| 584 | 78× | let (method, path) = (parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string()); |
| 585 | 78× | let (mut length, mut auth, mut close) = (0usize, String::new(), false); |
| 586 | | loop { |
| 587 | 467× | let mut h = String::new(); |
| 588 | 467× | if r.read_line(&mut h)? == 0 { |
| 589 | 0× | return Ok(()); |
| 590 | 467× | } |
| 591 | 467× | let h = h.trim_end(); |
| 592 | 467× | if h.is_empty() { |
| 593 | 78× | break; |
| 594 | 389× | } |
| 595 | 389× | if let Some((k, v)) = h.split_once(':') { |
| 596 | 389× | match k.trim().to_ascii_lowercase().as_str() { |
| 597 | 389× | "content-length" => length = v.trim().parse().unwrap_or(usize::MAX), |
| 598 | 385× | "authorization" => auth = v.trim().to_string(), |
| 599 | 312× | "connection" => close = v.trim().eq_ignore_ascii_case("close"), |
| 600 | 312× | _ => {} |
| 601 | | } |
| 602 | 0× | } |
| 603 | | } |
| 604 | 78× | if length > MAX_BODY { |
| 605 | 0× | return respond(r.get_mut(), 400, "text/plain", b"too large"); |
| 606 | 78× | } |
| 607 | 78× | let mut body = vec![0; length]; |
| 608 | 78× | r.read_exact(&mut body)?; |
| 609 | 78× | if busy { |
| 610 | 0× | return respond(r.get_mut(), 503, "text/plain", b"busy: ask again shortly"); |
| 611 | 78× | } |
| 612 | 78× | let (code, kind, reply) = route(state, &method, &path, &auth, &body); |
| 613 | 78× | if code == 200 && kind == "application/octet-stream" { |
| 614 | 66× | throttle(state, reply.len()); |
| 615 | 66× | } |
| 616 | 78× | respond(r.get_mut(), code, kind, &reply)?; |
| 617 | 78× | if close { |
| 618 | 0× | return Ok(()); |
| 619 | 78× | } |
| 620 | | } |
| 621 | 20× | } |
| 622 | |
|
| 623 | 78× | fn route(state: &State, method: &str, path: &str, auth: &str, body: &[u8]) -> (u16, &'static str, Vec<u8>) { |
| 624 | 78× | let text = |code, s: &str| (code, "text/plain", s.as_bytes().to_vec()); |
| 625 | 78× | if method == "POST" && path == "/swarm/v1/hello" { |
| 626 | 4× | return match admit(state, &String::from_utf8_lossy(body)) { |
| 627 | 3× | Ok(session) => (200, "application/json", serde_json::json!({ "session": session }).to_string().into_bytes()), |
| 628 | 1× | Err(why) => text(403, &why), |
| 629 | | }; |
| 630 | 74× | } |
| 631 | 74× | let session = auth.strip_prefix("Bearer ").unwrap_or(""); |
| 632 | 74× | let admitted = { |
| 633 | 74× | let mut sessions = state.sessions.lock().unwrap(); |
| 634 | 74× | let now = now_ms(); |
| 635 | 74× | sessions.retain(|_, (_, until)| *until > now); |
| 636 | 74× | sessions.contains_key(session) |
| 637 | | }; |
| 638 | 74× | if !admitted { |
| 639 | 1× | return text(401, "show a ticket at /swarm/v1/hello first"); |
| 640 | 73× | } |
| 641 | 73× | let rest = path.strip_prefix("/swarm/v1/").unwrap_or(""); |
| 642 | 73× | match (method, rest.split('/').collect::<Vec<_>>().as_slice()) { |
| 643 | 73× | ("GET", ["images", image, "have"]) => match state.offers.lock().unwrap().get(*image) { |
| 644 | 1× | Some(Offer { bits: None, .. }) => (200, "text/plain", b"complete".to_vec()), |
| 645 | 6× | Some(Offer { bits: Some(b), fetching }) => (200, "application/x-have", [b.to_bytes(), fetching.to_bytes()].concat()), |
| 646 | 0× | None => text(404, "this runner does not hold that image"), |
| 647 | | }, |
| 648 | 66× | ("GET", ["chunks", chunk]) if is_hash(chunk) => match read_chunk(state, chunk) { |
| 649 | 66× | Some(b) => (200, "application/octet-stream", b), |
| 650 | 0× | None => text(404, "no such chunk here"), |
| 651 | | }, |
| 652 | 0× | _ => text(404, "not a swarm path"), |
| 653 | | } |
| 654 | 78× | } |
| 655 | |
|
| 656 | |
|
| 657 | |
|
| 658 | 4× | fn admit(state: &State, ticket: &str) -> std::result::Result<String, String> { |
| 659 | 4× | let hub = { |
| 660 | 4× | let mut hub = state.hub.lock().unwrap(); |
| 661 | 4× | if hub.is_none() { |
| 662 | 0× | *hub = state.client.hub_identity().ok(); |
| 663 | 4× | } |
| 664 | 4× | hub.clone().ok_or("this runner cannot reach its hub to check tickets")? |
| 665 | | }; |
| 666 | 4× | let t = Ticket::open(ticket, &hub, now_ms())?; |
| 667 | 4× | if t.site != state.site { |
| 668 | 1× | return Err(format!("this runner is in site {:?}, the ticket's runner in {:?}", state.site, t.site)); |
| 669 | 3× | } |
| 670 | 3× | let session = random_token().map_err(|e| format!("make a session: {e}"))?; |
| 671 | 3× | state.sessions.lock().unwrap().insert(session.clone(), (t.name, t.expires_ms)); |
| 672 | 3× | Ok(session) |
| 673 | 4× | } |
| 674 | |
|
| 675 | 3× | fn random_token() -> std::io::Result<String> { |
| 676 | 3× | let mut b = [0u8; 32]; |
| 677 | 3× | std::fs::File::open("/dev/urandom")?.read_exact(&mut b)?; |
| 678 | 3× | Ok(hex::encode(b)) |
| 679 | 3× | } |
| 680 | |
|
| 681 | 66× | fn read_chunk(state: &State, hash: &str) -> Option<Vec<u8>> { |
| 682 | 66× | if let Ok(b) = std::fs::read(cache_path(&state.root.join("chunks"), hash)) { |
| 683 | 28× | return Some(b); |
| 684 | 38× | } |
| 685 | 38× | let (path, at, len) = state.located.lock().unwrap().get(hash).cloned()?; |
| 686 | 38× | let mut f = std::fs::File::open(path).ok()?; |
| 687 | 38× | f.seek(SeekFrom::Start(at)).ok()?; |
| 688 | 38× | let mut b = vec![0; len as usize]; |
| 689 | 38× | f.read_exact(&mut b).ok()?; |
| 690 | 38× | Some(b) |
| 691 | 66× | } |
| 692 | |
|
| 693 | |
|
| 694 | 68× | fn throttle(state: &State, n: usize) { |
| 695 | 68× | let wait = { |
| 696 | 68× | let mut b = state.bucket.lock().unwrap(); |
| 697 | 68× | let now = Instant::now(); |
| 698 | 68× | b.0 = (b.0 + now.duration_since(b.1).as_secs_f64() * state.rate).min(state.rate); |
| 699 | 68× | b.1 = now; |
| 700 | 68× | b.0 -= n as f64; |
| 701 | 68× | if b.0 < 0.0 { |
| 702 | 1× | -b.0 / state.rate |
| 703 | | } else { |
| 704 | 67× | 0.0 |
| 705 | | } |
| 706 | | }; |
| 707 | 68× | if wait > 0.0 { |
| 708 | 1× | std::thread::sleep(Duration::from_secs_f64(wait)); |
| 709 | 67× | } |
| 710 | 68× | } |
| 711 | |
|
| 712 | | #[cfg(test)] |
| 713 | | mod tests { |
| 714 | | use super::*; |
| 715 | | use oxy_core::identity::SecretKey; |
| 716 | |
|
| 717 | | |
| 718 | | |
| 719 | | |
| 720 | | |
| 721 | | struct FakeHub { |
| 722 | | url: String, |
| 723 | | chunks_served: Arc<AtomicUsize>, |
| 724 | | runners: Arc<Mutex<Vec<(String, SwarmPeer)>>>, |
| 725 | | } |
| 726 | |
|
| 727 | 3× | fn fake_hub(m: Manifest, image: Vec<u8>, site: &str) -> FakeHub { |
| 728 | 3× | slow_hub(m, image, site, Duration::ZERO) |
| 729 | 3× | } |
| 730 | |
|
| 731 | | |
| 732 | 4× | fn slow_hub(m: Manifest, image: Vec<u8>, site: &str, delay: Duration) -> FakeHub { |
| 733 | 4× | let sk = SecretKey::generate(); |
| 734 | 4× | let id = sk.identity("hub", "hub@example", 1); |
| 735 | 4× | let key = id.hash(); |
| 736 | 4× | let mut chunks = HashMap::new(); |
| 737 | 4× | let mut at = 0usize; |
| 738 | 152× | for c in &m.chunks { |
| 739 | 152× | chunks.insert(c.hash().to_string(), image[at..at + c.len()].to_vec()); |
| 740 | 152× | at += c.len(); |
| 741 | 152× | } |
| 742 | 4× | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); |
| 743 | 4× | let url = format!("http://{}", listener.local_addr().unwrap()); |
| 744 | 4× | let served = Arc::new(AtomicUsize::new(0)); |
| 745 | 4× | let runners: Arc<Mutex<Vec<(String, SwarmPeer)>>> = Arc::new(Mutex::new(Vec::new())); |
| 746 | 4× | let (count, known, site) = (served.clone(), runners.clone(), site.to_string()); |
| 747 | 4× | let shared = Arc::new((m, chunks, id, key, sk)); |
| 748 | 4× | std::thread::spawn(move || { |
| 749 | 60× | for conn in listener.incoming() { |
| 750 | 60× | let Ok(conn) = conn else { continue }; |
| 751 | 60× | let (shared, count, known, site) = (shared.clone(), count.clone(), known.clone(), site.clone()); |
| 752 | 60× | std::thread::spawn(move || { |
| 753 | 60× | let (m, chunks, id, key, sk) = &*shared; |
| 754 | 60× | let mut out = conn.try_clone().unwrap(); |
| 755 | 60× | let mut r = BufReader::new(conn); |
| 756 | | loop { |
| 757 | 209× | let mut line = String::new(); |
| 758 | 209× | if r.read_line(&mut line).unwrap_or(0) == 0 { |
| 759 | 51× | return; |
| 760 | 158× | } |
| 761 | 158× | let path = line.split_whitespace().nth(1).unwrap_or("").to_string(); |
| 762 | 158× | let (mut len, mut token) = (0, String::new()); |
| 763 | | loop { |
| 764 | 907× | let mut h = String::new(); |
| 765 | 907× | r.read_line(&mut h).unwrap(); |
| 766 | 907× | let h = h.trim_end().to_string(); |
| 767 | 907× | if h.is_empty() { |
| 768 | 158× | break; |
| 769 | 749× | } |
| 770 | 749× | let lower = h.to_ascii_lowercase(); |
| 771 | 749× | if let Some(v) = lower.strip_prefix("content-length:") { |
| 772 | 2× | len = v.trim().parse().unwrap(); |
| 773 | 747× | } |
| 774 | 749× | if let Some(v) = h.strip_prefix("Authorization: Bearer ") { |
| 775 | 131× | token = v.to_string(); |
| 776 | 618× | } |
| 777 | | } |
| 778 | 158× | let mut body = vec![0; len]; |
| 779 | 158× | r.read_exact(&mut body).unwrap(); |
| 780 | 158× | let (code, reply): (u16, Vec<u8>) = if path == "/.well-known/oxyhub-key" { |
| 781 | 9× | (200, id.encode()) |
| 782 | 149× | } else if path == format!("/api/v1/flow/images/{}", m.hash) { |
| 783 | 5× | (200, serde_json::to_vec(m).unwrap()) |
| 784 | 144× | } else if let Some(c) = path.strip_prefix("/api/v1/flow/images/chunks/") { |
| 785 | 133× | count.fetch_add(1, Ordering::Relaxed); |
| 786 | 133× | std::thread::sleep(delay); |
| 787 | 133× | (200, chunks[c].clone()) |
| 788 | 11× | } else if path == "/api/v1/flow/swarm/announce" { |
| 789 | 11× | let runners = known.lock().unwrap(); |
| 790 | 17× | let me = runners.iter().find(|(t, _)| *t == token).map(|(_, p)| p.clone()).unwrap(); |
| 791 | 11× | let ticket = Ticket { |
| 792 | 11× | hub: key.to_hex(), |
| 793 | 11× | runner: me.runner, |
| 794 | 11× | name: me.name.clone(), |
| 795 | 11× | site: site.clone(), |
| 796 | 11× | expires_ms: now_ms() + 60_000, |
| 797 | 11× | }; |
| 798 | 20× | let peers: Vec<SwarmPeer> = runners.iter().filter(|(t, _)| *t != token).map(|(_, p)| p.clone()).collect(); |
| 799 | 11× | let a = flow_core::api::Announced { ticket: ticket.seal(key, sk), peers, interval_s: 5 }; |
| 800 | 11× | (200, serde_json::to_vec(&a).unwrap()) |
| 801 | | } else { |
| 802 | 0× | (404, b"no".to_vec()) |
| 803 | | }; |
| 804 | 158× | let head = format!("HTTP/1.1 {code} X\r\nContent-Length: {}\r\n\r\n", reply.len()); |
| 805 | 158× | if out.write_all(head.as_bytes()).and_then(|_| out.write_all(&reply)).is_err() { |
| 806 | 9× | return; |
| 807 | 149× | } |
| 808 | | } |
| 809 | 60× | }); |
| 810 | | } |
| 811 | 4× | }); |
| 812 | 4× | FakeHub { url, chunks_served: served, runners } |
| 813 | 4× | } |
| 814 | |
|
| 815 | 8× | fn free_port() -> u16 { |
| 816 | 8× | TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port() |
| 817 | 8× | } |
| 818 | |
|
| 819 | | |
| 820 | | |
| 821 | 8× | fn runner(dir: &Path, hub: &FakeHub, id: i64, token: &str, port: u16, site: &str) -> (Home, Swarm) { |
| 822 | 8× | let toml = format!( |
| 823 | | "hub = \"{}\"\ntoken = \"{token}\"\nname = \"r{id}\"\n[swarm]\nlisten = \"127.0.0.1:{port}\"\nsite = \"{site}\"\n", |
| 824 | | hub.url |
| 825 | | ); |
| 826 | 8× | std::fs::write(dir.join(crate::home::RUNNER_TOML), toml).unwrap(); |
| 827 | 8× | let home = Home::locate(Some(dir)).unwrap(); |
| 828 | 8× | let cfg = home.config.runner.clone().unwrap(); |
| 829 | 8× | let swarm = Swarm::start(&home, &cfg).unwrap().unwrap(); |
| 830 | 8× | hub.runners.lock().unwrap().push(( |
| 831 | 8× | token.into(), |
| 832 | 8× | SwarmPeer { |
| 833 | 8× | runner: id, |
| 834 | 8× | name: format!("r{id}"), |
| 835 | 8× | addr: format!("127.0.0.1:{port}"), |
| 836 | 8× | cert: swarm.addr.cert.clone(), |
| 837 | 8× | complete: true, |
| 838 | 8× | }, |
| 839 | 8× | )); |
| 840 | 8× | (home, swarm) |
| 841 | 8× | } |
| 842 | |
|
| 843 | 4× | fn image() -> Vec<u8> { |
| 844 | | |
| 845 | 4× | let mut x = 0x9e3779b97f4a7c15u64; |
| 846 | 4× | let mut b: Vec<u8> = (0..3 << 20) |
| 847 | 12582912× | .map(|_| { |
| 848 | 12582912× | x ^= x << 13; |
| 849 | 12582912× | x ^= x >> 7; |
| 850 | 12582912× | x ^= x << 17; |
| 851 | 12582912× | x as u8 |
| 852 | 12582912× | }) |
| 853 | 4× | .collect(); |
| 854 | 4× | b[1 << 20..(1 << 20) + (300 << 10)].fill(0); |
| 855 | 4× | b |
| 856 | 4× | } |
| 857 | |
|
| 858 | | #[test] |
| 859 | 1× | fn a_runner_fetches_an_image_from_its_peer_and_not_the_hub() { |
| 860 | 1× | let bytes = image(); |
| 861 | 38× | let m = jet::images::scan(&bytes[..], Some("x86_64".into()), |_, _| Ok(())).unwrap(); |
| 862 | 1× | let hub = fake_hub(m.clone(), bytes.clone(), "lab"); |
| 863 | 1× | let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 864 | | |
| 865 | | |
| 866 | 1× | std::fs::create_dir_all(a_dir.path().join("cas")).unwrap(); |
| 867 | 1× | std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap(); |
| 868 | 1× | let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab"); |
| 869 | 1× | let deadline = Instant::now() + Duration::from_secs(20); |
| 870 | 4× | while !a.state.offers.lock().unwrap().contains_key(&m.hash) { |
| 871 | 3× | assert!(Instant::now() < deadline, "runner A never offered its image"); |
| 872 | 3× | std::thread::sleep(Duration::from_millis(20)); |
| 873 | | } |
| 874 | 1× | let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab"); |
| 875 | 1× | let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap(); |
| 876 | 1× | assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes, "the image came back whole"); |
| 877 | 1× | assert_eq!(moved.peer_bytes, moved.bytes, "every chunk came from the peer"); |
| 878 | 1× | assert_eq!(hub.chunks_served.load(Ordering::Relaxed), 0, "the hub sent no chunks"); |
| 879 | 1× | assert_eq!(moved.chunks, m.needed().len()); |
| 880 | | |
| 881 | 1× | assert!(matches!(b.state.offers.lock().unwrap().get(&m.hash), Some(Offer { bits: None, .. }))); |
| 882 | 1× | } |
| 883 | |
|
| 884 | | #[test] |
| 885 | 1× | fn runners_starting_together_share_what_each_has_fetched() { |
| 886 | 1× | let bytes = image(); |
| 887 | 38× | let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap(); |
| 888 | 1× | let needed = m.needed().len(); |
| 889 | | |
| 890 | 1× | let hub = slow_hub(m.clone(), bytes.clone(), "lab", Duration::from_millis(400)); |
| 891 | 1× | let (b_dir, c_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 892 | 1× | let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab"); |
| 893 | 1× | let (c_home, c) = runner(c_dir.path(), &hub, 3, "tc", free_port(), "lab"); |
| 894 | 1× | let (mb, mc) = std::thread::scope(|s| { |
| 895 | 1× | let fb = s.spawn(|| b.fetch(&b_home, &m.hash).unwrap().unwrap()); |
| 896 | 1× | let fc = s.spawn(|| c.fetch(&c_home, &m.hash).unwrap().unwrap()); |
| 897 | 1× | (fb.join().unwrap(), fc.join().unwrap()) |
| 898 | 1× | }); |
| 899 | 1× | assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes); |
| 900 | 1× | assert_eq!(std::fs::read(c_home.rootfs(&m.hash)).unwrap(), bytes); |
| 901 | 1× | let served = hub.chunks_served.load(Ordering::Relaxed); |
| 902 | 1× | assert!(served < 2 * needed, "the hub sent {served} chunks for two runners needing {needed} each"); |
| 903 | 1× | assert!(mb.peer_bytes + mc.peer_bytes > 0, "they shared"); |
| 904 | 1× | } |
| 905 | |
|
| 906 | | #[test] |
| 907 | 1× | fn without_peers_the_hub_sends_everything_and_a_ticket_from_another_site_is_refused() { |
| 908 | 1× | let bytes = image(); |
| 909 | 38× | let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap(); |
| 910 | 1× | let hub = fake_hub(m.clone(), bytes.clone(), "lab"); |
| 911 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 912 | 1× | let (home, s) = runner(dir.path(), &hub, 1, "t1", free_port(), "lab"); |
| 913 | 1× | let moved = s.fetch(&home, &m.hash).unwrap().unwrap(); |
| 914 | 1× | assert_eq!(moved.peer_bytes, 0); |
| 915 | 1× | assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len()); |
| 916 | 1× | assert_eq!(std::fs::read(home.rootfs(&m.hash)).unwrap(), bytes); |
| 917 | |
|
| 918 | | |
| 919 | 1× | let other = tempfile::tempdir().unwrap(); |
| 920 | 1× | let port = free_port(); |
| 921 | 1× | let (_h, o) = runner(other.path(), &hub, 3, "t3", port, "elsewhere"); |
| 922 | 1× | let ticket = s.state.client.announce(&m.hash, "fetching").unwrap().ticket; |
| 923 | 1× | let err = hello(&format!("127.0.0.1:{port}"), &o.addr.cert, &ticket).unwrap_err(); |
| 924 | 1× | assert!(format!("{err:#}").contains("HTTP 403"), "{err:#}"); |
| 925 | | |
| 926 | 1× | let url = format!("https://127.0.0.1:{port}/swarm/v1/chunks/{}", m.chunks[0].hash()); |
| 927 | 1× | assert!(matches!(peer_agent(&o.addr.cert).unwrap().get(&url).call(), Err(ureq::Error::Status(401, _)))); |
| 928 | | |
| 929 | 1× | assert!(ureq::get(&url.replacen("https", "http", 1)).call().is_err()); |
| 930 | 1× | } |
| 931 | |
|
| 932 | | #[test] |
| 933 | 1× | fn a_peer_with_another_certificate_is_not_trusted() { |
| 934 | 1× | let bytes = image(); |
| 935 | 38× | let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap(); |
| 936 | 1× | let hub = fake_hub(m.clone(), bytes.clone(), "lab"); |
| 937 | 1× | let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 938 | 1× | std::fs::create_dir_all(a_dir.path().join("cas")).unwrap(); |
| 939 | 1× | std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap(); |
| 940 | 1× | let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab"); |
| 941 | 4× | while !a.state.offers.lock().unwrap().contains_key(&m.hash) { |
| 942 | 3× | std::thread::sleep(Duration::from_millis(20)); |
| 943 | 3× | } |
| 944 | 1× | let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab"); |
| 945 | | |
| 946 | | |
| 947 | 1× | hub.runners.lock().unwrap()[0].1.cert = b.addr.cert.clone(); |
| 948 | 1× | let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap(); |
| 949 | 1× | assert_eq!(moved.peer_bytes, 0, "nothing was taken from the stranger"); |
| 950 | 1× | assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len()); |
| 951 | 1× | assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes); |
| 952 | | |
| 953 | 1× | let mut cfg = b_home.config.runner.clone().unwrap(); |
| 954 | 1× | cfg.swarm.as_mut().unwrap().listen = "127.0.0.1:0".into(); |
| 955 | 1× | let again = Swarm::start(&b_home, &cfg).unwrap().unwrap(); |
| 956 | 1× | assert_eq!(again.addr.cert, b.addr.cert); |
| 957 | 1× | } |
| 958 | |
|
| 959 | | #[test] |
| 960 | 1× | fn the_upload_rate_holds_back_a_burst() { |
| 961 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 962 | 1× | let (cert, key) = certificate(dir.path()).unwrap(); |
| 963 | 1× | let state = State { |
| 964 | 1× | tls: server_tls(cert, key).unwrap(), |
| 965 | 1× | root: PathBuf::new(), |
| 966 | 1× | site: String::new(), |
| 967 | 1× | client: Client::new("http://127.0.0.1:1", "t", None).unwrap(), |
| 968 | 1× | hub: Mutex::new(None), |
| 969 | 1× | offers: Mutex::new(HashMap::new()), |
| 970 | 1× | located: Mutex::new(HashMap::new()), |
| 971 | 1× | sessions: Mutex::new(HashMap::new()), |
| 972 | 1× | conns: AtomicUsize::new(0), |
| 973 | 1× | bucket: Mutex::new((1_000_000.0, Instant::now())), |
| 974 | 1× | rate: 1_000_000.0, |
| 975 | 1× | }; |
| 976 | 1× | let start = Instant::now(); |
| 977 | 1× | throttle(&state, 1_000_000); |
| 978 | 1× | assert!(start.elapsed() < Duration::from_millis(100), "the first second's worth goes at once"); |
| 979 | 1× | throttle(&state, 300_000); |
| 980 | 1× | assert!(start.elapsed() >= Duration::from_millis(250), "then it waits for the rate"); |
| 981 | 1× | } |
| 982 | | } |