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_FRIENDLY_NAME ?= rqbit-dev
export RQBIT_HTTP_API_LISTEN_ADDR ?= [::]:3030
export RQBIT_EXPERIMENTAL_UTP_LISTEN_ENABLE ?= true
export RQBIT_FASTRESUME = true
# Don't expose devserver
export RQBIT_UPNP_PORT_FORWARD_DISABLE = true
export RQBIT_TCP_LISTEN_DISABLE = true
export RQBIT_LISTEN_IP = 127.0.0.1
CARGO_RUN_FLAGS ?=
RQBIT_OUTPUT_FOLDER ?= /tmp/scratch

View File

@ -96,11 +96,14 @@ impl ListenerOptions {
if !self.mode.utp_enabled() {
return Ok::<_, anyhow::Error>(None);
}
Ok(Some(
UtpSocketUdp::new_udp_with_opts(self.listen_addr, utp_opts)
let socket = UtpSocketUdp::new_udp_with_opts(self.listen_addr, utp_opts)
.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() {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use std::{
collections::HashSet,
io,
net::{Ipv4Addr, SocketAddr},
net::{IpAddr, SocketAddr},
num::NonZeroU32,
path::{Path, PathBuf},
sync::Arc,
@ -133,6 +133,10 @@ struct Opts {
#[arg(long = "disable-tcp-listen", env = "RQBIT_TCP_LISTEN_DISABLE")]
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
#[arg(
long = "experimental-enable-utp-listen",
@ -148,15 +152,19 @@ struct Opts {
)]
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 the listen-ip is localhost, this will not be used.
#[arg(
long = "disable-upnp-port-forward",
env = "RQBIT_UPNP_PORT_FORWARD_DISABLE"
)]
disable_upnp_port_forward: bool,
/// If set, will run a UPNP Media server and stream all the torrents through it.
/// Should be set to your hostname/IP as seen by your LAN neighbors.
/// If set, will run a UPNP Media server on RQBIT_HTTP_API_LISTEN_ADDR.
#[arg(long = "enable-upnp-server", env = "RQBIT_UPNP_SERVER_ENABLE")]
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 {
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,
..Default::default()
});
@ -505,7 +513,7 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
listen,
connect: Some(ConnectionOptions {
proxy_url: opts.socks_url,
enable_tcp: true,
enable_tcp: !opts.disable_tcp_connect,
peer_opts: Some(PeerConnectionOptions {
connect_timeout: Some(opts.peer_connect_timeout),
read_write_timeout: Some(opts.peer_read_write_timeout),

View File

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

View File

@ -4,7 +4,10 @@ use std::{
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_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)]
pub struct RqbitDesktopConfigListen {
pub enable_tcp: bool,
pub struct RqbitDesktopConfigConnections {
pub enable_tcp_listen: bool,
pub enable_tcp_outgoing: bool,
pub enable_utp: 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 {
pub fn as_listener_opts(&self) -> Option<ListenerOptions> {
let mode = match (self.enable_tcp, self.enable_utp) {
(true, true) => ListenerMode::TcpAndUtp,
(true, false) => ListenerMode::TcpOnly,
(false, true) => ListenerMode::UtpOnly,
(false, false) => return None,
impl RqbitDesktopConfigConnections {
pub fn as_listener_and_connect_opts(&self) -> (Option<ListenerOptions>, ConnectionOptions) {
let mode = match (self.enable_tcp_listen, self.enable_utp) {
(true, true) => Some(ListenerMode::TcpAndUtp),
(true, false) => Some(ListenerMode::TcpOnly),
(false, true) => Some(ListenerMode::UtpOnly),
(false, false) => None,
};
Some(ListenerOptions {
let listener_opts = mode.map(|mode| ListenerOptions {
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,
..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 {
Self {
enable_tcp: true,
enable_tcp_listen: true,
enable_tcp_outgoing: true,
enable_utp: false,
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]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
@ -163,11 +172,9 @@ pub struct RqbitDesktopConfig {
pub disable_upload: bool,
pub dht: RqbitDesktopConfigDht,
#[serde(default)]
pub listen: RqbitDesktopConfigListen,
pub connections: RqbitDesktopConfigConnections,
pub upnp: RqbitDesktopConfigUpnp,
pub persistence: RqbitDesktopConfigPersistence,
pub peer_opts: RqbitDesktopConfigPeerOpts,
pub http_api: RqbitDesktopConfigHttpApi,
#[serde(default)]
@ -185,10 +192,9 @@ impl Default for RqbitDesktopConfig {
Self {
default_download_location: download_folder,
dht: Default::default(),
listen: Default::default(),
connections: Default::default(),
upnp: Default::default(),
persistence: Default::default(),
peer_opts: Default::default(),
http_api: Default::default(),
ratelimits: Default::default(),
#[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(
config.default_download_location.clone(),
SessionOptions {
@ -94,16 +96,8 @@ async fn api_from_config(
..Default::default()
}),
persistence,
connect: Some(librqbit::ConnectionOptions {
enable_tcp: true,
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(),
connect: Some(connect),
listen,
fastresume: config.persistence.fastresume,
ratelimits: config.ratelimits,
#[cfg(feature = "disable-upload")]

View File

@ -8,10 +8,15 @@ interface RqbitDesktopConfigDht {
persistence_filename: PathLike;
}
interface RqbitDesktopConfigTcpListen {
disable: boolean;
min_port: number;
max_port: number;
interface RqbitDesktopConfigConnections {
enable_tcp_listen: boolean;
enable_tcp_outgoing: boolean;
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 {
@ -20,11 +25,6 @@ interface RqbitDesktopConfigPersistence {
fastresume: boolean;
}
interface RqbitDesktopConfigPeerOpts {
connect_timeout: Duration;
read_write_timeout: Duration;
}
interface RqbitDesktopConfigHttpApi {
disable: boolean;
listen_addr: SocketAddr;
@ -48,10 +48,9 @@ export interface RqbitDesktopConfig {
default_download_location: PathLike;
disable_upload?: boolean;
dht: RqbitDesktopConfigDht;
tcp_listen: RqbitDesktopConfigTcpListen;
connections: RqbitDesktopConfigConnections;
upnp: RqbitDesktopConfigUpnp;
persistence: RqbitDesktopConfigPersistence;
peer_opts: RqbitDesktopConfigPeerOpts;
http_api: RqbitDesktopConfigHttpApi;
ratelimits: LimitsConfig;
}

View File

@ -58,17 +58,15 @@ type TAB =
| "Home"
| "DHT"
| "Session"
| "Peer options"
| "HTTP API"
| "TCP Listen"
| "Connection"
| "UPnP Server";
const TABS: readonly TAB[] = [
"Home",
"DHT",
"Session",
"TCP Listen",
"Peer options",
"Connection",
"HTTP API",
"UPnP Server",
] as const;
@ -133,7 +131,7 @@ export const ConfigModal: React.FC<{
};
const handleToggleChange: React.ChangeEventHandler<HTMLInputElement> = (
e
e,
) => {
const name: string = e.target.name;
const [mainField, subField] = name.split(".", 2);
@ -169,7 +167,7 @@ export const ConfigModal: React.FC<{
text: "Error saving configuration",
details: e,
});
}
},
);
};
@ -257,42 +255,78 @@ Might be useful e.g. if rqbit upload consumes all your upload bandwidth and inte
</Fieldset>
</Tab>
<Tab name="TCP Listen" currentTab={tab}>
<Tab name="Connection" currentTab={tab}>
<Fieldset>
<FormCheck
label="Listen on TCP"
name="tcp_listen.disable"
checked={!config.tcp_listen.disable}
name="connections.enable_tcp_listen"
checked={config.connections.enable_tcp_listen}
onChange={handleToggleChange}
help="Listen for torrent requests on TCP. Required for peers to be able to connect to you, mainly for uploading."
/>
<FormCheck
label="Advertise TCP port over UPnP"
name="tcp_listen.disable"
checked={!config.tcp_listen.disable}
label="Listen on uTP (over UDP)"
name="connections.enable_utp"
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}
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
inputType="number"
label="Min port"
name="tcp_listen.min_port"
value={config.tcp_listen.min_port}
disabled={config.tcp_listen.disable}
inputType="text"
label="Socks proxy"
name="connections.socks_proxy"
value={config.connections.socks_proxy}
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
inputType="number"
label="Max port"
name="tcp_listen.max_port"
value={config.tcp_listen.max_port}
disabled={config.tcp_listen.disable}
label="Port"
name="connections.listen_port"
value={config.connections.listen_port}
disabled={
!config.connections.enable_tcp_listen &&
!config.connections.enable_utp
}
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>
</Tab>
@ -378,28 +412,6 @@ Might be useful e.g. if rqbit upload consumes all your upload bandwidth and inte
</Fieldset>
</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}>
<Fieldset>
<FormCheck