Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Change Transport to allow for implementing partially reliable transport like QUIC #95

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

emillynge
Copy link

This PR is meant as a way to discuss how the Transport trait could be changed such that protocols that are partially reliable can leverage their ability to send forwarded packets unordered and unreliably.

I will repeat my thought from #64 (comment)

Benefits from forwarding packets unordered and unreliably

An ordered reliable transport, mans that:

  1. all packets will be received
  2. packets will be read in the order they are sent.

Now, this is nice (almost necessary) for the control channel. But it is actually a problem for the data channel.

As an example, retransmission of TCP packets in our "tunnel" will make it seem as if there is 0 loss from point of view of the original sender, which will make it impossible for them to detect congestion. (see TCP-meltdown)

Also, having all packets be ordered will introduce head-of-line blocking in the transport itself. If we are carrying UDP, this unnecessarily chkoes bandwidth. If we are carrying TCP, then we now have 2 layers of HOL blocking, where just one should suffice.

Some protocols, such as QUIC allows us to choose where any packet should be sent reliably, or unreliably. We can use that to simplify control channel and command packet code (no need for manual retransmission etc), whilst having all the forwarded packets be carried by unreliable streams.

The main protocol I envision building atop this PR is QUIC. However, I would like the approch to be useful for a TCP/UDP combination, such as seen in FTP:

  • establish connection over TCP/TLS
  • Server opens a UDP port for the client to use for forwarding
  • forward packets as encrypted UDP payloads using AES key shared over the TLS connection)

Approaches

The first approach in this PR is to introduce an enum TransportStream that may either contain 1 or 2 streams depending on whether the protocol wishes (is capable) of providing an unreliable stream in addition to the reliable one that is must provide.

An alternative would be to define a new "downgrade" method in the Transport trait that consumes the reliable stream into an unreliable one. This would likely suffice given that, as soon as we start the actual forwarding, no more commands needs to be sent reliable on the data channel. The control channel will keep needing a reliable connection, but it will simply never downgrade.

Either way, I don't see how to do this without requiring all Transport implementers define an UnreliableStream type. Unless of course, we just have 1 Transport::Stream type can be put into either reliable or unreliable mode. Likely, that would entail some flag or state that must be checked every time a packet is received or sent which may incur a slight amount of unwanted overhead. Yet, it may well be the simplest solution.

change handshake and connect to allow implementer to opt into providing a
second unreliable unordered stream to carry the forwarded packets.

rathole commands will still be sent over reliable ordered streams.
@emillynge emillynge mentioned this pull request Jan 12, 2022
@emillynge emillynge changed the title WIP: Change Transport to allow for implementing partially reliable trnasport like QUIC WIP: Change Transport to allow for implementing partially reliable transport like QUIC Jan 12, 2022
@emillynge
Copy link
Author

@rapiz1 I would need some feedback, in order to proceed. I am not yet sure whether the currently implemented approach is the best way to do it.

@rapiz1
Copy link
Owner

rapiz1 commented Jan 14, 2022

@emillynge Oh. I thought we're focused on #98 first. I will take a look

Copy link
Owner

@rapiz1 rapiz1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I miss something, but this doesn't seem right 🤔

run_data_channel_for_tcp::<T, T::ReliableStream>(reliable, &args.local_addr).await?;
}
TransportStream::PartiallyReliable(_, unreliable) => {
run_data_channel_for_tcp::<T, T::UnreliableStream>(unreliable, &args.local_addr).await?;
Copy link
Owner

@rapiz1 rapiz1 Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right. After reading quinn::Datagrams, I see it's really just datagram, just like what UDP provides. And your AsyncWrite wrapper simply wrap poll_write to send a datagram. But when you do the forwarding for a TCP service, copy_bidirectional is called and byte stream is forwarded, in which every byte is from the application layer, not a TCP packet, and should be guaranteed to be sent to the destination, while with T::UnreliableStream, it can be lost.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I must have misunderstood what the rathole client<->server data channel was carrying.

If we are ACK'ing data from the end user, then we absolutely have to ensure that the packet does indeed arrive (reliability).

But what we can still try to take advantage of is the ability to send QUIC packet unordered.

run_data_channel_for_udp::<T, T::ReliableStream>(reliable, &args.local_addr).await?;
}
TransportStream::PartiallyReliable(_, unreliable) => {
run_data_channel_for_udp::<T, T::UnreliableStream>(unreliable, &args.local_addr).await?;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may also not work as expect. run_data_channel_for_udp call write_all to write packets. And write_all can call poll_write multiple times and there's no guarantee that a packet passed to write_all will not be fragmented and passed to poll_write at a whole, which means it's possible multiple quic datagrams are used to carry one UdpTraffic, and some of them are lost, causing the deserialization of UdpTraffic to fail.

@rapiz1
Copy link
Owner

rapiz1 commented Jan 14, 2022

To move this forward, I think

  1. There doesn't seems a simple way to use unreliable transport for TCP service. So maybe we should leave TCP alone for now?
  2. There may need separate logic to handle UDP on unreliable transport. How to do that neatly is a problem. Trying to abstract an interface from stream based protocol and packet based protocol doesn't end well, from my own experience :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants