ptadapter Developer Guide¶
The ptadapter module provides several classes to interface with Pluggable
Transports.
ClientAdapter controls a PT client, and can be used to
initiate an obfuscated connection towards a PT server;
ServerAdapter and ExtServerAdapter
controls a PT server, and can be used to receive obfuscated
connection from a PT client.
Common aspects¶
Note: ptadapter runs the PT as a subprocess. On Windows platforms
before Python 3.8, the default event loop is
SelectorEventLoop, which does not support subprocesses.
A ProactorEventLoop must be used instead.
To set ProactorEventLoop as default globally, do this
before calling any async code:
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
There are several common arguments when initializing any of the *Adapter
classes:
pt_execis the command line of the PT executable, optionally including arguments. This must be a List ofstrorbytes, where the first element is the path + filename of the executable, and each subsequent element is a command line argument. This is the same style as used by thesubprocessmodule.stateis the path of the PT’s state directory. The PT specification requires that if the PT saves data, it must be saved in the state directory. A location writable by the PT should be specified; if it does not exist, the PT will try to create it. Alternatively,Nonecan be specified, in which case a temporary directory will be created as the state directory before starting the PT, and deleted once the PT exits.exit_on_stdin_closecan usually be left unspecified.
Instances of the *Adapter classes can be used as async context managers,
like this:
async with ptadapter.ClientAdapter(...) as adapter:
# The PT is automatically started
reader, writer = await adapter.open_transport_connection(...)
# ... use the adapter, etc.
# once exiting the `async with` block, the PT is stopped
PT Client¶
When initializing a ClientAdapter,
a list of transport method names are required,
but not individual upstream destinations or per-connection arguments.
These arguments are specified when making a connection.
# It's possible to add command line arguments
pt_exec = ['/usr/bin/obfs4proxy', '-enableLogging']
state = '/var/run/obfs4-state'
transports = ['obfs4', 'obfs3']
# An optional upstream proxy server can be specified
proxy = 'socks5://127.0.0.1:1080'
async with ptadapter.ClientAdapter(pt_exec, state, transports, proxy=proxy) as adapter:
# connect to an upstream
args = {'cert': '...'}
reader, writer = await adapter.open_transport_connection('obfs4', '127.0.0.1', 7900, args)
# use reader and writer as usual
PT Server¶
There are two classes providing PT server functionality:
ServerAdapter and ExtServerAdapter.
With ServerAdapter, when each obfuscated connection is
received, an unobfuscated connection is made to a TCP address:port specified
by you. This is mostly useful for forwarding unobfuscated traffic to something
else, although if you would like to receive the connections, you could simply
listen on a port and direct unobfuscated traffic there.
With ExtServerAdapter, when each obfuscated connection is
received, an async callback function is called where you
can handle the incoming connection. There is also a “pre-connect callback”,
where you are provided with client info and can deny the connection quickly.
This class is more useful when you want to handle incoming connections.
The main callback function is called with 3 arguments:
a StreamReader, a StreamWriter, and a
ptadapter.ExtOrPortClientConnection Named Tuple containing information
about the connecting client.
The pre-connect callback is called with a single argument, a
ptadapter.ExtOrPortClientConnection, and should return a boolean value.
If False is returned, then this connection will be terminated, and the
main callback will not be called.
In both these callbacks, the provided
ptadapter.ExtOrPortClientConnection Named Tuple has 3 elements:
transport is the name of the transport method the client is connecting through;
host is an instance of
ipaddress.IPv4Addressoripaddress.IPv6Addresscontaining the peer address of the client;port is the peer address port number of the client.
As an example, to write a server that only allows connection from localhost:
pt_exec = ['/usr/bin/obfs4proxy', '-enableLogging']
state = '/var/run/obfs4-state'
obfs4_args = {
'node-id': ...,
'public-key': ...,
...
}
async def pre_connect_cb(info):
# in pre-connect callback, allow connections from loopback addresses,
# and deny all others
if info.host.is_loopback:
return True
return False
async def connect_cb(reader, writer, info):
# this is the main connection callback
...
adapter = ptadapter.ExtServerAdapter(pt_exec, state, connect_cb, preconnect_cb=pre_connect_cb)
adapter.add_transport('obfs4', '127.0.0.1', 7900, obfs4_args)
adapter.add_transport('obfs3', '127.0.0.1', 7901)
async with adapter:
# now the PT is accepting connections
await adapter.wait()