This is my talk for the the Turbo Tunnel paper at FOCI 2020.
David Fifield <david@bamsoftware.com>
This talk is directed towards designers of censorship circumvention systems. I want to convince you that you should include a session layer in your circumvention protocol stack, separate from the obfuscation layer. What do I mean by that? I think it's easiest to show by example, so let's start by taking a step back in time.
Back in 2013 I was presenting a system called OSS. And OSS was based on the principle of smuggling information in HTTP URLs. I was presenting this very visual, which shows the structure of an OSS URL. I want to call your attention to the part highlighted in green. That's the session identifier. That's just a random number generated by the client and its purpose is to enable the OSS server to disambiguate the requests of potentially multiple clients simultaneously. Another important part of the URL structure are these sequence and acknowledgement numbers. And those are there because we expect some non-negligible fraction of HTTP requests to fail, and when that happens there's a need to retransmit any lost data. It's a very TCP-like retransmission / reliability scheme. Now, OSS was never very widely deployed, and to be honest I was never super confident in the sequence number / acknowledgement reliability scheme that I had made for OSS. I am sure that, if the system had been deployed more widely, certain bugs and problems would have been uncovered in it, just because we know that these types of things are hard to design.
I was not the only one thinking about these things. In fact, even earlier, Code Talker Tunnel—SkypeMorph—was a system based on UDP packets. And UDP is, as we know, unreliable. So every Code Talker Tunnel packet includes a sequence number and an acknowledgement number to facilitate retransmission of unacknowledged data. Similarly, StegoTorus, a really innovative circumvention design, split up an incoming stream into little pieces, and each one of those pieces could be delivered over potentially very different cover channels. Every piece had a sequence number attached, so that, if they happened to arrive out of order, the server on the other end could reorder them before delivering the stream.
Fast forward now to the early days of when I was working on meek. meek builds a bidirectional channel out of a sequence of HTTP requests and responses. And the notion of a "session" is baked very deeply into meek. You can see here, there's a header field "X-Session-Id" that contains a session identifier. Now, one of the challenges of building a system on top of HTTP is that the order of simultaneous HTTP requests is not guaranteed. So, for a long time I was thinking about OSS, thinking about sequence numbers and acknowledgements, thinking that I would need something like this for meek. Now, I knew that meek was going to be big, and I knew it was something that I wanted to deploy for real, so, more than anything, I knew I needed something that was practical and deployable, and I was never confident enough in what I had done for OSS for that to be true. So, in the end, what I ended up doing for meek is very simple. I designed the system so that there could never be more than one request outstanding at a time. No simultaneous requests means there's no question of misordering. But even then, I knew that there was a right way to do this sort of thing and it was just a matter of putting in the work necessary to realize it.
Now, coming closer to the present, I have been helping develop the Snowflake circumvention system. Snowflake is based on the use of temporary browser-based proxies. When you use Snowflake, your traffic is actually going through someone else's web browser. One of the problems with Snowflake, until recently, is that your entire browsing session was coupled very tightly to the first temporary proxy that you were assigned. So you would start a browsing session, you would start using a proxy, and these proxies are by nature temporary, they don't stick around forever, and when that proxy stopped working, your session would just end and you would have no recourse but to restart the Snowflake software. What was missing was some idea of a session independent of any single proxy connection. And by now I had seen this pattern recur enough times that I knew, there is a right way to do this. We need some sort of interior session/reliability protocol, rather than just transmitting raw, unstructured chunks of an underlying stream through these proxies. So it was about a year ago I decided, enough is enough, let's sit down, figure it out, and do this right.
So, that's what I've been working on for about the past year, in a project I call Turbo Tunnel. Turbo Tunnel is just my name for this design pattern of incorporating a session/reliability layer in the middle of a circumvention protocol stack. If you adopt the design, you're under no obligation to also adopt the name, I just find it's useful to have a label to discuss things like this. Now, in a typical contemporary circumvention system, you receive a bunch of user streams, perhaps over SOCKS, and you pass those into some circumvention [obfuscation] layer, and then that circumvention [obfuscation] layer transforms them in a way such that they won't get blocked by a censor. In a Turbo Tunnel design, there is an additional design element, an additional protocol layer, that session/reliability layer. That is what receives the user streams and it takes those streams and breaks them up into packets, it attaches sequence numbers, it handles things like timeouts and retransmissions, and then it passes those packets into the obfuscation layer. The obfuscation layer's only responsibility is to deliver those packets in a way that doesn't get blocked by the censor.
It's worth thinking about this handoff between the session layer and the obfuscation layer, which I call "encapsulation." You're fundamentally transmitting packets now. Your obfuscation channel, however, may not even be packet-based, it may be stream-based, and in that case you need to encode the packets in way such that the packet boundaries can be recovered at the other end. Even if your obfuscation channel is packet-based, you may not want a 1:1 correspondence between session-layer packets and obfuscation-layer packets. In general, adopting a Turbo Tunnel design may increase your system's susceptibility to certain forms of traffic analysis attack, because you're imposing structure that wasn't there now [before], you're now transmitting encapsulated packet. So, it's worth thinking about doing things like adding padding, or adding timing delays, or doing other things to break up your protocol signature, in order to resist these types of attacks. Although I admit, I haven't investigated very far in that direction.
Speaking of the session/reliability protocol, there's no need to invent something new here. Good third-party libraries exist to do this sort of thing, and you can more or less just plug them into an existing design. I've listed two here: kcp-go and quic-go. These are the two that I've prototyped with extensively over the past year. I'm sure there are others. But the crucial feature of these two libraries is that they don't send their own network packets. Whenever they need to send or receive a packet, or do any other sort of network interaction, they call into your application, which gives you an opportunity to obfuscate the packets in whatever way is appropriate for your circumvention system. My way of developing the Turbo Tunnel idea has been to implement it in a variety of circumvention systems. So, I did it in obfs4, in meek, in Snowflake, and in a new DNS tunnel that can do DNS over HTTPS. meek and Snowflake we've talked about. So, in meek, the Turbo Tunnel design enables the client to send data whenever it wants, without having to wait for a response to its previous request. In Snowflake, it enables a session to migrate across multiple temporary proxy connections. And in fact, the Snowflake implementation has been merged into mainline, and if you download Tor Browser now, and turn on the Snowflake option, you will be getting this Turbo Tunnel–enabled Snowflake. It's been a really big benefit for usability. A DNS tunnel—it should be pretty clear—DNS is based on UDP, UDP is unreliable, so you need some sort of reliability layer. Rather than inventing something custom, purely for a DNS tunnel, you can use one of these existing session protocol libraries. And finally, you may be asking yourself, why would you need a session/reliability protocol for obfs4? obfs4 is already based on the use of TCP, which is already a reliable, in-order protocol. Well, the answer to that is that obfs4 is vulnerable to TCP termination attacks. A censor can reset a TCP connection and that tears down the whole obfs4 connection and all the session state attached to it, so any ongoing downloads you had going on are all terminated. The Turbo Tunnel version of obfs4 is resistant to TCP termination attacks because it has a notion of a session that is independent of any one TCP connection. So when one TCP connection gets terminated, you can start a new one, and resume the session on that new connection as if nothing had happened.
Now, I'm not going to claim that this is the right, or the only way to design a circumvention system, but this is how I'm planning to design my circumvention systems in the future. If you want to help, I think you can help by trying to implement this idea in one of your own designs. I've prepared some example code that shows how to adapt a simple client–server system to a Turbo Tunnel design, and I've tried to focus on just the essential elements and make the diffs small and comprehensible. So, the main programs are less than a hundred lines, and I've included all the ancillary support code that I've found necessary in implementing Turbo Tunnel. For feedback, there's a discussion thread linked here, and of course you can email me directly. Thank you for your attention, and keep on fighting the good fight.