aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: da4d01f9b8ccc2bf9d4c62e714362765278b0b3b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
[![crates.io](https://img.shields.io/crates/v/bevy_quinnet)](https://crates.io/crates/bevy_quinnet)

# Bevy Quinnet

A Client/Server game networking plugin using [QUIC](https://www.chromium.org/quic/), for the Bevy game engine.

- [Bevy Quinnet](#bevy-quinnet)
  - [QUIC as a game networking protocol](#quic-as-a-game-networking-protocol)
  - [Features](#features)
  - [Roadmap](#roadmap)
  - [Quickstart](#quickstart)
    - [Client](#client)
    - [Server](#server)
  - [Channels](#channels)
  - [Certificates and server authentication](#certificates-and-server-authentication)
  - [Logs](#logs)
  - [Examples](#examples)
  - [Compatible Bevy versions](#compatible-bevy-versions)
  - [Limitations](#limitations)
  - [Credits](#credits)
  - [License](#license)

## QUIC as a game networking protocol

QUIC was really attractive to me as a game networking protocol because most of the hard-work is done by the protocol specification and the implementation (here [Quinn](https://github.com/quinn-rs/quinn)). No need to reinvent the wheel once again on error-prones subjects such as a UDP reliability wrapper, some encryption & authentication mechanisms, congestion-control, and so on.

Most of the features proposed by the big networking libs are supported by default through QUIC. As an example, here is the list of features presented in [GameNetworkingSockets](https://github.com/ValveSoftware/GameNetworkingSockets):

* *Connection-oriented API (like TCP)*: -> by default
* *... but message-oriented (like UDP), not stream-oriented*: -> by default (*)
* *Supports both reliable and unreliable message types*: ->by default
* *Messages can be larger than underlying MTU. The protocol performs fragmentation, reassembly, and retransmission for reliable messages*: -> by default (frag & reassembly is not done by the protocol for unreliable packets)
* *A reliability layer [...]. It is based on the "ack vector" model from DCCP (RFC 4340, section 11.4) and Google QUIC and discussed in the context of games by Glenn Fiedler [...]*: -> by default.
* *Encryption. [...] The details for shared key derivation and per-packet IV are based on the design used by Google's QUIC protocol*: -> by default
* *Tools for simulating packet latency/loss, and detailed stats measurement*: -> Not by default
* *Head-of-line blocking control and bandwidth sharing of multiple message streams on the same connection.*: -> by default
* *IPv6 support*: -> by default
* *Peer-to-peer networking (NAT traversal with ICE + signaling + symmetric connect mode)*: -> Not by default
* *Cross platform*: -> by default, where UDP is available

-> Roughly 9 points out of 11 by default.

(*) Kinda, when sharing a QUIC stream, reliable messages need to be framed.

## Features

Quinnet has basic features, I made it mostly to satisfy my own needs for my own game projects.

It currently features:

- A Client plugin which can:
    - Connect/disconnect to/from one or more server
    - Send & receive unreliable and ordered/unordered reliable messages
- A Server plugin which can:
    - Accept client connections & disconnect them
    - Send & receive unreliable and ordered/unordered reliable messages
- Both client & server accept custom protocol structs/enums defined by the user as the message format.
- Communications are encrypted, and the client can [authenticate the server](#certificates-and-server-authentication).

Although Quinn and parts of Quinnet are asynchronous, the APIs exposed by Quinnet for the client and server are synchronous. This makes the surface API easy to work with and adapted to a Bevy usage.
The implementation uses [tokio channels](https://tokio.rs/tokio/tutorial/channels) internally to communicate with the networking async tasks.

##  Roadmap

This is a bird-eye view of the features/tasks that will probably be worked on next (in no particular order):

- [x] [Previous roadmap tasks](ROADMAP.md)
- [x] Security: More certificates support, see [certificates-and-server-authentication](#certificates-and-server-authentication)
- [x] Feature: Implement `unordered` & `ordered` `reliable` message channels on client & server, see [channels](#channels)
- [x] Feature: Implement `unreliable` message channel on client & server
- [ ] Feature: Implement `unreliable` messages larger than the path MTU from client & server
- [ ] Performance: feed multiples messages before flushing ordered reliable channels
- [ ] Clean: Rework the error handling in the async back-end
- [ ] Clean: Rework the error handling on collections to not fail at the first error
- [ ] Documentation: Fully document the API

## Quickstart

### Client

- Add the `QuinnetClientPlugin` to the bevy app:

```rust
 App::new()
        // ...
        .add_plugins(QuinnetClientPlugin::default())
        // ...
        .run();
```

- You can then use the `Client` resource to connect, send & receive messages:

```rust
fn start_connection(client: ResMut<Client>) {
    client
        .open_connection(
            ClientConfigurationData::from_ips(
                IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
                6000,
                IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
                0,
            ),
            CertificateVerificationMode::SkipVerification,
        );
    
    // When trully connected, you will receive a ConnectionEvent
```

- To process server messages, you can use a bevy system such as the one below. The function `receive_message` is generic, here `ServerMessage` is a user provided enum deriving `Serialize` and `Deserialize`.

```rust
fn handle_server_messages(
    mut client: ResMut<Client>,
    /*...*/
) {
    while let Ok(Some(message)) = client.connection().receive_message::<ServerMessage>() {
        match message {
            // Match on your own message types ...
            ServerMessage::ClientConnected { client_id, username} => {/*...*/}
            ServerMessage::ClientDisconnected { client_id } => {/*...*/}
            ServerMessage::ChatMessage { client_id, message } => {/*...*/}
        }
    }
}
```

### Server

- Add the `QuinnetServerPlugin` to the bevy app:

```rust
 App::new()
        /*...*/
        .add_plugins(QuinnetServerPlugin::default())
        /*...*/
        .run();
```

- You can then use the `Server` resource to start the listening server:

```rust
fn start_listening(mut server: ResMut<Server>) {
    server
        .start_endpoint(
            ServerConfigurationData::from_ip(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6000),
            CertificateRetrievalMode::GenerateSelfSigned,
        )
        .unwrap();
}
```

- To process client messages & send messages, you can use a bevy system such as the one below. The function `receive_message` is generic, here `ClientMessage` is a user provided enum deriving `Serialize` and `Deserialize`.

```rust
fn handle_client_messages(
    mut server: ResMut<Server>,
    /*...*/
) {
    let mut endpoint = server.endpoint_mut();
    for client_id in endpoint.clients() {
        while let Some(message) = endpoint.try_receive_message_from::<ClientMessage>(client_id) {
            match message {
                // Match on your own message types ...
                ClientMessage::Join { username} => {
                    // Send a messsage to 1 client
                    endpoint.send_message(client_id, ServerMessage::InitClient {/*...*/}).unwrap();
                    /*...*/
                }
                ClientMessage::Disconnect { } => {
                    // Disconnect a client
                    endpoint.disconnect_client(client_id);
                    /*...*/
                }
                ClientMessage::ChatMessage { message } => {
                    // Send a message to a group of clients
                    endpoint.send_group_message(
                            client_group, // Iterator of ClientId
                            ServerMessage::ChatMessage {/*...*/}
                        )
                        .unwrap();
                    /*...*/
                }           
            }
        }
    }
}
```

You can also use `endpoint.broadcast_message`, which will send a message to all connected clients. "Connected" here means connected to the server plugin, which happens before your own app handshakes/verifications if you have any. Use `send_group_message` if you want to control the recipients.

## Channels

There are currently 3 types of channels available when you send a message:
- `OrderedReliable`: ensure that messages sent are delivered, and are processed by the receiving end in the same order as they were sent (exemple usage: chat messages)
- `UnorderedReliable`: ensure that messages sent are delivered, in any order (exemple usage: an animation trigger)
- `Unreliable`: no guarantees on the delivery or the order of processing by the receiving end (exemple usage: an entity position sent every ticks)

By default for the server as well as the client, Quinnet creates 1 channel instance of each type, each with their own `ChannelId`. Among those, there is a `default` channel which will be used when you don't specify the channel. At startup, this default channel is an `OrderedReliable` channel.

```rust
let connection = client.connection();
// No channel specified, default channel is used
connection.send_message(message);
// Specifying the channel id
connection.send_message_on(ChannelId::UnorderedReliable, message);
// Changing the default channel
connection.set_default_channel(ChannelId::Unreliable);
```

One channel instance is more than enough for `UnorderedReliable` and `Unreliable` since messages are not ordered on those, in fact even if you tried to create more, Quinnet would just reuse the existing ones. This is why you can directly use their `ChannelId` when sending messages, as seen above.

In some cases, you may however want to create more than one channel instance, it may be the case for `OrderedReliable` channels to avoid some [Head of line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking) issues. Channels can be opened & closed at any time.

```rust
// If you want to create more channels
let chat_channel = client.connection().open_channel(ChannelType::OrderedReliable).unwrap();
client.connection().send_message_on(chat_channel, chat_message);
```

On the server, channels are created and closed at the endpoint level and exist for all current & future clients.
```rust
let chat_channel = server.endpoint().open_channel(ChannelType::OrderedReliable).unwrap();
server.endpoint().send_message_on(client_id, chat_channel, chat_message);
```

## Certificates and server authentication

Bevy Quinnet (through Quinn & QUIC) uses TLS 1.3 for authentication, the server needs to provide the client with a certificate confirming its identity, and the client must be configured to trust the certificates it receives from the server.

Here are the current options available to the server and client plugins for the server authentication:
- Client : 
    - [x] Skip certificate verification (messages are still encrypted, but the server is not authentified)
    - [x] Accept certificates issued by a Certificate Authority (implemented in [Quinn](https://github.com/quinn-rs/quinn), using [rustls](https://github.com/rustls/rustls))
    - [x] [Trust on first use](https://en.wikipedia.org/wiki/Trust_on_first_use) certificates (implemented in Quinnet, using [rustls](https://github.com/rustls/rustls))
- Server:
    - [x] Generate and issue a self-signed certificate
    - [x] Issue an already existing certificate (CA or self-signed)

On the client:

```rust
    // To accept any certificate
    client.open_connection(/*...*/, CertificateVerificationMode::SkipVerification);
    // To only accept certificates issued by a Certificate Authority
    client.open_connection(/*...*/, CertificateVerificationMode::SignedByCertificateAuthority);
    // To use the default configuration of the Trust on first use authentication scheme
    client.open_connection(/*...*/, CertificateVerificationMode::TrustOnFirstUse(TrustOnFirstUseConfig {
            // You can configure TrustOnFirstUse through the TrustOnFirstUseConfig:
            // Provide your own fingerprint store variable/file,
            // or configure the actions to apply for each possible certificate verification status.
            ..Default::default()
        }),
    );
```

On the server:

```rust
    // To generate a new self-signed certificate on each startup 
    server.start_endpoint(/*...*/, CertificateRetrievalMode::GenerateSelfSigned { 
        server_hostname: "127.0.0.1".to_string(),
    });
    // To load a pre-existing one from files
    server.start_endpoint(/*...*/, CertificateRetrievalMode::LoadFromFile {
        cert_file: "./certificates.pem".into(),
        key_file: "./privkey.pem".into(),
    });
    // To load one from files, or to generate a new self-signed one if the files do not exist.
    server.start_endpoint(/*...*/, CertificateRetrievalMode::LoadFromFileOrGenerateSelfSigned {
        cert_file: "./certificates.pem".into(),
        key_file: "./privkey.pem".into(),
        save_on_disk: true, // To persist on disk if generated
        server_hostname: "127.0.0.1".to_string(),
    });
```

See more about certificates in the [certificates readme](docs/Certificates.md)

## Logs

For logs configuration, see the unoffical [bevy cheatbook](https://bevy-cheatbook.github.io/features/log.html).

## Examples

<details>
  <summary>Chat example</summary>

This demo comes with an headless [server](examples/chat/server.rs), a [terminal client](examples/chat/client.rs) and a shared [protocol](examples/chat/protocol.rs).

Start the server with `cargo run --example chat-server` and as many clients as needed with `cargo run --example chat-client`. Type `quit` to disconnect with a client.

![terminal_chat_demo](https://user-images.githubusercontent.com/19689618/197757086-0643e6e7-6c69-4760-9af6-cb323529dc52.gif)

</details>

<details>
  <summary>Breakout versus example</summary>

This demo is a modification of the classic [Bevy breakout](https://bevyengine.org/examples/games/breakout/) example to turn it into a 2 players versus game.

It hosts a local server from inside a client, instead of a dedicated headless server as in the chat demo. You can find a [server module](examples/breakout/server.rs), a [client module](examples/breakout/client.rs), a shared [protocol](examples/breakout/protocol.rs) and the [bevy app schedule](examples/breakout/breakout.rs).

It also makes uses of [`Channels`](#channels). The server broadcasts the paddle position every tick via the `PaddleMoved` message on an `Unreliable` channel, the `BrickDestroyed` and `BallCollided` events are emitted on an `UnorderedReliable` channel, while the game setup and start are using the default `OrderedReliable` channel.

Start two clients with `cargo run --example breakout`, "Host" on one and "Join" on the other.

[breakout_versus_demo_short.mp4](https://user-images.githubusercontent.com/19689618/213700921-85967bd7-9a47-44ac-9471-77a33938569f.mp4)
</details>

Examples can be found in the [examples](examples) directory.
## Compatible Bevy versions

Compatibility of `bevy_quinnet` versions:

| `bevy_quinnet` | `bevy` |
| :------------- | :----- |
| `0.5`          | `0.11` |
| `0.4`          | `0.10` |
| `0.2`-`0.3`    | `0.9`  |
| `0.1`          | `0.8`  |

## Limitations

* QUIC is not available in a Browser (used in browsers but not exposed as an API). For now I would rather wait on [WebTransport](https://web.dev/webtransport/)("QUIC" on the Web) than hack on WebRTC data channels.

## Credits

Thanks to the [Renet](https://github.com/lucaspoffo/renet) crate for the inspiration on the high level API.

## License

bevy-quinnet is free and open source! All code in this repository is dual-licensed under either:

* MIT License ([LICENSE-MIT](docs/LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
* Apache License, Version 2.0 ([LICENSE-APACHE](docs/LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))

at your option. This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are [very good reasons](https://github.com/bevyengine/bevy/issues/2373) to include both.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.