Some questions about SPTPS
Etienne Dechamps
etienne at edechamps.fr
Sun Jul 27 12:34:23 CEST 2014
On 16/07/2014 10:52, Etienne Dechamps wrote:
> - The way SPTPS is currently implemented in tinc, sending packets over
> TCP is extremely inefficient because instead of using PACKET messages
> like the legacy protocol does, it encapsulates the packet in a REQ_KEY
> message (for backwards compatibility reasons, I guess).
Looking into this some more, I realized it's even worse than I thought:
not only is TCP packet transport inefficient, it's also the only way to
indirect a packet through a relay. This is because, as far as I can
tell, there is no way to relay UDP SPTPS packets, since there's no way
to know what their recipient is without decrypting them, but only the
recipient has the key.
This is a significant regression when compared to the legacy protocol:
tinc used to try (1) direct UDP communication, (2) indirect UDP
communication, (3) indirect TCP communication, thus always choosing the
most efficient path to the destination. With SPTPS, (2) is impossible
and tinc falls back directly to (3).
What's worse, with the way SPTPS over TCP is currently implemented using
REQ_KEY/ANS_KEY messages, the packet will be transported over TCP
through the entire graph. In contrast, with the legacy protocol tinc
uses TCP over as few edges as possible, i.e. as soon as the packet lands
on a node with an UDP path to the destination the packet would get an
"upgrade" to "business class" UDP. This is not the case with SPTPS. One
possible fix is to make nodes intercept SPTPS over TCP packets and
resend them over UDP instead of TCP if possible, but it's not as simple
as it sounds: indeed, when the final recipient receives the packet, it
will fail to decrypt it because it will use the relay's key instead of
the original sender's.
One simple solution to fix these relaying issues would be to send
packets in SPTPS records that are encrypted using the relay's key, but
of course that would defeat the whole point of end-to-end encryption.
I have a rough proposal that will hopefully bring the best of both
worlds. Basically, when a node wants to send an SPTPS record through a
relay via UDP, it would add a header to the UDP packet containing the
first 4 bytes of the SHA-256 hash of the recipient node name and the
source node name (total 8 bytes). The rest of the packet would be the
original SPTPS datagram, encrypted as usual with the end-to-end key. The
relay node would then use the hash to figure out what the final
recipient is, and then blindly relay the packet to that recipient (or to
the next relay). If multiple nodes have the same hash (which, according
to the birthday problem, has a 0.01% chance of happening with 1000
nodes), then UDP relaying is considered impossible and communication
would fall back to TCP.
Now this is where it gets tricky: there is no simple way on the relay
side to differentiate a "normal" incoming packet (where the first 4
bytes is the seqno) and a "relay" incoming packet (where the first 4
bytes is the recipient hash). I have a solution in mind, which is kinda
ugly but should work just fine: try to decrypt the packet as usual, if
it fails, and the first 8 bytes are two known hashes, then relay. This
is not as inefficient as it sounds: we can make the decryption fail-fast
in the vast majority of cases by checking the seqno first.
One thing to note though: for this to work the relay node needs to know
the actual UDP address of the source node, otherwise it would try to get
it from trying node keys on the packet, but the packet is not encrypted
using any of those keys, so that wouldn't work. The obvious solution is
to make the source node and the relay probe MTU between each other
before attempting to relay, which will provide reliable UDP address
information (as well as UDP hole punching, etc.) and is required anyway
to ensure the UDP packets will get through. In that sense the behavior
is identical to the legacy protocol.
When the final relay sends the packet over UDP to the final recipient,
it will preserve the relay header when sending the packet. This is
because the recipient needs the source node hash in order to know which
key to use (otherwise it would use the relay node's key, and fail).
One could suggest that it would be useful to have this relay header for
every packet (including direct ones), since that would optimize the
unknown UDP source address code path (try_harder()) by using the source
information instead of trying all keys. IMHO I don't think it's worth
the overhead, especially since it would affect the case that's supposed
to be the most efficient (direct UDP communication).
Of course, MTU calculations would have to take the relay header overhead
(8 bytes) into account when sending a data packet through a relay.
The relaying feature would be accompanied by a minor protocol version
bump. Sending nodes will fall back to TCP if the recipient node doesn't
understand relaying.
In terms of security, while this doesn't allow an attacker to break the
secrecy of communications (since end-to-end encryption is still used),
it does allow an attacker to trick nodes into relaying bogus packets,
assuming it can impersonate a node's UDP address. Therefore the worst
that can be done would be to perhaps increase the load on the network
somewhat (no potential for amplification). Considering this requires a
MITM, I don't think this is important enough for us to care. In
addition, one can deduce what the source and destination nodes are just
by looking at a relay packet, but that's no worse than direct
communication where this information is given away by the source and
destination IP addresses anyway.
Thoughts?
--
Etienne Dechamps
More information about the tinc
mailing list