OXYGENHUB
# 35cd8ccb29ea oxygenflow / crates / flow / src / swarm.rs
43.8 KiB · Rust · last changed Swarm: TLS between peers, pinned by the hub 24 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
            std::thread::scope(|s| {
264
                for _ in 0..HUB_WORKERS {
265
                    s.spawn(move || worker(Source::Hub));
266
                }
267
                let mut spawned = 0;
268
                let mut spawn_new = || {
269
                    let n = peers.lock().unwrap().len();
270
                    for p in spawned..n {
271
                        for _ in 0..PEER_WORKERS {
272
                            s.spawn(move || worker(Source::Peer(p)));
273
                        }
274
                    }
275
                    spawned = n;
276
                };
277
                spawn_new();
278
                // Keep the peers' maps fresh, and find new peers while
279
                // fetching, until every chunk is here.
280
                let mut asked_hub = Instant::now();
281
                let interval = Duration::from_secs(announced.as_ref().map(|a| a.interval_s).unwrap_or(30).max(5));
282
                let mut last = Instant::now();
283
                while picker.lock().unwrap().missing() > 0 && failed.lock().unwrap().is_none() {
284
                    std::thread::sleep(Duration::from_millis(100));
285
                    if last.elapsed() < REFRESH {
286
                        continue;
287
                    }
288
                    last = Instant::now();
289
                    let now: Vec<(usize, Arc<Peer>)> =
290
                        peers.lock().unwrap().iter().cloned().enumerate().filter(|(_, p)| p.alive.load(Ordering::Relaxed)).collect();
291
                    for (idx, p) in now {
292
                        match self.peer_have(&p, hash, needed.len()) {
293
                            Ok((bits, fetching)) => picker.lock().unwrap().peer_fetching(idx, bits, fetching),
294
                            Err(e) if e.is::<Busy>() => {}
295
                            Err(_) => {
296
                                p.alive.store(false, Ordering::Relaxed);
297
                                picker.lock().unwrap().drop_peer(idx);
298
                            }
299
                        }
300
                    }
301
                    if announced.is_some() && asked_hub.elapsed() >= interval {
302
                        asked_hub = Instant::now();
303
                        if let Ok(a) = client.announce(hash, "fetching") {
304
                            self.join(&a.peers, &a.ticket, hash, needed.len(), peers, picker);
305
                            spawn_new();
306
                        }
307
                    }
308
                }
309
            });
310
        }
311
        if let Some(e) = failed.into_inner().unwrap() {
312
            client.announce(hash, "stopped").ok();
313
            return Err(e);
314
        }
315
        write_image(home, &m)?;
316
        offer_whole(&self.state, &m, &home.rootfs(hash));
317
        client.announce(hash, "done").ok();
318
        Ok(Some(Moved {
319
            chunks: got.into_inner(),
320
            bytes: bytes.into_inner(),
321
            peer_bytes: peer_bytes.into_inner(),
322
            elapsed: start.elapsed(),
323
        }))
324
    }
325
326
    /// Admit this runner to the peers it does not know yet: a session from
327
    /// each, and what each has.
328
    fn join(&self, found: &[SwarmPeer], ticket: &str, hash: &str, len: usize, peers: &Mutex<Vec<Arc<Peer>>>, picker: &Mutex<Picker>) {
329
        for sp in found {
330
            if peers.lock().unwrap().iter().any(|p| p.runner == sp.runner) {
331
                continue;
332
            }
333
            if !is_hash(&sp.cert) {
334
                eprintln!("swarm: peer {} gave no certificate (an older flow); not asking it", sp.name);
335
                continue;
336
            }
337
            let joined = hello(&sp.addr, &sp.cert, ticket).and_then(|(agent, session)| {
338
                let p = Peer {
339
                    runner: sp.runner,
340
                    name: sp.name.clone(),
341
                    base: format!("https://{}/swarm/v1", sp.addr),
342
                    agent,
343
                    session,
344
                    alive: AtomicBool::new(true),
345
                };
346
                let (bits, fetching) = self.peer_have(&p, hash, len)?;
347
                Ok((p, bits, fetching))
348
            });
349
            match joined {
350
                Ok((p, bits, fetching)) => {
351
                    let mut list = peers.lock().unwrap();
352
                    picker.lock().unwrap().peer_fetching(list.len(), bits, fetching);
353
                    list.push(Arc::new(p));
354
                }
355
                Err(e) => eprintln!("swarm: peer {} ({}): {e:#}", sp.name, sp.addr),
356
            }
357
        }
358
    }
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).
363
    fn peer_have(&self, p: &Peer, hash: &str, len: usize) -> Result<(Bits, Bits)> {
364
        let url = format!("{}/images/{hash}/have", p.base);
365
        let r = match p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call() {
366
            Ok(r) => r,
367
            Err(ureq::Error::Status(404, _)) => return Ok((Bits::new(len), Bits::new(len))),
368
            Err(e) => return Err(peer_error(&url, e)),
369
        };
370
        let mut body = Vec::new();
371
        r.into_reader().take(len as u64 / 4 + 64).read_to_end(&mut body)?;
372
        if body == b"complete" {
373
            return Ok((Bits::full(len), Bits::new(len)));
374
        }
375
        let (have, fetching) = body.split_at(len.div_ceil(8).min(body.len()));
376
        Ok((Bits::from_bytes(len, have), Bits::from_bytes(len, fetching)))
377
    }
378
379
    fn peer_chunk(&self, p: &Peer, hash: &str) -> Result<Vec<u8>> {
380
        let url = format!("{}/chunks/{hash}", p.base);
381
        let r = p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call().map_err(|e| peer_error(&url, e))?;
382
        let mut out = Vec::new();
383
        r.into_reader().take(oxy_core::chunk::MAX_SIZE as u64 + 1).read_to_end(&mut out)?;
384
        Ok(out)
385
    }
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 {
404
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405
        f.write_str("the peer is busy")
406
    }
407
}
408
409
impl std::error::Error for Busy {}
410
411
fn peer_error(url: &str, e: ureq::Error) -> anyhow::Error {
412
    match e {
413
        ureq::Error::Status(503, _) => anyhow::Error::new(Busy),
414
        ureq::Error::Status(code, r) => anyhow::anyhow!("{url}: HTTP {code}: {}", r.into_string().unwrap_or_default().trim()),
415
        e => anyhow::anyhow!("{url}: {e}"),
416
    }
417
}
418
419
/// Show `ticket` to the peer at `addr`, trusting only the certificate
420
/// `cert`: a connection to it and a session.
421
fn hello(addr: &str, cert: &str, ticket: &str) -> Result<(ureq::Agent, String)> {
422
    let agent = peer_agent(cert)?;
423
    let url = format!("https://{addr}/swarm/v1/hello");
424
    let r: serde_json::Value = agent.post(&url).send_string(ticket).map_err(|e| peer_error(&url, e))?.into_json()?;
425
    let session = r["session"].as_str().map(String::from).context("the peer gave no session")?;
426
    Ok((agent, session))
427
}
428
429
fn provider() -> Arc<CryptoProvider> {
430
    Arc::new(rustls::crypto::ring::default_provider())
431
}
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.
435
fn peer_agent(cert: &str) -> Result<ureq::Agent> {
436
    let provider = provider();
437
    let cfg = rustls::ClientConfig::builder_with_provider(provider.clone())
438
        .with_safe_default_protocol_versions()?
439
        .dangerous()
440
        .with_custom_certificate_verifier(Arc::new(Pinned { cert: cert.to_string(), provider }))
441
        .with_no_client_auth();
442
    Ok(ureq::AgentBuilder::new().tls_config(Arc::new(cfg)).timeout_connect(Duration::from_secs(2)).timeout(Duration::from_secs(20)).build())
443
}
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 {
455
    fn verify_server_cert(
456
        &self,
457
        end_entity: &CertificateDer<'_>,
458
        _intermediates: &[CertificateDer<'_>],
459
        _name: &ServerName<'_>,
460
        _ocsp: &[u8],
461
        _now: UnixTime,
462
    ) -> std::result::Result<ServerCertVerified, rustls::Error> {
463
        if blake3::hash(end_entity.as_ref()).to_hex().as_str() == self.cert {
464
            Ok(ServerCertVerified::assertion())
465
        } else {
466
            Err(rustls::Error::General("not the peer the hub named: its certificate is another".into()))
467
        }
468
    }
469
470
    fn verify_tls12_signature(
471
        &self,
472
        message: &[u8],
473
        cert: &CertificateDer<'_>,
474
        dss: &DigitallySignedStruct,
475
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
476
        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
477
    }
478
479
    fn verify_tls13_signature(
480
        &self,
481
        message: &[u8],
482
        cert: &CertificateDer<'_>,
483
        dss: &DigitallySignedStruct,
484
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
485
        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
486
    }
487
488
    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
489
        self.provider.signature_verification_algorithms.supported_schemes()
490
    }
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.
496
fn certificate(root: &Path) -> Result<(Vec<u8>, Vec<u8>)> {
497
    let dir = root.join("swarm");
498
    let (cert_path, key_path) = (dir.join("cert.der"), dir.join("key.der"));
499
    if let (Ok(c), Ok(k)) = (std::fs::read(&cert_path), std::fs::read(&key_path)) {
500
        return Ok((c, k));
501
    }
502
    std::fs::create_dir_all(&dir)?;
503
    let made = rcgen::generate_simple_self_signed(vec!["flow-swarm".to_string()]).context("make the swarm's certificate")?;
504
    let (cert, key) = (made.cert.der().to_vec(), made.signing_key.serialize_der());
505
    {
506
        use std::os::unix::fs::OpenOptionsExt;
507
        let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(&key_path)?;
508
        f.write_all(&key)?;
509
    }
510
    std::fs::write(&cert_path, &cert)?;
511
    Ok((cert, key))
512
}
513
514
fn server_tls(cert: Vec<u8>, key: Vec<u8>) -> Result<Arc<rustls::ServerConfig>> {
515
    let cfg = rustls::ServerConfig::builder_with_provider(provider())
516
        .with_safe_default_protocol_versions()?
517
        .with_no_client_auth()
518
        .with_single_cert(vec![CertificateDer::from(cert)], PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)))
519
        .context("the swarm's certificate")?;
520
    Ok(Arc::new(cfg))
521
}
522
523
/// Offer a whole image: every chunk of it can be read out of `path`.
524
fn offer_whole(state: &State, m: &Manifest, path: &Path) {
525
    let mut located = state.located.lock().unwrap();
526
    let mut at = 0u64;
527
    for c in &m.chunks {
528
        if !c.is_zeros() {
529
            located.entry(c.hash().to_string()).or_insert_with(|| (path.to_path_buf(), at, c.1));
530
        }
531
        at += c.1 as u64;
532
    }
533
    state.offers.lock().unwrap().insert(m.hash.clone(), Offer { bits: None, fetching: Bits::new(0) });
534
}
535
536
fn accept(listener: TcpListener, state: Arc<State>) {
537
    for conn in listener.incoming() {
538
        let Ok(conn) = conn else { continue };
539
        if state.conns.load(Ordering::Relaxed) >= MAX_OPEN {
540
            continue;
541
        }
542
        let state = state.clone();
543
        std::thread::spawn(move || {
544
            // Past the limit a peer is still answered, with 503, so it
545
            // comes back rather than giving up on this runner.
546
            let busy = state.conns.fetch_add(1, Ordering::Relaxed) >= MAX_CONNS;
547
            serve_tls(conn, &state, busy).ok();
548
            state.conns.fetch_sub(1, Ordering::Relaxed);
549
        });
550
    }
551
}
552
553
fn serve_tls(conn: TcpStream, state: &State, busy: bool) -> std::io::Result<()> {
554
    conn.set_read_timeout(Some(Duration::from_secs(30)))?;
555
    conn.set_write_timeout(Some(Duration::from_secs(30)))?;
556
    conn.set_nodelay(true)?;
557
    let tls = rustls::ServerConnection::new(state.tls.clone()).map_err(std::io::Error::other)?;
558
    serve_conn(rustls::StreamOwned::new(tls, conn), state, busy)
559
}
560
561
fn respond(conn: &mut impl Write, code: u16, kind: &str, body: &[u8]) -> std::io::Result<()> {
562
    let reason = match code {
563
        200 => "OK",
564
        400 => "Bad Request",
565
        401 => "Unauthorized",
566
        403 => "Forbidden",
567
        404 => "Not Found",
568
        503 => "Service Unavailable",
569
        _ => "Error",
570
    };
571
    write!(conn, "HTTP/1.1 {code} {reason}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\n\r\n", body.len())?;
572
    conn.write_all(body)?;
573
    conn.flush()
574
}
575
576
/// One connection: requests one after another while the peer keeps it.
577
fn serve_conn<S: Read + Write>(conn: S, state: &State, busy: bool) -> std::io::Result<()> {
578
    let mut r = BufReader::new(conn);
579
    loop {
580
        let mut line = String::new();
581
        if r.read_line(&mut line)? == 0 {
582
            return Ok(());
583
        }
584
        let mut parts = line.split_whitespace();
585
        let (method, path) = (parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string());
586
        let (mut length, mut auth, mut close) = (0usize, String::new(), false);
587
        loop {
588
            let mut h = String::new();
589
            if r.read_line(&mut h)? == 0 {
590
                return Ok(());
591
            }
592
            let h = h.trim_end();
593
            if h.is_empty() {
594
                break;
595
            }
596
            if let Some((k, v)) = h.split_once(':') {
597
                match k.trim().to_ascii_lowercase().as_str() {
598
                    "content-length" => length = v.trim().parse().unwrap_or(usize::MAX),
599
                    "authorization" => auth = v.trim().to_string(),
600
                    "connection" => close = v.trim().eq_ignore_ascii_case("close"),
601
                    _ => {}
602
                }
603
            }
604
        }
605
        if length > MAX_BODY {
606
            return respond(r.get_mut(), 400, "text/plain", b"too large");
607
        }
608
        let mut body = vec![0; length];
609
        r.read_exact(&mut body)?;
610
        if busy {
611
            return respond(r.get_mut(), 503, "text/plain", b"busy: ask again shortly");
612
        }
613
        let (code, kind, reply) = route(state, &method, &path, &auth, &body);
614
        if code == 200 && kind == "application/octet-stream" {
615
            throttle(state, reply.len());
616
        }
617
        respond(r.get_mut(), code, kind, &reply)?;
618
        if close {
619
            return Ok(());
620
        }
621
    }
622
}
623
624
fn route(state: &State, method: &str, path: &str, auth: &str, body: &[u8]) -> (u16, &'static str, Vec<u8>) {
625
    let text = |code, s: &str| (code, "text/plain", s.as_bytes().to_vec());
626
    if method == "POST" && path == "/swarm/v1/hello" {
627
        return match admit(state, &String::from_utf8_lossy(body)) {
628
            Ok(session) => (200, "application/json", serde_json::json!({ "session": session }).to_string().into_bytes()),
629
            Err(why) => text(403, &why),
630
        };
631
    }
632
    let session = auth.strip_prefix("Bearer ").unwrap_or("");
633
    let admitted = {
634
        let mut sessions = state.sessions.lock().unwrap();
635
        let now = now_ms();
636
        sessions.retain(|_, (_, until)| *until > now);
637
        sessions.contains_key(session)
638
    };
639
    if !admitted {
640
        return text(401, "show a ticket at /swarm/v1/hello first");
641
    }
642
    let rest = path.strip_prefix("/swarm/v1/").unwrap_or("");
643
    match (method, rest.split('/').collect::<Vec<_>>().as_slice()) {
644
        ("GET", ["images", image, "have"]) => match state.offers.lock().unwrap().get(*image) {
645
            Some(Offer { bits: None, .. }) => (200, "text/plain", b"complete".to_vec()),
646
            Some(Offer { bits: Some(b), fetching }) => (200, "application/x-have", [b.to_bytes(), fetching.to_bytes()].concat()),
647
            None => text(404, "this runner does not hold that image"),
648
        },
649
        ("GET", ["chunks", chunk]) if is_hash(chunk) => match read_chunk(state, chunk) {
650
            Some(b) => (200, "application/octet-stream", b),
651
            None => text(404, "no such chunk here"),
652
        },
653
        _ => text(404, "not a swarm path"),
654
    }
655
}
656
657
/// Admit a peer that shows a ticket from this runner's hub, for this
658
/// site: a session until the ticket expires.
659
fn admit(state: &State, ticket: &str) -> std::result::Result<String, String> {
660
    let hub = {
661
        let mut hub = state.hub.lock().unwrap();
662
        if hub.is_none() {
663
            *hub = state.client.hub_identity().ok();
664
        }
665
        hub.clone().ok_or("this runner cannot reach its hub to check tickets")?
666
    };
667
    let t = Ticket::open(ticket, &hub, now_ms())?;
668
    if t.site != state.site {
669
        return Err(format!("this runner is in site {:?}, the ticket's runner in {:?}", state.site, t.site));
670
    }
671
    let session = random_token().map_err(|e| format!("make a session: {e}"))?;
672
    state.sessions.lock().unwrap().insert(session.clone(), (t.name, t.expires_ms));
673
    Ok(session)
674
}
675
676
fn random_token() -> std::io::Result<String> {
677
    let mut b = [0u8; 32];
678
    std::fs::File::open("/dev/urandom")?.read_exact(&mut b)?;
679
    Ok(hex::encode(b))
680
}
681
682
fn read_chunk(state: &State, hash: &str) -> Option<Vec<u8>> {
683
    if let Ok(b) = std::fs::read(cache_path(&state.root.join("chunks"), hash)) {
684
        return Some(b);
685
    }
686
    let (path, at, len) = state.located.lock().unwrap().get(hash).cloned()?;
687
    let mut f = std::fs::File::open(path).ok()?;
688
    f.seek(SeekFrom::Start(at)).ok()?;
689
    let mut b = vec![0; len as usize];
690
    f.read_exact(&mut b).ok()?;
691
    Some(b)
692
}
693
694
/// Hold back until sending `n` more bytes keeps within the upload rate.
695
fn throttle(state: &State, n: usize) {
696
    let wait = {
697
        let mut b = state.bucket.lock().unwrap();
698
        let now = Instant::now();
699
        b.0 = (b.0 + now.duration_since(b.1).as_secs_f64() * state.rate).min(state.rate);
700
        b.1 = now;
701
        b.0 -= n as f64;
702
        if b.0 < 0.0 {
703
            -b.0 / state.rate
704
        } else {
705
            0.0
706
        }
707
    };
708
    if wait > 0.0 {
709
        std::thread::sleep(Duration::from_secs_f64(wait));
710
    }
711
}
712
713
#[cfg(test)]
714
mod tests {
715
    use super::*;
716
    use oxy_core::identity::SecretKey;
717
718
    /// A hub that holds one image and runs the tracker: it serves the
719
    /// manifest and chunks (counting the chunks), and answers announces
720
    /// with every other runner as a peer and a ticket for the caller,
721
    /// known by its token.
722
    struct FakeHub {
723
        url: String,
724
        chunks_served: Arc<AtomicUsize>,
725
        runners: Arc<Mutex<Vec<(String, SwarmPeer)>>>,
726
    }
727
728
    fn fake_hub(m: Manifest, image: Vec<u8>, site: &str) -> FakeHub {
729
        slow_hub(m, image, site, Duration::ZERO)
730
    }
731
732
    /// A hub that takes `delay` over each chunk.
733
    fn slow_hub(m: Manifest, image: Vec<u8>, site: &str, delay: Duration) -> FakeHub {
734
        let sk = SecretKey::generate();
735
        let id = sk.identity("hub", "hub@example", 1);
736
        let key = id.hash();
737
        let mut chunks = HashMap::new();
738
        let mut at = 0usize;
739
        for c in &m.chunks {
740
            chunks.insert(c.hash().to_string(), image[at..at + c.len()].to_vec());
741
            at += c.len();
742
        }
743
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
744
        let url = format!("http://{}", listener.local_addr().unwrap());
745
        let served = Arc::new(AtomicUsize::new(0));
746
        let runners: Arc<Mutex<Vec<(String, SwarmPeer)>>> = Arc::new(Mutex::new(Vec::new()));
747
        let (count, known, site) = (served.clone(), runners.clone(), site.to_string());
748
        let shared = Arc::new((m, chunks, id, key, sk));
749
        std::thread::spawn(move || {
750
            for conn in listener.incoming() {
751
                let Ok(conn) = conn else { continue };
752
                let (shared, count, known, site) = (shared.clone(), count.clone(), known.clone(), site.clone());
753
                std::thread::spawn(move || {
754
                    let (m, chunks, id, key, sk) = &*shared;
755
                    let mut out = conn.try_clone().unwrap();
756
                    let mut r = BufReader::new(conn);
757
                    loop {
758
                        let mut line = String::new();
759
                        if r.read_line(&mut line).unwrap_or(0) == 0 {
760
                            return;
761
                        }
762
                        let path = line.split_whitespace().nth(1).unwrap_or("").to_string();
763
                        let (mut len, mut token) = (0, String::new());
764
                        loop {
765
                            let mut h = String::new();
766
                            r.read_line(&mut h).unwrap();
767
                            let h = h.trim_end().to_string();
768
                            if h.is_empty() {
769
                                break;
770
                            }
771
                            let lower = h.to_ascii_lowercase();
772
                            if let Some(v) = lower.strip_prefix("content-length:") {
773
                                len = v.trim().parse().unwrap();
774
                            }
775
                            if let Some(v) = h.strip_prefix("Authorization: Bearer ") {
776
                                token = v.to_string();
777
                            }
778
                        }
779
                        let mut body = vec![0; len];
780
                        r.read_exact(&mut body).unwrap();
781
                        let (code, reply): (u16, Vec<u8>) = if path == "/.well-known/oxyhub-key" {
782
                            (200, id.encode())
783
                        } else if path == format!("/api/v1/flow/images/{}", m.hash) {
784
                            (200, serde_json::to_vec(m).unwrap())
785
                        } else if let Some(c) = path.strip_prefix("/api/v1/flow/images/chunks/") {
786
                            count.fetch_add(1, Ordering::Relaxed);
787
                            std::thread::sleep(delay);
788
                            (200, chunks[c].clone())
789
                        } else if path == "/api/v1/flow/swarm/announce" {
790
                            let runners = known.lock().unwrap();
791
                            let me = runners.iter().find(|(t, _)| *t == token).map(|(_, p)| p.clone()).unwrap();
792
                            let ticket = Ticket {
793
                                hub: key.to_hex(),
794
                                runner: me.runner,
795
                                name: me.name.clone(),
796
                                site: site.clone(),
797
                                expires_ms: now_ms() + 60_000,
798
                            };
799
                            let peers: Vec<SwarmPeer> = runners.iter().filter(|(t, _)| *t != token).map(|(_, p)| p.clone()).collect();
800
                            let a = flow_core::api::Announced { ticket: ticket.seal(key, sk), peers, interval_s: 5 };
801
                            (200, serde_json::to_vec(&a).unwrap())
802
                        } else {
803
                            (404, b"no".to_vec())
804
                        };
805
                        let head = format!("HTTP/1.1 {code} X\r\nContent-Length: {}\r\n\r\n", reply.len());
806
                        if out.write_all(head.as_bytes()).and_then(|_| out.write_all(&reply)).is_err() {
807
                            return;
808
                        }
809
                    }
810
                });
811
            }
812
        });
813
        FakeHub { url, chunks_served: served, runners }
814
    }
815
816
    fn free_port() -> u16 {
817
        TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
818
    }
819
820
    /// A runner home in `dir` with a swarm on `port`, known to `hub` by
821
    /// `token` as runner `id`.
822
    fn runner(dir: &Path, hub: &FakeHub, id: i64, token: &str, port: u16, site: &str) -> (Home, Swarm) {
823
        let toml = format!(
824
            "hub = \"{}\"\ntoken = \"{token}\"\nname = \"r{id}\"\n[swarm]\nlisten = \"127.0.0.1:{port}\"\nsite = \"{site}\"\n",
825
            hub.url
826
        );
827
        std::fs::write(dir.join(crate::home::RUNNER_TOML), toml).unwrap();
828
        let home = Home::locate(Some(dir)).unwrap();
829
        let cfg = home.config.runner.clone().unwrap();
830
        let swarm = Swarm::start(&home, &cfg).unwrap().unwrap();
831
        hub.runners.lock().unwrap().push((
832
            token.into(),
833
            SwarmPeer {
834
                runner: id,
835
                name: format!("r{id}"),
836
                addr: format!("127.0.0.1:{port}"),
837
                cert: swarm.addr.cert.clone(),
838
                complete: true,
839
            },
840
        ));
841
        (home, swarm)
842
    }
843
844
    fn image() -> Vec<u8> {
845
        // 3 MiB of varied bytes, with a run of zeros the swarm never moves.
846
        let mut x = 0x9e3779b97f4a7c15u64;
847
        let mut b: Vec<u8> = (0..3 << 20)
848
            .map(|_| {
849
                x ^= x << 13;
850
                x ^= x >> 7;
851
                x ^= x << 17;
852
                x as u8
853
            })
854
            .collect();
855
        b[1 << 20..(1 << 20) + (300 << 10)].fill(0);
856
        b
857
    }
858
859
    #[test]
860
    fn a_runner_fetches_an_image_from_its_peer_and_not_the_hub() {
861
        let bytes = image();
862
        let m = jet::images::scan(&bytes[..], Some("x86_64".into()), |_, _| Ok(())).unwrap();
863
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
864
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
865
        // Runner A holds the image, imported rather than fetched: its swarm
866
        // scans it for a manifest before offering it.
867
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
868
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
869
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
870
        let deadline = Instant::now() + Duration::from_secs(20);
871
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
872
            assert!(Instant::now() < deadline, "runner A never offered its image");
873
            std::thread::sleep(Duration::from_millis(20));
874
        }
875
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
876
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
877
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes, "the image came back whole");
878
        assert_eq!(moved.peer_bytes, moved.bytes, "every chunk came from the peer");
879
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), 0, "the hub sent no chunks");
880
        assert_eq!(moved.chunks, m.needed().len());
881
        // B now offers the whole image too, read out of its rootfs.
882
        assert!(matches!(b.state.offers.lock().unwrap().get(&m.hash), Some(Offer { bits: None, .. })));
883
    }
884
885
    #[test]
886
    fn runners_starting_together_share_what_each_has_fetched() {
887
        let bytes = image();
888
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
889
        let needed = m.needed().len();
890
        // A slow hub, as a remote one is: the runners have time to swap.
891
        let hub = slow_hub(m.clone(), bytes.clone(), "lab", Duration::from_millis(400));
892
        let (b_dir, c_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
893
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
894
        let (c_home, c) = runner(c_dir.path(), &hub, 3, "tc", free_port(), "lab");
895
        let (mb, mc) = std::thread::scope(|s| {
896
            let fb = s.spawn(|| b.fetch(&b_home, &m.hash).unwrap().unwrap());
897
            let fc = s.spawn(|| c.fetch(&c_home, &m.hash).unwrap().unwrap());
898
            (fb.join().unwrap(), fc.join().unwrap())
899
        });
900
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
901
        assert_eq!(std::fs::read(c_home.rootfs(&m.hash)).unwrap(), bytes);
902
        let served = hub.chunks_served.load(Ordering::Relaxed);
903
        assert!(served < 2 * needed, "the hub sent {served} chunks for two runners needing {needed} each");
904
        assert!(mb.peer_bytes + mc.peer_bytes > 0, "they shared");
905
    }
906
907
    #[test]
908
    fn without_peers_the_hub_sends_everything_and_a_ticket_from_another_site_is_refused() {
909
        let bytes = image();
910
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
911
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
912
        let dir = tempfile::tempdir().unwrap();
913
        let (home, s) = runner(dir.path(), &hub, 1, "t1", free_port(), "lab");
914
        let moved = s.fetch(&home, &m.hash).unwrap().unwrap();
915
        assert_eq!(moved.peer_bytes, 0);
916
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len());
917
        assert_eq!(std::fs::read(home.rootfs(&m.hash)).unwrap(), bytes);
918
919
        // A runner in another site shows a ticket for "lab": refused.
920
        let other = tempfile::tempdir().unwrap();
921
        let port = free_port();
922
        let (_h, o) = runner(other.path(), &hub, 3, "t3", port, "elsewhere");
923
        let ticket = s.state.client.announce(&m.hash, "fetching").unwrap().ticket;
924
        let err = hello(&format!("127.0.0.1:{port}"), &o.addr.cert, &ticket).unwrap_err();
925
        assert!(format!("{err:#}").contains("HTTP 403"), "{err:#}");
926
        // And no session, no chunks.
927
        let url = format!("https://127.0.0.1:{port}/swarm/v1/chunks/{}", m.chunks[0].hash());
928
        assert!(matches!(peer_agent(&o.addr.cert).unwrap().get(&url).call(), Err(ureq::Error::Status(401, _))));
929
        // Nor any answer at all without TLS.
930
        assert!(ureq::get(&url.replacen("https", "http", 1)).call().is_err());
931
    }
932
933
    #[test]
934
    fn a_peer_with_another_certificate_is_not_trusted() {
935
        let bytes = image();
936
        let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap();
937
        let hub = fake_hub(m.clone(), bytes.clone(), "lab");
938
        let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
939
        std::fs::create_dir_all(a_dir.path().join("cas")).unwrap();
940
        std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap();
941
        let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab");
942
        while !a.state.offers.lock().unwrap().contains_key(&m.hash) {
943
            std::thread::sleep(Duration::from_millis(20));
944
        }
945
        let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab");
946
        // The hub names A, but with B's certificate: whoever answers at A's
947
        // address is not who the hub vouched for.
948
        hub.runners.lock().unwrap()[0].1.cert = b.addr.cert.clone();
949
        let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap();
950
        assert_eq!(moved.peer_bytes, 0, "nothing was taken from the stranger");
951
        assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len());
952
        assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes);
953
        // The certificate is kept: the same runner, the same fingerprint.
954
        let mut cfg = b_home.config.runner.clone().unwrap();
955
        cfg.swarm.as_mut().unwrap().listen = "127.0.0.1:0".into();
956
        let again = Swarm::start(&b_home, &cfg).unwrap().unwrap();
957
        assert_eq!(again.addr.cert, b.addr.cert);
958
    }
959
960
    #[test]
961
    fn the_upload_rate_holds_back_a_burst() {
962
        let dir = tempfile::tempdir().unwrap();
963
        let (cert, key) = certificate(dir.path()).unwrap();
964
        let state = State {
965
            tls: server_tls(cert, key).unwrap(),
966
            root: PathBuf::new(),
967
            site: String::new(),
968
            client: Client::new("http://127.0.0.1:1", "t", None).unwrap(),
969
            hub: Mutex::new(None),
970
            offers: Mutex::new(HashMap::new()),
971
            located: Mutex::new(HashMap::new()),
972
            sessions: Mutex::new(HashMap::new()),
973
            conns: AtomicUsize::new(0),
974
            bucket: Mutex::new((1_000_000.0, Instant::now())),
975
            rate: 1_000_000.0,
976
        };
977
        let start = Instant::now();
978
        throttle(&state, 1_000_000);
979
        assert!(start.elapsed() < Duration::from_millis(100), "the first second's worth goes at once");
980
        throttle(&state, 300_000);
981
        assert!(start.elapsed() >= Duration::from_millis(250), "then it waits for the rate");
982
    }
983
}