refactor(proto): unify request codecs

Replace the three per-kind request encoders and decoders with one
directional pair, ahead of introducing irtt-server.

Sender side: encode_request(RequestToEncode, hmac_key) constructs OPEN,
CLOSE and ECHO requests from borrowed values. Params and the ECHO payload
are borrowed rather than owned, removing a Params clone and a payload Vec
from the client's hot paths. Wire output is byte-identical; all captured
vectors still pass unchanged.

Receiver side: decode_request(packet) structurally classifies an inbound
request with no HMAC key, no negotiated Params, no session state and no
allocation. A server must extract a token before it knows which key
applies, so authentication is a separate step. Classification follows the
verified server spec: reserved bits and FLAG_REPLY are rejected, OPEN
takes precedence over CLOSE and over an echo-shaped body, and only the
structural minimums (4/12/16, plus 16 with FLAG_HMAC) are enforced.
Trailing bytes are tolerated, no negotiated length ceiling is applied, and
a zero token is structurally valid. OPEN parameters stay encoded bytes so
an authenticating receiver can verify before decoding untrusted data, and
everything after an ECHO sequence number is exposed as an opaque tail.

Add verify_packet_hmac(key, packet), which locates the standard field
itself and rejects a packet lacking FLAG_HMAC. hmac_present reports packet
layout, never authentication. The offset-based verify_hmac stays public as
the low-level primitive for hand-built and captured wire vectors, matching
the existing compute_hmac pair.

Refactor the private envelope into structural header decoding, codec flag
rules, HMAC presence policy and verification as separate steps. The reply
codecs compose the same steps in the same order, so decode_open_reply and
decode_echo_reply keep their exact error ordering and the client's
fatal-versus-ignore classification is unchanged. Reply decoding stays
state-specific: the client already knows which reply to expect, and the
two need different semantic context.

Migrate the production client and every fake peer to the new API; the
fake peers now admit requests the way a server does, instead of parsing
packets by hand.
This commit is contained in:
Nils Andreas Svee committed 2026-08-11 23:22:20 +07:00
1 parent c91e7917f7
commit 5a4272e7fd
18 files changed
+1296 -820

No files matched your search

+69 -43
View File
@@ -9,9 +9,8 @@ use std::{
};
use irtt_proto::{
decode_close_request, decode_echo_request, decode_open_request, encode_echo_reply,
encode_open_reply, flags, EchoReply, OpenReply, Params, ReceivedStats, StampAt,
TimestampFields,
decode_request, encode_echo_reply, encode_open_reply, flags, verify_packet_hmac,
DecodedRequestKind, EchoReply, OpenReply, Params, ReceivedStats, StampAt, TimestampFields,
};
use tokio::runtime::{Builder, Runtime};
@@ -102,6 +101,47 @@ fn recv_packet(socket: &UdpSocket, tx: &mpsc::Sender<Vec<u8>>) -> (Vec<u8>, Sock
(packet, peer)
}
/// Admits a request the way a compliant server does: structural decode first,
/// then HMAC presence policy, then authentication. Returns `None` for anything
/// a server would silently discard.
fn decode_server_request<'a>(
packet: &'a [u8],
key: Option<&[u8]>,
) -> Option<DecodedRequestKind<'a>> {
let request = decode_request(packet).ok()?;
if request.hmac_present != key.is_some() {
return None;
}
if let Some(key) = key {
verify_packet_hmac(key, packet).ok()?;
}
Some(request.kind)
}
/// Returns the requested parameters and whether this was a no-test open.
fn open_request(packet: &[u8], key: Option<&[u8]>) -> (Params, bool) {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Open { params, no_test }) => {
(Params::decode(params).unwrap(), no_test)
}
other => panic!("expected an authenticated open request, got {other:?}"),
}
}
fn echo_request_sequence(packet: &[u8], key: Option<&[u8]>) -> u32 {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Echo { sequence, .. }) => sequence,
other => panic!("expected an authenticated echo request, got {other:?}"),
}
}
fn close_request_token(packet: &[u8], key: Option<&[u8]>) -> u64 {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Close { token }) => token,
other => panic!("expected an authenticated close request, got {other:?}"),
}
}
fn send_open_reply(
socket: &UdpSocket,
peer: SocketAddr,
@@ -128,16 +168,16 @@ fn open_session(
key: Option<&[u8]>,
) -> (Params, SocketAddr) {
let (packet, peer) = recv_packet(socket, tx);
let request = decode_open_request(&packet, key).unwrap();
let (params, _) = open_request(&packet, key);
send_open_reply(
socket,
peer,
request.params.clone(),
params.clone(),
key,
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
);
(request.params, peer)
(params, peer)
}
fn echo_reply(
@@ -175,12 +215,12 @@ fn start_open_server(key: Option<Vec<u8>>, packets_after_open: usize) -> TestSer
fn start_no_test_server() -> TestServer {
start_server(move |socket, tx| {
let (open_packet, peer) = recv_packet(&socket, &tx);
let request = decode_open_request(&open_packet, None).unwrap();
assert!(request.close);
let (params, no_test) = open_request(&open_packet, None);
assert!(no_test);
send_open_reply(
&socket,
peer,
request.params,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY | flags::FLAG_CLOSE,
0,
@@ -192,12 +232,12 @@ fn start_peer_close_server() -> TestServer {
start_server(move |socket, tx| {
let (params, peer) = open_session(&socket, &tx, None);
let (probe_packet, _) = recv_packet(&socket, &tx);
let probe = decode_echo_request(&probe_packet, &params, None).unwrap();
let sequence = echo_request_sequence(&probe_packet, None);
socket
.send_to(
&echo_reply(
&params,
probe.sequence,
sequence,
TOKEN,
flags::FLAG_REPLY | flags::FLAG_CLOSE,
None,
@@ -213,27 +253,16 @@ fn start_echo_close_server(key: Option<Vec<u8>>) -> TestServer {
let (params, peer) = open_session(&socket, &tx, key.as_deref());
let (probe_packet, _) = recv_packet(&socket, &tx);
let probe = decode_echo_request(&probe_packet, &params, key.as_deref()).unwrap();
let sequence = echo_request_sequence(&probe_packet, key.as_deref());
socket
.send_to(
&echo_reply(
&params,
probe.sequence,
TOKEN,
flags::FLAG_REPLY,
key.as_deref(),
),
&echo_reply(&params, sequence, TOKEN, flags::FLAG_REPLY, key.as_deref()),
peer,
)
.unwrap();
let (close_packet, _) = recv_packet(&socket, &tx);
assert_eq!(
decode_close_request(&close_packet, key.as_deref())
.unwrap()
.token,
TOKEN
);
assert_eq!(close_request_token(&close_packet, key.as_deref()), TOKEN);
})
}
@@ -275,13 +304,13 @@ fn open_ignores_malformed_and_bad_hmac_without_resending() {
let server_key = key.clone();
let server = start_server(move |socket, tx| {
let (open_packet, peer) = recv_packet(&socket, &tx);
let request = decode_open_request(&open_packet, Some(&server_key)).unwrap();
let (params, _) = open_request(&open_packet, Some(&server_key));
socket.send_to(&[0_u8], peer).unwrap();
let mut bad_hmac = encode_open_reply(
&OpenReply {
flags: flags::FLAG_OPEN | flags::FLAG_REPLY,
token: TOKEN,
params: request.params.clone(),
params: params.clone(),
},
Some(&server_key),
)
@@ -291,7 +320,7 @@ fn open_ignores_malformed_and_bad_hmac_without_resending() {
send_open_reply(
&socket,
peer,
request.params,
params,
Some(&server_key),
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
@@ -342,14 +371,14 @@ fn opening_poll_budget_yields_then_public_open_completes() {
let (queued, queue_ready) = mpsc::channel();
let server = start_server(move |socket, tx| {
let (open_packet, peer) = recv_packet(&socket, &tx);
let request = decode_open_request(&open_packet, None).unwrap();
let (params, _) = open_request(&open_packet, None);
for _ in 0..=OPEN_POLL_WORK_BUDGET {
socket.send_to(&[0_u8], peer).unwrap();
}
send_open_reply(
&socket,
peer,
request.params,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
@@ -391,7 +420,7 @@ fn opening_poll_budget_yields_then_public_open_completes() {
fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
let server = start_server(move |socket, tx| {
let (first_packet, peer) = recv_packet(&socket, &tx);
let request = decode_open_request(&first_packet, None).unwrap();
let (params, _) = open_request(&first_packet, None);
socket
.set_read_timeout(Some(Duration::from_millis(10)))
.unwrap();
@@ -411,7 +440,7 @@ fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
send_open_reply(
&socket,
second_peer,
request.params,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
@@ -445,11 +474,11 @@ fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
fn authenticated_rejection_is_terminal_without_retry() {
let server = start_server(move |socket, tx| {
let (open_packet, peer) = recv_packet(&socket, &tx);
let request = decode_open_request(&open_packet, None).unwrap();
let (params, _) = open_request(&open_packet, None);
send_open_reply(
&socket,
peer,
request.params,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY | flags::FLAG_CLOSE,
0,
@@ -502,12 +531,12 @@ fn opening_cancellation_before_submission_is_transactional() {
fn opening_cancellation_after_submission_can_retry() {
let server = start_server(move |socket, tx| {
let (first, _) = recv_packet(&socket, &tx);
let first = decode_open_request(&first, None).unwrap();
let (first_params, _) = open_request(&first, None);
let (_, peer) = recv_packet(&socket, &tx);
send_open_reply(
&socket,
peer,
first.params,
first_params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
@@ -541,7 +570,7 @@ fn adapter_failure_sends_preencoded_cleanup_without_committing_open() {
let server = start_server(move |socket, tx| {
open_session(&socket, &tx, None);
let (close, _) = recv_packet(&socket, &tx);
assert_eq!(decode_close_request(&close, None).unwrap().token, TOKEN);
assert_eq!(close_request_token(&close, None), TOKEN);
});
runtime().block_on(async {
@@ -837,10 +866,10 @@ fn receive_retries_false_readiness_and_shares_packet_classification() {
let server = start_server(move |socket, tx| {
let (params, peer) = open_session(&socket, &tx, None);
let (probe_packet, _) = recv_packet(&socket, &tx);
let probe = decode_echo_request(&probe_packet, &params, None).unwrap();
let sequence = echo_request_sequence(&probe_packet, None);
socket
.send_to(
&echo_reply(&params, probe.sequence, TOKEN, flags::FLAG_REPLY, None),
&echo_reply(&params, sequence, TOKEN, flags::FLAG_REPLY, None),
peer,
)
.unwrap();
@@ -891,10 +920,7 @@ fn recv_after_local_close_and_no_test_fails_on_first_poll() {
let close_server = start_server(move |socket, tx| {
open_session(&socket, &tx, None);
let (close_packet, _) = recv_packet(&socket, &tx);
assert_eq!(
decode_close_request(&close_packet, None).unwrap().token,
TOKEN
);
assert_eq!(close_request_token(&close_packet, None), TOKEN);
});
runtime().block_on(async {
let mut client = opened_client(close_server.addr, 0).await;
+4 -4
View File
@@ -8,7 +8,7 @@ fn hmac_open_success() {
let params = params_from_config(&config).unwrap();
let server = start_fake_server(move |socket, tx| {
let (request, peer) = recv_request(&socket, &tx);
verify_hmac(&key, &request, HMAC_OFFSET).unwrap();
verify_packet_hmac(&key, &request).unwrap();
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &params, Some(&key));
socket.send_to(&reply, peer).unwrap();
});
@@ -85,7 +85,7 @@ fn post_token_hmac_negotiation_failure_sends_authenticated_cleanup_close() {
let packets: Vec<_> = server.rx.iter().take(2).collect();
let cleanup = &packets[1];
assert_eq!(cleanup[3], flags::FLAG_CLOSE | FLAG_HMAC);
verify_hmac(&key, cleanup, HMAC_OFFSET).unwrap();
verify_packet_hmac(&key, cleanup).unwrap();
assert_eq!(
u64::from_le_bytes(cleanup[4 + HMAC_SIZE..12 + HMAC_SIZE].try_into().unwrap()),
TOKEN
@@ -113,7 +113,7 @@ fn hmac_close_packet_includes_valid_hmac() {
let packets: Vec<_> = server.rx.iter().take(2).collect();
let close = &packets[1];
assert_eq!(close[3], flags::FLAG_CLOSE | FLAG_HMAC);
verify_hmac(&key, close, HMAC_OFFSET).unwrap();
verify_packet_hmac(&key, close).unwrap();
assert_eq!(
u64::from_le_bytes(close[4 + HMAC_SIZE..12 + HMAC_SIZE].try_into().unwrap()),
TOKEN
@@ -191,7 +191,7 @@ fn hmac_echo_request_reply_works() {
let mut buf = [0_u8; 2048];
if let Ok((size, _)) = socket.recv_from(&mut buf) {
tx.send(buf[..size].to_vec()).unwrap();
verify_hmac(&server_key, &buf[..size], HMAC_OFFSET).unwrap();
verify_packet_hmac(&server_key, &buf[..size]).unwrap();
let seq = u32::from_le_bytes(
buf[4 + HMAC_SIZE + 8..4 + HMAC_SIZE + 12]
.try_into()
+2 -2
View File
@@ -1,8 +1,8 @@
use super::*;
use crate::config::{NegotiationPolicy, DEFAULT_OPEN_TIMEOUTS};
use irtt_proto::{
flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_hmac, Clock, ReceivedStats,
StampAt, HMAC_SIZE, MAGIC,
flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_packet_hmac, Clock,
ReceivedStats, StampAt, HMAC_SIZE, MAGIC,
};
use std::{thread, time::SystemTime};
@@ -6,8 +6,8 @@ use std::{
};
use irtt_proto::{
decode_close_request, decode_echo_request, decode_open_request, encode_echo_reply,
encode_open_reply, flags, EchoReply, OpenReply, ReceivedStats, StampAt, TimestampFields,
decode_request, encode_echo_reply, encode_open_reply, flags, DecodedRequestKind, EchoReply,
OpenReply, Params, ReceivedStats, StampAt, TimestampFields,
};
use tokio::{runtime::Builder, time::timeout};
@@ -69,8 +69,10 @@ impl EchoServer {
let worker = thread::spawn(move || {
let mut buffer = [0_u8; 8192];
let (open_len, peer) = socket.recv_from(&mut buffer).unwrap();
let request = decode_open_request(&buffer[..open_len], None).unwrap();
let negotiated = request.params;
let negotiated = match decode_request(&buffer[..open_len]).unwrap().kind {
DecodedRequestKind::Open { params, .. } => Params::decode(params).unwrap(),
other => panic!("expected an open request, got {other:?}"),
};
let reply = encode_open_reply(
&OpenReply {
flags: flags::FLAG_OPEN | flags::FLAG_REPLY,
@@ -84,27 +86,33 @@ impl EchoServer {
loop {
let (len, packet_peer) = socket.recv_from(&mut buffer).unwrap();
let packet = &buffer[..len];
if let Ok(probe) = decode_echo_request(packet, &negotiated, None) {
worker_probe.set();
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY,
token: TOKEN,
sequence: probe.sequence,
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
None,
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
} else if decode_close_request(packet, None).is_ok() {
worker_close.set();
return;
let Ok(request) = decode_request(&buffer[..len]) else {
continue;
};
match request.kind {
DecodedRequestKind::Echo { sequence, .. } => {
worker_probe.set();
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY,
token: TOKEN,
sequence,
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
None,
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
}
DecodedRequestKind::Close { .. } => {
worker_close.set();
return;
}
DecodedRequestKind::Open { .. } => {}
}
}
});
+94 -69
View File
@@ -10,8 +10,8 @@ use std::{
};
use irtt_proto::{
decode_close_request, decode_echo_request, decode_open_request, encode_echo_reply,
encode_open_reply, flags, EchoReply, OpenReply, ReceivedStats, StampAt, TimestampFields,
decode_request, encode_echo_reply, encode_open_reply, flags, verify_packet_hmac,
DecodedRequestKind, EchoReply, OpenReply, Params, ReceivedStats, StampAt, TimestampFields,
};
use tokio::runtime::{Builder, Runtime};
@@ -86,6 +86,31 @@ impl PacketGate {
self.release_ready.notify_all();
}
}
/// Admits a request the way a compliant server does: structural decode first,
/// then HMAC presence policy, then authentication. Returns `None` for anything
/// a server would silently discard.
fn decode_server_request<'a>(
packet: &'a [u8],
key: Option<&[u8]>,
) -> Option<DecodedRequestKind<'a>> {
let request = decode_request(packet).ok()?;
if request.hmac_present != key.is_some() {
return None;
}
if let Some(key) = key {
verify_packet_hmac(key, packet).ok()?;
}
Some(request.kind)
}
fn decode_open_params(packet: &[u8], key: Option<&[u8]>) -> Params {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Open { params, .. }) => Params::decode(params).unwrap(),
other => panic!("expected an authenticated open request, got {other:?}"),
}
}
fn start_server(behavior: ServerBehavior, key: Option<Vec<u8>>) -> TestServer {
start_server_negotiating(behavior, key, None)
}
@@ -124,11 +149,10 @@ fn start_server_with_gates(
kind: PacketKind::Open,
at: Instant::now(),
});
let request = decode_open_request(&buffer[..open_len], key.as_deref()).unwrap();
let mut negotiated = decode_open_params(&buffer[..open_len], key.as_deref());
if let Some(gate) = &open_gate {
gate.arrive_and_wait();
}
let mut negotiated = request.params.clone();
if let Some(interval) = interval {
negotiated.interval_ns = i64::try_from(interval.as_nanos()).unwrap();
}
@@ -164,77 +188,78 @@ fn start_server_with_gates(
return;
};
let packet = &buffer[..len];
if let Ok(probe) = decode_echo_request(packet, &negotiated, key.as_deref()) {
thread_records.lock().unwrap().push(PacketRecord {
kind: PacketKind::Probe,
at: Instant::now(),
});
let (seen, ready) = &*thread_probe_seen;
*seen.lock().unwrap() = true;
ready.notify_all();
if let ServerBehavior::DelayedEcho(delay) = behavior {
thread::sleep(delay);
}
if let ServerBehavior::DeferredBurst(count) = behavior {
if deferred_burst_sent {
match decode_server_request(packet, key.as_deref()) {
Some(DecodedRequestKind::Echo { sequence, .. }) => {
thread_records.lock().unwrap().push(PacketRecord {
kind: PacketKind::Probe,
at: Instant::now(),
});
let (seen, ready) = &*thread_probe_seen;
*seen.lock().unwrap() = true;
ready.notify_all();
if let ServerBehavior::DelayedEcho(delay) = behavior {
thread::sleep(delay);
}
if let ServerBehavior::DeferredBurst(count) = behavior {
if deferred_burst_sent {
continue;
}
deferred_burst_sent = true;
thread::sleep(Duration::from_millis(30));
for reply_index in 0..count {
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY,
token: TOKEN,
sequence: if reply_index < 2 { sequence } else { u32::MAX },
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
key.as_deref(),
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
}
continue;
}
deferred_burst_sent = true;
thread::sleep(Duration::from_millis(30));
for reply_index in 0..count {
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY,
token: TOKEN,
sequence: if reply_index < 2 {
probe.sequence
} else {
u32::MAX
},
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
key.as_deref(),
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
if let Some(gate) = &reply_gate {
gate.arrive_and_wait();
}
let peer_close = matches!(behavior, ServerBehavior::PeerClose);
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY
| if peer_close { flags::FLAG_CLOSE } else { 0 },
token: TOKEN,
sequence,
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
key.as_deref(),
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
let (sent, ready) = &*thread_reply_sent;
*sent.lock().unwrap() = true;
ready.notify_all();
if peer_close {
return;
}
continue;
}
if let Some(gate) = &reply_gate {
gate.arrive_and_wait();
}
let peer_close = matches!(behavior, ServerBehavior::PeerClose);
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY | if peer_close { flags::FLAG_CLOSE } else { 0 },
token: TOKEN,
sequence: probe.sequence,
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
key.as_deref(),
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
let (sent, ready) = &*thread_reply_sent;
*sent.lock().unwrap() = true;
ready.notify_all();
if peer_close {
Some(DecodedRequestKind::Close { .. }) => {
thread_records.lock().unwrap().push(PacketRecord {
kind: PacketKind::Close,
at: Instant::now(),
});
return;
}
} else if decode_close_request(packet, key.as_deref()).is_ok() {
thread_records.lock().unwrap().push(PacketRecord {
kind: PacketKind::Close,
at: Instant::now(),
});
return;
Some(DecodedRequestKind::Open { .. }) | None => {}
}
}
});
+22 -22
View File
@@ -1,9 +1,8 @@
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use irtt_proto::{
close::CloseRequest, decode_echo_reply, echo_packet_len, encode_close_request,
encode_echo_request, encode_open_request, flags, EchoReply, EchoRequest, OpenReply,
OpenRequest, Params, ServerFill, TimestampFields, PROTOCOL_VERSION,
decode_echo_reply, echo_packet_len, encode_request, flags, EchoReply, OpenReply, Params,
RequestToEncode, ServerFill, TimestampFields, PROTOCOL_VERSION,
};
use crate::{
@@ -211,11 +210,13 @@ impl SessionMachine {
pub(crate) fn prepare_open_request(&self) -> Result<PreparedOpenRequest, ClientError> {
self.ensure_connected()?;
let request = OpenRequest {
params: self.requested.clone(),
close: self.config.run_mode == RunMode::NoTest,
};
let bytes = encode_open_request(&request, self.config.hmac_key.as_deref())?;
let bytes = encode_request(
RequestToEncode::Open {
params: &self.requested,
no_test: self.config.run_mode == RunMode::NoTest,
},
self.config.hmac_key.as_deref(),
)?;
Ok(PreparedOpenRequest {
bytes: bytes.into_boxed_slice(),
})
@@ -252,8 +253,8 @@ impl SessionMachine {
let reply_is_close = flags::has(reply.flags, flags::FLAG_CLOSE);
let cleanup_close = if !reply_is_close && reply.token != 0 {
let bytes = encode_close_request(
&CloseRequest { token: reply.token },
let bytes = encode_request(
RequestToEncode::Close { token: reply.token },
self.config.hmac_key.as_deref(),
)
.map_err(ClientError::from)
@@ -310,14 +311,13 @@ impl SessionMachine {
pub(crate) fn prepare_probe(&self) -> Result<Option<PreparedProbe>, ClientError> {
let session = self.open_session()?;
let request = EchoRequest {
token: session.token,
sequence: session.next_wire_seq,
payload: vec![],
};
let bytes = encode_echo_request(
&request,
&session.negotiated.params,
let bytes = encode_request(
RequestToEncode::Echo {
token: session.token,
sequence: session.next_wire_seq,
params: &session.negotiated.params,
payload: &[],
},
self.config.hmac_key.as_deref(),
)?;
Ok(Some(PreparedProbe {
@@ -1077,8 +1077,8 @@ mod tests {
machine.state = MachineState::Open(Box::new(ActiveSession {
token: 0x0102_0304_0506_0708,
negotiated,
local_close_packet: encode_close_request(
&CloseRequest {
local_close_packet: encode_request(
RequestToEncode::Close {
token: 0x0102_0304_0506_0708,
},
None,
@@ -1452,8 +1452,8 @@ mod tests {
let prepared = machine.prepare_close().unwrap();
assert_eq!(
prepared.bytes,
encode_close_request(
&CloseRequest {
encode_request(
RequestToEncode::Close {
token: 0x0102_0304_0506_0708,
},
None,
+13 -6
View File
@@ -123,7 +123,14 @@ fn hmac_close_success_sends_authenticated_close_and_closes_session() {
#[test]
fn hmac_required_server_rejects_missing_or_wrong_client_key() {
for (hmac_key, expected_hmac_flag) in [(None, false), (Some(b"wrong-secret".to_vec()), true)] {
// Both rejections leave the client timing out, but they are different
// failures on the wire: a client with no key presents no HMAC field at
// all, while a client with the wrong key presents one that does not
// authenticate.
for (hmac_key, expected_hmac_flag, expected_bad_hmac) in [
(None, false, false),
(Some(b"wrong-secret".to_vec()), true, true),
] {
let server_key = b"compat-secret".to_vec();
let server = start_hmac_required_open_drop_server(server_key, Duration::from_millis(250));
let mut config = config_for_params(server.addr, &default_params());
@@ -134,13 +141,13 @@ fn hmac_required_server_rejects_missing_or_wrong_client_key() {
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
let observations = server.observations(1);
assert!(matches!(
assert_eq!(
observations.as_slice(),
[ServerObservation::RejectedHmac {
hmac,
bad_hmac: true
}] if *hmac == expected_hmac_flag
));
hmac: expected_hmac_flag,
bad_hmac: expected_bad_hmac,
}]
);
server.join();
}
}
+47 -49
View File
@@ -12,9 +12,10 @@ use std::{
use irtt_client::{Client, ClientConfig, ClientEvent, NegotiatedParams, OpenOutcome, SocketConfig};
use irtt_proto::{
compute_hmac_in_place, echo_packet_len, encode_echo_reply, encode_open_reply, flags,
verify_hmac, Clock, EchoReply, OpenReply, PacketLayout, Params, ProtoError, ReceivedStats,
ServerFill, StampAt, TimestampFields, HMAC_SIZE, MAGIC, PROTOCOL_VERSION,
compute_hmac_in_place, decode_request, echo_packet_len, encode_echo_reply, encode_open_reply,
flags, verify_packet_hmac, Clock, DecodedRequest, DecodedRequestKind, EchoReply, OpenReply,
PacketLayout, Params, ProtoError, ReceivedStats, ServerFill, StampAt, TimestampFields,
HMAC_SIZE, MAGIC, PROTOCOL_VERSION,
};
pub use real_irtt::RealIrtServer;
@@ -252,7 +253,7 @@ where
pub fn start_open_server(params: Params, hmac_key: Option<Vec<u8>>) -> FakeServer {
start_fake_server(move |socket, tx| {
let (request, peer) = recv_request(&socket);
let (open_params, hmac) = decode_open_request_params(&request, hmac_key.as_deref());
let (open_params, hmac) = open_request_params(&request, hmac_key.as_deref());
tx.send(ServerObservation::Open {
params: open_params,
hmac,
@@ -273,10 +274,11 @@ pub fn start_hmac_required_open_drop_server(key: Vec<u8>, wait: Duration) -> Fak
start_fake_server(move |socket, tx| {
socket.set_read_timeout(Some(wait)).unwrap();
while let Some(request) = recv_request_timeout(&socket) {
let hmac = request
.get(3)
.is_some_and(|flags| flags & flags::FLAG_HMAC != 0);
match verify_hmac(&key, &request, HMAC_OFFSET) {
let hmac = decode_request(&request).is_ok_and(|request| request.hmac_present);
// A server with a key drops any request that fails authentication,
// whether it carried no HMAC field at all or a field signed with
// the wrong key.
match verify_packet_hmac(&key, &request) {
Ok(()) => {}
Err(error) => {
tx.send(ServerObservation::RejectedHmac {
@@ -293,7 +295,7 @@ pub fn start_hmac_required_open_drop_server(key: Vec<u8>, wait: Duration) -> Fak
pub fn start_hmac_close_server(params: Params, key: Vec<u8>) -> FakeServer {
start_fake_server(move |socket, tx| {
let (request, peer) = recv_request(&socket);
let (open_params, hmac) = decode_open_request_params(&request, Some(&key));
let (open_params, hmac) = open_request_params(&request, Some(&key));
tx.send(ServerObservation::Open {
params: open_params,
hmac,
@@ -323,7 +325,7 @@ pub fn start_hmac_close_server(params: Params, key: Vec<u8>) -> FakeServer {
pub fn start_bad_hmac_echo_reply_server(params: Params, key: Vec<u8>) -> FakeServer {
start_fake_server(move |socket, tx| {
let (request, peer) = recv_request(&socket);
let (open_params, hmac) = decode_open_request_params(&request, Some(&key));
let (open_params, hmac) = open_request_params(&request, Some(&key));
tx.send(ServerObservation::Open {
params: open_params,
hmac,
@@ -368,7 +370,7 @@ pub fn start_bad_hmac_open_reply_server(
) -> FakeServer {
start_fake_server(move |socket, tx| {
let (request, peer) = recv_request(&socket);
let (open_params, hmac) = decode_open_request_params(&request, Some(&request_key));
let (open_params, hmac) = open_request_params(&request, Some(&request_key));
tx.send(ServerObservation::Open {
params: open_params,
hmac,
@@ -398,7 +400,7 @@ fn start_one_probe_server(
) -> FakeServer {
start_fake_server(move |socket, tx| {
let (request, peer) = recv_request(&socket);
let (open_params, hmac) = decode_open_request_params(&request, hmac_key.as_deref());
let (open_params, hmac) = open_request_params(&request, hmac_key.as_deref());
tx.send(ServerObservation::Open {
params: open_params,
hmac,
@@ -463,52 +465,48 @@ fn assert_started(outcome: OpenOutcome) -> NegotiatedParams {
}
}
fn decode_open_request_params(packet: &[u8], hmac_key: Option<&[u8]>) -> (Params, bool) {
assert_eq!(&packet[..3], &MAGIC);
assert!(packet[3] & flags::FLAG_OPEN != 0);
let hmac = packet[3] & flags::FLAG_HMAC != 0;
assert_eq!(hmac, hmac_key.is_some());
/// Admits a request the way a compliant server does: structural decode first,
/// then HMAC presence policy, then authentication.
fn admit_request<'a>(packet: &'a [u8], hmac_key: Option<&[u8]>) -> DecodedRequest<'a> {
let request = decode_request(packet).expect("fake peers only receive well-formed requests");
assert_eq!(request.hmac_present, hmac_key.is_some());
if let Some(key) = hmac_key {
verify_hmac(key, packet, HMAC_OFFSET).unwrap();
verify_packet_hmac(key, packet).unwrap();
}
request
}
fn open_request_params(packet: &[u8], hmac_key: Option<&[u8]>) -> (Params, bool) {
let request = admit_request(packet, hmac_key);
match request.kind {
DecodedRequestKind::Open { params, .. } => {
(Params::decode(params).unwrap(), request.hmac_present)
}
other => panic!("expected an open request, got {other:?}"),
}
let params_offset = 4 + if hmac { HMAC_SIZE } else { 0 };
(Params::decode(&packet[params_offset..]).unwrap(), hmac)
}
fn observe_echo_request(packet: &[u8], hmac_key: Option<&[u8]>) -> ServerObservation {
assert_eq!(&packet[..3], &MAGIC);
assert_eq!(packet[3] & flags::FLAG_OPEN, 0);
assert_eq!(packet[3] & flags::FLAG_REPLY, 0);
assert_eq!(packet[3] & flags::FLAG_CLOSE, 0);
let hmac = packet[3] & flags::FLAG_HMAC != 0;
assert_eq!(hmac, hmac_key.is_some());
if let Some(key) = hmac_key {
verify_hmac(key, packet, HMAC_OFFSET).unwrap();
}
let mut pos = 4 + if hmac { HMAC_SIZE } else { 0 };
let token = u64::from_le_bytes(packet[pos..pos + 8].try_into().unwrap());
pos += 8;
let sequence = u32::from_le_bytes(packet[pos..pos + 4].try_into().unwrap());
ServerObservation::Echo {
len: packet.len(),
hmac,
token,
sequence,
let request = admit_request(packet, hmac_key);
match request.kind {
DecodedRequestKind::Echo {
token, sequence, ..
} => ServerObservation::Echo {
len: packet.len(),
hmac: request.hmac_present,
token,
sequence,
},
other => panic!("expected an echo request, got {other:?}"),
}
}
fn observe_close_request(packet: &[u8], hmac_key: &[u8]) -> ServerObservation {
assert_eq!(&packet[..3], &MAGIC);
assert_eq!(packet[3] & flags::FLAG_CLOSE, flags::FLAG_CLOSE);
assert_eq!(packet[3] & flags::FLAG_HMAC, flags::FLAG_HMAC);
verify_hmac(hmac_key, packet, HMAC_OFFSET).unwrap();
let token_offset = 4 + HMAC_SIZE;
let token = u64::from_le_bytes(packet[token_offset..token_offset + 8].try_into().unwrap());
ServerObservation::Close { hmac: true, token }
let request = admit_request(packet, Some(hmac_key));
match request.kind {
DecodedRequestKind::Close { token } => ServerObservation::Close { hmac: true, token },
other => panic!("expected a close request, got {other:?}"),
}
}
// irtt-proto has no encode_close_reply/CloseReply API yet, so this stays a