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