Skip to content

Commit 64c9046

Browse files
committed
fix(network): introduce Socks5 proxy connection
The Arti project does not have an official MSRV, and the dependency list has a number of failed security audits caught by the CI job in this repository. The legacy Tor client is sufficient for desktop and some mobile users, and no developers have expressed interest in using Arti directly. A simple Socks5 proxy that connects to a Tor daemon allows for Tor connections without an explicit dependency that causes MSRV conflicts, security audits, and lib-sqlite version conflicts.
1 parent 5a5531c commit 64c9046

File tree

5 files changed

+155
-15
lines changed

5 files changed

+155
-15
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ async fn main() {
101101

102102
The `kyoto` core library with default features supports an MSRV of Rust 1.63.
103103

104-
While connections over the Tor protocol are supported by the feature `tor`, the dependencies required cannot support the MSRV. As such, no MSRV guarantees will be made when using Tor, and the feature should be considered experimental.
105-
106104
## Integration Testing
107105

108106
The preferred workflow is by using `just`. If you do not have `just` installed, check out the [installation page](https://just.systems/man/en/chapter_4.html).

src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@
7676
//! `database`: use the default `rusqlite` database implementations. Default and recommend feature.
7777
//!
7878
//! `filter-control`: check filters and request blocks directly. Recommended for silent payments or strict chain ordering implementations.
79-
//!
80-
//! `tor` *No MSRV guarantees*: connect to nodes over the Tor network.
8179
8280
#![warn(missing_docs)]
8381
pub mod chain;

src/network/error.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl_sourceless_error!(PeerReadError);
2929

3030
// TODO: (@leonardo) Should the error variants wrap inner errors for richer information ?
3131
#[derive(Debug)]
32-
pub enum PeerError {
32+
pub(crate) enum PeerError {
3333
ConnectionFailed,
3434
MessageEncryption,
3535
MessageSerialization,
@@ -39,6 +39,7 @@ pub enum PeerError {
3939
DisconnectCommand,
4040
Reader,
4141
UnreachableSocketAddr,
42+
Socks5(Socks5Error),
4243
}
4344

4445
impl core::fmt::Display for PeerError {
@@ -68,12 +69,50 @@ impl core::fmt::Display for PeerError {
6869
PeerError::MessageEncryption => {
6970
write!(f, "encrypting a serialized message failed.")
7071
}
72+
PeerError::Socks5(err) => {
73+
write!(f, "could not connect via Socks5 proxy: {err}")
74+
}
7175
}
7276
}
7377
}
7478

7579
impl_sourceless_error!(PeerError);
7680

81+
#[derive(Debug, Clone)]
82+
pub(crate) enum Socks5Error {
83+
WrongVersion,
84+
AuthRequired,
85+
ConnectionTimeout,
86+
ConnectionFailed,
87+
IO,
88+
}
89+
90+
impl core::fmt::Display for Socks5Error {
91+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
92+
match self {
93+
Socks5Error::WrongVersion => write!(f, "server responded with an unsupported version."),
94+
Socks5Error::AuthRequired => write!(f, "server requires authentication."),
95+
Socks5Error::ConnectionTimeout => write!(f, "connection to server timed out."),
96+
Socks5Error::ConnectionFailed => write!(
97+
f,
98+
"the server could not connect to the requested destination."
99+
),
100+
Socks5Error::IO => write!(
101+
f,
102+
"reading or writing to the TCP stream failed unexpectedly."
103+
),
104+
}
105+
}
106+
}
107+
108+
impl_sourceless_error!(Socks5Error);
109+
110+
impl From<std::io::Error> for Socks5Error {
111+
fn from(_value: std::io::Error) -> Self {
112+
Socks5Error::IO
113+
}
114+
}
115+
77116
#[derive(Debug)]
78117
pub(crate) enum DnsBootstrapError {
79118
NotEnoughPeersError,

src/network/mod.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use bitcoin::{
88
io::Read,
99
p2p::{address::AddrV2, message::CommandString, Magic},
1010
};
11+
use socks::create_socks5;
1112
use tokio::{
1213
io::{AsyncRead, AsyncWrite},
1314
net::TcpStream,
@@ -32,6 +33,7 @@ pub(crate) mod traits;
3233
pub const PROTOCOL_VERSION: u32 = 70016;
3334
pub const KYOTO_VERSION: &str = "0.8.0";
3435
pub const RUST_BITCOIN_VERSION: &str = "0.32.4";
36+
3537
const THIRTY_MINS: u64 = 60 * 30;
3638
const CONNECTION_TIMEOUT: u64 = 2;
3739

@@ -124,18 +126,28 @@ impl ConnectionType {
124126
AddrV2::Ipv6(ip) => IpAddr::V6(ip),
125127
_ => return Err(PeerError::UnreachableSocketAddr),
126128
};
127-
let timeout = tokio::time::timeout(
128-
Duration::from_secs(CONNECTION_TIMEOUT),
129-
TcpStream::connect((socket_addr, port)),
130-
)
131-
.await
132-
.map_err(|_| PeerError::ConnectionFailed)?;
133-
match timeout {
134-
Ok(stream) => {
135-
let (reader, writer) = stream.into_split();
129+
match &self {
130+
Self::ClearNet => {
131+
let timeout = tokio::time::timeout(
132+
Duration::from_secs(CONNECTION_TIMEOUT),
133+
TcpStream::connect((socket_addr, port)),
134+
)
135+
.await
136+
.map_err(|_| PeerError::ConnectionFailed)?;
137+
let tcp_stream = timeout.map_err(|_| PeerError::ConnectionFailed)?;
138+
let (reader, writer) = tcp_stream.into_split();
136139
Ok((Box::new(reader), Box::new(writer)))
137140
}
138-
Err(_) => Err(PeerError::ConnectionFailed),
141+
Self::Socks5Proxy(proxy) => {
142+
let socks5_timeout = tokio::time::timeout(
143+
Duration::from_secs(CONNECTION_TIMEOUT),
144+
create_socks5(*proxy, socket_addr, port),
145+
)
146+
.await
147+
.map_err(|_| PeerError::ConnectionFailed)?;
148+
let (reader, writer) = socks5_timeout.map_err(PeerError::Socks5)?;
149+
Ok((reader, writer))
150+
}
139151
}
140152
}
141153
}

src/network/socks.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,94 @@
1+
// Partial implementation of RFC 1928, Socks5 protocol
2+
// ref: https://datatracker.ietf.org/doc/html/rfc1928#section-1
13

4+
use std::{
5+
net::{IpAddr, SocketAddr},
6+
time::Duration,
7+
};
8+
9+
use tokio::{
10+
io::{AsyncReadExt, AsyncWriteExt},
11+
net::TcpStream,
12+
};
13+
14+
use crate::network::{StreamReader, StreamWriter};
15+
16+
use super::error::Socks5Error;
17+
18+
const CONNECTION_TIMEOUT: u64 = 2;
19+
const VERSION: u8 = 5;
20+
const NOAUTH: u8 = 0;
21+
const METHODS: u8 = 1;
22+
const CMD_CONNECT: u8 = 1;
23+
const RESPONSE_SUCCESS: u8 = 0;
24+
const RSV: u8 = 0;
25+
const ADDR_TYPE_IPV4: u8 = 1;
26+
const ADDR_TYPE_IPV6: u8 = 4;
27+
28+
pub(crate) async fn create_socks5(
29+
proxy: SocketAddr,
30+
ip_addr: IpAddr,
31+
port: u16,
32+
) -> Result<(StreamReader, StreamWriter), Socks5Error> {
33+
// Connect to the proxy, likely a local Tor daemon.
34+
let timeout = tokio::time::timeout(
35+
Duration::from_secs(CONNECTION_TIMEOUT),
36+
TcpStream::connect(proxy),
37+
)
38+
.await
39+
.map_err(|_| Socks5Error::ConnectionTimeout)?;
40+
// Format the destination IP address and port according to the Socks5 spec
41+
let dest_ip_bytes = match ip_addr {
42+
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
43+
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
44+
};
45+
let dest_port_bytes = port.to_be_bytes();
46+
let ip_type_byte = match ip_addr {
47+
IpAddr::V4(_) => ADDR_TYPE_IPV4,
48+
IpAddr::V6(_) => ADDR_TYPE_IPV6,
49+
};
50+
// Begin the handshake by requesting a connection to the proxy.
51+
let mut tcp_stream = timeout.map_err(|_| Socks5Error::ConnectionFailed)?;
52+
tcp_stream.write_all(&[VERSION, METHODS, NOAUTH]).await?;
53+
// Read the response from the proxy
54+
let mut buf = [0_u8; 2];
55+
tcp_stream.read_exact(&mut buf).await?;
56+
if buf[0] != VERSION {
57+
return Err(Socks5Error::WrongVersion);
58+
}
59+
if buf[1] != NOAUTH {
60+
return Err(Socks5Error::AuthRequired);
61+
}
62+
// Write the request to the proxy to connect to our destination
63+
tcp_stream
64+
.write_all(&[VERSION, CMD_CONNECT, RSV, ip_type_byte])
65+
.await?;
66+
tcp_stream.write_all(&dest_ip_bytes).await?;
67+
tcp_stream.write_all(&dest_port_bytes).await?;
68+
// First 4 bytes of the response: version, success/failure, reserved byte, ip type
69+
let mut buf = [0_u8; 4];
70+
tcp_stream.read_exact(&mut buf).await?;
71+
if buf[0] != VERSION {
72+
return Err(Socks5Error::WrongVersion);
73+
}
74+
if buf[1] != RESPONSE_SUCCESS {
75+
return Err(Socks5Error::ConnectionFailed);
76+
}
77+
// Read off the destination of our request
78+
match buf[3] {
79+
ADDR_TYPE_IPV4 => {
80+
// Read the IPv4 address and additonal two bytes for the port
81+
let mut buf = [0_u8; 6];
82+
tcp_stream.read_exact(&mut buf).await?;
83+
}
84+
ADDR_TYPE_IPV6 => {
85+
// Read the IPv6 address and additonal two bytes for the port
86+
let mut buf = [0_u8; 18];
87+
tcp_stream.read_exact(&mut buf).await?;
88+
}
89+
_ => return Err(Socks5Error::ConnectionFailed),
90+
}
91+
// Proxy handshake is complete, the TCP reader/writer can be returned
92+
let (reader, writer) = tcp_stream.into_split();
93+
Ok((Box::new(reader), Box::new(writer)))
94+
}

0 commit comments

Comments
 (0)