irtt-server 0.5.0

IRTT-compatible Rust server library
Documentation
use irtt_proto::{verify_packet_hmac, FLAG_HMAC};

use super::support::{
    client_params, core_with_tokens, expect_normal_open_reply, open_request,
    open_request_with_raw_params, param_int, peer, ScriptedTokens, KEY, OTHER_KEY,
};
use crate::ServerConfig;

const TOKEN_A: u64 = 0x0102_0304_0506_0708;

/// Asserts a datagram is silently discarded, leaving no trace at all: no reply,
/// no session, and no session token drawn.
fn assert_discarded(config: ServerConfig, name: &str, packet: &[u8]) {
    let tokens = ScriptedTokens::new([TOKEN_A]);
    let mut core = core_with_tokens(config, tokens.clone());

    let reply = core
        .handle_datagram(peer(), packet)
        .expect("a rejected datagram is not a server error");

    assert!(reply.is_none(), "{name} must be answered with silence");
    assert_eq!(core.session_count(), 0, "{name} must create no session");
    assert_eq!(
        tokens.remaining(),
        1,
        "{name} must not consume session capacity or a token"
    );
}

#[test]
fn an_authenticating_server_discards_everything_that_does_not_authenticate() {
    let config = ServerConfig::default().with_hmac_key(KEY);

    assert_discarded(
        config.clone(),
        "open with no MAC field at all",
        &open_request(&client_params(), None),
    );
    assert_discarded(
        config.clone(),
        "open signed with a different key",
        &open_request(&client_params(), Some(OTHER_KEY)),
    );

    let mut corrupted = open_request(&client_params(), Some(KEY));
    corrupted[5] ^= 0x80;
    assert_discarded(
        config.clone(),
        "open with one bit flipped in the MAC",
        &corrupted,
    );

    let mut zeroed = open_request(&client_params(), Some(KEY));
    zeroed[4..20].fill(0);
    assert_discarded(config.clone(), "open with an all-zero MAC", &zeroed);

    let mut truncated = open_request(&client_params(), Some(KEY));
    truncated.drain(4..12);
    assert_discarded(
        config.clone(),
        "open with a truncated MAC field",
        &truncated,
    );

    let mut flag_cleared = open_request(&client_params(), Some(KEY));
    flag_cleared[3] &= !FLAG_HMAC;
    assert_discarded(
        config,
        "open with a valid MAC but the HMAC flag cleared",
        &flag_cleared,
    );
}

#[test]
fn a_non_authenticating_server_discards_authenticated_requests() {
    assert_discarded(
        ServerConfig::default(),
        "authenticated open to a server with no key",
        &open_request(&client_params(), Some(KEY)),
    );
}

#[test]
fn an_authenticating_server_answers_a_valid_open_with_an_authenticated_reply() {
    let mut core = core_with_tokens(
        ServerConfig::default().with_hmac_key(KEY),
        ScriptedTokens::new([TOKEN_A]),
    );

    let packet = core
        .handle_datagram(peer(), &open_request(&client_params(), Some(KEY)))
        .unwrap()
        .expect("a correctly authenticated open must be answered");

    let reply = expect_normal_open_reply(&packet, Some(KEY));
    assert_eq!(reply.token, TOKEN_A);
    assert_ne!(
        packet.bytes()[3] & FLAG_HMAC,
        0,
        "the reply must set FLAG_HMAC"
    );
    verify_packet_hmac(KEY, packet.bytes()).expect("the reply must carry a valid MAC");
    assert_eq!(core.session_count(), 1);
}

#[test]
fn a_non_authenticating_server_answers_an_ordinary_open() {
    let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));

    let packet = core
        .handle_datagram(peer(), &open_request(&client_params(), None))
        .unwrap()
        .expect("an unauthenticated open must be answered");

    expect_normal_open_reply(&packet, None);
    assert_eq!(packet.bytes()[3] & FLAG_HMAC, 0);
    assert_eq!(core.session_count(), 1);
}

#[test]
fn authentication_is_checked_before_parameters_are_decoded() {
    // Both of these are dropped, and neither reveals which stage rejected it.
    // The pairing is the point: bad parameters under a valid MAC still drop,
    // and a bad MAC drops without the parameters ever being parsed.
    let config = ServerConfig::default().with_hmac_key(KEY);
    let bad_params = param_int(2, 0);

    assert_discarded(
        config.clone(),
        "authenticated open with an explicit zero duration",
        &open_request_with_raw_params(&bad_params, Some(KEY)),
    );
    assert_discarded(
        config,
        "wrongly keyed open with an explicit zero duration",
        &open_request_with_raw_params(&bad_params, Some(OTHER_KEY)),
    );
}

#[test]
fn a_failed_authentication_leaves_a_live_session_untouched() {
    let mut core = core_with_tokens(
        ServerConfig::default().with_hmac_key(KEY),
        ScriptedTokens::new([TOKEN_A]),
    );
    core.handle_datagram(peer(), &open_request(&client_params(), Some(KEY)))
        .unwrap()
        .expect("the session-creating open must succeed");
    let session = core.session(TOKEN_A).cloned().expect("session must exist");

    for bad in [
        open_request(&client_params(), None),
        open_request(&client_params(), Some(OTHER_KEY)),
    ] {
        assert_eq!(core.handle_datagram(peer(), &bad).unwrap(), None);
    }

    assert_eq!(core.session_count(), 1);
    assert_eq!(core.session(TOKEN_A), Some(&session));
}