Skip to content

Commit 921aae0

Browse files
committed
Add proxy and onion support for tentacle
Signed-off-by: Eval EXEC <execvy@gmail.com>
1 parent 9cb5985 commit 921aae0

File tree

16 files changed

+391
-31
lines changed

16 files changed

+391
-31
lines changed

multiaddr/src/onion_addr.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{borrow::Cow, fmt};
22

3+
use data_encoding::BASE32;
4+
35
/// Represents an Onion v3 address
46
#[derive(Clone)]
57
pub struct Onion3Addr<'a>(Cow<'a, [u8; 35]>, u16);
@@ -19,6 +21,11 @@ impl Onion3Addr<'_> {
1921
pub fn acquire<'b>(self) -> Onion3Addr<'b> {
2022
Onion3Addr(Cow::Owned(self.0.into_owned()), self.1)
2123
}
24+
25+
pub fn hash_string(&self) -> String {
26+
let s = BASE32.encode(self.hash());
27+
s.to_lowercase()
28+
}
2229
}
2330

2431
impl PartialEq for Onion3Addr<'_> {

multiaddr/src/protocol.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,7 @@ impl fmt::Display for Protocol<'_> {
273273
Wss => write!(f, "/wss"),
274274
Memory(port) => write!(f, "/memory/{}", port),
275275
Onion3(addr) => {
276-
let s = BASE32.encode(addr.hash());
277-
write!(f, "/onion3/{}:{}", s.to_lowercase(), addr.port())
276+
write!(f, "/onion3/{}:{}", addr.hash_string(), addr.port())
278277
}
279278
}
280279
}

tentacle/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ httparse = { version = "1.9", optional = true }
3636
futures-timer = { version = "3.0.2", optional = true }
3737

3838
multiaddr = { path = "../multiaddr", package = "tentacle-multiaddr", version = "0.3.4" }
39+
url = "2.5.4"
3940
molecule = "0.8.0"
4041

4142
# upnp
@@ -48,6 +49,7 @@ tokio-rustls = { version = "0.26.0", optional = true }
4849
# rand 0.8 not support wasm32
4950
rand = "0.8"
5051
socket2 = { version = "0.5.0", features = ["all"] }
52+
fast-socks5 = "0.10.0"
5153

5254
[target.'cfg(target_family = "wasm")'.dependencies]
5355
js-sys = "0.3"

tentacle/src/builder.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{io, sync::Arc, time::Duration};
22

3+
use crate::service::config::TransformerContext;
34
use nohash_hasher::IntMap;
45
use tokio_util::codec::LengthDelimitedCodec;
56

@@ -217,9 +218,49 @@ where
217218
#[cfg(not(target_family = "wasm"))]
218219
pub fn tcp_config<F>(mut self, f: F) -> Self
219220
where
220-
F: Fn(TcpSocket) -> Result<TcpSocket, std::io::Error> + Send + Sync + 'static,
221+
F: Fn(TcpSocket, TransformerContext) -> Result<TcpSocket, std::io::Error>
222+
+ Send
223+
+ Sync
224+
+ 'static,
221225
{
222-
self.config.tcp_config.tcp = Arc::new(f);
226+
self.config.tcp_config.tcp.socket_transformer = Arc::new(f);
227+
self
228+
}
229+
230+
fn must_parse_proxy_url(url: String) -> url::Url {
231+
let url = url::Url::parse(&url)
232+
.unwrap_or_else(|err| panic!("parse {} to url::Url failed: {}", url, err));
233+
if url.scheme().ne("socks5") {
234+
panic!("tentacle only support socks5 proxy");
235+
}
236+
url
237+
}
238+
239+
/// Proxy config for tcp
240+
///
241+
/// Example: socks5://127.0.0.1:9050
242+
/// Example: socks5://username:password@127.0.0.1:9050
243+
#[cfg(not(target_family = "wasm"))]
244+
pub fn tcp_proxy_config(mut self, proxy_url: String) -> Self {
245+
let proxy_url: url::Url = Self::must_parse_proxy_url(proxy_url);
246+
self.config.tcp_config.tcp.proxy_url = Some(proxy_url);
247+
self
248+
}
249+
250+
/// Onion config for tcp
251+
///
252+
/// Example: socks5://127.0.0.1:9050
253+
#[cfg(not(target_family = "wasm"))]
254+
pub fn tcp_onion_config(mut self, onion_url: String) -> Self {
255+
let onion_url: url::Url = Self::must_parse_proxy_url(onion_url);
256+
self.config.tcp_config.tcp.onion_url = Some(onion_url);
257+
self
258+
}
259+
260+
/// Onion config for tcp
261+
#[cfg(not(target_family = "wasm"))]
262+
pub fn tcp_onion_random_socks_auth(mut self, onion_random_socks_auth: bool) -> Self {
263+
self.config.tcp_config.tcp.onion_random_socks_auth = onion_random_socks_auth;
223264
self
224265
}
225266

@@ -228,9 +269,12 @@ where
228269
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
229270
pub fn tcp_config_on_ws<F>(mut self, f: F) -> Self
230271
where
231-
F: Fn(TcpSocket) -> Result<TcpSocket, std::io::Error> + Send + Sync + 'static,
272+
F: Fn(TcpSocket, TransformerContext) -> Result<TcpSocket, std::io::Error>
273+
+ Send
274+
+ Sync
275+
+ 'static,
232276
{
233-
self.config.tcp_config.ws = Arc::new(f);
277+
self.config.tcp_config.ws.socket_transformer = Arc::new(f);
234278
self
235279
}
236280

@@ -252,9 +296,12 @@ where
252296
#[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
253297
pub fn tcp_config_on_tls<F>(mut self, f: F) -> Self
254298
where
255-
F: Fn(TcpSocket) -> Result<TcpSocket, std::io::Error> + Send + Sync + 'static,
299+
F: Fn(TcpSocket, TransformerContext) -> Result<TcpSocket, std::io::Error>
300+
+ Send
301+
+ Sync
302+
+ 'static,
256303
{
257-
self.config.tcp_config.tls = Arc::new(f);
304+
self.config.tcp_config.tls.socket_transformer = Arc::new(f);
258305
self
259306
}
260307
}

tentacle/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ pub enum TransportErrorKind {
99
/// IO error
1010
#[error("transport io error: `{0:?}`")]
1111
Io(#[from] IOError),
12+
/// Proxy server error
13+
#[error("proxy error: `{0:?}`")]
14+
ProxyError(IOError),
1215
/// Protocol not support
1316
#[error("multiaddr `{0:?}` is not supported")]
1417
NotSupported(Multiaddr),

tentacle/src/runtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
all(target_family = "wasm", feature = "wasm-timer")
1515
))]
1616
mod generic_timer;
17+
pub(crate) mod proxy;
1718
#[cfg(all(not(target_family = "wasm"), feature = "tokio-runtime"))]
1819
mod tokio_runtime;
1920
#[cfg(target_family = "wasm")]

tentacle/src/runtime/proxy/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#[cfg(not(target_family = "wasm"))]
2+
pub(crate) mod socks5;
3+
#[cfg(not(target_family = "wasm"))]
4+
pub(crate) mod socks5_config;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use fast_socks5::{
2+
AuthenticationMethod, Socks5Command,
3+
client::{Config as ConnectConfig, Socks5Stream},
4+
};
5+
use tokio::net::TcpStream;
6+
7+
pub async fn connect(
8+
socks_server: url::Url,
9+
target_addr: String,
10+
target_port: u16,
11+
) -> Result<TcpStream, fast_socks5::SocksError> {
12+
let auth = {
13+
if socks_server.username().is_empty() {
14+
AuthenticationMethod::None
15+
} else {
16+
AuthenticationMethod::Password {
17+
username: socks_server.username().to_string(),
18+
password: socks_server.password().unwrap_or_default().to_string(),
19+
}
20+
}
21+
};
22+
let socks_server_str = format!(
23+
"{}:{}",
24+
socks_server.host_str().ok_or_else(|| {
25+
fast_socks5::SocksError::ArgumentInputError("socks_server should have host")
26+
})?,
27+
socks_server.port().ok_or_else(|| {
28+
fast_socks5::SocksError::ArgumentInputError("socks_server should have port")
29+
})?
30+
);
31+
Socks5Stream::connect_raw(
32+
Socks5Command::TCPConnect,
33+
socks_server_str,
34+
target_addr,
35+
target_port,
36+
Some(auth),
37+
ConnectConfig::default(),
38+
)
39+
.await
40+
.map(|socket| socket.get_socket())
41+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use rand::Rng;
2+
3+
pub(crate) fn random_auth() -> (String, String) {
4+
let username = rand::thread_rng()
5+
.sample_iter(&rand::distributions::Alphanumeric)
6+
.take(8)
7+
.map(char::from)
8+
.collect();
9+
let password = rand::thread_rng()
10+
.sample_iter(&rand::distributions::Alphanumeric)
11+
.take(16)
12+
.map(char::from)
13+
.collect();
14+
(username, password)
15+
}
Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
use super::proxy::{socks5, socks5_config::random_auth};
2+
use multiaddr::{MultiAddr, Protocol};
13
pub use tokio::{
24
net::{TcpListener, TcpStream},
35
spawn,
46
task::{JoinHandle, block_in_place, spawn_blocking, yield_now},
57
};
68

7-
use crate::service::config::{TcpSocket, TcpSocketConfig};
9+
use crate::service::config::{
10+
TcpSocket, TcpSocketConfig, TcpSocketTransformer, TransformerContext,
11+
};
812
use socket2::{Domain, Protocol as SocketProtocol, Socket, Type};
913
#[cfg(unix)]
1014
use std::os::unix::io::{FromRawFd, IntoRawFd};
@@ -88,7 +92,8 @@ pub(crate) fn listen(addr: SocketAddr, tcp_config: TcpSocketConfig) -> io::Resul
8892
// user can disable it on tcp_config
8993
#[cfg(not(windows))]
9094
socket.set_reuse_address(true)?;
91-
let t = tcp_config(TcpSocket { inner: socket })?;
95+
let transformer_context = TransformerContext::new_listen(addr);
96+
let t = (tcp_config.socket_transformer)(TcpSocket { inner: socket }, transformer_context)?;
9297
t.inner.set_nonblocking(true)?;
9398
// safety: fd convert by socket2
9499
unsafe {
@@ -113,15 +118,16 @@ pub(crate) fn listen(addr: SocketAddr, tcp_config: TcpSocketConfig) -> io::Resul
113118
socket.listen(1024)
114119
}
115120

116-
pub(crate) async fn connect(
121+
async fn connect_direct(
117122
addr: SocketAddr,
118-
tcp_config: TcpSocketConfig,
123+
socket_transformer: TcpSocketTransformer,
119124
) -> io::Result<TcpStream> {
120125
let domain = Domain::for_address(addr);
121126
let socket = Socket::new(domain, Type::STREAM, Some(SocketProtocol::TCP))?;
122127

123128
let socket = {
124-
let t = tcp_config(TcpSocket { inner: socket })?;
129+
let transformer_context = TransformerContext::new_dial(addr);
130+
let t = socket_transformer(TcpSocket { inner: socket }, transformer_context)?;
125131
t.inner.set_nonblocking(true)?;
126132
// safety: fd convert by socket2
127133
unsafe {
@@ -135,3 +141,93 @@ pub(crate) async fn connect(
135141

136142
socket.connect(addr).await
137143
}
144+
145+
async fn connect_by_proxy(
146+
target_addr: String,
147+
target_port: u16,
148+
proxy_url: url::Url,
149+
) -> io::Result<TcpStream> {
150+
socks5::connect(proxy_url.clone(), target_addr.clone(), target_port)
151+
.await
152+
.map_err(|err| {
153+
io::Error::new(
154+
io::ErrorKind::Other,
155+
format!(
156+
"socks5_connect to target_addr: {}, target_port: {} by proxy_server: {} failed, err: {}",
157+
target_addr, target_port, proxy_url, err
158+
),
159+
)
160+
})
161+
}
162+
163+
pub(crate) async fn connect(
164+
target_addr: SocketAddr,
165+
tcp_config: TcpSocketConfig,
166+
) -> io::Result<TcpStream> {
167+
let TcpSocketConfig {
168+
socket_transformer,
169+
proxy_url,
170+
onion_url: _,
171+
onion_random_socks_auth: _,
172+
} = tcp_config;
173+
174+
match proxy_url {
175+
Some(proxy_url) => connect_by_proxy(
176+
target_addr.ip().to_string(),
177+
target_addr.port(),
178+
proxy_url.clone(),
179+
)
180+
.await
181+
.map_err(|err| {
182+
io::Error::new(
183+
io::ErrorKind::Other,
184+
format!("connect_by_proxy: {}, error: {}", proxy_url, err),
185+
)
186+
}),
187+
None => connect_direct(target_addr, socket_transformer).await,
188+
}
189+
}
190+
191+
pub(crate) async fn connect_onion(
192+
onion_addr: MultiAddr,
193+
tcp_config: TcpSocketConfig,
194+
) -> io::Result<TcpStream> {
195+
let TcpSocketConfig {
196+
socket_transformer: _,
197+
proxy_url,
198+
onion_url,
199+
onion_random_socks_auth,
200+
} = tcp_config;
201+
let mut tor_server_url = onion_url.or(proxy_url).ok_or(io::Error::other(
202+
"need tor proxy server to connect to onion address",
203+
))?;
204+
205+
if onion_random_socks_auth {
206+
let (random_username, random_passwd) = random_auth();
207+
tor_server_url
208+
.set_username(&random_username)
209+
.map_err(|_| io::Error::other("failed to set username"))?;
210+
tor_server_url
211+
.set_password(Some(&random_passwd))
212+
.map_err(|_| io::Error::other("failed to set password"))?;
213+
}
214+
215+
let onion_protocol = onion_addr
216+
.iter()
217+
.find_map(|protocol| {
218+
if let Protocol::Onion3(onion_address) = protocol {
219+
Some(onion_address)
220+
} else {
221+
None
222+
}
223+
})
224+
.ok_or(io::Error::other(format!(
225+
"No Onion3 address found. in {}",
226+
onion_addr
227+
)))?;
228+
229+
let onion_str = onion_protocol.hash_string() + ".onion";
230+
let onion_port = onion_protocol.port();
231+
232+
connect_by_proxy(onion_str, onion_port, tor_server_url).await
233+
}

0 commit comments

Comments
 (0)