OXYGENHUB

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

663 of 739 lines run, 70 of 131 functions · run #205 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
5296×
fn now_ms() -> u64 {
5396×
    SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
5496×
}
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.
909×
    pub fn start(home: &Home, cfg: &RunnerConfig) -> Result<Option<Swarm>> {
919×
        let Some(sc) = &cfg.swarm else { return Ok(None) };
929×
        let listener = TcpListener::bind(&sc.listen).with_context(|| format!("swarm: listen on {}", sc.listen))?;
939×
        let client = Client::new(&cfg.hub, &cfg.token, cfg.ca.as_deref())?;
949×
        let hub = client.hub_identity().map_err(|e| eprintln!("swarm: the hub's key: {e:#}; trying again when a peer comes")).ok();
959×
        let rate = sc.upload_mib_s.unwrap_or(50).max(1) as f64 * (1 << 20) as f64;
969×
        let (cert, key) = certificate(&home.root)?;
979×
        let fingerprint = blake3::hash(&cert).to_hex().to_string();
989×
        let tls = server_tls(cert, key)?;
999×
        let state = Arc::new(State {
1009×
            root: home.root.clone(),
1019×
            site: sc.site.clone(),
1029×
            client,
1039×
            hub: Mutex::new(hub),
1049×
            offers: Mutex::new(HashMap::new()),
1059×
            located: Mutex::new(HashMap::new()),
1069×
            sessions: Mutex::new(HashMap::new()),
1079×
            conns: AtomicUsize::new(0),
1089×
            bucket: Mutex::new((rate, Instant::now())),
1099×
            rate,
1109×
            tls,
1119×
        });
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.
1159×
        let mut unscanned = Vec::new();
1169×
        for h in home.images() {
1173×
            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)),
1192×
                _ => unscanned.push((h.clone(), home.rootfs(&h), home.image_arch(&h))),
120
            }
121
        }
1229×
        let scanning = state.clone();
1239×
        std::thread::spawn(move || {
1249×
            for (h, path, arch) in unscanned {
12576×
                match jet::images::scan_file(&path, arch, |_, _| Ok(())) {
1262×
                    Ok(m) if m.hash == h => {
1272×
                        save_manifest(&scanning.root, &m).ok();
1282×
                        offer_whole(&scanning, &m, &path);
1292×
                    }
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
            }
1349×
        });
1359×
        let serving = state.clone();
1369×
        std::thread::spawn(move || accept(listener, serving));
1379×
        let addr = SwarmAddr { addr: sc.advertise.clone().unwrap_or_else(|| sc.listen.clone()), site: sc.site.clone(), cert: fingerprint };
1389×
        eprintln!(
139
            "swarm: serving image chunks on {} over TLS (certificate {}, site {:?}, up to {} MiB/s)",
140
            sc.listen,
1419×
            &addr.cert[..12],
142
            sc.site,
1439×
            sc.upload_mib_s.unwrap_or(50)
144
        );
1459×
        Ok(Some(Swarm { state, addr }))
1469×
    }
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.
1515×
    pub fn fetch(&self, home: &Home, hash: &str) -> Result<Option<Moved>> {
1525×
        let start = Instant::now();
1535×
        let client = &self.state.client;
1545×
        let Some(m) = client.manifest(hash)? else { return Ok(None) };
1555×
        if m.hash != hash {
1560×
            bail!("the hub's manifest for {} names {}", &hash[..12], &m.hash[..12]);
1575×
        }
1585×
        save_manifest(&home.root, &m)?;
1595×
        let needed: Vec<String> = m.needed().into_iter().map(String::from).collect();
1605×
        let cache = chunk_cache(home);
1615×
        let mut have = Bits::new(needed.len());
162190×
        for (i, h) in needed.iter().enumerate() {
163190×
            if cache_path(&cache, h).is_file() {
1640×
                have.set(i);
165190×
            }
166
        }
1675×
        self.state.offers.lock().unwrap().insert(hash.into(), Offer { bits: Some(have.clone()), fetching: Bits::new(needed.len()) });
1685×
        let announced = client
1695×
            .announce(hash, "fetching")
1705×
            .map_err(|e| eprintln!("swarm: announce {}: {e:#}; fetching from the hub alone", &hash[..12]))
1715×
            .ok();
1725×
        let seed = now_ms() ^ (std::process::id() as u64) << 32;
1735×
        let picker = Mutex::new(Picker::new(needed.len(), have, seed));
1745×
        let peers: Mutex<Vec<Arc<Peer>>> = Mutex::new(Vec::new());
1755×
        if let Some(a) = &announced {
1765×
            self.join(&a.peers, &a.ticket, hash, needed.len(), &peers, &picker);
1775×
        }
1785×
        let (bytes, peer_bytes, got) = (AtomicU64::new(0), AtomicU64::new(0), AtomicUsize::new(0));
1795×
        let (failed, hub_failures) = (Mutex::new(None::<anyhow::Error>), AtomicUsize::new(0));
180
        {
1815×
            let (picker, peers, needed, cache) = (&picker, &peers, &needed, &cache);
1825×
            let (bytes, peer_bytes, got, failed, hub_failures) = (&bytes, &peer_bytes, &got, &failed, &hub_failures);
1835×
            let (state, m) = (&self.state, &m);
1845×
            let worker = move |src: Source| loop {
1851154×
                if failed.lock().unwrap().is_some() {
1860×
                    break;
1871154×
                }
1881154×
                let peer = match src {
189655×
                    Source::Peer(p) => Some(peers.lock().unwrap()[p].clone()),
190499×
                    Source::Hub => None,
191
                };
1921154×
                if peer.as_ref().is_some_and(|p| !p.alive.load(Ordering::Relaxed)) {
1930×
                    break;
1941154×
                }
195
                // Taken apart from the match, so the lock is not held
196
                // while waiting.
1971154×
                let pick = picker.lock().unwrap().pick(src);
1981154×
                let i = match pick {
19952×
                    Pick::Done => break,
200
                    Pick::Wait => {
201912×
                        std::thread::sleep(Duration::from_millis(20));
202912×
                        continue;
203
                    }
204190×
                    Pick::Chunk(i) => i,
205
                };
206190×
                if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) {
207190×
                    o.fetching.set(i);
208190×
                }
209190×
                let h = &needed[i];
210190×
                let fetched = match &peer {
21166×
                    Some(p) => self.peer_chunk(p, h),
212124×
                    None => client.chunk(h),
213
                };
214190×
                let kept = fetched.and_then(|b| {
215190×
                    if blake3::hash(&b).to_hex().as_str() != h {
2160×
                        bail!("chunk {} came back with other bytes", &h[..12]);
217190×
                    }
218190×
                    let p = cache_path(cache, h);
219190×
                    std::fs::create_dir_all(p.parent().expect("a chunk directory"))?;
220190×
                    let tmp = p.with_extension(format!("part{}", PART.fetch_add(1, Ordering::Relaxed)));
221190×
                    std::fs::write(&tmp, &b)?;
222190×
                    std::fs::rename(&tmp, &p)?;
223190×
                    Ok(b.len() as u64)
224190×
                });
225190×
                match kept {
226190×
                    Ok(n) => {
227190×
                        if let Some(Offer { bits: Some(b), fetching }) = state.offers.lock().unwrap().get_mut(&m.hash) {
228190×
                            b.set(i);
229190×
                            fetching.clear(i);
230190×
                        }
231190×
                        picker.lock().unwrap().got(i);
232190×
                        bytes.fetch_add(n, Ordering::Relaxed);
233190×
                        got.fetch_add(1, Ordering::Relaxed);
234190×
                        if peer.is_some() {
23566×
                            peer_bytes.fetch_add(n, Ordering::Relaxed);
236124×
                        }
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
                }
26252×
            };
2635×
            std::thread::scope(|s| {
2645×
                for _ in 0..HUB_WORKERS {
26540×
                    s.spawn(move || worker(Source::Hub));
266
                }
2675×
                let mut spawned = 0;
2685×
                let mut spawn_new = || {
2695×
                    let n = peers.lock().unwrap().len();
2705×
                    for p in spawned..n {
2713×
                        for _ in 0..PEER_WORKERS {
27212×
                            s.spawn(move || worker(Source::Peer(p)));
273
                        }
274
                    }
2755×
                    spawned = n;
2765×
                };
2775×
                spawn_new();
278
                // Keep the peers' maps fresh, and find new peers while
279
                // fetching, until every chunk is here.
2805×
                let mut asked_hub = Instant::now();
2815×
                let interval = Duration::from_secs(announced.as_ref().map(|a| a.interval_s).unwrap_or(30).max(5));
2825×
                let mut last = Instant::now();
28340×
                while picker.lock().unwrap().missing() > 0 && failed.lock().unwrap().is_none() {
28435×
                    std::thread::sleep(Duration::from_millis(100));
28535×
                    if last.elapsed() < REFRESH {
28629×
                        continue;
2876×
                    }
2886×
                    last = Instant::now();
2896×
                    let now: Vec<(usize, Arc<Peer>)> =
2906×
                        peers.lock().unwrap().iter().cloned().enumerate().filter(|(_, p)| p.alive.load(Ordering::Relaxed)).collect();
2916×
                    for (idx, p) in now {
2926×
                        match self.peer_have(&p, hash, needed.len()) {
2936×
                            Ok((bits, fetching)) => picker.lock().unwrap().peer_fetching(idx, bits, fetching),
2940×
                            Err(e) if e.is::<Busy>() => {}
2950×
                            Err(_) => {
2960×
                                p.alive.store(false, Ordering::Relaxed);
2970×
                                picker.lock().unwrap().drop_peer(idx);
2980×
                            }
299
                        }
300
                    }
3016×
                    if announced.is_some() && asked_hub.elapsed() >= interval {
3020×
                        asked_hub = Instant::now();
3030×
                        if let Ok(a) = client.announce(hash, "fetching") {
3040×
                            self.join(&a.peers, &a.ticket, hash, needed.len(), peers, picker);
3050×
                            spawn_new();
3060×
                        }
3076×
                    }
308
                }
3095×
            });
310
        }
3115×
        if let Some(e) = failed.into_inner().unwrap() {
3120×
            client.announce(hash, "stopped").ok();
3130×
            return Err(e);
3145×
        }
3155×
        write_image(home, &m)?;
3165×
        offer_whole(&self.state, &m, &home.rootfs(hash));
3175×
        client.announce(hash, "done").ok();
3185×
        Ok(Some(Moved {
3195×
            chunks: got.into_inner(),
3205×
            bytes: bytes.into_inner(),
3215×
            peer_bytes: peer_bytes.into_inner(),
3225×
            elapsed: start.elapsed(),
3235×
        }))
3245×
    }
325
326
    /// Admit this runner to the peers it does not know yet: a session from
327
    /// each, and what each has.
3285×
    fn join(&self, found: &[SwarmPeer], ticket: &str, hash: &str, len: usize, peers: &Mutex<Vec<Arc<Peer>>>, picker: &Mutex<Picker>) {
3295×
        for sp in found {
3304×
            if peers.lock().unwrap().iter().any(|p| p.runner == sp.runner) {
3310×
                continue;
3324×
            }
3334×
            if !is_hash(&sp.cert) {
3340×
                eprintln!("swarm: peer {} gave no certificate (an older flow); not asking it", sp.name);
3350×
                continue;
3364×
            }
3374×
            let joined = hello(&sp.addr, &sp.cert, ticket).and_then(|(agent, session)| {
3383×
                let p = Peer {
3393×
                    runner: sp.runner,
3403×
                    name: sp.name.clone(),
3413×
                    base: format!("https://{}/swarm/v1", sp.addr),
3423×
                    agent,
3433×
                    session,
3443×
                    alive: AtomicBool::new(true),
3453×
                };
3463×
                let (bits, fetching) = self.peer_have(&p, hash, len)?;
3473×
                Ok((p, bits, fetching))
3483×
            });
3494×
            match joined {
3503×
                Ok((p, bits, fetching)) => {
3513×
                    let mut list = peers.lock().unwrap();
3523×
                    picker.lock().unwrap().peer_fetching(list.len(), bits, fetching);
3533×
                    list.push(Arc::new(p));
3543×
                }
3551×
                Err(e) => eprintln!("swarm: peer {} ({}): {e:#}", sp.name, sp.addr),
356
            }
357
        }
3585×
    }
359
360
    /// What a peer has of image `hash`, and what it is fetching now:
361
    /// nothing yet when it has not begun (the tracker may name it a moment
362
    /// before it starts).
3639×
    fn peer_have(&self, p: &Peer, hash: &str, len: usize) -> Result<(Bits, Bits)> {
3649×
        let url = format!("{}/images/{hash}/have", p.base);
3659×
        let r = match p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call() {
3669×
            Ok(r) => r,
3670×
            Err(ureq::Error::Status(404, _)) => return Ok((Bits::new(len), Bits::new(len))),
3680×
            Err(e) => return Err(peer_error(&url, e)),
369
        };
3709×
        let mut body = Vec::new();
3719×
        r.into_reader().take(len as u64 / 4 + 64).read_to_end(&mut body)?;
3729×
        if body == b"complete" {
3731×
            return Ok((Bits::full(len), Bits::new(len)));
3748×
        }
3758×
        let (have, fetching) = body.split_at(len.div_ceil(8).min(body.len()));
3768×
        Ok((Bits::from_bytes(len, have), Bits::from_bytes(len, fetching)))
3779×
    }
378
37966×
    fn peer_chunk(&self, p: &Peer, hash: &str) -> Result<Vec<u8>> {
38066×
        let url = format!("{}/chunks/{hash}", p.base);
38166×
        let r = p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call().map_err(|e| peer_error(&url, e))?;
38266×
        let mut out = Vec::new();
38366×
        r.into_reader().take(oxy_core::chunk::MAX_SIZE as u64 + 1).read_to_end(&mut out)?;
38466×
        Ok(out)
38566×
    }
386
}
387
388
/// A peer this fetch asks for chunks, over a connection that trusts only
389
/// its certificate.
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
/// A peer at its connection limit: ask it again shortly.
400
#[derive(Debug)]
401
struct Busy;
402
403
impl std::fmt::Display for Busy {
4040×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4050×
        f.write_str("the peer is busy")
4060×
    }
407
}
408
409
impl std::error::Error for Busy {}
410
4112×
fn peer_error(url: &str, e: ureq::Error) -> anyhow::Error {
4122×
    match e {
4130×
        ureq::Error::Status(503, _) => anyhow::Error::new(Busy),
4141×
        ureq::Error::Status(code, r) => anyhow::anyhow!("{url}: HTTP {code}: {}", r.into_string().unwrap_or_default().trim()),
4151×
        e => anyhow::anyhow!("{url}: {e}"),
416
    }
4172×
}
418
419
/// Show `ticket` to the peer at `addr`, trusting only the certificate
420
/// `cert`: a connection to it and a session.
4215×
fn hello(addr: &str, cert: &str, ticket: &str) -> Result<(ureq::Agent, String)> {
4225×
    let agent = peer_agent(cert)?;
4235×
    let url = format!("https://{addr}/swarm/v1/hello");
4245×
    let r: serde_json::Value = agent.post(&url).send_string(ticket).map_err(|e| peer_error(&url, e))?.into_json()?;
4253×
    let session = r["session"].as_str().map(String::from).context("the peer gave no session")?;
4263×
    Ok((agent, session))
4275×
}
428
42916×
fn provider() -> Arc<CryptoProvider> {
43016×
    Arc::new(rustls::crypto::ring::default_provider())
43116×
}
432
433
/// A client that trusts one certificate, by its BLAKE3, whatever name
434
/// the peer goes by: the one the hub said this peer has.
4356×
fn peer_agent(cert: &str) -> Result<ureq::Agent> {
4366×
    let provider = provider();
4376×
    let cfg = rustls::ClientConfig::builder_with_provider(provider.clone())
4386×
        .with_safe_default_protocol_versions()?
4396×
        .dangerous()
4406×
        .with_custom_certificate_verifier(Arc::new(Pinned { cert: cert.to_string(), provider }))
4416×
        .with_no_client_auth();
4426×
    Ok(ureq::AgentBuilder::new().tls_config(Arc::new(cfg)).timeout_connect(Duration::from_secs(2)).timeout(Duration::from_secs(20)).build())
4436×
}
444
445
/// Trusts the certificate with this BLAKE3 and no other. The handshake's
446
/// signature is still checked against it, so only the holder of its key
447
/// passes.
448
#[derive(Debug)]
449
struct Pinned {
450
    cert: String,
451
    provider: Arc<CryptoProvider>,
452
}
453
454
impl ServerCertVerifier for Pinned {
4557×
    fn verify_server_cert(
4567×
        &self,
4577×
        end_entity: &CertificateDer<'_>,
4587×
        _intermediates: &[CertificateDer<'_>],
4597×
        _name: &ServerName<'_>,
4607×
        _ocsp: &[u8],
4617×
        _now: UnixTime,
4627×
    ) -> std::result::Result<ServerCertVerified, rustls::Error> {
4637×
        if blake3::hash(end_entity.as_ref()).to_hex().as_str() == self.cert {
4646×
            Ok(ServerCertVerified::assertion())
465
        } else {
4661×
            Err(rustls::Error::General("not the peer the hub named: its certificate is another".into()))
467
        }
4687×
    }
469
4700×
    fn verify_tls12_signature(
4710×
        &self,
4720×
        message: &[u8],
4730×
        cert: &CertificateDer<'_>,
4740×
        dss: &DigitallySignedStruct,
4750×
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
4760×
        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
4770×
    }
478
4796×
    fn verify_tls13_signature(
4806×
        &self,
4816×
        message: &[u8],
4826×
        cert: &CertificateDer<'_>,
4836×
        dss: &DigitallySignedStruct,
4846×
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
4856×
        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
4866×
    }
487
48824×
    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
48924×
        self.provider.signature_verification_algorithms.supported_schemes()
49024×
    }
491
}
492
493
/// The runner's swarm certificate and key (`swarm/cert.der`, and
494
/// `swarm/key.der` readable by the runner alone), made on first use and
495
/// kept, so its fingerprint holds across restarts.
49610×
fn certificate(root: &Path) -> Result<(Vec<u8>, Vec<u8>)> {
49710×
    let dir = root.join("swarm");
49810×
    let (cert_path, key_path) = (dir.join("cert.der"), dir.join("key.der"));
49910×
    if let (Ok(c), Ok(k)) = (std::fs::read(&cert_path), std::fs::read(&key_path)) {
5001×
        return Ok((c, k));
5019×
    }
5029×
    std::fs::create_dir_all(&dir)?;
5039×
    let made = rcgen::generate_simple_self_signed(vec!["flow-swarm".to_string()]).context("make the swarm's certificate")?;
5049×
    let (cert, key) = (made.cert.der().to_vec(), made.signing_key.serialize_der());
505
    {
5069×
        let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&key_path)?;
5079×
        f.write_all(&key)?;
508
    }
5099×
    std::fs::write(&cert_path, &cert)?;
5109×
    Ok((cert, key))
51110×
}
512
51310×
fn server_tls(cert: Vec<u8>, key: Vec<u8>) -> Result<Arc<rustls::ServerConfig>> {
51410×
    let cfg = rustls::ServerConfig::builder_with_provider(provider())
51510×
        .with_safe_default_protocol_versions()?
51610×
        .with_no_client_auth()
51710×
        .with_single_cert(vec![CertificateDer::from(cert)], PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)))
51810×
        .context("the swarm's certificate")?;
51910×
    Ok(Arc::new(cfg))
52010×
}
521
522
/// Offer a whole image: every chunk of it can be read out of `path`.
5238×
fn offer_whole(state: &State, m: &Manifest, path: &Path) {
5248×
    let mut located = state.located.lock().unwrap();
5258×
    let mut at = 0u64;
526304×
    for c in &m.chunks {
527304×
        if !c.is_zeros() {
528304×
            located.entry(c.hash().to_string()).or_insert_with(|| (path.to_path_buf(), at, c.1));
5290×
        }
530304×
        at += c.1 as u64;
531
    }
5328×
    state.offers.lock().unwrap().insert(m.hash.clone(), Offer { bits: None, fetching: Bits::new(0) });
5338×
}
534
5359×
fn accept(listener: TcpListener, state: Arc<State>) {
53625×
    for conn in listener.incoming() {
53725×
        let Ok(conn) = conn else { continue };
53825×
        if state.conns.load(Ordering::Relaxed) >= MAX_OPEN {
5390×
            continue;
54025×
        }
54125×
        let state = state.clone();
54225×
        std::thread::spawn(move || {
543
            // Past the limit a peer is still answered, with 503, so it
544
            // comes back rather than giving up on this runner.
54525×
            let busy = state.conns.fetch_add(1, Ordering::Relaxed) >= MAX_CONNS;
54625×
            serve_tls(conn, &state, busy).ok();
54725×
            state.conns.fetch_sub(1, Ordering::Relaxed);
54825×
        });
549
    }
5509×
}
551
55225×
fn serve_tls(conn: TcpStream, state: &State, busy: bool) -> std::io::Result<()> {
55325×
    conn.set_read_timeout(Some(Duration::from_secs(30)))?;
55425×
    conn.set_write_timeout(Some(Duration::from_secs(30)))?;
55525×
    conn.set_nodelay(true)?;
55625×
    let tls = rustls::ServerConnection::new(state.tls.clone()).map_err(std::io::Error::other)?;
55725×
    serve_conn(rustls::StreamOwned::new(tls, conn), state, busy)
55825×
}
559
56080×
fn respond(conn: &mut impl Write, code: u16, kind: &str, body: &[u8]) -> std::io::Result<()> {
56180×
    let reason = match code {
56278×
        200 => "OK",
5630×
        400 => "Bad Request",
5641×
        401 => "Unauthorized",
5651×
        403 => "Forbidden",
5660×
        404 => "Not Found",
5670×
        503 => "Service Unavailable",
5680×
        _ => "Error",
569
    };
57080×
    write!(conn, "HTTP/1.1 {code} {reason}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\n\r\n", body.len())?;
57180×
    conn.write_all(body)?;
57280×
    conn.flush()
57380×
}
574
575
/// One connection: requests one after another while the peer keeps it.
57625×
fn serve_conn<S: Read + Write>(conn: S, state: &State, busy: bool) -> std::io::Result<()> {
57725×
    let mut r = BufReader::new(conn);
578
    loop {
579105×
        let mut line = String::new();
580105×
        if r.read_line(&mut line)? == 0 {
5810×
            return Ok(());
58280×
        }
58380×
        let mut parts = line.split_whitespace();
58480×
        let (method, path) = (parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string());
58580×
        let (mut length, mut auth, mut close) = (0usize, String::new(), false);
586
        loop {
587479×
            let mut h = String::new();
588479×
            if r.read_line(&mut h)? == 0 {
5890×
                return Ok(());
590479×
            }
591479×
            let h = h.trim_end();
592479×
            if h.is_empty() {
59380×
                break;
594399×
            }
595399×
            if let Some((k, v)) = h.split_once(':') {
596399×
                match k.trim().to_ascii_lowercase().as_str() {
597399×
                    "content-length" => length = v.trim().parse().unwrap_or(usize::MAX),
598395×
                    "authorization" => auth = v.trim().to_string(),
599320×
                    "connection" => close = v.trim().eq_ignore_ascii_case("close"),
600320×
                    _ => {}
601
                }
6020×
            }
603
        }
60480×
        if length > MAX_BODY {
6050×
            return respond(r.get_mut(), 400, "text/plain", b"too large");
60680×
        }
60780×
        let mut body = vec![0; length];
60880×
        r.read_exact(&mut body)?;
60980×
        if busy {
6100×
            return respond(r.get_mut(), 503, "text/plain", b"busy: ask again shortly");
61180×
        }
61280×
        let (code, kind, reply) = route(state, &method, &path, &auth, &body);
61380×
        if code == 200 && kind == "application/octet-stream" {
61466×
            throttle(state, reply.len());
61566×
        }
61680×
        respond(r.get_mut(), code, kind, &reply)?;
61780×
        if close {
6180×
            return Ok(());
61980×
        }
620
    }
62125×
}
622
62380×
fn route(state: &State, method: &str, path: &str, auth: &str, body: &[u8]) -> (u16, &'static str, Vec<u8>) {
62480×
    let text = |code, s: &str| (code, "text/plain", s.as_bytes().to_vec());
62580×
    if method == "POST" && path == "/swarm/v1/hello" {
6264×
        return match admit(state, &String::from_utf8_lossy(body)) {
6273×
            Ok(session) => (200, "application/json", serde_json::json!({ "session": session }).to_string().into_bytes()),
6281×
            Err(why) => text(403, &why),
629
        };
63076×
    }
63176×
    let session = auth.strip_prefix("Bearer ").unwrap_or("");
63276×
    let admitted = {
63376×
        let mut sessions = state.sessions.lock().unwrap();
63476×
        let now = now_ms();
63576×
        sessions.retain(|_, (_, until)| *until > now);
63676×
        sessions.contains_key(session)
637
    };
63876×
    if !admitted {
6391×
        return text(401, "show a ticket at /swarm/v1/hello first");
64075×
    }
64175×
    let rest = path.strip_prefix("/swarm/v1/").unwrap_or("");
64275×
    match (method, rest.split('/').collect::<Vec<_>>().as_slice()) {
64375×
        ("GET", ["images", image, "have"]) => match state.offers.lock().unwrap().get(*image) {
6441×
            Some(Offer { bits: None, .. }) => (200, "text/plain", b"complete".to_vec()),
6458×
            Some(Offer { bits: Some(b), fetching }) => (200, "application/x-have", [b.to_bytes(), fetching.to_bytes()].concat()),
6460×
            None => text(404, "this runner does not hold that image"),
647
        },
64866×
        ("GET", ["chunks", chunk]) if is_hash(chunk) => match read_chunk(state, chunk) {
64966×
            Some(b) => (200, "application/octet-stream", b),
6500×
            None => text(404, "no such chunk here"),
651
        },
6520×
        _ => text(404, "not a swarm path"),
653
    }
65480×
}
655
656
/// Admit a peer that shows a ticket from this runner's hub, for this
657
/// site: a session until the ticket expires.
6584×
fn admit(state: &State, ticket: &str) -> std::result::Result<String, String> {
6594×
    let hub = {
6604×
        let mut hub = state.hub.lock().unwrap();
6614×
        if hub.is_none() {
6620×
            *hub = state.client.hub_identity().ok();
6634×
        }
6644×
        hub.clone().ok_or("this runner cannot reach its hub to check tickets")?
665
    };
6664×
    let t = Ticket::open(ticket, &hub, now_ms())?;
6674×
    if t.site != state.site {
6681×
        return Err(format!("this runner is in site {:?}, the ticket's runner in {:?}", state.site, t.site));
6693×
    }
6703×
    let session = random_token().map_err(|e| format!("make a session: {e}"))?;
6713×
    state.sessions.lock().unwrap().insert(session.clone(), (t.name, t.expires_ms));
6723×
    Ok(session)
6734×
}
674
6753×
fn random_token() -> std::io::Result<String> {
6763×
    let mut b = [0u8; 32];
6773×
    std::fs::File::open("/dev/urandom")?.read_exact(&mut b)?;
6783×
    Ok(hex::encode(b))
6793×
}
680
68166×
fn read_chunk(state: &State, hash: &str) -> Option<Vec<u8>> {
68266×
    if let Ok(b) = std::fs::read(cache_path(&state.root.join("chunks"), hash)) {
68328×
        return Some(b);
68438×
    }
68538×
    let (path, at, len) = state.located.lock().unwrap().get(hash).cloned()?;
68638×
    let mut f = std::fs::File::open(path).ok()?;
68738×
    f.seek(SeekFrom::Start(at)).ok()?;
68838×
    let mut b = vec![0; len as usize];
68938×
    f.read_exact(&mut b).ok()?;
69038×
    Some(b)
69166×
}
692
693
/// Hold back until sending `n` more bytes keeps within the upload rate.
69468×
fn throttle(state: &State, n: usize) {
69568×
    let wait = {
69668×
        let mut b = state.bucket.lock().unwrap();
69768×
        let now = Instant::now();
69868×
        b.0 = (b.0 + now.duration_since(b.1).as_secs_f64() * state.rate).min(state.rate);
69968×
        b.1 = now;
70068×
        b.0 -= n as f64;
70168×
        if b.0 < 0.0 {
7021×
            -b.0 / state.rate
703
        } else {
70467×
            0.0
705
        }
706
    };
70768×
    if wait > 0.0 {
7081×
        std::thread::sleep(Duration::from_secs_f64(wait));
70967×
    }
71068×
}
711
712
#[cfg(test)]
713
mod tests {
714
    use super::*;
715
    use oxy_core::identity::SecretKey;
716
717
    /// A hub that holds one image and runs the tracker: it serves the
718
    /// manifest and chunks (counting the chunks), and answers announces
719
    /// with every other runner as a peer and a ticket for the caller,
720
    /// known by its token.
721
    struct FakeHub {
722
        url: String,
723
        chunks_served: Arc<AtomicUsize>,
724
        runners: Arc<Mutex<Vec<(String, SwarmPeer)>>>,
725
    }
726
7273×
    fn fake_hub(m: Manifest, image: Vec<u8>, site: &str) -> FakeHub {
7283×
        slow_hub(m, image, site, Duration::ZERO)
7293×
    }
730
731
    /// A hub that takes `delay` over each chunk.
7324×
    fn slow_hub(m: Manifest, image: Vec<u8>, site: &str, delay: Duration) -> FakeHub {
7334×
        let sk = SecretKey::generate();
7344×
        let id = sk.identity("hub", "hub@example", 1);
7354×
        let key = id.hash();
7364×
        let mut chunks = HashMap::new();
7374×
        let mut at = 0usize;
738152×
        for c in &m.chunks {
739152×
            chunks.insert(c.hash().to_string(), image[at..at + c.len()].to_vec());
740152×
            at += c.len();
741152×
        }
7424×
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
7434×
        let url = format!("http://{}", listener.local_addr().unwrap());
7444×
        let served = Arc::new(AtomicUsize::new(0));
7454×
        let runners: Arc<Mutex<Vec<(String, SwarmPeer)>>> = Arc::new(Mutex::new(Vec::new()));
7464×
        let (count, known, site) = (served.clone(), runners.clone(), site.to_string());
7474×
        let shared = Arc::new((m, chunks, id, key, sk));
7484×
        std::thread::spawn(move || {
74945×
            for conn in listener.incoming() {
75045×
                let Ok(conn) = conn else { continue };
75145×
                let (shared, count, known, site) = (shared.clone(), count.clone(), known.clone(), site.clone());
75245×
                std::thread::spawn(move || {
75345×
                    let (m, chunks, id, key, sk) = &*shared;
75445×
                    let mut out = conn.try_clone().unwrap();
75545×
                    let mut r = BufReader::new(conn);
756
                    loop {
757192×
                        let mut line = String::new();
758192×
                        if r.read_line(&mut line).unwrap_or(0) == 0 {
75936×
                            return;
760156×
                        }
761156×
                        let path = line.split_whitespace().nth(1).unwrap_or("").to_string();
762156×
                        let (mut len, mut token) = (0, String::new());
763
                        loop {
764907×
                            let mut h = String::new();
765907×
                            r.read_line(&mut h).unwrap();
766907×
                            let h = h.trim_end().to_string();
767907×
                            if h.is_empty() {
768156×
                                break;
769751×
                            }
770751×
                            let lower = h.to_ascii_lowercase();
771751×
                            if let Some(v) = lower.strip_prefix("content-length:") {
7724×
                                len = v.trim().parse().unwrap();
773747×
                            }
774751×
                            if let Some(v) = h.strip_prefix("Authorization: Bearer ") {
775133×
                                token = v.to_string();
776618×
                            }
777
                        }
778156×
                        let mut body = vec![0; len];
779156×
                        r.read_exact(&mut body).unwrap();
780156×
                        let (code, reply): (u16, Vec<u8>) = if path == "/.well-known/oxyhub-key" {
7819×
                            (200, id.encode())
782147×
                        } else if path == format!("/api/v1/flow/images/{}", m.hash) {
7835×
                            (200, serde_json::to_vec(m).unwrap())
784142×
                        } else if let Some(c) = path.strip_prefix("/api/v1/flow/images/chunks/") {
785131×
                            count.fetch_add(1, Ordering::Relaxed);
786131×
                            std::thread::sleep(delay);
787131×
                            (200, chunks[c].clone())
78811×
                        } else if path == "/api/v1/flow/swarm/announce" {
78911×
                            let runners = known.lock().unwrap();
79017×
                            let me = runners.iter().find(|(t, _)| *t == token).map(|(_, p)| p.clone()).unwrap();
79111×
                            let ticket = Ticket {
79211×
                                hub: key.to_hex(),
79311×
                                runner: me.runner,
79411×
                                name: me.name.clone(),
79511×
                                site: site.clone(),
79611×
                                expires_ms: now_ms() + 60_000,
79711×
                            };
79820×
                            let peers: Vec<SwarmPeer> = runners.iter().filter(|(t, _)| *t != token).map(|(_, p)| p.clone()).collect();
79911×
                            let a = flow_core::api::Announced { ticket: ticket.seal(key, sk), peers, interval_s: 5 };
80011×
                            (200, serde_json::to_vec(&a).unwrap())
801
                        } else {
8020×
                            (404, b"no".to_vec())
803
                        };
804156×
                        let head = format!("HTTP/1.1 {code} X\r\nContent-Length: {}\r\n\r\n", reply.len());
805156×
                        if out.write_all(head.as_bytes()).and_then(|_| out.write_all(&reply)).is_err() {
8069×
                            return;
807147×
                        }
808
                    }
80945×
                });
810
            }
8114×
        });
8124×
        FakeHub { url, chunks_served: served, runners }
8134×
    }
814
8158×
    fn free_port() -> u16 {
8168×
        TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
8178×
    }
818
819
    /// A runner home in `dir` with a swarm on `port`, known to `hub` by
820
    /// `token` as runner `id`.
8218×
    fn runner(dir: &Path, hub: &FakeHub, id: i64, token: &str, port: u16, site: &str) -> (Home, Swarm) {
8228×
        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
        );
8268×
        std::fs::write(dir.join(crate::home::RUNNER_TOML), toml).unwrap();
8278×
        let home = Home::locate(Some(dir)).unwrap();
8288×
        let cfg = home.config.runner.clone().unwrap();
8298×
        let swarm = Swarm::start(&home, &cfg).unwrap().unwrap();
8308×
        hub.runners.lock().unwrap().push((
8318×
            token.into(),
8328×
            SwarmPeer {
8338×
                runner: id,
8348×
                name: format!("r{id}"),
8358×
                addr: format!("127.0.0.1:{port}"),
8368×
                cert: swarm.addr.cert.clone(),
8378×
                complete: true,
8388×
            },
8398×
        ));
8408×
        (home, swarm)
8418×
    }
842
8434×
    fn image() -> Vec<u8> {
844
        // 3 MiB of varied bytes, with a run of zeros the swarm never moves.
8454×
        let mut x = 0x9e3779b97f4a7c15u64;
8464×
        let mut b: Vec<u8> = (0..3 << 20)
84712582912×
            .map(|_| {
84812582912×
                x ^= x << 13;
84912582912×
                x ^= x >> 7;
85012582912×
                x ^= x << 17;
85112582912×
                x as u8
85212582912×
            })
8534×
            .collect();
8544×
        b[1 << 20..(1 << 20) + (300 << 10)].fill(0);
8554×
        b
8564×
    }
857
858
    #[test]
8591×
    fn a_runner_fetches_an_image_from_its_peer_and_not_the_hub() {
8601×
        let bytes = image();
86138×
        let m = jet::images::scan(&bytes[..], Some("x86_64".into()), |_, _| Ok(())).unwrap();
8621×
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
8631×
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
864
        // Runner A holds the image, imported rather than fetched: its swarm
865
        // scans it for a manifest before offering it.
8661×
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
8671×
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
8681×
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
8691×
        let deadline = Instant::now() + Duration::from_secs(20);
8704×
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
8713×
            assert!(Instant::now() < deadline, "runner A never offered its image");
8723×
            std::thread::sleep(Duration::from_millis(20));
873
        }
8741×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
8751×
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
8761×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes, "the image came back whole");
8771×
        assert_eq!(moved.peer_bytes, moved.bytes, "every chunk came from the peer");
8781×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), 0, "the hub sent no chunks");
8791×
        assert_eq!(moved.chunks, m.needed().len());
880
        // B now offers the whole image too, read out of its rootfs.
8811×
        assert!(matches!(b.state.offers.lock().unwrap().get(&m.hash), Some(Offer { bits: None, .. })));
8821×
    }
883
884
    #[test]
8851×
    fn runners_starting_together_share_what_each_has_fetched() {
8861×
        let bytes = image();
88738×
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
8881×
        let needed = m.needed().len();
889
        // A slow hub, as a remote one is: the runners have time to swap.
8901×
        let hub = slow_hub(m.clone(), bytes.clone(), "lab", Duration::from_millis(400));
8911×
        let (b_dir, c_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
8921×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
8931×
        let (c_home, c) = runner(c_dir.path(), &hub, 3, "tc", free_port(), "lab");
8941×
        let (mb, mc) = std::thread::scope(|s| {
8951×
            let fb = s.spawn(|| b.fetch(&b_home, &m.hash).unwrap().unwrap());
8961×
            let fc = s.spawn(|| c.fetch(&c_home, &m.hash).unwrap().unwrap());
8971×
            (fb.join().unwrap(), fc.join().unwrap())
8981×
        });
8991×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
9001×
        assert_eq!(std::fs::read(c_home.rootfs(&m.hash)).unwrap(), bytes);
9011×
        let served = hub.chunks_served.load(Ordering::Relaxed);
9021×
        assert!(served < 2 * needed, "the hub sent {served} chunks for two runners needing {needed} each");
9031×
        assert!(mb.peer_bytes + mc.peer_bytes > 0, "they shared");
9041×
    }
905
906
    #[test]
9071×
    fn without_peers_the_hub_sends_everything_and_a_ticket_from_another_site_is_refused() {
9081×
        let bytes = image();
90938×
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
9101×
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
9111×
        let dir = tempfile::tempdir().unwrap();
9121×
        let (home, s) = runner(dir.path(), &hub, 1, "t1", free_port(), "lab");
9131×
        let moved = s.fetch(&home, &m.hash).unwrap().unwrap();
9141×
        assert_eq!(moved.peer_bytes, 0);
9151×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len());
9161×
        assert_eq!(std::fs::read(home.rootfs(&m.hash)).unwrap(), bytes);
917
918
        // A runner in another site shows a ticket for "lab": refused.
9191×
        let other = tempfile::tempdir().unwrap();
9201×
        let port = free_port();
9211×
        let (_h, o) = runner(other.path(), &hub, 3, "t3", port, "elsewhere");
9221×
        let ticket = s.state.client.announce(&m.hash, "fetching").unwrap().ticket;
9231×
        let err = hello(&format!("127.0.0.1:{port}"), &o.addr.cert, &ticket).unwrap_err();
9241×
        assert!(format!("{err:#}").contains("HTTP 403"), "{err:#}");
925
        // And no session, no chunks.
9261×
        let url = format!("https://127.0.0.1:{port}/swarm/v1/chunks/{}", m.chunks[0].hash());
9271×
        assert!(matches!(peer_agent(&o.addr.cert).unwrap().get(&url).call(), Err(ureq::Error::Status(401, _))));
928
        // Nor any answer at all without TLS.
9291×
        assert!(ureq::get(&url.replacen("https", "http", 1)).call().is_err());
9301×
    }
931
932
    #[test]
9331×
    fn a_peer_with_another_certificate_is_not_trusted() {
9341×
        let bytes = image();
93538×
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
9361×
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
9371×
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
9381×
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
9391×
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
9401×
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
9414×
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
9423×
            std::thread::sleep(Duration::from_millis(20));
9433×
        }
9441×
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
945
        // The hub names A, but with B's certificate: whoever answers at A's
946
        // address is not who the hub vouched for.
9471×
        hub.runners.lock().unwrap()[0].1.cert = b.addr.cert.clone();
9481×
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
9491×
        assert_eq!(moved.peer_bytes, 0, "nothing was taken from the stranger");
9501×
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len());
9511×
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
952
        // The certificate is kept: the same runner, the same fingerprint.
9531×
        let mut cfg = b_home.config.runner.clone().unwrap();
9541×
        cfg.swarm.as_mut().unwrap().listen = "127.0.0.1:0".into();
9551×
        let again = Swarm::start(&b_home, &cfg).unwrap().unwrap();
9561×
        assert_eq!(again.addr.cert, b.addr.cert);
9571×
    }
958
959
    #[test]
9601×
    fn the_upload_rate_holds_back_a_burst() {
9611×
        let dir = tempfile::tempdir().unwrap();
9621×
        let (cert, key) = certificate(dir.path()).unwrap();
9631×
        let state = State {
9641×
            tls: server_tls(cert, key).unwrap(),
9651×
            root: PathBuf::new(),
9661×
            site: String::new(),
9671×
            client: Client::new("http://127.0.0.1:1", "t", None).unwrap(),
9681×
            hub: Mutex::new(None),
9691×
            offers: Mutex::new(HashMap::new()),
9701×
            located: Mutex::new(HashMap::new()),
9711×
            sessions: Mutex::new(HashMap::new()),
9721×
            conns: AtomicUsize::new(0),
9731×
            bucket: Mutex::new((1_000_000.0, Instant::now())),
9741×
            rate: 1_000_000.0,
9751×
        };
9761×
        let start = Instant::now();
9771×
        throttle(&state, 1_000_000);
9781×
        assert!(start.elapsed() < Duration::from_millis(100), "the first second's worth goes at once");
9791×
        throttle(&state, 300_000);
9801×
        assert!(start.elapsed() >= Duration::from_millis(250), "then it waits for the rate");
9811×
    }
982
}