You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Despite all the complexity required to enable dial by nodeid and direct connections, iroh has a very simple API. You create an endpoint, create connections, create streams, and then send data.
32
+
Despite all the complexity required to enable dial-by-node-id and direct connections, iroh has a very simple API. You create an endpoint, create connections, create streams, and then send data.
33
33
34
34
There is some complexity involved in properly [closing QUIC connections](https://www.iroh.computer/blog/closing-a-quic-connection), but
35
-
other than that everything is pretty straightforward.
35
+
other than that, everything is pretty straightforward.
36
36
37
37
One aspect of iroh connections that is a bit more complex is 0-RTT. This blog post explains what 0-RTT is, when to use it, when **not** to use it, and will show a small example.
38
38
39
39
# What is 0-RTT
40
40
41
-
Iroh connections are just peer to peer QUIC connections, using a fork of the Quinn rust crate. QUIC is using TLS for encryption.
41
+
Iroh connections are just peer-to-peer QUIC connections, using a fork of the Quinn rust crate. QUIC uses TLS for encryption.
42
42
43
43
So to explain what 0-RTT is, it is helpful to explain how a normal TLS handshake works in detail.
44
44
@@ -54,7 +54,7 @@ We refer to logical TLS messages as just messages. Messages can be split into mu
54
54
55
55
### ClientHello
56
56
57
-
The connection is initiated from the client by sending a `ClientHello`. This message contains the TLS protocol version, random data, and the set of supported cipher suites. In addition iroh always includes a set of ALPN strings that identify the applicationlevel protocols that the client wishes to speak.
57
+
The connection is initiated from the client by sending a `ClientHello`. This message contains the TLS protocol version, random data, and the set of supported cipher suites. In addition, iroh always includes a set of ALPN strings that identify the application-level protocols that the client wishes to speak.
58
58
59
59
When receiving the `ClientHello`, the server has information to narrow down the set of cryptographic primitives to use. The server endpoint also has a set of ALPNs it supports, so it can downselect those as well.
60
60
@@ -74,61 +74,61 @@ The server will send the `ServerHello`, possibly additional encrypted messages f
74
74
75
75
### Session setup
76
76
77
-
Once the client has received the `ServerHello`, it has collected all the required information to set up the session for transporting userdata. At this point the cipher suite and ALPN are fixed for the rest of the session.
77
+
Once the client has received the `ServerHello`, it has collected all the required information to set up the session for transporting user-data. At this point the cipher suite and ALPN are fixed for the rest of the session.
78
78
79
79
<Note>
80
80
In the case where there is no overlap between client and server ALPNs, in principle it is possible to fall back to a default ALPN, but this is not relevant for iroh connections.
81
81
</Note>
82
82
83
-
Since the cipher suite is now fixed, the client can now use the client and server random to derive two symmetric keys, one for additional handshake messages and one for application data.
83
+
Since the cipher suite is now fixed, the client can now use the client and server random data to derive two symmetric keys, one for additional handshake messages and one for application data.
84
84
85
85
Every message that the client receives after the `ServerHello` is encrypted with the symmetric keys that are now shared knowledge on both sides.
86
86
87
-
The client processes possible additional packages from the server until it receives the `Finished` message. At that point it validates that it has received all setup messages form the server using a secure checksum contained in the server's `Finished` message.
87
+
The client processes possible additional packages from the server until it receives the `Finished` message. At that point it validates that it has received all setup messages from the server using a secure checksum contained in the server's `Finished` message.
88
88
89
-
### Userdata
89
+
### User-data
90
90
91
-
Now the client has ingested and validated all the messages relevant for connection setup, and has derived the keys required for userdata encryption. It can finally send a `Finished` message itself, finishing the handshake from its side.
91
+
Now the client has ingested and validated all the messages relevant for connection setup, and has derived the keys required for user-data encryption. It can finally send a `Finished` message itself, finishing the handshake from its side.
92
92
93
-
Immediately after this, the client can send the first message of userdata, encrypted with the derived application keys.
93
+
Immediately after this, the client can send the first message of user-data, encrypted with the derived application keys.
94
94
95
-
After the server receives the `Finished` message from the client, it can forward userdata to the application space. Also at this time it will send a number of `NewSessionTicket` messages to the client that contains a pre-shared key for session resumption or 0-RTT in subsequent connections.
95
+
After the server receives the `Finished` message from the client, it can forward user-data to the application space. Also at this time it will send a number of `NewSessionTicket` messages to the client that contains a pre-shared key for session resumption or 0-RTT in subsequent connections.
96
96
97
-
As you can see, the handshake is somewhat expensive in terms of computation, but more importantly requires a roundtrip from client to server and back before the first bit of userdata can flow, even in the case where both sides have talked recently.
97
+
As you can see, the handshake is somewhat expensive in terms of computation, but more importantly, requires a roundtrip from client to server and back before the first bit of user-data can flow, even in the case where both sides have talked recently.
98
98
99
99
## 0-RTT handshake
100
100
101
-
In many cases, in particular for long lived connections, the overhead of the handshake is completely acceptable. But there are many protocols where there is just a very brief information exchange between a client and a server, and latency is critical. In these cases, it would be a big advantage for the client to optimistically send userdata immediately after the `ClientHello`.
101
+
In many cases, in particular for long lived connections, the overhead of the handshake is completely acceptable. But there are many protocols where there is just a very brief information exchange between a client and a server, and latency is critical. In these cases, it would be a big advantage for the client to optimistically send user-data immediately after the `ClientHello`.
102
102
103
-
For latency critical protocols, you simply get your answer faster. But even for nonlatency critical protocols, the overhead is reduced because the total duration of the interaction is reduced, so there are fewer requests in flight.
103
+
For latency critical protocols, you simply get your answer faster. But even for non-latency critical protocols, the overhead is reduced because the total duration of the interaction is reduced, so there are fewer requests in flight.
104
104
105
-
0-RTT is only possible if a client has received preshared keys from the server via `NewSessionTicket` messages, which is the case if they performed a full handshake in the recent past.
105
+
0-RTT is only possible if a client has received pre-shared keys from the server via `NewSessionTicket` messages, which is the case if they performed a full handshake in the recent past.
Like before, the client sends a `ClientHello` message. The `ClientHello` for a connection attempt using 0-RTT must contain a set of ids of preshared keys, otherwise it is not possible for the server to decrypt subsequent userdata before the full handshake is complete. In addition it has an `early data` flag set.
113
+
Like before, the client sends a `ClientHello` message. The `ClientHello` for a connection attempt using 0-RTT must contain a set of ids of pre-shared keys, otherwise it is not possible for the server to decrypt subsequent user-data before the full handshake is complete. In addition it has an `early data` flag set.
114
114
115
-
Immediately after the `ClientHello`, the client will send a message containing userdata. Since a full exchange has not happened yet, the client has to choose one of the previously used PSKs for encryption, typically the most recent one.
115
+
Immediately after the `ClientHello`, the client will send a message containing user-data. Since a full exchange has not happened yet, the client has to choose one of the previously used PSKs for encryption, typically the most recent one.
116
116
117
117
### Server side
118
118
119
-
On the server side, the server receives the special `ClientHello`. In the happy case, it still has the key for the preshared key used by the client and will use it to decrypt subsequent messages. It will proceed with sending a `ServerHello` immediately, as before. This message will also have the `early data` flag set to indicate that the server might also send data before the handshake is complete.
119
+
On the server side, the server receives the special `ClientHello`. In the happy case, it still has the key for the pre-shared key used by the client and will use it to decrypt subsequent messages. It will proceed with sending a `ServerHello` immediately, as before. This message will also have the `early data` flag set to indicate that the server might also send data before the handshake is complete.
120
120
121
-
Once the server receives the early userdata message, if it accepts 0-rtt requests, it will attempt to decrypt the message and forward it to the application space. The application space can then answer with an early data message on its own.
121
+
Once the server receives the early user-data message, if it accepts 0-rtt requests, it will attempt to decrypt the message and forward it to the application space. The application space can then answer with an early data message on its own.
122
122
123
123
### Handshake completes
124
124
125
125
Regardless of early data in any direction, the normal handshake continues as before. Once the client receives a `Finished` message from the server, it will compute symmetric encryption keys as before and then switch to this set of keys for all subsequent messages. Likewise, once the server receives a `Finished` message from the client, it will compute keys and switch encryption.
126
126
127
127
### Replay attacks
128
128
129
-
By now we should have a pretty good idea what 0-RTT is and why it is useful. Being able to send userdata in the first UDP message seems tremendously useful. So why isn't it the default?
129
+
By now we should have a pretty good idea what 0-RTT is and why it is useful. Being able to send user-data in the first UDP message seems tremendously useful. So why isn't it the default?
130
130
131
-
The client message uses a previously received preshared key for encryption. For as long as this PSK is valid on the server, you can just re-send the *exact same packet*, and the userdata will be sent to the application on the server side. This could be used to create unwanted changes on the server in case of a non-idempotent request, or to create a large load by sending the same request millions of times.
131
+
The client message uses a previously received pre-shared key for encryption. For as long as this PSK is valid on the server, you can just re-send the *exact same packet*, and the user-data will be sent to the application on the server side. This could be used to create unwanted changes on the server in case of a non-idempotent request, or to create a large load by sending the same request millions of times.
132
132
133
133
In addition, due to the fact that the process of computing the session keys from the PSK is - and has to be - fully deterministic, once an attacker has access to the key, they can decrypt all past or future 0-RTT data encrypted using that key. Since the session switches encryption after the handshake completes, data after handshake completion is not affected by this.
134
134
@@ -146,7 +146,7 @@ Now let's implement a simple 0rtt service in iroh. It is going ot be a simple ec
146
146
147
147
On the accept side, this is relatively easy. We do have to opt in to allowing 0rtt requests, since as we have seen 0rtt has reduced security guarantees compared to normal requests with a full handshake before the data starts to flow.
148
148
149
-
When we get a `Connecting` by calling `incoming.accept()?`, we try to convert it into a 0rtt connection using `.into_0rtt()`. This will always succeed on the server side, but we have to handle the error anyway because we use the same types on the server and client side. Once that is done, the code is identical to a non0rtt echo service.
149
+
When we get a `Connecting` by calling `incoming.accept()?`, we try to convert it into a 0rtt connection using `.into_0rtt()`. This will always succeed on the server side, but we have to handle the error anyway because we use the same types on the server and client side. Once that is done, the code is identical to a non-0rtt echo service.
150
150
151
151
```rust
152
152
letconnecting=incoming.accept()?;
@@ -277,15 +277,15 @@ round 6: 510681 us
277
277
278
278
Now this is weird.
279
279
280
-
The initial connection is not 0-RTT, which is expected. The two nodes haven't ever talked, so there is no preshared secret they can use to communicate.
280
+
The initial connection is not 0-RTT, which is expected. The two nodes haven't ever talked, so there is no pre-shared secret they can use to communicate.
281
281
282
282
The next two rounds are 0-RTT, as expected, and have roughly the expected roundtrip times.
283
283
284
284
But then we get a non 0-RTT connection on the 4th round. What's going on?
285
285
286
286
The issue is that 0-RTT is done using *single-use* tickets. *After* each completed handshake, you get a number of these tickets, by default 2. And as soon as the tickets are used up, the client can not even *attempt* to do a 0-rtt request because it has run out of tickets to use.
287
287
288
-
In the code so far we close the connection immediately after receiving the userdata response. So we don't wait for the full handshake to complete, and also don't get any additional tickets.
288
+
In the code so far we close the connection immediately after receiving the user-data response. So we don't wait for the full handshake to complete, and also don't get any additional tickets.
289
289
290
290
```rust
291
291
pingpong_0rtt(connecting, i).await?
@@ -300,7 +300,7 @@ The tickets for 0-RTT connections are single use in Quinn, as recommended in the
300
300
301
301
There is currently no API to wait for the handshake to complete or to wait for session tickets. But we can just sleep for two times the current round trip time before closing the connection. This is a crude way to allow for enough time for the two `NewSessionTicket`s to be received.
302
302
303
-
Since we don't want to hold up the userspace just because we are waiting for an additional message, we do this in a tokio task.
303
+
Since we don't want to hold up the user-space just because we are waiting for an additional message, we do this in a tokio task.
304
304
305
305
```rust
306
306
tokio::spawn(asyncmove {
@@ -329,7 +329,7 @@ So in theory you could modify the client to reuse the ticket. This is nothing we
329
329
330
330
Let's take a step back and think about when you would use 0-RTT. If you have two nodes that communicate frequently every few seconds or even minutes, it is probably best to just leave the connection open. A normal QUIC connection has very low overhead, and even for a hole-punched iroh QUIC connection the overhead of occasional messages to keep the connection open is very acceptable.
331
331
332
-
So you would use 0-RTT if communication is in small bursts spaced many minutes to hours apart, but nevertheless latency is important. In this case the most important property of 0-RTT is that the application gets a response after the minimal physically possible delay. Keeping the connection open for another small time period to refresh the preshared keys is not a big deal.
332
+
So you would use 0-RTT if communication is in small bursts spaced many minutes to hours apart, but nevertheless latency is important. In this case, the most important property of 0-RTT is that the application gets a response after the minimal physically possible delay. Keeping the connection open for another small time period to refresh the pre-shared keys is not a big deal.
333
333
334
334
So this is the option we ended up implementing. Here is the output after this modification. We now get a 0-rtt connection every time.
335
335
@@ -434,7 +434,7 @@ cargo run --release --features=examples --example 0rtt
434
434
For the connect side, you have to provide the ticket produced by the accept side.
435
435
436
436
```
437
-
cargo run --release --features=examples --example 0rtt <ticket>
437
+
cargo run --release --features=examples --example 0rtt TICKET
438
438
```
439
439
440
440
You can also connect to our test node that should be far away from most readers of this blog post:
0 commit comments