BREAKING: add uTP support to desktop

This commit is contained in:
Igor Katson 2025-02-03 19:01:12 +00:00
parent 789cc26a36
commit 3d31175a0c
No known key found for this signature in database
GPG Key ID: B4EC22B66D61A3F5
10 changed files with 147 additions and 125 deletions

View File

@ -14,11 +14,11 @@ webui-dev: webui-deps
export RQBIT_UPNP_SERVER_ENABLE ?= true export RQBIT_UPNP_SERVER_ENABLE ?= true
export RQBIT_UPNP_SERVER_FRIENDLY_NAME ?= rqbit-dev export RQBIT_UPNP_SERVER_FRIENDLY_NAME ?= rqbit-dev
export RQBIT_HTTP_API_LISTEN_ADDR ?= [::]:3030 export RQBIT_HTTP_API_LISTEN_ADDR ?= [::]:3030
export RQBIT_EXPERIMENTAL_UTP_LISTEN_ENABLE ?= true
export RQBIT_FASTRESUME = true export RQBIT_FASTRESUME = true
# Don't expose devserver # Don't expose devserver
export RQBIT_UPNP_PORT_FORWARD_DISABLE = true export RQBIT_LISTEN_IP = 127.0.0.1
export RQBIT_TCP_LISTEN_DISABLE = true
CARGO_RUN_FLAGS ?= CARGO_RUN_FLAGS ?=
RQBIT_OUTPUT_FOLDER ?= /tmp/scratch RQBIT_OUTPUT_FOLDER ?= /tmp/scratch

View File

@ -96,11 +96,14 @@ impl ListenerOptions {
if !self.mode.utp_enabled() { if !self.mode.utp_enabled() {
return Ok::<_, anyhow::Error>(None); return Ok::<_, anyhow::Error>(None);
} }
Ok(Some( let socket = UtpSocketUdp::new_udp_with_opts(self.listen_addr, utp_opts)
UtpSocketUdp::new_udp_with_opts(self.listen_addr, utp_opts) .await
.await .context("error starting uTP listener")?;
.context("error starting uTP listener")?, info!(
)) "Listening on UDP {:?} for incoming uTP peer connections",
self.listen_addr
);
Ok(Some(socket))
}; };
let announce_port = if self.listen_addr.ip().is_loopback() { let announce_port = if self.listen_addr.ip().is_loopback() {

View File

@ -57,7 +57,7 @@ pub enum WriterRequest {
} }
#[serde_as] #[serde_as]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct PeerConnectionOptions { pub struct PeerConnectionOptions {
#[serde_as(as = "Option<serde_with::DurationSeconds>")] #[serde_as(as = "Option<serde_with::DurationSeconds>")]
pub connect_timeout: Option<Duration>, pub connect_timeout: Option<Duration>,

View File

@ -700,6 +700,7 @@ impl Session {
} }
if let Some(announce_port) = listen.announce_port { if let Some(announce_port) = listen.announce_port {
if listen.enable_upnp_port_forwarding { if listen.enable_upnp_port_forwarding {
info!(port = announce_port, "starting UPnP port forwarder");
session.spawn( session.spawn(
error_span!(parent: session.rs(), "upnp_forward", port = announce_port), error_span!(parent: session.rs(), "upnp_forward", port = announce_port),
Self::task_upnp_port_forwarder(announce_port), Self::task_upnp_port_forwarder(announce_port),

View File

@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
io, io,
net::{Ipv4Addr, SocketAddr}, net::{IpAddr, SocketAddr},
num::NonZeroU32, num::NonZeroU32,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@ -133,6 +133,10 @@ struct Opts {
#[arg(long = "disable-tcp-listen", env = "RQBIT_TCP_LISTEN_DISABLE")] #[arg(long = "disable-tcp-listen", env = "RQBIT_TCP_LISTEN_DISABLE")]
disable_tcp_listen: bool, disable_tcp_listen: bool,
// Disable connecting over TCP. Only uTP will be used (if enabled).
#[arg(long = "disable-tcp-connect", env = "RQBIT_TCP_CONNECT_DISABLE")]
disable_tcp_connect: bool,
// Enable to listen and connect over uTP // Enable to listen and connect over uTP
#[arg( #[arg(
long = "experimental-enable-utp-listen", long = "experimental-enable-utp-listen",
@ -148,15 +152,19 @@ struct Opts {
)] )]
listen_port: u16, listen_port: u16,
/// What's the IP to listen on. Default is to listen on all interfaces.
#[arg(long = "listen-ip", default_value = "0.0.0.0", env = "RQBIT_LISTEN_IP")]
listen_ip: IpAddr,
/// If set, will try to publish the chosen port through upnp on your router. /// If set, will try to publish the chosen port through upnp on your router.
/// If the listen-ip is localhost, this will not be used.
#[arg( #[arg(
long = "disable-upnp-port-forward", long = "disable-upnp-port-forward",
env = "RQBIT_UPNP_PORT_FORWARD_DISABLE" env = "RQBIT_UPNP_PORT_FORWARD_DISABLE"
)] )]
disable_upnp_port_forward: bool, disable_upnp_port_forward: bool,
/// If set, will run a UPNP Media server and stream all the torrents through it. /// If set, will run a UPNP Media server on RQBIT_HTTP_API_LISTEN_ADDR.
/// Should be set to your hostname/IP as seen by your LAN neighbors.
#[arg(long = "enable-upnp-server", env = "RQBIT_UPNP_SERVER_ENABLE")] #[arg(long = "enable-upnp-server", env = "RQBIT_UPNP_SERVER_ENABLE")]
enable_upnp_server: bool, enable_upnp_server: bool,
@ -490,7 +498,7 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
}; };
let listen = listen_mode.map(|mode| ListenerOptions { let listen = listen_mode.map(|mode| ListenerOptions {
mode, mode,
listen_addr: (Ipv4Addr::UNSPECIFIED, opts.listen_port).into(), listen_addr: (opts.listen_ip, opts.listen_port).into(),
enable_upnp_port_forwarding: !opts.disable_upnp_port_forward, enable_upnp_port_forwarding: !opts.disable_upnp_port_forward,
..Default::default() ..Default::default()
}); });
@ -505,7 +513,7 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
listen, listen,
connect: Some(ConnectionOptions { connect: Some(ConnectionOptions {
proxy_url: opts.socks_url, proxy_url: opts.socks_url,
enable_tcp: true, enable_tcp: !opts.disable_tcp_connect,
peer_opts: Some(PeerConnectionOptions { peer_opts: Some(PeerConnectionOptions {
connect_timeout: Some(opts.peer_connect_timeout), connect_timeout: Some(opts.peer_connect_timeout),
read_write_timeout: Some(opts.peer_read_write_timeout), read_write_timeout: Some(opts.peer_read_write_timeout),

View File

@ -1,12 +1,11 @@
[package] [package]
name = "rqbit-desktop" name = "rqbit-desktop"
edition = "2024"
version = "8.1.0" version = "8.1.0"
description = "rqbit torrent client" description = "rqbit torrent client"
authors = ["you"] authors = ["Igor Katson igor.katson@gmail.com"]
license = "" license = ""
repository = "" repository = ""
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]

View File

@ -4,7 +4,10 @@ use std::{
time::Duration, time::Duration,
}; };
use librqbit::{dht::PersistentDht, limits::LimitsConfig, ListenerMode, ListenerOptions}; use librqbit::{
dht::PersistentDht, limits::LimitsConfig, ConnectionOptions, ListenerMode, ListenerOptions,
PeerConnectionOptions,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
@ -26,39 +29,65 @@ impl Default for RqbitDesktopConfigDht {
} }
} }
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde_as]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)] #[serde(default)]
pub struct RqbitDesktopConfigListen { pub struct RqbitDesktopConfigConnections {
pub enable_tcp: bool, pub enable_tcp_listen: bool,
pub enable_tcp_outgoing: bool,
pub enable_utp: bool, pub enable_utp: bool,
pub enable_upnp_port_forward: bool, pub enable_upnp_port_forward: bool,
pub port: u16, pub socks_proxy: String,
pub listen_port: u16,
#[serde_as(as = "serde_with::DurationSeconds")]
pub peer_connect_timeout: Duration,
#[serde_as(as = "serde_with::DurationSeconds")]
pub peer_read_write_timeout: Duration,
} }
impl RqbitDesktopConfigListen { impl RqbitDesktopConfigConnections {
pub fn as_listener_opts(&self) -> Option<ListenerOptions> { pub fn as_listener_and_connect_opts(&self) -> (Option<ListenerOptions>, ConnectionOptions) {
let mode = match (self.enable_tcp, self.enable_utp) { let mode = match (self.enable_tcp_listen, self.enable_utp) {
(true, true) => ListenerMode::TcpAndUtp, (true, true) => Some(ListenerMode::TcpAndUtp),
(true, false) => ListenerMode::TcpOnly, (true, false) => Some(ListenerMode::TcpOnly),
(false, true) => ListenerMode::UtpOnly, (false, true) => Some(ListenerMode::UtpOnly),
(false, false) => return None, (false, false) => None,
}; };
Some(ListenerOptions { let listener_opts = mode.map(|mode| ListenerOptions {
mode, mode,
listen_addr: (Ipv4Addr::UNSPECIFIED, self.port).into(), listen_addr: (Ipv4Addr::UNSPECIFIED, self.listen_port).into(),
enable_upnp_port_forwarding: self.enable_upnp_port_forward, enable_upnp_port_forwarding: self.enable_upnp_port_forward,
..Default::default() ..Default::default()
}) });
let connect_opts = ConnectionOptions {
proxy_url: if self.socks_proxy.is_empty() {
None
} else {
Some(self.socks_proxy.clone())
},
enable_tcp: self.enable_tcp_outgoing,
peer_opts: Some(PeerConnectionOptions {
connect_timeout: Some(self.peer_connect_timeout),
read_write_timeout: Some(self.peer_read_write_timeout),
..Default::default()
}),
};
(listener_opts, connect_opts)
} }
} }
impl Default for RqbitDesktopConfigListen { impl Default for RqbitDesktopConfigConnections {
fn default() -> Self { fn default() -> Self {
Self { Self {
enable_tcp: true, enable_tcp_listen: true,
enable_tcp_outgoing: true,
enable_utp: false, enable_utp: false,
enable_upnp_port_forward: true, enable_upnp_port_forward: true,
port: 4240, listen_port: 4240,
socks_proxy: String::new(),
peer_connect_timeout: Duration::from_secs(2),
peer_read_write_timeout: Duration::from_secs(10),
} }
} }
} }
@ -104,26 +133,6 @@ impl Default for RqbitDesktopConfigPersistence {
} }
} }
#[serde_as]
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct RqbitDesktopConfigPeerOpts {
#[serde_as(as = "serde_with::DurationSeconds")]
pub connect_timeout: Duration,
#[serde_as(as = "serde_with::DurationSeconds")]
pub read_write_timeout: Duration,
}
impl Default for RqbitDesktopConfigPeerOpts {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(2),
read_write_timeout: Duration::from_secs(10),
}
}
}
#[serde_as] #[serde_as]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)] #[serde(default)]
@ -163,11 +172,9 @@ pub struct RqbitDesktopConfig {
pub disable_upload: bool, pub disable_upload: bool,
pub dht: RqbitDesktopConfigDht, pub dht: RqbitDesktopConfigDht,
#[serde(default)] pub connections: RqbitDesktopConfigConnections,
pub listen: RqbitDesktopConfigListen,
pub upnp: RqbitDesktopConfigUpnp, pub upnp: RqbitDesktopConfigUpnp,
pub persistence: RqbitDesktopConfigPersistence, pub persistence: RqbitDesktopConfigPersistence,
pub peer_opts: RqbitDesktopConfigPeerOpts,
pub http_api: RqbitDesktopConfigHttpApi, pub http_api: RqbitDesktopConfigHttpApi,
#[serde(default)] #[serde(default)]
@ -185,10 +192,9 @@ impl Default for RqbitDesktopConfig {
Self { Self {
default_download_location: download_folder, default_download_location: download_folder,
dht: Default::default(), dht: Default::default(),
listen: Default::default(), connections: Default::default(),
upnp: Default::default(), upnp: Default::default(),
persistence: Default::default(), persistence: Default::default(),
peer_opts: Default::default(),
http_api: Default::default(), http_api: Default::default(),
ratelimits: Default::default(), ratelimits: Default::default(),
#[cfg(feature = "disable-upload")] #[cfg(feature = "disable-upload")]

View File

@ -84,6 +84,8 @@ async fn api_from_config(
}) })
}; };
let (listen, connect) = config.connections.as_listener_and_connect_opts();
let session = Session::new_with_opts( let session = Session::new_with_opts(
config.default_download_location.clone(), config.default_download_location.clone(),
SessionOptions { SessionOptions {
@ -94,16 +96,8 @@ async fn api_from_config(
..Default::default() ..Default::default()
}), }),
persistence, persistence,
connect: Some(librqbit::ConnectionOptions { connect: Some(connect),
enable_tcp: true, listen,
peer_opts: Some(PeerConnectionOptions {
connect_timeout: Some(config.peer_opts.connect_timeout),
read_write_timeout: Some(config.peer_opts.read_write_timeout),
..Default::default()
}),
..Default::default()
}),
listen: config.listen.as_listener_opts(),
fastresume: config.persistence.fastresume, fastresume: config.persistence.fastresume,
ratelimits: config.ratelimits, ratelimits: config.ratelimits,
#[cfg(feature = "disable-upload")] #[cfg(feature = "disable-upload")]

View File

@ -8,10 +8,15 @@ interface RqbitDesktopConfigDht {
persistence_filename: PathLike; persistence_filename: PathLike;
} }
interface RqbitDesktopConfigTcpListen { interface RqbitDesktopConfigConnections {
disable: boolean; enable_tcp_listen: boolean;
min_port: number; enable_tcp_outgoing: boolean;
max_port: number; enable_utp: boolean;
enable_upnp_port_forward: boolean;
socks_proxy: string;
listen_port: number;
peer_connect_timeout: Duration;
peer_read_write_timeout: Duration;
} }
interface RqbitDesktopConfigPersistence { interface RqbitDesktopConfigPersistence {
@ -20,11 +25,6 @@ interface RqbitDesktopConfigPersistence {
fastresume: boolean; fastresume: boolean;
} }
interface RqbitDesktopConfigPeerOpts {
connect_timeout: Duration;
read_write_timeout: Duration;
}
interface RqbitDesktopConfigHttpApi { interface RqbitDesktopConfigHttpApi {
disable: boolean; disable: boolean;
listen_addr: SocketAddr; listen_addr: SocketAddr;
@ -48,10 +48,9 @@ export interface RqbitDesktopConfig {
default_download_location: PathLike; default_download_location: PathLike;
disable_upload?: boolean; disable_upload?: boolean;
dht: RqbitDesktopConfigDht; dht: RqbitDesktopConfigDht;
tcp_listen: RqbitDesktopConfigTcpListen; connections: RqbitDesktopConfigConnections;
upnp: RqbitDesktopConfigUpnp; upnp: RqbitDesktopConfigUpnp;
persistence: RqbitDesktopConfigPersistence; persistence: RqbitDesktopConfigPersistence;
peer_opts: RqbitDesktopConfigPeerOpts;
http_api: RqbitDesktopConfigHttpApi; http_api: RqbitDesktopConfigHttpApi;
ratelimits: LimitsConfig; ratelimits: LimitsConfig;
} }

View File

@ -58,17 +58,15 @@ type TAB =
| "Home" | "Home"
| "DHT" | "DHT"
| "Session" | "Session"
| "Peer options"
| "HTTP API" | "HTTP API"
| "TCP Listen" | "Connection"
| "UPnP Server"; | "UPnP Server";
const TABS: readonly TAB[] = [ const TABS: readonly TAB[] = [
"Home", "Home",
"DHT", "DHT",
"Session", "Session",
"TCP Listen", "Connection",
"Peer options",
"HTTP API", "HTTP API",
"UPnP Server", "UPnP Server",
] as const; ] as const;
@ -133,7 +131,7 @@ export const ConfigModal: React.FC<{
}; };
const handleToggleChange: React.ChangeEventHandler<HTMLInputElement> = ( const handleToggleChange: React.ChangeEventHandler<HTMLInputElement> = (
e e,
) => { ) => {
const name: string = e.target.name; const name: string = e.target.name;
const [mainField, subField] = name.split(".", 2); const [mainField, subField] = name.split(".", 2);
@ -169,7 +167,7 @@ export const ConfigModal: React.FC<{
text: "Error saving configuration", text: "Error saving configuration",
details: e, details: e,
}); });
} },
); );
}; };
@ -257,42 +255,78 @@ Might be useful e.g. if rqbit upload consumes all your upload bandwidth and inte
</Fieldset> </Fieldset>
</Tab> </Tab>
<Tab name="TCP Listen" currentTab={tab}> <Tab name="Connection" currentTab={tab}>
<Fieldset> <Fieldset>
<FormCheck <FormCheck
label="Listen on TCP" label="Listen on TCP"
name="tcp_listen.disable" name="connections.enable_tcp_listen"
checked={!config.tcp_listen.disable} checked={config.connections.enable_tcp_listen}
onChange={handleToggleChange} onChange={handleToggleChange}
help="Listen for torrent requests on TCP. Required for peers to be able to connect to you, mainly for uploading." help="Listen for torrent requests on TCP. Required for peers to be able to connect to you, mainly for uploading."
/> />
<FormCheck <FormCheck
label="Advertise TCP port over UPnP" label="Listen on uTP (over UDP)"
name="tcp_listen.disable" name="connections.enable_utp"
checked={!config.tcp_listen.disable} checked={config.connections.enable_utp}
onChange={handleToggleChange}
help="Listen for torrent requests on uTP over UDP. Required for uTP support in general, both outgoing and incoming."
/>
<FormCheck
label="Advertise port over UPnP"
name="connections.enable_upnp_port_forward"
checked={config.connections.enable_upnp_port_forward}
onChange={handleToggleChange} onChange={handleToggleChange}
help="Advertise your port over UPnP to your router(s). This is required for peers to be able to connect to you from the internet. Will only work if your router has a static IP." help="Advertise your port over UPnP to your router(s). This is required for peers to be able to connect to you from the internet. Will only work if your router has a static IP."
/> />
<FormCheck
label="[ADVANCED] Disable outgoing connections over TCP"
name="connections.enable_tcp_outgoing"
checked={!config.connections.enable_tcp_outgoing}
onChange={handleToggleChange}
help="WARNING: leave this unchecked unless you know what you are doing."
/>
<FormInput <FormInput
inputType="number" inputType="text"
label="Min port" label="Socks proxy"
name="tcp_listen.min_port" name="connections.socks_proxy"
value={config.tcp_listen.min_port} value={config.connections.socks_proxy}
disabled={config.tcp_listen.disable}
onChange={handleInputChange} onChange={handleInputChange}
help="The min port to try to listen on. First successful is taken." help="Socks5 proxy for outgoing connections. Format: socks5://[username:password@]host:port"
/> />
<FormInput <FormInput
inputType="number" inputType="number"
label="Max port" label="Port"
name="tcp_listen.max_port" name="connections.listen_port"
value={config.tcp_listen.max_port} value={config.connections.listen_port}
disabled={config.tcp_listen.disable} disabled={
!config.connections.enable_tcp_listen &&
!config.connections.enable_utp
}
onChange={handleInputChange} onChange={handleInputChange}
help="The max port to try to listen on." help="The port to listen on for both TCP and UDP (if enabled)."
/>
<FormInput
label="Peer connect timeout (seconds)"
inputType="number"
name="connections.peer_connect_timeout"
value={config.connections.peer_connect_timeout}
onChange={handleInputChange}
help="How much to wait for outgoing connections to connect. Default is low to prefer faster peers."
/>
<FormInput
label="Peer read/write timeout (seconds)"
inputType="number"
name="connections.peer_read_write_timeout"
value={config.connections.peer_read_write_timeout}
onChange={handleInputChange}
help="Peer socket read/write timeout."
/> />
</Fieldset> </Fieldset>
</Tab> </Tab>
@ -378,28 +412,6 @@ Might be useful e.g. if rqbit upload consumes all your upload bandwidth and inte
</Fieldset> </Fieldset>
</Tab> </Tab>
<Tab name="Peer options" currentTab={tab}>
<Fieldset>
<FormInput
label="Connect timeout (seconds)"
inputType="number"
name="peer_opts.connect_timeout"
value={config.peer_opts.connect_timeout}
onChange={handleInputChange}
help="How much to wait for outgoing connections to connect. Default is low to prefer faster peers."
/>
<FormInput
label="Read/write timeout (seconds)"
inputType="number"
name="peer_opts.read_write_timeout"
value={config.peer_opts.read_write_timeout}
onChange={handleInputChange}
help="Peer socket read/write timeout."
/>
</Fieldset>
</Tab>
<Tab name="HTTP API" currentTab={tab}> <Tab name="HTTP API" currentTab={tab}>
<Fieldset> <Fieldset>
<FormCheck <FormCheck