In working on circumvention protocols, I have repeatedly felt the need for a piece that is missing from our current designs. This document summarizes the problems I perceive, and how I propose to solve them.
In short, I think that every circumvention transport should incorporate some kind of session/reliability protocol—even the ones built on reliable channels that seemingly don't need it. It solves all kinds of problems related to performance and robustness. By session/reliability protocol, I mean something that offers a reliable stream abstraction, with sequence numbers and retransmissions, like QUIC or SCTP. Instead of a raw unstructured data stream, the obfuscation layer will carry encoded datagrams that are provided by the session/reliability layer.
When I say that circumvention transports should incorporate something like QUIC, for example, I don't mean that QUIC UDP packets are what we should send on the wire. No—I am not proposing a new outer layer, but an additional inner layer. We take the datagrams provided by the session/reliability layer, and encode them as appropriate for whatever obfuscation layer we happen to be using. So with meek, for example, instead of sending an unstructured blob of data in each HTTP request/response, we would send a handful of QUIC packets, encoded into the HTTP body. The receiving side would decode the packets and feed them into a local QUIC engine, which would reassemble them and output the original stream. A way to think about it is that the the sequencing/reliability layer is the "TCP" to the obfuscation layer's "IP". The obfuscation layer just needs to deliver chunks of data, on a best-effort basis, without getting blocked by a censor. The sequencing/reliability layer builds a reliable data stream atop that foundation.
I believe this design can improve existing transports, as well as enable new transports that are now possible now, such as those built on unreliable channels. Here is a list of selected problems with existing or potential transports, and how a sequencing/reliability layer helps solve them:
- Problem: Censors can disrupt obfs4 by terminating long-lived TCP connections, as Iran did in 2013, killing connections after 60 seconds.
- This problem exists because the obfs4 session is coupled with the TCP connection. The obfs4 session begins and ends exactly when the TCP connection does. We need an additional layer of abstraction, a virtual session that exists independently of any particular TCP connection. That way, if a TCP connection is terminated, it doesn't destroy all the state of the obfs4 session—you can open another TCP connection and resume where you left off, without needing to re-bootstrap Tor or your VPN or whatever was using the channel.
- Problem: The performance of meek is limited because it is half-duplex: it never sends and receives at the same time. This is because, while the bytes in a single HTTP request arrive in order, the ordering of multiple simultaneous requests is not guaranteed. Therefore, the client sends a request, then waits for the server's response before sending another, resulting in a delay of an RTT between successive sends.
- The session/reliability layer provides sequence numbers and reordering. Both sides can send data whenever is convenient, or as needed for traffic shaping, and any unordered data will be put back in order at the other end. A client could even split its traffic over two or more CDNs, with different latency characteristics, and know that the server will buffer and reorder the encoded packets to correctly recover the data stream.
- Problem: A Snowflake client can only use one proxy at a time, and that proxy may be slow or unreliable. Finding a working proxy is slow because each non-working one must time out in succession before trying another one.
- The problem exists because even though each WebRTC DataChannel is reliable (DataChannel uses SCTP internally), there's no ordering between multiple simultaneous DataChannels on separate Snowflake proxies. Furthermore, if and when a proxy goes offline, we cannot tell whether the last piece of data we sent was received by the bridge or not—the SCTP ACK information is not accessible to us higher in the stack—so even if we reconnect to the bridge through another proxy, we don't know whether we need to retransmit the last piece of data or not. All we can do is tear down the entire session and start it up again from scratch. As in the obfs4 case, this problem is solved by having an independent virtual session that persists across transient WebRTC sessions. An added bonus is the opportunity to use more than one proxy at once, to increase bandwidth or as a hedge against one of them disappearing.
- Problem: DNS over HTTPS is an unreliable channel: it is reliable TCP up to the DoH server, but after that, recursive resolutions are plain old unreliable UDP. And as with meek, the ordering of simultaneous DNS-over-HTTPS requests is not guaranteed.
- Solved by retransmission in the session layer. There's no DNS pluggable transport yet, but I think some kind of retransmission layer will be a requirement for it. Existing DNS tunnel software uses various ad-hoc sequencing/retransmission protocols. I think that a proper user-space reliability layer is the "right" way to do it.
- Problem: Shadowsocks opens a separate encrypted TCP connection for every connection made to the proxy. If a web page loads resources from 5 third parties, then the Shadowsocks client makes 5 parallel connections to the proxy.
- This problem is really about multiplexing, not session/reliability, but several candidate session/reliability protocols additionally offer multiplexing, for example streams in QUIC, streams in SCTP, or smux for KCP. Tor does not have this problem, because Tor already is a multiplexing protocol, with multiple virtual circuits and streams in one TCP/TLS connection. But every system could benefit from adding multiplexing at some level. Shadowsocks, for example, could open up one long-lived connection, and each new connection to the proxy would only open up a new stream inside the long-lived connection. And if the long-lived connection were killed, all the stream state would still exist at both endpoints and could be resumed on a new connection.
As an illustration of what I'm proposing, here's the protocol layering of meek (which sends chunks of the Tor TLS stream inside HTTP bodies), and where the new session/reliability layer would be inserted. Tor can remain oblivious to what's happening: just as before it didn't "know" that it was being carried over HTTP, it doesn't now need to know that it is being carried over QUIC-in-HTTP (for example).
[TLS]
[HTTP]
[session/reliability layer] ⇐ 🆕
[Tor]
[application data]
I've done a little survey and identified some suitable candidate protocols that also seem to have good Go packages:
I plan to evaluate at least these three candidates and develop some small proofs of concept. The overall goal of my proposal is to liberate the circumvention context from particular network connections and IP addresses.