feat(torad): upgrade librqbit to 9.0.0-rc.0 and add --bind-device
Pinned exactly (=9.0.0-rc.0) — no v9 stable exists yet, so a caret range would silently drift onto a future prerelease. Two source changes were needed for the upgrade: - `torrent_from_bytes_ext` moved to `librqbit_core::torrent_metainfo:: torrent_from_bytes` and no longer wraps the result in a `meta` field. - `peer_stats.live` / `.not_needed` are now `u32`, so the casts are redundant. v9 also adds `SessionOptions::bind_device_name`, which is the reason for the upgrade: it applies SO_BINDTODEVICE to every librqbit socket — peer connections, trackers, DHT and LSD. Binding those to a VPN interface means that when the interface goes away the sockets error out instead of falling back to the host route, giving a kernel-enforced kill switch. Exposed as --bind-device / TORAD_BIND_DEVICE; unset reproduces today's behaviour exactly. Note that `listen` stays at its default of None, so there is no listener and no uTP socket in either direction — torad is TCP-only and leech-only. That is unchanged from v8 but now written down, since incoming connections need NAT-PMP port forwarding that we have not built. Also drops torad's `nix` pin from 0.29 to 0.31.3 to match the sibling tora crate; the workspace was carrying three copies. Verified end-to-end against a real swarm rather than by compiling alone: a 755 MiB torrent added via HTTP .torrent URL, driven through pending -> downloading -> finished, with pause/resume on an active torrent, remove, and the notification stream all exercised. Evidence in .omo/evidence/task-5-torad-vpn-namespace.txt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,12 @@ struct Args {
|
||||
/// (e.g. `kill $(cat /tmp/torad.pid)`).
|
||||
#[arg(long, env = "TORAD_PID_FILE")]
|
||||
pid_file: Option<PathBuf>,
|
||||
|
||||
/// Network device to bind torrent sockets to (Linux: SO_BINDTODEVICE).
|
||||
/// When set, all BT peer, tracker, and DHT traffic egresses only through
|
||||
/// this interface. When unset, librqbit uses default routing.
|
||||
#[arg(long, env = "TORAD_BIND_DEVICE")]
|
||||
bind_device: Option<String>,
|
||||
}
|
||||
|
||||
fn default_socket_path() -> PathBuf {
|
||||
@@ -70,7 +76,7 @@ async fn main() -> Result<()> {
|
||||
.context("failed to connect to Postgres")?;
|
||||
info!("connected to Postgres");
|
||||
|
||||
let manager = TorrentManager::new(pool.clone(), download_dir)
|
||||
let manager = TorrentManager::new(pool.clone(), download_dir, args.bind_device)
|
||||
.await
|
||||
.context("failed to start torrent manager")?;
|
||||
manager.clone().spawn_poller();
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
//! parsing it) yields the correct hash even for spec-violating files.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use librqbit::torrent_from_bytes_ext;
|
||||
use librqbit_core::torrent_metainfo::torrent_from_bytes;
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Extracts the v1 info-hash from `.torrent` bytes.
|
||||
pub(super) fn from_bytes(bytes: &[u8]) -> Result<String> {
|
||||
// Fast path: spec-compliant torrent, librqbit's parser handles it.
|
||||
if let Ok(parsed) = torrent_from_bytes_ext::<Vec<u8>>(bytes) {
|
||||
return Ok(parsed.meta.info_hash.as_string());
|
||||
if let Ok(parsed) = torrent_from_bytes(bytes) {
|
||||
return Ok(parsed.info_hash.as_string());
|
||||
}
|
||||
// Fallback: locate the `info` value's byte span and SHA1 it directly.
|
||||
let span = locate_info_value(bytes)
|
||||
|
||||
@@ -5,7 +5,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use librqbit::api::TorrentIdOrHash;
|
||||
use librqbit::{AddTorrent, AddTorrentOptions, ManagedTorrent, Session, TorrentStatsState};
|
||||
use librqbit::{
|
||||
AddTorrent, AddTorrentOptions, ManagedTorrent, Session, SessionOptions, TorrentStatsState,
|
||||
};
|
||||
use librqbit_core::Id20;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
@@ -52,16 +54,36 @@ pub struct TorrentManager {
|
||||
}
|
||||
|
||||
impl TorrentManager {
|
||||
pub async fn new(pool: PgPool, download_dir: PathBuf) -> Result<Arc<Self>> {
|
||||
pub async fn new(
|
||||
pool: PgPool,
|
||||
download_dir: PathBuf,
|
||||
bind_device: Option<String>,
|
||||
) -> Result<Arc<Self>> {
|
||||
std::fs::create_dir_all(&download_dir).with_context(|| {
|
||||
format!(
|
||||
"failed to create download directory {}",
|
||||
download_dir.display()
|
||||
)
|
||||
})?;
|
||||
let session = Session::new(download_dir.clone())
|
||||
.await
|
||||
.context("failed to create librqbit session")?;
|
||||
// `bind_device` scopes every librqbit socket — peers, trackers, DHT, LSD —
|
||||
// to one interface via SO_BINDTODEVICE. When the interface goes away those
|
||||
// sockets error instead of falling back to the host route, which is the
|
||||
// kill switch VPN mode relies on. `None` reproduces the default behaviour:
|
||||
// `Session::new` is itself `new_with_opts(dir, SessionOptions::default())`.
|
||||
//
|
||||
// `listen` stays at its default of `None`, so there is no listener and no
|
||||
// uTP socket in either direction — torad is TCP-only and leech-only. That
|
||||
// is a deliberate limitation for now; incoming connections need NAT-PMP
|
||||
// port forwarding, which is deferred.
|
||||
let session = Session::new_with_opts(
|
||||
download_dir.clone(),
|
||||
SessionOptions {
|
||||
bind_device_name: bind_device,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to create librqbit session")?;
|
||||
let source = SourceResolver::new().context("failed to build source resolver")?;
|
||||
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
|
||||
Ok(Arc::new(Self {
|
||||
@@ -654,8 +676,8 @@ fn live_stats_from_stats(stats: &librqbit::TorrentStats) -> LiveTorrentStats {
|
||||
eta_seconds: eta,
|
||||
uploaded_bytes,
|
||||
upload_speed_bps: upload_bps,
|
||||
peers: live.snapshot.peer_stats.live as u32,
|
||||
seeds: live.snapshot.peer_stats.not_needed as u32,
|
||||
peers: live.snapshot.peer_stats.live,
|
||||
seeds: live.snapshot.peer_stats.not_needed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user