Updated
This is a worked example of adapting a sample client–server system (an echo server) to a Turbo Tunnel design. The Turbo Tunnel version is resistant to TCP connection termination attacks. This example uses KCP and kcp-go to implement the inner session/reliability layer.
Download source code:
git clone https://www.bamsoftware.com/git/turbotunnel-paper.git
See the example/ subdirectory.
To run:
server$ ./server 127.0.0.1:8000 client$ ./client 127.0.0.1:8000
To test the turbotunnel version's resistance to TCP termination, you can run through a TCP proxy that terminates connections after a timeout. One such proxy is
$ git clone https://www.bamsoftware.com/git/lilbastard.git
To run the proxy,
lilbastard$ cargo run -- -w 20 127.0.0.1:7000 127.0.0.1:8000
Then run the example programs as before, having the client connect to the proxy instead of directly to the server.
server$ ./server 127.0.0.1:7000 client$ ./client 127.0.0.1:8000
This code is in the public domain.
Related links:
Rather than open a bare TCP connection,
the turbotunnel client first generates a
session identifier,
then creates a RedialPacketConn
to serve as the packet-sending and -receiving interface.
This component will be different in designs that do not use TCP
as the underlying transport.
On top of the RedialPacketConn the turbotunnel client then
opens a kcp.UDPSession.
(Despite the name, the kcp.UDPSession does not use UDP.
It sends and receives packets using the RedialPacketConn.)
It then opens a smux.Session
and then finally a single smux.Stream over that.
The smux.Stream plays the role that the
bare TCP connection did in the original code.
| plain/client/client.go | turbotunnel/client/client.go |
|---|---|
|
|
Where the original code created a bare TCP listener,
the turbotunnel code creates a kcp.Listener,
defined over a ListenerPacketConn.
The ListenerPacketConn is the basic packet-sending and -receiving
interface used by the KCP engine.
ListenerPacketConn, in this program,
happens to be make use of an underlying TCP listener,
but don't be fooled into thinking that this TCP listener is analogous
to the TCP listener in the original program.
In the turbotunnel program, it is the kcp.Listener
that is analogous to the TCP listener in the original program.
In the original program, the chain of calls goes
acceptConnections→handleConnection.
In the turbotunnel program, there is an extra level of indirection:
acceptSessions→acceptStreams→handleStream.
Each smux.Session can contain multiple streams,
even though this program uses only one stream per session.
| plain/server/server.go | turbotunnel/server/server.go |
|---|---|
|
|
RedialPacketConn
RedialPacketConn is the basic
packet-sending and -receiving interface used in the client.
It implements the net.PacketConn interface.
The loop method of RedialPacketConn
repeatedly dials the same address, redialing whenever a connection is interrupted.
The first thing sent on the connection is the
session identifier, and that session identifier
applies to all the packets contained therein.
It implements the ReadFrom and WriteTo methods
by encapsulating and decapsulating packets on whatever TCP connection
happens to be live at the time.
| turbotunnel/client/redialpacketconn.go | |
|---|---|
|
ListenerPacketConn
ListenerPacketConn is the basic
packet-sending and -receiving interface used in the server.
It implements the net.PacketConn interface.
ListenerPacketConn contains a
QueuePacketConn
that keeps track of packet send and receive queues for each live session identifier.
After a TCP connection is accepted, ListenerPacketConn
reads its session identifier and then begins decapsulating packets
and feeding them to the QueuePacketConn,
which will make them available through its ReadFrom method.
At the same time ListenerPacketConn reads from the send queue
for the session identifier and encapsulates outgoing packets
that were placed in the queue by the WriteTo method of QueuePacketConn.
| turbotunnel/server/listenerpacketconn.go | |
|---|---|
|
QueuePacketConn
QueuePacketConn transforms the "push" interface of
net.PacketConn
into a "pull" interface.
The QueueIncoming method places an incoming packet in a queue,
from where it may later be returned from a call to ReadFrom.
The WriteTo method places an outgoing packet in a
per–session identifier queue, from which it may be later retrieved
using the OutgoingQueue method.
QueuePacketConn contains a
RemoteMap
that keeps track of mapping of session identifiers to outgoing queues.
| turbotunnel/turbotunnel/queuepacketconn.go | |
|---|---|
|
RemoteMap
RemoteMap manages a mapping of
session identifiers to send queues.
It automatically discards send queues that have not been used in a while,
to avoid keeping state forever for disconnected peers.
(If a send queue is discarded and the peer later reappears,
it doesn't cause any loss of data—the send queue will be reinstantiated
and the KCP layer will retransmit any unacknowledged packets.)
| turbotunnel/turbotunnel/remotemap.go | |
|---|---|
|
The ReadPacket and WritePacket functions
define how packets are represented in a temporary TCP connection.
This example uses a simple length-prefixed scheme.
In your own design you may want to use
something more sophisticated
that allows for padding.
Systems that are based on different substrates will, of course, have to use different encapsulation schemes. For example, see the DNS message encoding used by dnstt.
| turbotunnel/turbotunnel/encapsulation.go | |
|---|---|
|
SessionID
SessionID defines the format of a session identifier.
Here, it is just a 64-bit random string.
The session identifier should be long enough to prevent guessing and random collisions.
SessionID implements the
net.Addr interface.
| turbotunnel/turbotunnel/sessionid.go | |
|---|---|
|