mtp/docs/NATIVE-HOST.md
Alex Emmet 08aa193fd1
Some checks failed
CI / checks (push) Failing after 2m23s
General Upgrade, NEW: WebServers, Better Docs
2026-07-18 03:21:44 +02:00

9.8 KiB

MTP Native Host

The native host is a Rust library (mtp-host) that runs a QUIC server, accepts MTP client connections, negotiates protocol versions, and optionally performs a mutual-authentication handshake (login/register) using Ed25519 and ML-DSA-65 signatures.

Cargo Dependency

Add the mtp umbrella crate with host. Add crypto for authenticated connections and pipes for raw streams. The feature table is in the README.

HostConfig

HostConfig::new takes the bind address, port, PEM certificate chain, and PEM private key. Configure authentication and transport behavior with builders:

let config = HostConfig::new(ip, port, certificate, private_key)
    .with_pongs(true)
    .with_policy(Policy::default())
    .with_authentication(host_keyring, get_existing_client, complete_register)
    .with_authentication_policy(AuthenticationPolicy::ForceAuthentication);
Field Type Description
ip IpAddr Bind address
port u16 Listen port
tls_fullchain Vec<u8> PEM-encoded TLS certificate chain
tls_key Vec<u8> PEM-encoded TLS private key
send_pongs bool Sends a Pong for each received Ping (default true)
authentication_policy AuthenticationPolicy (crypto) ForceAuthentication, AllowAuthentication, or Unauthenticated
host_keyring Keyring (crypto) Host's signing and KEM keys
get_existing_client Async callback returning Option<PublicKeyBundle> Receives (client_id, description). Some supplies the stored key bundle. description = None is used for guest-ID collision checks.
guest_id_generator Async callback returning Option<u64> Custom guest ID assignment. The default generates random IDs.
complete_register Async callback returning u64 Stores the public bundle and returns its assigned client ID.

AuthenticationPolicy

ForceAuthentication requires every client to complete the login or registration handshake. AllowAuthentication accepts both authenticated and unauthenticated connections; unauthenticated clients receive an ID and AuthState::Unauthenticated. Unauthenticated rejects authentication attempts and is the default. Authentication policy details are in Security.

TLS

HostConfig::new always uses the certificate and key supplied by the caller. Certificate trust and development settings are in Security.

Ping-Pong

Keepalive behavior is defined in Protocol Reference.

let config = HostConfig::new(ip, port, cert, key)
    .with_pongs(true);

Disable automatic responses only when the application needs to handle Ping frames itself:

let config = HostConfig::new(ip, port, cert, key)
    .with_pongs(false);

Follow the responder contract in Protocol Reference.

Accepting Connections

use mtp::host::MTPHost;

let mut host = MTPHost::new(config).await?;
println!("Listening on {}", host.local_addr());

while let Some(conn) = host.accept().await? {
    // conn is an MTPConnection ready for I/O
}

MTPConnection

accept() returns the shared connection shape in MTP Connections after version negotiation and authentication, when enabled. The host-specific codec is scoped to the negotiated version, and client_public_key is set for authenticated clients.

Version Negotiation

accept() uses the version-bearing opening frame and registry flow in Connector. The host registry is built from the type maps in type-maps.yaml by Registry::builtin().

Registry

use mtp::codec::registry::Registry;

let registry = host.registry();
assert!(registry.supports(&Version(2, 0)));

let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
// -> Some(Version(2, 0)) if both versions are registered

Authentication Flow

The connection lifecycle and authentication sequence are in Protocol Reference. Host callback contracts are documented below.

After a successful handshake, MTPConnection exposes AuthState::Authenticated, the client ID, and the client's public key bundle when one is available.

Handling Messages

Use conn.sender and conn.receiver for bidirectional message exchange:

while let Some(conn) = host.accept().await? {
    tokio::spawn(async move {
        loop {
            match conn.receiver.receive().await {
                Ok(msg) => {
                    let response = process_message(&msg, &conn);
                    conn.sender.send(&response).await.ok();
                }
                Err(_) => break,
            }
        }
    });
}

Versioned Codec

The conn.codec is a VersionedCodec pre-configured with the negotiated version. Use it to encode/decode with version-specific type maps:

let tm = conn.codec.registry().get(&conn.version).unwrap();

// Look up type IDs for the negotiated version
let desc_id = DataTypeId(tm.data_id_enum(DataType::Description).unwrap());
let value = msg.get_data(desc_id);

Pipes

The complete pipe protocol and host API are documented in Pipes.

Host Callbacks

get_existing_client

Called during login to retrieve a client's public key bundle for signature verification, and also during guest ID generation to check whether a random candidate collides with a registered client. When used for collision checking the description argument is None.

Must return Some(PublicKeyBundle) if the client ID is known, or None otherwise.

// db: Arc<tokio::sync::Mutex<HashMap<u64, PublicKeyBundle>>>
let get_existing_client = |id: u64, _description: Option<String>| {
    let db = db.clone();
    Box::pin(async move { db.lock().await.get(&id).cloned() })
};

guest_id_generator

Optional callback that controls how unauthenticated connections receive their client ID. When None (the default), the host generates a random 48-bit ID and checks it against get_existing_client to avoid collisions.

Return Some(id) to accept the guest with that ID, or None to reject the connection. The ID must fit in 48 bits (id <= mtp_codec::MAX_WIRE_ID); values outside that range are rejected automatically and fall back to the built-in generator.

use std::sync::atomic::{AtomicU64, Ordering};

// Sequential guest IDs:
let counter = AtomicU64::new(1);
let guest_id_generator = Box::new(move || {
    Box::pin(async move { Some(counter.fetch_add(1, Ordering::SeqCst)) })
});

// Reject all guests (no unauthenticated connections):
let guest_id_generator = Box::new(|| Box::pin(async { None }));

let config = HostConfig::new(ip, port, cert, key)
    .with_authentication(host_keyring, get_existing_client, complete_register)
    .with_authentication_policy(AuthenticationPolicy::AllowAuthentication)
    .with_guest_id_generator(guest_id_generator);

complete_register

Called during registration to persist a new client's public key bundle and assign a client ID. The returned u64 becomes the client's permanent identifier.

// db: Arc<tokio::sync::Mutex<HashMap<u64, PublicKeyBundle>>>
let complete_register = |bundle: PublicKeyBundle, _description: Option<String>| {
    let db = db.clone();
    let id = next_id.fetch_add(1, Ordering::SeqCst);
    Box::pin(async move {
        db.lock().await.insert(id, bundle);
        id
    })
};

All callbacks are called from within accept() and must be Send + Sync. They are async (returning Pin<Box<dyn Future<...>>) and are .awaited by the host, so they can perform I/O or other async work as needed. The complete_register callback returns no error value. A panic aborts the normal callback flow; validate storage and ID allocation before returning the ID.

Host Key Generation

Generate a host keyring once and persist it:

use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner};
use mtp::crypto::kem::HybridKem;

let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
let (kem_sk, kem_pk) = HybridKem::generate_keypair();

let host_keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);

// Save to disk
let bytes = host_keyring.to_bytes();
std::fs::write("host_keys.bin", bytes)?;

Export the public key bundle so clients can verify the host identity:

let bundle = host_keyring.public_key_bundle();
std::fs::write("host_enc_kem_pk.bin", bundle.kem_public_key.as_bytes())?;
std::fs::write("host_sig_pk.bin", bundle.sig_cl_public_key.as_bytes())?;
std::fs::write("host_sig_pq_pk.bin", bundle.sig_pq_public_key.as_bytes())?;

Policy

Customize transport limits and timeouts through HostConfig::with_policy:

let config = HostConfig::new(ip, port, cert, key)
    .with_policy(custom_policy);
let host = MTPHost::new(config).await?;

Graceful Shutdown

Drop the MTPHost to stop accepting new connections. Active connections continue until their Sender/Receiver are dropped or the peer disconnects.

Run one accept loop per MTPHost and spawn one task per accepted connection. Stop the accept loop before dropping the host, then close active senders and wait for application tasks to finish. Use Operations for the deployment sequence and monitoring signals.