mtp/CONNECTOR.md
2026-06-21 22:57:50 +02:00

4.3 KiB

Connector

This file documents the Connection and Version Negotiation logic.

Registry

The registry crate provides a multi-version Registry used by the host for version negotiation. A Registry holds one TypeMap per protocol version and supports negotiate():

use registry::Registry;

let registry = Registry::builtin(); // loads all TypeMaps from config

// Check if a version is supported
assert!(registry.supports(&Version(1, 0)));

// Find highest mutual version for a client
let client_versions = &[Version(0, 0), Version(1, 0)];
let negotiated = registry.negotiate(client_versions);
assert_eq!(negotiated, Some(Version(1, 0)));

// Look up a version's TypeMap
let tm = registry.get(&Version(2, 0)).unwrap();

The Registry::builtin() constructor uses the TypeMap::vX_Y() methods generated from the config.


Host

The host creates a QUIC server, manages the registry, and handles version negotiation with each connecting client.

Initialization

use mtp_host::{MTPHost, HostConfig};

let config = HostConfig {
    ip: "::".into(),
    port: 4433,
    tls_fullchain: std::fs::read("cert.pem")?,
    tls_key: std::fs::read("key.pem")?,
};

let mut host = MTPHost::new(config).await?;

Accepting Connections with Version Negotiation

while let Some(conn) = host.accept().await {
    /*
     *  conn.version is the negotiated version
     * conn.codec is a VersionedCodec scoped to that version
     * conn.sender / conn.receiver for raw CommunicationValue I/O
     */

    let msg = conn.receiver.receive().await?;
    // ...
}

The host's accept() method:

  1. Accepts a QUIC connection
  2. Reads the first CommunicationValue (always encoded with reserved type IDs)
  3. Extracts the client's protocol version from DataType::Version (wire ID 3)
  4. Calls registry.negotiate(&[client_version])
  5. Returns None if the version is unsupported (caller sends ErrorBadVersion and disconnects)
  6. Returns an MTPConnection with the negotiated version otherwise

Login/Register Flow

The complete login/register handshake (see design docs) builds on top of MTPConnection:

  1. Client sends Identification with version, ID, nonce, signature
  2. Host verifies signature via get_key callback
  3. Host responds with approval + nonces + signature
  4. Client verifies response

New clients use the Register variant instead, presenting their public key for registration.


Client

The client connects to a host and uses a single compiled-in protocol version.

use mtp_client::{MTPClient, ClientConfig};

let config = ClientConfig {
    url: "https://host.example.com:4433".into(),
    server_cert: None, // or Some(cert_pem_bytes)
};

// Connect (existing client)
let conn = MTPClient::connect(config, 8765).await?;

/*
 *  conn.version is the compiled-in PROTOCOL_VERSION
 *  conn.sender / conn.receiver for I/O
 */

The client's PROTOCOL_VERSION constant is set by protocol_version in type-maps.yaml and baked in at compile time. The client never imports the registry crate; it only uses type-map for enum types and codec for encoding.


Version Negotiation Flow

Client (v2.0)         Host (v0.0, v1.0, v2.0)
    │                        │
    │  QUIC connect          │
    │───────────────────────→│
    │                        │
    │  CommValue{ Ident. }   │
    │    Version → "2.0"     │
    │    Id → 8765           │
    │    Nonce → ...         │
    │    Signature → ...     │
    │───────────────────────→│
    │                        │  registry.negotiate(&[Version(2,0)])
    │                        │  → Some(Version(2,0))
    │                        │
    │  Response              │
    │←───────────────────────│  (uses v2.0 TypeMap for encoding)
    │    Status, Nonces,     │
    │    Signature           │
    │                        │
    │  (subsequent messages  │
    │   use v2.0 TypeMap)    │

If the client sends an unsupported version (e.g. v3.0 when the host only knows up to v2.0), negotiate returns None and the host sends ErrorBadVersion using reserved types.