OXYGENHUB

Coverage / crates/flow/src/swarm.rs 91.4%

722 of 790 lines run, 77 of 139 functions · run #219 on main · run missed

1
//! The swarm (oxygenflow#47, docs/jetstream.md §8.1): a runner with a
2
//! `[runner.swarm]` section serves the image chunks it holds to its peers,
3
//! and fetches an image it lacks from its peers and the hub together.
4
//!
5
//! The server is a small HTTPS endpoint on the swarm address: a peer
6
//! shows the ticket its hub signed and gets a session, then asks which
7
//! chunks of an image this runner has and for chunks by hash. A chunk is
8
//! served from the chunk cache, or read out of a whole image by its
9
//! manifest. Every chunk is checked by the side that asked for it, so a
10
//! peer can only fail to help.
11
//!
12
//! Each runner makes its own TLS certificate on first use and reports its
13
//! BLAKE3 with its swarm address; the hub hands that on with the peer, and
14
//! a fetcher talks only to a peer that shows that certificate and holds
15
//! its key. Nobody on the network reads the chunks, tickets or sessions.
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
/// Connections served at once; more are told 503 and come back shortly.
37
const MAX_CONNS: usize = 16;
38
/// Connections past this are closed without a word.
39
const MAX_OPEN: usize = 4 * MAX_CONNS;
40
/// Requests in flight to one peer, and to the hub.
41
const PEER_WORKERS: usize = 4;
42
const HUB_WORKERS: usize = 8;
43
/// How often a fetcher asks its peers what they have.
44
const REFRESH: Duration = Duration::from_millis(500);
45
/// Hub failures a fetch survives before it gives up.
46
const HUB_FAILURES: usize = 5;
47
/// The largest request body the server reads: a ticket.
48
const MAX_BODY: usize = 64 * 1024;
49
/// Names the part files of chunks two sources fetch at once apart.
50
static PART: AtomicU64 = AtomicU64::new(0);
51
52141×
fn now_ms() -> u64 {
53141×
    SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
54141×
}
55
56
/// An image this runner offers: which of its needed chunks are here
57
/// (`None`: all of them, the image is whole) and which it is fetching now.
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
    /// The hub's identity, which signs tickets; fetched when first needed.
68
    hub: Mutex<Option<Identity>>,
69
    offers: Mutex<HashMap<String, Offer>>,
70
    /// Where a chunk lies in a whole image: the image, offset and length.
71
    located: Mutex<HashMap<String, (PathBuf, u64, u32)>>,
72
    /// Sessions: token to (peer name, expiry).
73
    sessions: Mutex<HashMap<String, (String, u64)>>,
74
    conns: AtomicUsize,
75
    /// The upload allowance: bytes in hand (negative: owed) and when it
76
    /// was last topped up.
77
    bucket: Mutex<(f64, Instant)>,
78
    rate: f64,
79
    tls: Arc<rustls::ServerConfig>,
80
}
81
82
/// This runner's place in the swarm.
83
pub struct Swarm {
84
    state: Arc<State>,
85
    pub addr: SwarmAddr,
86
}
87
88
impl Swarm {
89
    /// Start serving peers when the runner is configured for it.
9012×
    pub fn start(home: &Home, cfg: &RunnerConfig) -> Result<Option<Swarm>> {
9112×
        let Some(sc) = &cfg.swarm else { return Ok(None) };
9212×
        let listener = TcpListener::bind(&sc.listen).with_context(|| format!("swarm: listen on {}", sc.listen))?;
9312×
        let client = Client::new(&cfg.hub, &cfg.token, cfg.ca.as_deref())?;
9412×
        let hub = client.hub_identity().map_err(|e| eprintln!("swarm: the hub's key: {e:#}; trying again when a peer comes")).ok();
9512×
        let rate = sc.upload_mib_s.unwrap_or(50).max(1) as f64 * (1 << 20) as f64;
9612×
        let (cert, key) = certificate(&home.root)?;
9712×
        let fingerprint = blake3::hash(&cert).to_hex().to_string();
9812×
        let tls = server_tls(cert, key)?;
9912×
        let state = Arc::new(State {
10012×
            root: home.root.clone(),
10112×
            site: sc.site.clone(),
10212×
            client,
10312×
            hub: Mutex::new(hub),
10412×
            offers: Mutex::new(HashMap::new()),
10512×
            located: Mutex::new(HashMap::new()),
10612×
            sessions: Mutex::new(HashMap::new()),
10712×
            conns: AtomicUsize::new(0),
10812×
            bucket: Mutex::new((rate, Instant::now())),
10912×
            rate,
11012×
            tls,
11112×
        });
112
        // Whole images: offered from their manifests; one without a
113
        // manifest (imported here, not fetched) is scanned for one, in the
114
        // background and one at a time.
11512×
        let mut unscanned = Vec::new();
11612×
        for h in home.images() {
1174×
            match std::fs::read(manifest_path(&home.root, &h)).ok().and_then(|b| serde_json::from_slice::<Manifest>(&b).ok()) {
1181×
                Some(m) if m.hash == h => offer_whole(&state, &m, &home.rootfs(&h)),
1193×
                _ => unscanned.push((h.clone(), home.rootfs(&h), home.image_arch(&h))),
120
            }
121
        }
12212×
        let scanning = state.clone();
12312×
        std::thread::spawn(move || {
12412×
            for (h, path, arch) in unscanned {
125114×
                match jet::images::scan_file(&path, arch, |_, _| Ok(())) {
1263×
                    Ok(m) if m.hash == h => {
1273×
                        save_manifest(&scanning.root, &m).ok();
1283×
                        offer_whole(&scanning, &m, &path);
1293×
                    }
1300×
                    Ok(_) => eprintln!("swarm: {} does not hold the image its name says; not offering it", path.display()),
1310×
                    Err(e) => eprintln!("swarm: scan {}: {e}", path.display()),
132
                }
133
            }
13412×
        });
13512×
        let serving = state.clone();
13612×
        std::thread::spawn(move || accept(listener, serving));
13712×
        let addr = SwarmAddr { addr: sc.advertise.clone().unwrap_or_else(|| sc.listen.clone()), site: sc.site.clone(), cert: fingerprint };
13812×
        eprintln!(
139
            "swarm: serving image chunks on {} over TLS (certificate {}, site {:?}, up to {} MiB/s)",
140
            sc.listen,
14112×
            &addr.cert[..12],
142
            sc.site,
14312×
            sc.upload_mib_s.unwrap_or(50)
144
        );
14512×
        Ok(Some(Swarm { state, addr }))
14612×
    }
147
148
    /// Fetch image `hash` into `home` from the peers and the hub (§8.1):
149
    /// the rarest chunks from peers first, the hub for what no peer has.
150
    /// `Ok(None)` when the hub does not hold it.
1517×
    pub fn fetch(&self, home: &Home, hash: &str) -> Result<Option<Moved>> {
1527×
        let start = Instant::now();
1537×
        let client = &self.state.client;
1547×
        let Some(m) = client.manifest(hash)? else { return Ok(None) };
1557×
        if m.hash != hash {
1560×
            bail!("the hub's manifest for {} names {}", &hash[..12], &m.hash[..12]);
1577×
        }
1587×
        save_manifest(&home.root, &m)?;
1597×
        let needed: Vec<String> = m.needed().into_iter().map(String::from).collect();
1607×
        let cache = chunk_cache(home);
1617×
        let mut have = Bits::new(needed.len());
162266×
        for (i, h) in needed.iter().enumerate() {
163266×
            if cache_path(&cache, h).is_file() {
1640×
                have.set(i);
165266×
            }
166
        }
1677×
        self.state.offers.lock().unwrap().insert(hash.into(), Offer { bits: Some(have.clone()), fetching: Bits::new(needed.len()) });
1687×
        let announced = client
1697×
            .announce(hash, "fetching")
1707×
            .map_err(|e| eprintln!("swarm: announce {}: {e:#}; fetching from the hub alone", &hash[..12]))
1717×
            .ok();
1727×
        let seed = now_ms() ^ (std::process::id() as u64) << 32;
1737×
        let picker = Mutex::new(Picker::new(needed.len(), have, seed));
1747×
        let peers: Mutex<Vec<Arc<Peer>>> = Mutex::new(Vec::new());
1757×
        if let Some(a) = &announced {
1767×
            self.join(&a.peers, &a.ticket, hash, needed.len(), &peers, &picker);
1777×
        }
1787×
        let (bytes, peer_bytes, got) = (AtomicU64::new(0), AtomicU64::new(0), AtomicUsize::new(0));
1797×
        let (failed, hub_failures) = (Mutex::new(None::<anyhow::Error>), AtomicUsize::new(0));
180
        {
1817×
            let (picker, peers, needed, cache) = (&picker, &peers, &needed, &cache);
1827×
            let (bytes, peer_bytes, got, failed, hub_failures) = (&bytes, &peer_bytes, &got, &failed, &hub_failures);
1837×
            let (state, m) = (&self.state, &m);
1847×
            let worker = move |src: Source| loop {
1851231×
                if failed.lock().unwrap().is_some() {
1860×
                    break;
1871231×
                }
1881231×
                let peer = match src {
189765×
                    Source::Peer(p) => Some(peers.lock().unwrap()[p].clone()),
190466×
                    Source::Hub => None,
191
                };
1921231×
                if peer.as_ref().is_some_and(|p| !p.alive.load(Ordering::Relaxed)) {
1930×
                    break;
1941231×
                }
195
                // Taken apart from the match, so the lock is not held
196
                // while waiting.
1971231×
                let pick = picker.lock().unwrap().pick(src);
1981231×
                let i = match pick {
19960×
                    Pick::Done => break,
200
                    Pick::Wait => {
201943×
                        std::thread::sleep(Duration::from_millis(20));
202943×
                        continue;
203
                    }
204228×
                    Pick::Chunk(i) => i,
205
                };
206228×
                if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) {
207228×
                    o.fetching.set(i);
208228×
                }
209228×
                let h = &needed[i];
210228×
                let fetched = match &peer {
211100×
                    Some(p) => self.peer_chunk(p, h),
212128×
                    None => client.chunk(h),
213
                };
214228×
                let kept = fetched.and_then(|b| {
215228×
                    if blake3::hash(&b).to_hex().as_str() != h {
2160×
                        bail!("chunk {} came back with other bytes", &h[..12]);
217228×
                    }
218228×
                    let p = cache_path(cache, h);
219228×
                    std::fs::create_dir_all(p.parent().expect("a chunk directory"))?;
220228×
                    let tmp = p.with_extension(format!("part{}", PART.fetch_add(1, Ordering::Relaxed)));
221228×
                    std::fs::write(&tmp, &b)?;
222228×
                    std::fs::rename(&tmp, &p)?;
223228×
                    Ok(b.len() as u64)
224228×
                });
225228×
                match kept {
226228×
                    Ok(n) => {
227228×
                        if let Some(Offer { bits: Some(b), fetching }) = state.offers.lock().unwrap().get_mut(&m.hash) {
228228×
                            b.set(i);
229228×
                            fetching.clear(i);
230228×
                        }
231228×
                        picker.lock().unwrap().got(i);
232228×
                        bytes.fetch_add(n, Ordering::Relaxed);
233228×
                        got.fetch_add(1, Ordering::Relaxed);
234228×
                        if peer.is_some() {
235100×
                            peer_bytes.fetch_add(n, Ordering::Relaxed);
236128×
                        }
237
                    }
2380×
                    Err(e) => {
2390×
                        picker.lock().unwrap().failed(i);
2400×
                        if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) {
2410×
                            o.fetching.clear(i);
2420×
                        }
2430×
                        match (&peer, src) {
244
                            // A busy peer is asked again shortly.
2450×
                            (Some(_), _) if e.is::<Busy>() => std::thread::sleep(Duration::from_millis(200)),
2460×
                            (Some(p), Source::Peer(idx)) => {
2470×
                                eprintln!("swarm: peer {} failed ({e:#}); carrying on without it", p.name);
2480×
                                p.alive.store(false, Ordering::Relaxed);
2490×
                                picker.lock().unwrap().drop_peer(idx);
2500×
                                break;
251
                            }
252
                            _ => {
2530×
                                if hub_failures.fetch_add(1, Ordering::Relaxed) + 1 >= HUB_FAILURES {
2540×
                                    *failed.lock().unwrap() = Some(e);
2550×
                                    break;
2560×
                                }
2570×
                                std::thread::sleep(Duration::from_millis(500));
258
                            }
259
                        }
260
                    }
261
                }
26260×
            };
263
            // An image only runners hold (jetstream.md §8.1.1): the hub
264
            // has its manifest but none of its chunks, so only peers are asked.
2657×
            let hub_seeds = announced.as_ref().is_none_or(|a| a.seed != Some(false));
2667×
            std::thread::scope(|s| {
2677×
                if hub_seeds {
2685×
                    for _ in 0..HUB_WORKERS {
26940×
                        s.spawn(move || worker(Source::Hub));
270
                    }
2712×
                }
2727×
                let mut spawned = 0;
2738×
                let mut spawn_new = || {
2748×
                    let n = peers.lock().unwrap().len();
2758×
                    for p in spawned..n {
2765×
                        for _ in 0..PEER_WORKERS {
27720×
                            s.spawn(move || worker(Source::Peer(p)));
278
                        }
279
                    }
2808×
                    spawned = n;
2818×
                };
2827×
                spawn_new();
283
                // Keep the peers' maps fresh, and find new peers while
284
                // fetching, until every chunk is here.
2857×
                let mut asked_hub = Instant::now();
2867×
                let interval = Duration::from_secs(announced.as_ref().map(|a| a.interval_s).unwrap_or(30).max(5));
2877×
                let mut last = Instant::now();
2887×
                let mut peerless = Instant::now();
289144×
                while picker.lock().unwrap().missing() > 0 && failed.lock().unwrap().is_none() {
290137×
                    std::thread::sleep(Duration::from_millis(100));
291137×
                    if !hub_seeds {
292100×
                        if peers.lock().unwrap().iter().any(|p| p.alive.load(Ordering::Relaxed)) {
2931×
                            peerless = Instant::now();
29499×
                        } else if peerless.elapsed() >= interval * 2 {
2951×
                            *failed.lock().unwrap() = Some(anyhow::anyhow!(
2961×
                                "image {} is held only by runners, and none in this site could serve it for {} s; send it to the hub with `flow rootfs push`",
2971×
                                &hash[..12],
2981×
                                (interval * 2).as_secs()
2991×
                            ));
30098×
                        }
30137×
                    }
302137×
                    if last.elapsed() < REFRESH {
303112×
                        continue;
30425×
                    }
30525×
                    last = Instant::now();
30625×
                    let now: Vec<(usize, Arc<Peer>)> =
30725×
                        peers.lock().unwrap().iter().cloned().enumerate().filter(|(_, p)| p.alive.load(Ordering::Relaxed)).collect();
30825×
                    for (idx, p) in now {
3096×
                        match self.peer_have(&p, hash, needed.len()) {
3106×
                            Ok((bits, fetching)) => picker.lock().unwrap().peer_fetching(idx, bits, fetching),
3110×
                            Err(e) if e.is::<Busy>() => {}
3120×
                            Err(_) => {
3130×
                                p.alive.store(false, Ordering::Relaxed);
3140×
                                picker.lock().unwrap().drop_peer(idx);
3150×
                            }
316
                        }
317
                    }
31825×
                    if announced.is_some() && asked_hub.elapsed() >= interval {
3191×
                        asked_hub = Instant::now();
3201×
                        if let Ok(a) = client.announce(hash, "fetching") {
3211×
                            self.join(&a.peers, &a.ticket, hash, needed.len(), peers, picker);
3221×
                            spawn_new();
3231×
                        }
32424×
                    }
325
                }
3267×
            });
327
        }
3287×
        if let Some(e) = failed.into_inner().unwrap() {
3291×
            client.announce(hash, "stopped").ok();
3301×
            return Err(e);
3316×
        }
3326×
        write_image(home, &m)?;
3336×
        offer_whole(&self.state, &m, &home.rootfs(hash));
3346×
        client.announce(hash, "done").ok();
3356×
        Ok(Some(Moved {
3366×
            chunks: got.into_inner(),
3376×
            bytes: bytes.into_inner(),
3386×
            peer_bytes: peer_bytes.into_inner(),
3396×
            elapsed: start.elapsed(),
3406×
        }))
3417×
    }
342
343
    /// Admit this runner to the peers it does not know yet: a session from
344
    /// each, and what each has.
3458×
    fn join(&self, found: &[SwarmPeer], ticket: &str, hash: &str, len: usize, peers: &Mutex<Vec<Arc<Peer>>>, picker: &Mutex<Picker>) {
3468×
        for sp in found {
3476×
            if peers.lock().unwrap().iter().any(|p| p.runner == sp.runner) {
3480×
                continue;
3496×
            }
3506×
            if !is_hash(&sp.cert) {
3510×
                eprintln!("swarm: peer {} gave no certificate (an older flow); not asking it", sp.name);
3520×
                continue;
3536×
            }
3546×
            let joined = hello(&sp.addr, &sp.cert, ticket).and_then(|(agent, session)| {
3555×
                let p = Peer {
3565×
                    runner: sp.runner,
3575×
                    name: sp.name.clone(),
3585×
                    base: format!("https://{}/swarm/v1", sp.addr),
3595×
                    agent,
3605×
                    session,
3615×
                    alive: AtomicBool::new(true),
3625×
                };
3635×
                let (bits, fetching) = self.peer_have(&p, hash, len)?;
3645×
                Ok((p, bits, fetching))
3655×
            });
3666×
            match joined {
3675×
                Ok((p, bits, fetching)) => {
3685×
                    let mut list = peers.lock().unwrap();
3695×
                    picker.lock().unwrap().peer_fetching(list.len(), bits, fetching);
3705×
                    list.push(Arc::new(p));
3715×
                }
3721×
                Err(e) => eprintln!("swarm: peer {} ({}): {e:#}", sp.name, sp.addr),
373
            }
374
        }
3758×
    }
376
377
    /// What a peer has of image `hash`, and what it is fetching now:
378
    /// nothing yet when it has not begun (the tracker may name it a moment
379
    /// before it starts).
38011×
    fn peer_have(&self, p: &Peer, hash: &str, len: usize) -> Result<(Bits, Bits)> {
38111×
        let url = format!("{}/images/{hash}/have", p.base);
38211×
        let r = match p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call() {
38311×
            Ok(r) => r,
3840×
            Err(ureq::Error::Status(404, _)) => return Ok((Bits::new(len), Bits::new(len))),
3850×
            Err(e) => return Err(peer_error(&url, e)),
386
        };
38711×
        let mut body = Vec::new();
38811×
        r.into_reader().take(len as u64 / 4 + 64).read_to_end(&mut body)?;
38911×
        if body == b"complete" {
3902×
            return Ok((Bits::full(len), Bits::new(len)));
3919×
        }
3929×
        let (have, fetching) = body.split_at(len.div_ceil(8).min(body.len()));
3939×
        Ok((Bits::from_bytes(len, have), Bits::from_bytes(len, fetching)))
39411×
    }
395
396100×
    fn peer_chunk(&self, p: &Peer, hash: &str) -> Result<Vec<u8>> {
397100×
        let url = format!("{}/chunks/{hash}", p.base);
398100×
        let r = p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call().map_err(|e| peer_error(&url, e))?;
399100×
        let mut out = Vec::new();
400100×
        r.into_reader().take(oxy_core::chunk::MAX_SIZE as u64 + 1).read_to_end(&mut out)?;
401100×
        Ok(out)
402100×
    }
403
}
404
405
/// A peer this fetch asks for chunks, over a connection that trusts only
406
/// its certificate.
407
struct Peer {
408
    runner: i64,
409
    name: String,
410
    base: String,
411
    agent: ureq::Agent,
412
    session: String,
413
    alive: AtomicBool,
414
}
415
416
/// A peer at its connection limit: ask it again shortly.
417
#[derive(Debug)]
418
struct Busy;
419
420
impl std::fmt::Display for Busy {
4210×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4220×
        f.write_str("the peer is busy")
4230×
    }
424
}
425
426
impl std::error::Error for Busy {}
427
4282×
fn peer_error(url: &str, e: ureq::Error) -> anyhow::Error {
4292×
    match e {
4300×
        ureq::Error::Status(503, _) => anyhow::Error::new(Busy),
4311×
        ureq::Error::Status(code, r) => anyhow::anyhow!("{url}: HTTP {code}: {}", r.into_string().unwrap_or_default().trim()),
4321×
        e => anyhow::anyhow!("{url}: {e}"),
433
    }
4342×
}
435
436
/// Show `ticket` to the peer at `addr`, trusting only the certificate
437
/// `cert`: a connection to it and a session.
4387×
fn hello(addr: &str, cert: &str, ticket: &str) -> Result<(ureq::Agent, String)> {
4397×
    let agent = peer_agent(cert)?;
4407×
    let url = format!("https://{addr}/swarm/v1/hello");
4417×
    let r: serde_json::Value = agent.post(&url).send_string(ticket).map_err(|e| peer_error(&url, e))?.into_json()?;
4425×
    let session = r["session"].as_str().map(String::from).context("the peer gave no session")?;
4435×
    Ok((agent, session))
4447×
}
445
44621×
fn provider() -> Arc<CryptoProvider> {
44721×
    Arc::new(rustls::crypto::ring::default_provider())
44821×
}
449
450
/// A client that trusts one certificate, by its BLAKE3, whatever name
451
/// the peer goes by: the one the hub said this peer has.
4528×
fn peer_agent(cert: &str) -> Result<ureq::Agent> {
4538×
    let provider = provider();
4548×
    let cfg = rustls::ClientConfig::builder_with_provider(provider.clone())
4558×
        .with_safe_default_protocol_versions()?
4568×
        .dangerous()
4578×
        .with_custom_certificate_verifier(Arc::new(Pinned { cert: cert.to_string(), provider }))
4588×
        .with_no_client_auth();
4598×
    Ok(ureq::AgentBuilder::new().tls_config(Arc::new(cfg)).timeout_connect(Duration::from_secs(2)).timeout(Duration::from_secs(20)).build())
4608×
}
461
462
/// Trusts the certificate with this BLAKE3 and no other. The handshake's
463
/// signature is still checked against it, so only the holder of its key
464
/// passes.
465
#[derive(Debug)]
466
struct Pinned {
467
    cert: String,
468
    provider: Arc<CryptoProvider>,
469
}
470
471
impl ServerCertVerifier for Pinned {
47210×
    fn verify_server_cert(
47310×
        &self,
47410×
        end_entity: &CertificateDer<'_>,
47510×
        _intermediates: &[CertificateDer<'_>],
47610×
        _name: &ServerName<'_>,
47710×
        _ocsp: &[u8],
47810×
        _now: UnixTime,
47910×
    ) -> std::result::Result<ServerCertVerified, rustls::Error> {
48010×
        if blake3::hash(end_entity.as_ref()).to_hex().as_str() == self.cert {
4819×
            Ok(ServerCertVerified::assertion())
482
        } else {
4831×
            Err(rustls::Error::General("not the peer the hub named: its certificate is another".into()))
484
        }
48510×
    }
486
4870×
    fn verify_tls12_signature(
4880×
        &self,
4890×
        message: &[u8],
4900×
        cert: &CertificateDer<'_>,
4910×
        dss: &DigitallySignedStruct,
4920×
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
4930×
        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
4940×
    }
495
4969×
    fn verify_tls13_signature(
4979×
        &self,
4989×
        message: &[u8],
4999×
        cert: &CertificateDer<'_>,
5009×
        dss: &DigitallySignedStruct,
5019×
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
5029×
        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
5039×
    }
504
50540×
    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
50640×
        self.provider.signature_verification_algorithms.supported_schemes()
50740×
    }
508
}
509
510
/// The runner's swarm certificate and key (`swarm/cert.der`, and
511
/// `swarm/key.der` readable by the runner alone), made on first use and
512
/// kept, so its fingerprint holds across restarts.
51313×
fn certificate(root: &Path) -> Result<(Vec<u8>, Vec<u8>)> {
51413×
    let dir = root.join("swarm");
51513×
    let (cert_path, key_path) = (dir.join("cert.der"), dir.join("key.der"));
51613×
    if let (Ok(c), Ok(k)) = (std::fs::read(&cert_path), std::fs::read(&key_path)) {
5171×
        return Ok((c, k));
51812×
    }
51912×
    std::fs::create_dir_all(&dir)?;
52012×
    let made = rcgen::generate_simple_self_signed(vec!["flow-swarm".to_string()]).context("make the swarm's certificate")?;
52112×
    let (cert, key) = (made.cert.der().to_vec(), made.signing_key.serialize_der());
522
    {
52312×
        let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&key_path)?;
52412×
        f.write_all(&key)?;
525
    }
52612×
    std::fs::write(&cert_path, &cert)?;
52712×
    Ok((cert, key))
52813×
}
529
53013×
fn server_tls(cert: Vec<u8>, key: Vec<u8>) -> Result<Arc<rustls::ServerConfig>> {
53113×
    let cfg = rustls::ServerConfig::builder_with_provider(provider())
53213×
        .with_safe_default_protocol_versions()?
53313×
        .with_no_client_auth()
53413×
        .with_single_cert(vec![CertificateDer::from(cert)], PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)))
53513×
        .context("the swarm's certificate")?;
53613×
    Ok(Arc::new(cfg))
53713×
}
538
539
/// Offer a whole image: every chunk of it can be read out of `path`.
54010×
fn offer_whole(state: &State, m: &Manifest, path: &Path) {
54110×
    let mut located = state.located.lock().unwrap();
54210×
    let mut at = 0u64;
543380×
    for c in &m.chunks {
544380×
        if !c.is_zeros() {
545380×
            located.entry(c.hash().to_string()).or_insert_with(|| (path.to_path_buf(), at, c.1));
5460×
        }
547380×
        at += c.1 as u64;
548
    }
54910×
    state.offers.lock().unwrap().insert(m.hash.clone(), Offer { bits: None, fetching: Bits::new(0) });
55010×
}
551
55212×
fn accept(listener: TcpListener, state: Arc<State>) {
55341×
    for conn in listener.incoming() {
55441×
        let Ok(conn) = conn else { continue };
55541×
        if state.conns.load(Ordering::Relaxed) >= MAX_OPEN {
5560×
            continue;
55741×
        }
55841×
        let state = state.clone();
55941×
        std::thread::spawn(move || {
560
            // Past the limit a peer is still answered, with 503, so it
561
            // comes back rather than giving up on this runner.
56241×
            let busy = state.conns.fetch_add(1, Ordering::Relaxed) >= MAX_CONNS;
56341×
            serve_tls(conn, &state, busy).ok();
56441×
            state.conns.fetch_sub(1, Ordering::Relaxed);
56541×
        });
566
    }
56712×
}
568
56941×
fn serve_tls(conn: TcpStream, state: &State, busy: bool) -> std::io::Result<()> {
57041×
    conn.set_read_timeout(Some(Duration::from_secs(30)))?;
57141×
    conn.set_write_timeout(Some(Duration::from_secs(30)))?;
57241×
    conn.set_nodelay(true)?;
57341×
    let tls = rustls::ServerConnection::new(state.tls.clone()).map_err(std::io::Error::other)?;
57441×
    serve_conn(rustls::StreamOwned::new(tls, conn), state, busy)
57541×
}
576
577118×
fn respond(conn: &mut impl Write, code: u16, kind: &str, body: &[u8]) -> std::io::Result<()> {
578118×
    let reason = match code {
579116×
        200 => "OK",
5800×
        400 => "Bad Request",
5811×
        401 => "Unauthorized",
5821×
        403 => "Forbidden",
5830×
        404 => "Not Found",
5840×
        503 => "Service Unavailable",
5850×
        _ => "Error",
586
    };
587118×
    write!(conn, "HTTP/1.1 {code} {reason}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\n\r\n", body.len())?;
588118×
    conn.write_all(body)?;
589118×
    conn.flush()
590118×
}
591
592
/// One connection: requests one after another while the peer keeps it.
59341×
fn serve_conn<S: Read + Write>(conn: S, state: &State, busy: bool) -> std::io::Result<()> {
59441×
    let mut r = BufReader::new(conn);
595
    loop {
596159×
        let mut line = String::new();
597159×
        if r.read_line(&mut line)? == 0 {
5980×
            return Ok(());
599118×
        }
600118×
        let mut parts = line.split_whitespace();
601118×
        let (method, path) = (parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string());
602118×
        let (mut length, mut auth, mut close) = (0usize, String::new(), false);
603
        loop {
604707×
            let mut h = String::new();
605707×
            if r.read_line(&mut h)? == 0 {
6060×
                return Ok(());
607707×
            }
608707×
            let h = h.trim_end();
609707×
            if h.is_empty() {
610118×
                break;
611589×
            }
612589×
            if let Some((k, v)) = h.split_once(':') {
613589×
                match k.trim().to_ascii_lowercase().as_str() {
614589×
                    "content-length" => length = v.trim().parse().unwrap_or(usize::MAX),
615583×
                    "authorization" => auth = v.trim().to_string(),
616472×
                    "connection" => close = v.trim().eq_ignore_ascii_case("close"),
617472×
                    _ => {}
618
                }
6190×
            }
620
        }
621118×
        if length > MAX_BODY {
6220×
            return respond(r.get_mut(), 400, "text/plain", b"too large");
623118×
        }
624118×
        let mut body = vec![0; length];
625118×
        r.read_exact(&mut body)?;
626118×
        if busy {
6270×
            return respond(r.get_mut(), 503, "text/plain", b"busy: ask again shortly");
628118×
        }
629118×
        let (code, kind, reply) = route(state, &method, &path, &auth, &body);
630118×
        if code == 200 && kind == "application/octet-stream" {
631100×
            throttle(state, reply.len());
632100×
        }
633118×
        respond(r.get_mut(), code, kind, &reply)?;
634118×
        if close {
6350×
            return Ok(());
636118×
        }
637
    }
63841×
}
639
640118×
fn route(state: &State, method: &str, path: &str, auth: &str, body: &[u8]) -> (u16, &'static str, Vec<u8>) {
641118×
    let text = |code, s: &str| (code, "text/plain", s.as_bytes().to_vec());
642118×
    if method == "POST" && path == "/swarm/v1/hello" {
6436×
        return match admit(state, &String::from_utf8_lossy(body)) {
6445×
            Ok(session) => (200, "application/json", serde_json::json!({ "session": session }).to_string().into_bytes()),
6451×
            Err(why) => text(403, &why),
646
        };
647112×
    }
648112×
    let session = auth.strip_prefix("Bearer ").unwrap_or("");
649112×
    let admitted = {
650112×
        let mut sessions = state.sessions.lock().unwrap();
651112×
        let now = now_ms();
652112×
        sessions.retain(|_, (_, until)| *until > now);
653112×
        sessions.contains_key(session)
654
    };
655112×
    if !admitted {
6561×
        return text(401, "show a ticket at /swarm/v1/hello first");
657111×
    }
658111×
    let rest = path.strip_prefix("/swarm/v1/").unwrap_or("");
659111×
    match (method, rest.split('/').collect::<Vec<_>>().as_slice()) {
660111×
        ("GET", ["images", image, "have"]) => match state.offers.lock().unwrap().get(*image) {
6612×
            Some(Offer { bits: None, .. }) => (200, "text/plain", b"complete".to_vec()),
6629×
            Some(Offer { bits: Some(b), fetching }) => (200, "application/x-have", [b.to_bytes(), fetching.to_bytes()].concat()),
6630×
            None => text(404, "this runner does not hold that image"),
664
        },
665100×
        ("GET", ["chunks", chunk]) if is_hash(chunk) => match read_chunk(state, chunk) {
666100×
            Some(b) => (200, "application/octet-stream", b),
6670×
            None => text(404, "no such chunk here"),
668
        },
6690×
        _ => text(404, "not a swarm path"),
670
    }
671118×
}
672
673
/// Admit a peer that shows a ticket from this runner's hub, for this
674
/// site: a session until the ticket expires.
6756×
fn admit(state: &State, ticket: &str) -> std::result::Result<String, String> {
6766×
    let hub = {
6776×
        let mut hub = state.hub.lock().unwrap();
6786×
        if hub.is_none() {
6790×
            *hub = state.client.hub_identity().ok();
6806×
        }
6816×
        hub.clone().ok_or("this runner cannot reach its hub to check tickets")?
682
    };
6836×
    let t = Ticket::open(ticket, &hub, now_ms())?;
6846×
    if t.site != state.site {
6851×
        return Err(format!("this runner is in site {:?}, the ticket's runner in {:?}", state.site, t.site));
6865×
    }
6875×
    let session = random_token().map_err(|e| format!("make a session: {e}"))?;
6885×
    state.sessions.lock().unwrap().insert(session.clone(), (t.name, t.expires_ms));
6895×
    Ok(session)
6906×
}
691
6925×
fn random_token() -> std::io::Result<String> {
6935×
    let mut b = [0u8; 32];
6945×
    std::fs::File::open("/dev/urandom")?.read_exact(&mut b)?;
6955×
    Ok(hex::encode(b))
6965×
}
697
698100×
fn read_chunk(state: &State, hash: &str) -> Option<Vec<u8>> {
699100×
    if let Ok(b) = std::fs::read(cache_path(&state.root.join("chunks"), hash)) {
70024×
        return Some(b);
70176×
    }
70276×
    let (path, at, len) = state.located.lock().unwrap().get(hash).cloned()?;
70376×
    let mut f = std::fs::File::open(path).ok()?;
70476×
    f.seek(SeekFrom::Start(at)).ok()?;
70576×
    let mut b = vec![0; len as usize];
70676×
    f.read_exact(&mut b).ok()?;
70776×
    Some(b)
708100×
}
709
710
/// Hold back until sending `n` more bytes keeps within the upload rate.
711102×
fn throttle(state: &State, n: usize) {
712102×
    let wait = {
713102×
        let mut b = state.bucket.lock().unwrap();
714102×
        let now = Instant::now();
715102×
        b.0 = (b.0 + now.duration_since(b.1).as_secs_f64() * state.rate).min(state.rate);
716102×
        b.1 = now;
717102×
        b.0 -= n as f64;
718102×
        if b.0 < 0.0 {
7191×
            -b.0 / state.rate
720
        } else {
721101×
            0.0
722
        }
723
    };
724102×
    if wait > 0.0 {
7251×
        std::thread::sleep(Duration::from_secs_f64(wait));
726101×
    }
727102×
}
728
729
#[cfg(test)]
730
mod tests {
731
    use super::*;
732
    use oxy_core::identity::SecretKey;
733
734
    /// A hub that holds one image and runs the tracker: it serves the
735
    /// manifest and chunks (counting the chunks), and answers announces
736
    /// with every other runner as a peer and a ticket for the caller,
737
    /// known by its token.
738
    struct FakeHub {
739
        url: String,
740
        chunks_served: Arc<AtomicUsize>,
741
        runners: Arc<Mutex<Vec<(String, SwarmPeer)>>>,
742
    }
743
7443×
    fn fake_hub(m: Manifest, image: Vec<u8>, site: &str) -> FakeHub {
7453×
        hub_with(m, image, site, Duration::ZERO, true)
7463×
    }
747
748
    /// A hub that takes `delay` over each chunk.
7491×
    fn slow_hub(m: Manifest, image: Vec<u8>, site: &str, delay: Duration) -> FakeHub {
7501×
        hub_with(m, image, site, delay, true)
7511×
    }
752
753
    /// A hub that keeps the image's manifest but none of its chunks, as for
754
    /// an image only runners hold (jetstream.md §8.1.1).
7551×
    fn peer_only_hub(m: Manifest, image: Vec<u8>, site: &str) -> FakeHub {
7561×
        hub_with(m, image, site, Duration::ZERO, false)
7571×
    }
758
7595×
    fn hub_with(m: Manifest, image: Vec<u8>, site: &str, delay: Duration, seeds: bool) -> FakeHub {
7605×
        let sk = SecretKey::generate();
7615×
        let id = sk.identity("hub", "hub@example", 1);
7625×
        let key = id.hash();
7635×
        let mut chunks = HashMap::new();
7645×
        let mut at = 0usize;
765190×
        for c in &m.chunks {
766190×
            chunks.insert(c.hash().to_string(), image[at..at + c.len()].to_vec());
767190×
            at += c.len();
768190×
        }
7695×
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
7705×
        let url = format!("http://{}", listener.local_addr().unwrap());
7715×
        let served = Arc::new(AtomicUsize::new(0));
7725×
        let runners: Arc<Mutex<Vec<(String, SwarmPeer)>>> = Arc::new(Mutex::new(Vec::new()));
7735×
        let (count, known, site) = (served.clone(), runners.clone(), site.to_string());
7745×
        let shared = Arc::new((m, chunks, id, key, sk));
7755×
        std::thread::spawn(move || {
77664×
            for conn in listener.incoming() {
77764×
                let Ok(conn) = conn else { continue };
77864×
                let (shared, count, known, site) = (shared.clone(), count.clone(), known.clone(), site.clone());
77964×
                std::thread::spawn(move || {
78064×
                    let (m, chunks, id, key, sk) = &*shared;
78164×
                    let mut out = conn.try_clone().unwrap();
78264×
                    let mut r = BufReader::new(conn);
783
                    loop {
784227×
                        let mut line = String::new();
785227×
                        if r.read_line(&mut line).unwrap_or(0) == 0 {
78652×
                            return;
787175×
                        }
788175×
                        let path = line.split_whitespace().nth(1).unwrap_or("").to_string();
789175×
                        let (mut len, mut token) = (0, String::new());
790
                        loop {
791998×
                            let mut h = String::new();
792998×
                            r.read_line(&mut h).unwrap();
793998×
                            let h = h.trim_end().to_string();
794998×
                            if h.is_empty() {
795175×
                                break;
796823×
                            }
797823×
                            let lower = h.to_ascii_lowercase();
798823×
                            if let Some(v) = lower.strip_prefix("content-length:") {
7994×
                                len = v.trim().parse().unwrap();
800819×
                            }
801823×
                            if let Some(v) = h.strip_prefix("Authorization: Bearer ") {
802139×
                                token = v.to_string();
803684×
                            }
804
                        }
805175×
                        let mut body = vec![0; len];
806175×
                        r.read_exact(&mut body).unwrap();
807175×
                        let (code, reply): (u16, Vec<u8>) = if path == "/.well-known/oxyhub-key" {
80812×
                            (200, id.encode())
809163×
                        } else if path == format!("/api/v1/flow/images/{}", m.hash) {
8107×
                            (200, serde_json::to_vec(m).unwrap())
811156×
                        } else if let Some(c) = path.strip_prefix("/api/v1/flow/images/chunks/").filter(|_| seeds) {
812140×
                            count.fetch_add(1, Ordering::Relaxed);
813140×
                            std::thread::sleep(delay);
814140×
                            (200, chunks[c].clone())
81516×
                        } else if path == "/api/v1/flow/swarm/announce" {
81616×
                            let runners = known.lock().unwrap();
81726×
                            let me = runners.iter().find(|(t, _)| *t == token).map(|(_, p)| p.clone()).unwrap();
81816×
                            let ticket = Ticket {
81916×
                                hub: key.to_hex(),
82016×
                                runner: me.runner,
82116×
                                name: me.name.clone(),
82216×
                                site: site.clone(),
82316×
                                expires_ms: now_ms() + 60_000,
82416×
                            };
82529×
                            let peers: Vec<SwarmPeer> = runners.iter().filter(|(t, _)| *t != token).map(|(_, p)| p.clone()).collect();
82616×
                            let a = flow_core::api::Announced {
82716×
                                ticket: ticket.seal(key, sk),
82816×
                                peers,
82916×
                                interval_s: 5,
83016×
                                seed: (!seeds).then_some(false),
83116×
                            };
83216×
                            (200, serde_json::to_vec(&a).unwrap())
833
                        } else {
8340×
                            (404, b"no".to_vec())
835
                        };
836175×
                        let head = format!("HTTP/1.1 {code} X\r\nContent-Length: {}\r\n\r\n", reply.len());
837175×
                        if out.write_all(head.as_bytes()).and_then(|_| out.write_all(&reply)).is_err() {
83812×
                            return;
839163×
                        }
840
                    }
84164×
                });
842
            }
8435×
        });
8445×
        FakeHub { url, chunks_served: served, runners }
8455×
    }
846
84711×
    fn free_port() -> u16 {
84811×
        TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
84911×
    }
850
851
    /// A runner home in `dir` with a swarm on `port`, known to `hub` by
852
    /// `token` as runner `id`.
85311×
    fn runner(dir: &Path, hub: &FakeHub, id: i64, token: &str, port: u16, site: &str) -> (Home, Swarm) {
85411×
        let toml = format!(
855
            "hub = \"{}\"\ntoken = \"{token}\"\nname = \"r{id}\"\n[swarm]\nlisten = \"127.0.0.1:{port}\"\nsite = \"{site}\"\n",
856
            hub.url
857
        );
85811×
        std::fs::write(dir.join(crate::home::RUNNER_TOML), toml).unwrap();
85911×
        let home = Home::locate(Some(dir)).unwrap();
86011×
        let cfg = home.config.runner.clone().unwrap();
86111×
        let swarm = Swarm::start(&home, &cfg).unwrap().unwrap();
86211×
        hub.runners.lock().unwrap().push((
86311×
            token.into(),
86411×
            SwarmPeer {
86511×
                runner: id,
86611×
                name: format!("r{id}"),
86711×
                addr: format!("127.0.0.1:{port}"),
86811×
                cert: swarm.addr.cert.clone(),
86911×
                complete: true,
87011×
            },
87111×
        ));
87211×
        (home, swarm)
87311×
    }
874
8755×
    fn image() -> Vec<u8> {
876
        // 3 MiB of varied bytes, with a run of zeros the swarm never moves.
8775×
        let mut x = 0x9e3779b97f4a7c15u64;
8785×
        let mut b: Vec<u8> = (0..3 << 20)
87915728640×
            .map(|_| {
88015728640×
                x ^= x << 13;
88115728640×
                x ^= x >> 7;
88215728640×
                x ^= x << 17;
88315728640×
                x as u8
88415728640×
            })
8855×
            .collect();
8865×
        b[1 << 20..(1 << 20) + (300 << 10)].fill(0);
8875×
        b
8885×
    }
889
890
    #[test]
8911×
    fn a_runner_fetches_an_image_from_its_peer_and_not_the_hub() {
8921×
        let bytes = image();
89338×
        let m = jet::images::scan(&bytes[..], Some("x86_64".into()), |_, _| Ok(())).unwrap();
8941×
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
8951×
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
896
        // Runner A holds the image, imported rather than fetched: its swarm
897
        // scans it for a manifest before offering it.
8981×
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
8991×
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
9001×
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
9011×
        let deadline = Instant::now() + Duration::from_secs(20);
9024×
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
9033×
            assert!(Instant::now() < deadline, "runner A never offered its image");
9043×
            std::thread::sleep(Duration::from_millis(20));
905
        }
9061×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
9071×
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
9081×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes, "the image came back whole");
9091×
        assert_eq!(moved.peer_bytes, moved.bytes, "every chunk came from the peer");
9101×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), 0, "the hub sent no chunks");
9111×
        assert_eq!(moved.chunks, m.needed().len());
912
        // B now offers the whole image too, read out of its rootfs.
9131×
        assert!(matches!(b.state.offers.lock().unwrap().get(&m.hash), Some(Offer { bits: None, .. })));
9141×
    }
915
916
    #[test]
9171×
    fn an_image_only_runners_hold_comes_from_a_peer_and_fails_plainly_without_one() {
9181×
        let bytes = image();
91938×
        let m = jet::images::scan(&bytes[..], Some("x86_64".into()), |_, _| Ok(())).unwrap();
9201×
        let hub = peer_only_hub(m.clone(), bytes.clone(), "lab");
921
        // No peer yet: no hub to fall back on, so the fetch says why.
9221×
        let lone = tempfile::tempdir().unwrap();
9231×
        let (lone_home, lone_runner) = runner(lone.path(), &hub, 3, "tc", free_port(), "lab");
9241×
        let e = lone_runner.fetch(&lone_home, &m.hash).err().expect("a fetch with no peer and no seed fails");
9251×
        assert!(e.to_string().contains("held only by runners"), "{e:#}");
926
        // With a peer holding it, it all comes from the peer.
9271×
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
9281×
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
9291×
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
9301×
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
9311×
        let deadline = Instant::now() + Duration::from_secs(20);
9324×
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
9333×
            assert!(Instant::now() < deadline, "runner A never offered its image");
9343×
            std::thread::sleep(Duration::from_millis(20));
935
        }
9361×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
9371×
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
9381×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
9391×
        assert_eq!(moved.peer_bytes, moved.bytes, "every chunk came from the peer");
9401×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), 0);
9411×
    }
942
943
    #[test]
9441×
    fn runners_starting_together_share_what_each_has_fetched() {
9451×
        let bytes = image();
94638×
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
9471×
        let needed = m.needed().len();
948
        // A slow hub, as a remote one is: the runners have time to swap.
9491×
        let hub = slow_hub(m.clone(), bytes.clone(), "lab", Duration::from_millis(400));
9501×
        let (b_dir, c_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
9511×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
9521×
        let (c_home, c) = runner(c_dir.path(), &hub, 3, "tc", free_port(), "lab");
9531×
        let (mb, mc) = std::thread::scope(|s| {
9541×
            let fb = s.spawn(|| b.fetch(&b_home, &m.hash).unwrap().unwrap());
9551×
            let fc = s.spawn(|| c.fetch(&c_home, &m.hash).unwrap().unwrap());
9561×
            (fb.join().unwrap(), fc.join().unwrap())
9571×
        });
9581×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
9591×
        assert_eq!(std::fs::read(c_home.rootfs(&m.hash)).unwrap(), bytes);
9601×
        let served = hub.chunks_served.load(Ordering::Relaxed);
9611×
        assert!(served < 2 * needed, "the hub sent {served} chunks for two runners needing {needed} each");
9621×
        assert!(mb.peer_bytes + mc.peer_bytes > 0, "they shared");
9631×
    }
964
965
    #[test]
9661×
    fn without_peers_the_hub_sends_everything_and_a_ticket_from_another_site_is_refused() {
9671×
        let bytes = image();
96838×
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
9691×
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
9701×
        let dir = tempfile::tempdir().unwrap();
9711×
        let (home, s) = runner(dir.path(), &hub, 1, "t1", free_port(), "lab");
9721×
        let moved = s.fetch(&home, &m.hash).unwrap().unwrap();
9731×
        assert_eq!(moved.peer_bytes, 0);
9741×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len());
9751×
        assert_eq!(std::fs::read(home.rootfs(&m.hash)).unwrap(), bytes);
976
977
        // A runner in another site shows a ticket for "lab": refused.
9781×
        let other = tempfile::tempdir().unwrap();
9791×
        let port = free_port();
9801×
        let (_h, o) = runner(other.path(), &hub, 3, "t3", port, "elsewhere");
9811×
        let ticket = s.state.client.announce(&m.hash, "fetching").unwrap().ticket;
9821×
        let err = hello(&format!("127.0.0.1:{port}"), &o.addr.cert, &ticket).unwrap_err();
9831×
        assert!(format!("{err:#}").contains("HTTP 403"), "{err:#}");
984
        // And no session, no chunks.
9851×
        let url = format!("https://127.0.0.1:{port}/swarm/v1/chunks/{}", m.chunks[0].hash());
9861×
        assert!(matches!(peer_agent(&o.addr.cert).unwrap().get(&url).call(), Err(ureq::Error::Status(401, _))));
987
        // Nor any answer at all without TLS.
9881×
        assert!(ureq::get(&url.replacen("https", "http", 1)).call().is_err());
9891×
    }
990
991
    #[test]
9921×
    fn a_peer_with_another_certificate_is_not_trusted() {
9931×
        let bytes = image();
99438×
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
9951×
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
9961×
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
9971×
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
9981×
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
9991×
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
10005×
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
10014×
            std::thread::sleep(Duration::from_millis(20));
10024×
        }
10031×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
1004
        // The hub names A, but with B's certificate: whoever answers at A's
1005
        // address is not who the hub vouched for.
10061×
        hub.runners.lock().unwrap()[0].1.cert = b.addr.cert.clone();
10071×
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
10081×
        assert_eq!(moved.peer_bytes, 0, "nothing was taken from the stranger");
10091×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len());
10101×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
1011
        // The certificate is kept: the same runner, the same fingerprint.
10121×
        let mut cfg = b_home.config.runner.clone().unwrap();
10131×
        cfg.swarm.as_mut().unwrap().listen = "127.0.0.1:0".into();
10141×
        let again = Swarm::start(&b_home, &cfg).unwrap().unwrap();
10151×
        assert_eq!(again.addr.cert, b.addr.cert);
10161×
    }
1017
1018
    #[test]
10191×
    fn the_upload_rate_holds_back_a_burst() {
10201×
        let dir = tempfile::tempdir().unwrap();
10211×
        let (cert, key) = certificate(dir.path()).unwrap();
10221×
        let state = State {
10231×
            tls: server_tls(cert, key).unwrap(),
10241×
            root: PathBuf::new(),
10251×
            site: String::new(),
10261×
            client: Client::new("http://127.0.0.1:1", "t", None).unwrap(),
10271×
            hub: Mutex::new(None),
10281×
            offers: Mutex::new(HashMap::new()),
10291×
            located: Mutex::new(HashMap::new()),
10301×
            sessions: Mutex::new(HashMap::new()),
10311×
            conns: AtomicUsize::new(0),
10321×
            bucket: Mutex::new((1_000_000.0, Instant::now())),
10331×
            rate: 1_000_000.0,
10341×
        };
10351×
        let start = Instant::now();
10361×
        throttle(&state, 1_000_000);
10371×
        assert!(start.elapsed() < Duration::from_millis(100), "the first second's worth goes at once");
10381×
        throttle(&state, 300_000);
10391×
        assert!(start.elapsed() >= Duration::from_millis(250), "then it waits for the rate");
10401×
    }
1041
}