diff --git a/README.md b/README.md
index f408ba1..7f401af 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,6 @@
- Donations goes to Wife, so she can allow me to work on the project
@@ -22,7 +21,7 @@ My inspiration came from [this project](https://www.npmjs.com/package/wstunnel)
* Good error messages and debug informations
* Static forward and reverse tunneling (TCP and UDP)
-* Dynamic tunneling (socks5 proxy)
+* Dynamic tunneling (Socks5 proxy and Transparent Proxy)
* Support for http proxy (when behind one)
* Support for tls/https server (with embedded self-signed certificate, see comment in the example section)
* Support IPv6
diff --git a/src/main.rs b/src/main.rs
index 6735ee8..f98cf8f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -28,7 +28,7 @@ use tokio_rustls::rustls::{Certificate, PrivateKey, ServerName};
use tracing::{error, info, Level};
-use crate::LocalProtocol::ReverseTcp;
+use crate::tunnel::to_host_port;
use tracing_subscriber::EnvFilter;
use url::{Host, Url};
@@ -56,6 +56,7 @@ struct Client {
/// 'udp://1212:1.1.1.1:53?timeout_sec=10' timeout_sec on udp force close the tunnel after 10sec. Set it to 0 to disable the timeout [default: 30]
/// 'socks5://[::1]:1212' => listen locally with socks5 on port 1212 and forward dynamically requested tunnel
/// 'stdio://google.com:443' => listen for data from stdio, mainly for `ssh -o ProxyCommand="wstunnel client -L stdio://%h:%p ws://localhost:8080" my-server`
+ /// 'tproxy+tcp://[::1]:1212 => listen locally on tcp on port 1212 as a *transparent proxy* and forward dynamically requested tunnel (linux only requires sudo/CAP_NET_ADMIN)
#[arg(short='L', long, value_name = "{tcp,udp,socks5,stdio}://[BIND:]PORT:HOST:PORT", value_parser = parse_tunnel_arg, verbatim_doc_comment)]
local_to_remote: Vec,
@@ -172,6 +173,7 @@ enum LocalProtocol {
Udp { timeout: Option },
Stdio,
Socks5,
+ TProxyTcp,
ReverseTcp,
ReverseUdp { timeout: Option },
}
@@ -315,6 +317,16 @@ fn parse_tunnel_arg(arg: &str) -> Result {
remote: (dest_host, dest_port),
})
}
+ "tproxy+t" => {
+ let (local_bind, remaining) = parse_local_bind(&arg["tproxy+tcp://".len()..])?;
+ let x = format!("0.0.0.0:0?{}", remaining);
+ let (dest_host, dest_port, _options) = parse_tunnel_dest(&x)?;
+ Ok(LocalToRemote {
+ local_protocol: LocalProtocol::TProxyTcp,
+ local: local_bind,
+ remote: (dest_host, dest_port),
+ })
+ }
_ => Err(Error::new(
ErrorKind::InvalidInput,
format!("Invalid local protocol for tunnel {}", arg),
@@ -551,7 +563,7 @@ async fn main() {
let client_config = client_config.clone();
match &tunnel.local_protocol {
LocalProtocol::Tcp => {
- tunnel.local_protocol = ReverseTcp;
+ tunnel.local_protocol = LocalProtocol::ReverseTcp;
tokio::spawn(async move {
let remote = tunnel.remote.clone();
let cfg = client_config.clone();
@@ -592,7 +604,7 @@ async fn main() {
match &tunnel.local_protocol {
LocalProtocol::Tcp => {
let remote = tunnel.remote.clone();
- let server = tcp::run_server(tunnel.local)
+ let server = tcp::run_server(tunnel.local, false)
.await
.unwrap_or_else(|err| panic!("Cannot start TCP server on {}: {}", tunnel.local, err))
.map_err(anyhow::Error::new)
@@ -604,6 +616,28 @@ async fn main() {
}
});
}
+ #[cfg(target_os = "linux")]
+ LocalProtocol::TProxyTcp => {
+ let server = tcp::run_server(tunnel.local, true)
+ .await
+ .unwrap_or_else(|err| panic!("Cannot start TProxy server on {}: {}", tunnel.local, err))
+ .map_err(anyhow::Error::new)
+ .map_ok(move |stream| {
+ // In TProxy mode local destination is the final ip:port destination
+ let dest = to_host_port(stream.local_addr().unwrap());
+ (stream.into_split(), dest)
+ });
+
+ tokio::spawn(async move {
+ if let Err(err) = tunnel::client::run_tunnel(client_config, tunnel, server).await {
+ error!("{:?}", err);
+ }
+ });
+ }
+ #[cfg(not(target_os = "linux"))]
+ LocalProtocol::TProxyTcp => {
+ panic!("Transparent proxy is not available for non Linux platform")
+ }
LocalProtocol::Udp { timeout } => {
let remote = tunnel.remote.clone();
let server = udp::run_server(tunnel.local, *timeout)
@@ -630,28 +664,27 @@ async fn main() {
}
});
}
+
+ #[cfg(target_family = "unix")]
LocalProtocol::Stdio => {
- #[cfg(target_family = "unix")]
- {
- let server = stdio::run_server().await.unwrap_or_else(|err| {
- panic!("Cannot start STDIO server: {}", err);
- });
- tokio::spawn(async move {
- if let Err(err) = tunnel::client::run_tunnel(
- client_config,
- tunnel.clone(),
- stream::once(async move { Ok((server, tunnel.remote)) }),
- )
- .await
- {
- error!("{:?}", err);
- }
- });
- }
- #[cfg(not(target_family = "unix"))]
- {
- panic!("stdio is not implemented for non unix platform")
- }
+ let server = stdio::run_server().await.unwrap_or_else(|err| {
+ panic!("Cannot start STDIO server: {}", err);
+ });
+ tokio::spawn(async move {
+ if let Err(err) = tunnel::client::run_tunnel(
+ client_config,
+ tunnel.clone(),
+ stream::once(async move { Ok((server, tunnel.remote)) }),
+ )
+ .await
+ {
+ error!("{:?}", err);
+ }
+ });
+ }
+ #[cfg(not(target_family = "unix"))]
+ LocalProtocol::Stdio => {
+ panic!("stdio is not implemented for non unix platform")
}
LocalProtocol::ReverseTcp => {}
LocalProtocol::ReverseUdp { .. } => {}
diff --git a/src/tcp.rs b/src/tcp.rs
index 630c491..d957366 100644
--- a/src/tcp.rs
+++ b/src/tcp.rs
@@ -169,12 +169,19 @@ pub async fn connect_with_http_proxy(
Ok(socket)
}
-pub async fn run_server(bind: SocketAddr) -> Result {
+pub async fn run_server(bind: SocketAddr, ip_transparent: bool) -> Result {
info!("Starting TCP server listening cnx on {}", bind);
let listener = TcpListener::bind(bind)
.await
.with_context(|| format!("Cannot create TCP server {:?}", bind))?;
+
+ #[cfg(target_os = "linux")]
+ if ip_transparent {
+ info!("TCP server listening in TProxy mode");
+ socket2::SockRef::from(&listener).set_ip_transparent(ip_transparent)?;
+ }
+
Ok(TcpListenerStream::new(listener))
}
diff --git a/src/tunnel/mod.rs b/src/tunnel/mod.rs
index 3835e07..682e0f7 100644
--- a/src/tunnel/mod.rs
+++ b/src/tunnel/mod.rs
@@ -10,11 +10,13 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::io::{Error, IoSlice};
+use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
use tokio_rustls::client::TlsStream;
+use url::Host;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -36,6 +38,7 @@ impl JwtTunnelConfig {
LocalProtocol::Socks5 => LocalProtocol::Tcp,
LocalProtocol::ReverseTcp => LocalProtocol::ReverseTcp,
LocalProtocol::ReverseUdp { .. } => tunnel.local_protocol,
+ LocalProtocol::TProxyTcp => LocalProtocol::Tcp,
},
r: tunnel.remote.0.to_string(),
rp: tunnel.remote.1,
@@ -141,3 +144,10 @@ impl ManageConnection for WsClientConfig {
conn.is_none()
}
}
+
+pub fn to_host_port(addr: SocketAddr) -> (Host, u16) {
+ match addr.ip() {
+ IpAddr::V4(ip) => (Host::Ipv4(ip), addr.port()),
+ IpAddr::V6(ip) => (Host::Ipv6(ip), addr.port()),
+ }
+}
diff --git a/src/tunnel/server.rs b/src/tunnel/server.rs
index b6ff519..41838b1 100644
--- a/src/tunnel/server.rs
+++ b/src/tunnel/server.rs
@@ -88,7 +88,7 @@ async fn from_query(
let local_srv = (Host::parse(&jwt.claims.r)?, jwt.claims.rp);
let bind = format!("{}:{}", local_srv.0, local_srv.1);
- let listening_server = tcp::run_server(bind.parse()?);
+ let listening_server = tcp::run_server(bind.parse()?, false);
let tcp = run_listening_server(&local_srv, SERVERS.deref(), listening_server).await?;
let (local_rx, local_tx) = tcp.into_split();