PTRS
Pluggable Transports in Rust (PTRS) is a pure rust framework for working with pluggable transports. The ptrs library itself contains traits and tools used for implementing transports and integrating those transports into clients. Along with this, the repo provides and implementation of the obfs4 protocol as an example transport, and a tor bridge client implementation that supports any transport that implements the ptrs traits.
Crate | Crates.io | Docs |
---|---|---|
ptrs | ||
lyrebird | ||
obfs4 |
Pluggable Transports in Rust (PTRS)
This library (currently) revolves around the abstraction of connections as anything that implement
the traits [tokio::io:AsyncRead
] + [tokio::io::AsyncWrite
] + Unpin + Send + Sync
. This allows
us to define the expected shared behavior of pluggable transports as a transform of these
[Stream
]s.
/// Future containing a generic result. We use this for functions that take
/// and/or return futures that will produce Read/Write tunnels once awaited.
pub type FutureResult<T, E> = Box<dyn Future<Output = Result<T, E>> + Send>;
/// Future containing a generic result, shorthand for ['FutureResult']. We use
/// this for functions that take and/or return futures that will produce
/// Read/Write tunnels once awaited.
pub(crate) type F<T, E> = FutureResult<T, E>;
/// Client Transport
pub trait ClientTransport<InRW, InErr>
where
InRW: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
{
type OutRW: AsyncRead + AsyncWrite + Send + Unpin;
type OutErr: std::error::Error + Send + Sync;
type Builder: ClientBuilder<InRW>;
/// Create a pluggable transport connection given a future that will return
/// a Read/Write object that can be used as the underlying socket for the
/// connection.
fn establish(self, input: Pin<F<InRW, InErr>>) -> Pin<F<Self::OutRW, Self::OutErr>>;
/// Create a connection for the pluggable transport client using the provided
/// (pre-existing/pre-connected) Read/Write object as the underlying socket.
fn wrap(self, io: InRW) -> Pin<F<Self::OutRW, Self::OutErr>>;
/// Returns a string identifier for this transport
fn method_name() -> String;
}
/// Server Transport
pub trait ServerTransport<InRW>
where
InRW: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
{
type OutRW: AsyncRead + AsyncWrite + Send + Unpin;
type OutErr: std::error::Error + Send + Sync;
type Builder: ServerBuilder<InRW>;
/// Create/accept a connection for the pluggable transport client using the
/// provided (pre-existing/pre-connected) Read/Write object as the
/// underlying socket.
fn reveal(self, io: InRW) -> Pin<F<Self::OutRW, Self::OutErr>>;
/// Returns a string identifier for this transport
fn method_name() -> String;
}
Integrating ptrs Transports
Given this abstraction integrating transports into async rust applications becomes relatively straightforward, for example, integrating the identity transport (which performs a direct copy with no actual transform) could be done similar to:
/// TODO
Integration on the client side is similarly straightforward.
/// TODO
For more in depth integration examples see the binary examples in the
lyrebird
crate.
Configuration & State
Transports can be configured using their respective builder interface implementations
which require options(...)
and statedir(...)
functions. See the
obfs4 transport
for and example implementation of the
ptrs
interfaces.
Composition
Because the ptrs interface wraps objects implementing connection oriented traits and and returns trait objects implementing the same abstraction is is possible to wrap multiple transports on top of one another. One reason to do this might be to have separate reliability, obfuscation and padding strategies that can be composed interchangeably.
let listener = tokio::net::TcpListener::bind("127.0.0.1:8009")
.await
.unwrap();
let (tcp_sock, _) = listener.accept().await.unwrap();
let pb: &BuilderS = &<Passthrough as PluggableTransport<TcpStream>>::server_builder();
let client1 = <BuilderS as ServerBuilder<TcpStream>>::build(pb);
let conn1 = client1.reveal(tcp_sock).await.unwrap();
let client2 = <BuilderS as ServerBuilder<TcpStream>>::build(pb);
let conn2 = client2.reveal(conn1).await.unwrap();
let client3 = <BuilderS as ServerBuilder<TcpStream>>::build(pb);
let mut sock = client3.reveal(conn2).await.unwrap();
let (mut r, mut w) = tokio::io::split(&mut sock);
_ = tokio::io::copy(&mut r, &mut w).await;
In the client:
let pb: &BuilderC = &<Passthrough as PluggableTransport<TcpStream>>::client_builder();
let client = <BuilderC as ClientBuilder<TcpStream>>::build(pb);
let conn_fut1 = client.establish(Box::pin(tcp_dial_fut));
let client = <BuilderC as ClientBuilder<TcpStream>>::build(pb);
let conn_fut2 = client.establish(Box::pin(conn_fut1));
let client = <BuilderC as ClientBuilder<TcpStream>>::build(pb);
let conn_fut3 = client.establish(Box::pin(conn_fut2));
let mut conn = conn_fut3.await?;
let msg = b"a man a plan a canal panama";
_ = conn.write(&msg[..]).await?;
Implementing a Transport
There are several constructions that can be used to build up a pluggable transport, in part this is because no individual construction has proven demonstrably better than the others.
The obfs4 transport is implemented using the
tokio_util::codec
model.
Notes / Resources
While this is related to and motivated by the Tor pluggable transport system, the primary concern of this repository is creating a consistent and useful abstraction for building pluggable transports. For more information about Tor related pluggable transports see the following resources.
Obfs4 Transport in Rust
Installation
To install, add the following to your project’s Cargo.toml
:
[dependencies]
obfs4 = "0.1.0"
Integration Examples
Client example using ptrs
use ptrs::{Args, ClientBuilder as _, ClientTransport as _};
use obfs4;
use tokio::net::TcpStream;
let args = Args::from_str("")?;
let client = ClientBuilder::default()
.options(args)?
.build();
// future that opens a tcp connection when awaited
let conn_future = TcpStream::connect("127.0.0.1:9000");
// await (create) the tcp conn, attempt to handshake, and return a wrapped Read/Write object on success.
let obfs4_conn = client.wrap(box::pin(conn_future)).await?;
// ...
Server example
let message = b"Hello universe";
let (mut c, mut s) = tokio::io::duplex(65_536);
let mut rng = rand::thread_rng();
let o4_server = Server::new_from_random(&mut rng);
tokio::spawn(async move {
let mut o4s_stream = o4_server.wrap(&mut s).await.unwrap();
let mut buf = [0_u8; 50];
let n = o4s_stream.read(&mut buf).await.unwrap();
// echo the message back over the tunnel
o4s_stream.write_all(&buf[..n]).await.unwrap();
});