[anti-censorship-team] Turbo Tunnel in obfs4proxy (survives TCP connection termination)

David Fifield david at bamsoftware.com
Mon Oct 21 23:34:01 UTC 2019


I posted this at https://github.com/net4people/bbs/issues/14#issuecomment-544747519
but it's relevant to anti-censorship in Tor.


Here are proof-of-concept branches implementing the turbo tunnel idea in
obfs4proxy, one using kcp-go/smux and one using quic-go:

 * https://dip.torproject.org/dcf/obfs4/tree/reconnecting-kcp
 * https://dip.torproject.org/dcf/obfs4/tree/reconnecting-quic

As diffs:

 * [Changes needed to add kcp-go/smux to plain obfs4proxy](https://dip.torproject.org/dcf/obfs4/compare/aa7313b7eb78b7546a10f9612fb313280223612c...b5d6895542b8fce92973901958b2d7a2be1f2ebd)
 * [Changes needed to adapt kcp-go/smux to quic-go](https://dip.torproject.org/dcf/obfs4/compare/b5d6895542b8fce92973901958b2d7a2be1f2ebd...a3d32cb152c9bacfc31600b9689e4d3591acc376)

Using either of these branches, your circumvention session is decoupled
from any single TCP connection. If a TCP connection is terminated, the
obfs4proxy client will establish a new connection and pick up where it
left off. An error condition is signaled to the higher-level application
only when there's a problem establishing a new connection. Otherwise,
transient connection termination is invisible (except as a brief
increase in RTT) to Tor and whatever other application layers are being
tunnelled.

I did a small experiment showing how a Tor session can persist, despite
the obfs4 layer being interrupted every 20 seconds. I configured the
"little bastard" connection terminator to forward from a local port to a
remote bridge, and terminate connections after 20 seconds.

```
lilbastard$ cargo run -- -w 20 127.0.0.1:3000 192.81.135.242:4000
```

On the bridge, I ran tor using either plain obfs4proxy, or one of the
two turbo tunnel branches. (I did the experiment once for each of the
three configurations.)

```
DataDirectory datadir.server
SOCKSPort 0
ORPort auto
BridgeRelay 1
AssumeReachable 1
PublishServerDescriptor 0
ExtORPort auto
ServerTransportListenAddr obfs4 0.0.0.0:4000
ServerTransportPlugin obfs4 exec ./obfs4proxy -enableLogging -unsafeLogging -logLevel DEBUG
# ServerTransportPlugin obfs4 exec ./obfs4proxy.kcp -enableLogging -unsafeLogging -logLevel DEBUG
# ServerTransportPlugin obfs4 exec ./obfs4proxy.quic -enableLogging -unsafeLogging -logLevel DEBUG
```

On the client, I configured tor to use the corresponding obfs4proxy
executable, and connect to the bridge through the "little bastard"
proxy. (If you do this, your bridge fingerprint and cert will be
different.)

```
DataDirectory datadir.client
SOCKSPort 9250
UseBridges 1
Bridge obfs4 127.0.0.1:3000 94E4D617537C3E3CEA0D1D6D0BC852B5A7613B77 cert=6rB8kVd981U0G2b9nXioB5o0Zu7tDpDkoZyPe2aCmqFzGmfaSiNIfQvkJABakH+DfYwWRw iat-mode=0
ClientTransportPlugin obfs4 exec ./obfs4proxy -enableLogging -unsafeLogging -logLevel DEBUG
# ClientTransportPlugin obfs4 exec ./obfs4proxy.kcp -enableLogging -unsafeLogging -logLevel DEBUG
# ClientTransportPlugin obfs4 exec ./obfs4proxy.quic -enableLogging -unsafeLogging -logLevel DEBUG
```

Then, I captured traffic for 90 seconds while downloading a video file
through the tor proxy.

In the "plain obfs4proxy" case, the download stopped after the first
connection termination at 20 s. Every 20 s after that, there was a small
amount of activity, which was tor reconnecting to the bridge (and the
resulting obfs4 handshake). But it didn't matter, because tor has
already signaled the first connection termination to the application
layer, which gave up:

```
curl: (18) transfer closed with 111535615 bytes remaining to read
```

In comparison, the "kcp" and "quic" cases kept on downloading, being
only momentarily delayed by a connection termination.

Notes:
 * How this works architecturally, on the client side, we replace the
   original TCP Dial call with either kcp.NewConn2 or quic.Dial, over an
   abstract packet-sending interface (clientPacketConn).
   clientPacketConn runs a loop that repeatedly connects to the same
   destination and exchanges packets (represented as length-prefixed
   blobs in a TCP stream) as long as the connection is good, reporting
   an error only when a connection attempt fails. On the server side, we
   replace the TCP Listen call with either kcp.ServeConn or quic.Listen,
   over an abstract serverPacketConn. serverPacketConn opens a single
   TCP listener, takes length-prefixed packets from *all* the TCP
   streams that arrive at the listener, and feeds them into a *single*
   KCP or QUIC engine. Whenever we need to send a packet for a
   particular connection ID, we send it on the TCP stream that most
   recently sent us a packet for that connection ID.
 * There's no need for this functionality to be built into obfs4proxy
   itself. It could be done as a separate program:
   ```
   ------------ client ------------                ------------ bridge ------------    
   tor -> turbotunnel -> obfs4proxy -> internet -> obfs4proxy -> turbotunnel -> tor
   ```
   But this kind of process layering is cumbersome with pluggable transports.
 * I'm passing a blank client IP address to the pt.DialOr call—this
   information is used for geolocation in Metrics graphs. That's because
   an OR connection no longer corresponds to a single incoming IP
   address with its single IP address—instead it corresponds to an
   abstract "connection ID" that remains constant across potentially
   many TCP connections. In order to make this work, you would have to
   define some heuristic such as "the client IP address associated with
   the OR connection is that of the first TCP connection that carried
   that connection ID."



More information about the anti-censorship-team mailing list