bevy_quinnet

repository·main·Indexed 18 days ago

https://github.com/henauxg/bevy_quinnet

A Bevy engine networking plugin for client/server multiplayer games using the QUIC protocol. It provides a synchronous API for asynchronous QUIC streams integrated into Bevy's ECS architecture, supporting three channel types (OrderedReliable, UnorderedReliable, and Unreliable) and various TLS certificate verification modes including Trust On First Use (TOFU). Version 0.21.0 is compatible with Bevy 0.19.

Tokens
17.4K
Snippets
44
Records
72
Agent score
61%

What's inside bevy_quinnet

  1. How channels work in Quinnet

    main

    Quinnet provides three channel types to control delivery guarantees and ordering:

    • OrderedReliable: Guaranteed delivery and strict ordering (e.g., for chat).
    • UnorderedReliable: Guaranteed delivery, but order is not preserved (e.g., for animation triggers).
    • Unreliable: No guarantees on delivery or order (e.g., for high-frequency entity positions).

    Channels can be pre-configured during connection/endpoint setup using SendChannelsConfiguration, or opened/closed dynamically at runtime. You can have up to 256 channels open simultaneously. Each connection/endpoint has a default channel used when no specific ChannelId is provided.

    // Pre-configuring channels at startup
    let channels_config = SendChannelsConfiguration::from_configs(vec![
        ChannelConfig::default_ordered_reliable(), // ID 0
        ChannelConfig::default_ordered_reliable(), // ID 1
        ChannelConfig::default_unreliable(),       // ID 2
    ]);
    
    // Using channels on a client connection
    let connection = client.connection();
    connection.send_message(message); // Uses default
    connection.send_message_on(channel_id, message); // Uses specific ID
    connection.set_default_channel(channel_id); // Changes default
    
    // Opening a new channel dynamically
    let chat_channel = client.connection().open_channel(ChannelConfig::default()).unwrap();
    client.connection().send_message_on(chat_channel, chat_message);
  2. Quickstart: Set up a Quinnet Client

    main

    To implement a client, add the QuinnetClientPlugin to your Bevy app. Use the QuinnetClient resource to manage connections and exchange messages. Note that receive_message is generic and works with user-defined enums that implement Serialize and Deserialize.

    // 1. Add the plugin
    App::new()
        .add_plugins(QuinnetClientPlugin::default())
        .run();
    
    // 2. Connect to a server
    fn start_connection(mut client: ResMut<QuinnetClient>) {
        client
            .open_connection(ClientConnectionConfiguration {
                addr_config: ClientAddrConfiguration::from_ips(
                    SERVER_HOST,
                    SERVER_PORT,
                    LOCAL_BIND_IP,
                    0,
                ),
                cert_mode: CertificateVerificationMode::SkipVerification,
                defaultables: Default::default(),
            })
            .unwrap();
    }
    
    // 3. Handle messages
    fn handle_server_messages(mut client: ResMut<QuinnetClient>) {
        while let Some(message) = client.connection_mut().try_receive_message() {
            match message {
                ServerMessage::ChatMessage { client_id, message } => { /* ... */ }
                // ... other variants
            }
        }
    }
  3. Quickstart: Set up a Quinnet Server

    main

    To implement a server, add the QuinnetServerPlugin to your Bevy app. Use the QuinnetServer resource to start an endpoint. You can process messages from all clients via the endpoint and send messages to specific ClientIds or groups.

    // 1. Add the plugin
    App::new()
        .add_plugins(QuinnetServerPlugin::default())
        .run();
    
    // 2. Start listening
    fn start_listening(mut server: ResMut<QuinnetServer>) {
        server
            .start_endpoint(ServerEndpointConfiguration {
                addr_config: EndpointAddrConfiguration::from_ip(Ipv6Addr::UNSPECIFIED, 6000),
                cert_mode: CertificateRetrievalMode::GenerateSelfSigned {
                    server_hostname: Ipv6Addr::LOCALHOST.to_string(),
                },
                defaultables: Default::default(),
            })
            .unwrap();
    }
    
    // 3. Handle client messages
    fn handle_client_messages(mut server: ResMut<QuinnetServer>) {
        let mut endpoint = server.endpoint_mut();
        for client_id in endpoint.clients() {
            while let Some(message) = endpoint.try_receive_message(client_id) {
                match message {
                    ClientMessage::Join { username } => {
                        endpoint.try_send_message(client_id, ServerMessage::InitClient { /*...*/ });
                    }
                    // ... other variants
                }
            }
        }
    }
  4. Configure Trust On First Use (TOFU) for server authentication

    main

    To authenticate servers using the Trust On First Use (TOFU) model, pass CertificateVerificationMode::TrustOnFirstUse to the open_connection method.

    By default, Quinnet stores known hosts and their fingerprints in a file named quinnet/known_hosts. The default behavior is:

    • Unknown certificate: The client trusts the certificate, stores its fingerprint, and continues.
    • Trusted certificate: The fingerprint matches the store; the client trusts it and continues.
    • Untrusted certificate: The fingerprint does not match; the client raises an event and waits for user action.

    Use TrustOnFirstUseConfig to customize the storage location or the reaction to different certificate statuses.

    client.open_connection(/*...*/, CertificateVerificationMode::TrustOnFirstUse(TrustOnFirstUseConfig {
            ..Default::default()
        }),
    );
  5. Customize the Trust On First Use (TOFU) configuration

    main

    You can customize how TrustOnFirstUse behaves by configuring the TrustOnFirstUseConfig struct.

    Custom Store File

    To use a specific file instead of the default quinnet/known_hosts, use KnownHosts::HostsFile:

    client.open_connection(/*...*/, CertificateVerificationMode::TrustOnFirstUse(TrustOnFirstUseConfig {
        known_hosts: KnownHosts::HostsFile("MyCustomFile".to_string()),
        ..Default::default()
    }),
    );

    Custom Store and Verifier Behavior

    To use an in-memory store (e.g., a HashMap) and define specific automated actions for different certificate statuses, use KnownHosts::Store and the verifier_behaviour field.

    Available CertVerifierAction options:

    • TrustAndStore: Trust the certificate and save it to the store.
    • AbortConnection: Immediately terminate the connection.
    • TrustOnce: Trust the certificate for this session only.
    • RequestClientAction: Raise a CertInteractionEvent to ask the user for input.
    client.open_connection(/*...*/, CertificateVerificationMode::TrustOnFirstUse(TrustOnFirstUseConfig {
        known_hosts: KnownHosts::Store(my_cert_store),
        verifier_behaviour: HashMap::from([
                (CertVerificationStatus::UnknownCertificate, CertVerifierBehaviour::ImmediateAction(CertVerifierAction::TrustAndStore)),
                (CertVerificationStatus::UntrustedCertificate, CertVerifierBehaviour::ImmediateAction(CertVerifierAction::AbortConnection)),
                (CertVerificationStatus::TrustedCertificate, CertVerifierBehaviour::ImmediateAction(CertVerifierAction::TrustOnce)),
            ]),
    }),
    );
  6. Install bevy_quinnet with bincode-messages

    main

    To use high-level message helpers like send_message and try_receive_message, you must enable the bincode-messages feature in your Cargo.toml.

    bevy_quinnet = { version = "0.21", features = ["bincode-messages"] }
  7. Configure Certificate Verification Modes

    main

    You can control how the client validates server certificates using the CertificateVerificationMode enum. There are three available modes:

    1. SkipVerification: No verification is performed on the server certificate. Use this only for testing or in environments where security is not a concern.
    2. SignedByCertificateAuthority: The client only trusts certificates signed by a conventional Certificate Authority (CA).
    3. TrustOnFirstUse(TrustOnFirstUseConfig): Implements the Trust on First Use (TOFU) scheme. The client trusts the certificate the first time it connects and remembers it for future connections.
    use bevy_quinnet::client::certificate::{CertificateVerificationMode, TrustOnFirstUseConfig, KnownHosts};
    
    let config = TrustOnFirstUseConfig {
        known_hosts: KnownHosts::HostsFile("my_own_hosts_file".to_string()),
        ..Default::default()
    };
    
    let mode = CertificateVerificationMode::TrustOnFirstUse(config);
  8. Access Bevy Quinnet modules

    main

    The crate exposes three primary modules:

    • client: Contains APIs for connecting to servers and managing client-side QUIC connections.
    • server: Contains APIs for hosting connections and managing server-side QUIC listeners.
    • shared: Contains common types, traits, and logic used by both clients and servers.
  9. Handle client connection events

    main

    The client emits several Bevy messages during the connection lifecycle. These are raised in the CoreStage::PreUpdate stage.

    • ConnectionEvent: Raised when the client successfully connects. If the shared-client-id feature is enabled, it includes the client_id assigned by the server.
    • ConnectionFailedEvent: Raised when a connection attempt fails. Contains the QuinnetConnectionError.
    • ConnectionLostEvent: Raised when the client is disconnected from the server.
  10. Manage server connections with Endpoint

    main

    The Endpoint struct is the primary interface for managing a QUIC server. It handles client connections, channel management, and payload broadcasting.

    Key capabilities include:

    • Client Management: Retrieve all connected ClientIds via .clients(), access specific connection details via .connection(), or disconnect clients via .disconnect_client(client_id).
    • Payload Transmission: Send data to specific clients, groups of clients, or broadcast to all clients using various send_payload and broadcast_payload methods.
    • Channel Management: Open new channels for all clients using .open_channel(channel_type) or close existing ones with .close_channel(channel_id).
    • Statistics: Monitor connection activity via .endpoint_stats() and individual client performance via .get_connection_stats(client_id).
  11. Configure Bevy Quinnet features

    main

    Bevy Quinnet is organized into three main modules based on the enabled Cargo features. Depending on your networking needs, you should enable the corresponding features in your Cargo.toml:

    • client: Enables client-side networking capabilities.
    • server: Enables server-side networking capabilities.
    • (Default) Shared features: Provides common networking logic used by both client and server.
    # Example Cargo.toml configuration
    [dependencies]
    bevy_quinnet = {
        version = "0.21.0",
        features = ["client", "server"]
    }