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 | |
---|---|
|