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/aa7313b7eb78b7546a10f9612fb3132...) * [Changes needed to adapt kcp-go/smux to quic-go](https://dip.torproject.org/dcf/obfs4/compare/b5d6895542b8fce92973901958b2d7a...)
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."