bevy_quinnet 0.2.0

Bevy plugin for Client/Server multiplayer games using QUIC
Documentation

Bevy tracking crates.io

Bevy Quinnet

A Client/Server game networking plugin using QUIC, for the Bevy game engine.

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). 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:

  • 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
  • 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 does not have many 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 ordered reliable messages (same as messages over TCP) to the server
    • Receive (ordered or unordered) reliable messages from the server
  • A Server plugin which can:
    • Accept client connections & disconnect them
    • Send ordered reliable messages to the clients
    • Receive (ordered or unordered) reliable messages from any client
  • 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.

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 to communicate with the networking async tasks.

Roadmap

Those are the features/tasks that will probably come next (in no particular order):

  • Security: More certificates support, see certificates-and-server-authentication
  • Feature: Send messages from the server to a specific client
  • Feature: Send messages from the server to a selected group of clients
  • Feature: Raise connection/disconnection events from the plugins
  • Feature: Send unordered reliable messages from the server
  • Feature: Implementing a way to launch a local server from a client
  • Feature: Client should be capable to connect to another server after disconnecting
  • Performance: Messages aggregation before sending
  • Clean: Rework the error handling
  • Clean: Rework the configuration input for the client & server plugins
  • Documentation: Fully document the API
  • Tests: Add tests

Quickstart

Client

  • Add the QuinnetClientPlugin to the bevy app:
 App::new()
        // ...
        .add_plugin(QuinnetClientPlugin::default())
        // ...
        .run();
  • You can then use the Client resource to connect, send & receive messages:
fn start_connection(client: ResMut<Client>) {
    client
        .open_connection(
            ClientConfigurationData::new(
                "127.0.0.1".to_string(),
                6000,
                "0.0.0.0".to_string(),
                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.
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:
 App::new()
        /*...*/
        .add_plugin(QuinnetServerPlugin::default())
        /*...*/
        .run();
  • You can then use the Server resource to start the listening server:
fn start_listening(mut server: ResMut<Server>) {
    server
        .start_endpoint(
            ServerConfigurationData::new("127.0.0.1".to_string(), 6000, "0.0.0.0".to_string()),
            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.
fn handle_client_messages(
    mut server: ResMut<Server>,
    /*...*/
) {
    let mut endpoint = server.endpoint_mut();
    while let Ok(Some((message, client_id))) = endpoint.receive_message::<ClientMessage>() {
        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.

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 :
    • Skip certificate verification (messages are still encrypted, but the server is not authentified)
    • Accept certificates issued by a Certificate Authority (implemented in Quinn, using rustls)
    • Trust on first use certificates (implemented in Quinnet, using rustls)
  • Server:
    • Generate and issue a self-signed certificate
    • Issue an already existing certificate (CA or self-signed)

On the client:

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

    // To generate a new self-signed certificate on each startup 
    server.start_endpoint(/*...*/, CertificateRetrievalMode::GenerateSelfSigned);
    // 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
    });

See more about certificates in the certificates readme

Logs

For logs configuration, see the unoffical bevy cheatbook.

Examples

This demo comes with an headless server, a terminal client and a shared protocol.

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

This demo is a modification of the classic Bevy 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, a client module, a shared protocol and the bevy app schedule.

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

breakout_versus_demo_short

Examples can be found in the examples directory.

Compatible Bevy versions

Compatibility of bevy_quinnet versions:

bevy_quinnet bevy
0.1 0.8
0.2 0.9

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("QUIC" on the Web) than hack on WebRTC data channels.

Credits

Thanks to the 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:

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