add L2 reliability: seq + cumulative ACK + retransmit (protocol v2)
DATA frames now carry seq:4 and ack_seq:4 in a 16-byte extended header. Both sides maintain per-session send/recv state: Sender: - Monotonic seq counter, retransmit buffer (seq -> frame bytes) - Retransmit timer: 5ms timeout, 10 max retries -> CLOSE - Window advances on cumulative ACK Receiver: - In-order delivery to TCP socket (expected_seq) - Out-of-order buffering (SortedList by seq) - Duplicate detection (seq < expected -> discard + re-ACK) - Pure ACK frames (empty-payload DATA) for duplicate/OOO responses This prevents lost Ethernet frames from permanently corrupting TCP sessions, which was the key v1 limitation. The local kernel TCP stack ACKs data before we chunk it into DATA frames; without L2 reliability a dropped frame creates an unrecoverable gap. Version bumped to 2. Both sides must speak v2; no negotiation. Updated: PROTOCOL.md (full v2 spec), README.md, Rust frame.rs/ session.rs/main.rs, C# Frame.cs/SessionManager.cs/TunnelLink.cs.
This commit is contained in:
+46
-14
@@ -1,9 +1,10 @@
|
||||
//! gatuna wire protocol frame encode/decode.
|
||||
//! gatuna wire protocol frame encode/decode (v2).
|
||||
|
||||
pub const ETHERTYPE: u16 = 0x6969;
|
||||
pub const ETH_HEADER_LEN: usize = 14;
|
||||
pub const VERSION: u8 = 1;
|
||||
pub const VERSION: u8 = 2;
|
||||
pub const HEADER_LEN: usize = 8;
|
||||
pub const DATA_HEADER_LEN: usize = 16; // 8 common + 4 seq + 4 ack_seq
|
||||
pub const MAX_PAYLOAD: usize = 1480;
|
||||
|
||||
pub const TYPE_DISCOVER: u8 = 0x01;
|
||||
@@ -29,6 +30,7 @@ pub const REASON_CONNECT_FAILED: u8 = 2;
|
||||
#[allow(dead_code)]
|
||||
pub const REASON_OVERSIZE: u8 = 3;
|
||||
pub const REASON_UNKNOWN_SESSION: u8 = 4;
|
||||
pub const REASON_MAX_RETRIES: u8 = 5;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpstreamEntry {
|
||||
@@ -45,7 +47,7 @@ pub enum Frame {
|
||||
Open { upstream_id: u8 },
|
||||
OpenAck { session_id: u32, upstream_id: u8 },
|
||||
OpenNak { upstream_id: u8, reason: u8 },
|
||||
Data { session_id: u32, payload: Vec<u8> },
|
||||
Data { session_id: u32, seq: u32, ack_seq: u32, payload: Vec<u8> },
|
||||
Close { session_id: u32, reason: Option<u8> },
|
||||
Ping { nonce: u64 },
|
||||
Pong { nonce: u64 },
|
||||
@@ -81,8 +83,7 @@ fn encode_entry(buf: &mut Vec<u8>, e: &UpstreamEntry) {
|
||||
buf.extend_from_slice(&label_bytes[..label_len as usize]);
|
||||
}
|
||||
|
||||
/// Build the 8-byte header + payload. The payload_len field records the
|
||||
/// exact payload length so the receiver can ignore Ethernet padding.
|
||||
/// Build a non-DATA frame: 8-byte common header + payload.
|
||||
fn build(type_byte: u8, session_id: u32, payload: Vec<u8>) -> Vec<u8> {
|
||||
let len = payload.len() as u16;
|
||||
let mut buf = Vec::with_capacity(HEADER_LEN + payload.len());
|
||||
@@ -94,6 +95,21 @@ fn build(type_byte: u8, session_id: u32, payload: Vec<u8>) -> Vec<u8> {
|
||||
buf
|
||||
}
|
||||
|
||||
/// Build a DATA frame: 8-byte common header + seq + ack_seq + payload.
|
||||
/// payload_len counts only the raw bytes, not seq/ack_seq.
|
||||
fn build_data(session_id: u32, seq: u32, ack_seq: u32, payload: Vec<u8>) -> Vec<u8> {
|
||||
let len = payload.len() as u16;
|
||||
let mut buf = Vec::with_capacity(DATA_HEADER_LEN + payload.len());
|
||||
buf.push(VERSION);
|
||||
buf.push(TYPE_DATA);
|
||||
buf.extend_from_slice(&session_id.to_be_bytes());
|
||||
buf.extend_from_slice(&len.to_be_bytes());
|
||||
buf.extend_from_slice(&seq.to_be_bytes());
|
||||
buf.extend_from_slice(&ack_seq.to_be_bytes());
|
||||
buf.extend_from_slice(&payload);
|
||||
buf
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
@@ -116,7 +132,9 @@ impl Frame {
|
||||
Frame::OpenNak { upstream_id, reason } => {
|
||||
build(TYPE_OPEN_NAK, 0, vec![*upstream_id, *reason])
|
||||
}
|
||||
Frame::Data { session_id, payload } => build(TYPE_DATA, *session_id, payload.clone()),
|
||||
Frame::Data { session_id, seq, ack_seq, payload } => {
|
||||
build_data(*session_id, *seq, *ack_seq, payload.clone())
|
||||
}
|
||||
Frame::Close { session_id, reason } => {
|
||||
let p = match reason {
|
||||
Some(r) => vec![*r],
|
||||
@@ -140,11 +158,31 @@ impl Frame {
|
||||
let typ = buf[1];
|
||||
let session_id = u32::from_be_bytes([buf[2], buf[3], buf[4], buf[5]]);
|
||||
let payload_len = u16::from_be_bytes([buf[6], buf[7]]) as usize;
|
||||
|
||||
// DATA frames have seq + ack_seq after the common header.
|
||||
if typ == TYPE_DATA {
|
||||
if buf.len() < DATA_HEADER_LEN + payload_len {
|
||||
return Err(DecodeError::BadPayload("DATA: payload_len exceeds available data"));
|
||||
}
|
||||
let seq = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
|
||||
let ack_seq = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
|
||||
let payload = &buf[DATA_HEADER_LEN..DATA_HEADER_LEN + payload_len];
|
||||
if payload.len() > MAX_PAYLOAD {
|
||||
return Err(DecodeError::BadPayload("DATA payload exceeds max"));
|
||||
}
|
||||
return Ok(Frame::Data {
|
||||
session_id,
|
||||
seq,
|
||||
ack_seq,
|
||||
payload: payload.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
if buf.len() < HEADER_LEN + payload_len {
|
||||
return Err(DecodeError::BadPayload("payload_len exceeds available data"));
|
||||
}
|
||||
// Slice exactly payload_len bytes, ignoring any trailing Ethernet padding.
|
||||
let payload = &buf[HEADER_LEN..HEADER_LEN + payload_len];
|
||||
|
||||
match typ {
|
||||
TYPE_DISCOVER => {
|
||||
if !payload.is_empty() {
|
||||
@@ -204,12 +242,6 @@ impl Frame {
|
||||
}
|
||||
Ok(Frame::OpenNak { upstream_id: payload[0], reason: payload[1] })
|
||||
}
|
||||
TYPE_DATA => {
|
||||
if payload.len() > MAX_PAYLOAD {
|
||||
return Err(DecodeError::BadPayload("DATA payload exceeds max"));
|
||||
}
|
||||
Ok(Frame::Data { session_id, payload: payload.to_vec() })
|
||||
}
|
||||
TYPE_CLOSE => {
|
||||
let reason = match payload.len() {
|
||||
0 => None,
|
||||
@@ -233,7 +265,7 @@ impl Frame {
|
||||
Ok(Frame::Pong { nonce })
|
||||
}
|
||||
TYPE_UDP_OPEN | TYPE_UDP_DATA | TYPE_UDP_CLOSE => {
|
||||
Err(DecodeError::BadPayload("UDP frame types not implemented in v1"))
|
||||
Err(DecodeError::BadPayload("UDP frame types not implemented"))
|
||||
}
|
||||
other => Err(DecodeError::UnknownType(other)),
|
||||
}
|
||||
|
||||
+11
-4
@@ -22,7 +22,7 @@ use crate::frame::{
|
||||
Frame, REASON_CONNECT_FAILED, REASON_UNKNOWN_SESSION, REASON_UNKNOWN_UPSTREAM,
|
||||
};
|
||||
use crate::link::Link;
|
||||
use crate::session::{spawn_pump, write_to_session, SessionHandle, SessionStore};
|
||||
use crate::session::{spawn_pump, handle_data, send_pure_ack, SessionHandle, SessionStore};
|
||||
use crate::upstream::build_table;
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -166,6 +166,9 @@ async fn handle_frame(
|
||||
SessionHandle {
|
||||
upstream_id,
|
||||
write: w,
|
||||
send_state: Arc::new(Mutex::new(session::SendState::new())),
|
||||
recv_state: Arc::new(Mutex::new(session::RecvState::new())),
|
||||
peer_mac: src,
|
||||
},
|
||||
);
|
||||
let ack = Frame::OpenAck { session_id: sid, upstream_id };
|
||||
@@ -187,9 +190,13 @@ async fn handle_frame(
|
||||
}
|
||||
}
|
||||
}
|
||||
Frame::Data { session_id, payload } => {
|
||||
match write_to_session(store, session_id, &payload).await {
|
||||
Ok(()) => {}
|
||||
Frame::Data { session_id, seq, ack_seq, payload } => {
|
||||
match handle_data(store, session_id, seq, ack_seq, &payload, tx).await {
|
||||
Ok(need_ack) => {
|
||||
if need_ack {
|
||||
send_pure_ack(store, session_id, tx);
|
||||
}
|
||||
}
|
||||
Err(session::WriteError::UnknownSession) => {
|
||||
let close = Frame::Close {
|
||||
session_id,
|
||||
|
||||
+265
-30
@@ -1,20 +1,140 @@
|
||||
//! Per-session state and the localhost→tunnel pump.
|
||||
//! Per-session state: L2 reliability layer + localhost→tunnel pump.
|
||||
|
||||
use crate::frame::{Frame, MAX_PAYLOAD};
|
||||
use crate::frame::{Frame, MAX_PAYLOAD, REASON_MAX_RETRIES};
|
||||
use crate::link::MacAddr;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, BTreeMap};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::tcp::OwnedReadHalf;
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
pub type TxChan = Sender<(MacAddr, Vec<u8>)>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
const RETRANSMIT_TIMEOUT: Duration = Duration::from_millis(5);
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
const RETRANSMIT_TICK: Duration = Duration::from_millis(1);
|
||||
|
||||
/// Sender-side reliability state (per session).
|
||||
pub struct SendState {
|
||||
send_seq: u32,
|
||||
acked_seq: u32,
|
||||
/// seq -> (frame_bytes, send_time, retry_count)
|
||||
retransmit_buffer: BTreeMap<u32, (Vec<u8>, Instant, u32)>,
|
||||
}
|
||||
|
||||
/// Receiver-side reliability state (per session).
|
||||
pub struct RecvState {
|
||||
expected_seq: u32,
|
||||
deliver_seq: u32,
|
||||
/// seq -> payload (out-of-order buffer)
|
||||
receive_buffer: BTreeMap<u32, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl SendState {
|
||||
fn new() -> Self {
|
||||
SendState {
|
||||
send_seq: 0,
|
||||
acked_seq: 0,
|
||||
retransmit_buffer: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a sent DATA frame in the retransmit buffer.
|
||||
fn record_sent(&mut self, seq: u32, frame_bytes: Vec<u8>) {
|
||||
self.retransmit_buffer
|
||||
.insert(seq, (frame_bytes, Instant::now(), 0));
|
||||
}
|
||||
|
||||
/// Process an incoming ack_seq: advance window, remove acked frames.
|
||||
fn process_ack(&mut self, ack_seq: u32) {
|
||||
self.retransmit_buffer.retain(|&seq, _| seq > ack_seq);
|
||||
if ack_seq > self.acked_seq || self.retransmit_buffer.is_empty() {
|
||||
self.acked_seq = ack_seq.max(self.acked_seq);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for frames needing retransmit. Returns frames to resend and
|
||||
/// whether the session should be closed (max retries exceeded).
|
||||
fn check_retransmit(&mut self) -> (Vec<Vec<u8>>, bool) {
|
||||
let mut resend = Vec::new();
|
||||
let mut should_close = false;
|
||||
let now = Instant::now();
|
||||
for (_seq, (frame_bytes, send_time, retries)) in self.retransmit_buffer.iter_mut() {
|
||||
if now.duration_since(*send_time) > RETRANSMIT_TIMEOUT {
|
||||
if *retries >= MAX_RETRIES {
|
||||
should_close = true;
|
||||
break;
|
||||
}
|
||||
*retries += 1;
|
||||
*send_time = now;
|
||||
resend.push(frame_bytes.clone());
|
||||
}
|
||||
}
|
||||
(resend, should_close)
|
||||
}
|
||||
|
||||
fn next_seq(&mut self) -> u32 {
|
||||
let s = self.send_seq;
|
||||
self.send_seq = self.send_seq.wrapping_add(1);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl RecvState {
|
||||
fn new() -> Self {
|
||||
RecvState {
|
||||
expected_seq: 0,
|
||||
deliver_seq: 0,
|
||||
receive_buffer: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an incoming DATA frame's seq. Returns:
|
||||
/// - `Ok((payloads, need_ack))` — payloads to deliver (may be empty if
|
||||
/// out-of-order), need_ack is true if the frame was duplicate or
|
||||
/// out-of-order and the caller should send a pure ACK.
|
||||
/// - `Err(())` — should not happen with current callers.
|
||||
fn process_data(&mut self, seq: u32, payload: Vec<u8>) -> (Vec<Vec<u8>>, bool) {
|
||||
if seq < self.expected_seq {
|
||||
// Duplicate — already delivered.
|
||||
return (vec![], true);
|
||||
}
|
||||
if seq == self.expected_seq {
|
||||
// In-order: deliver immediately, then drain the receive buffer.
|
||||
let mut deliver = vec![payload];
|
||||
self.expected_seq = self.expected_seq.wrapping_add(1);
|
||||
self.deliver_seq = self.expected_seq.wrapping_sub(1);
|
||||
// Drain contiguous buffered frames.
|
||||
while let Some(payload) = self.receive_buffer.remove(&self.expected_seq) {
|
||||
self.expected_seq = self.expected_seq.wrapping_add(1);
|
||||
self.deliver_seq = self.expected_seq.wrapping_sub(1);
|
||||
deliver.push(payload);
|
||||
}
|
||||
(deliver, false)
|
||||
} else {
|
||||
// Out-of-order: buffer it.
|
||||
self.receive_buffer.insert(seq, payload);
|
||||
(vec![], true)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ack_seq to report in outgoing DATA frames (highest contiguous
|
||||
/// delivered seq). If nothing delivered yet, report expected_seq - 1
|
||||
/// (wrapping), which is the last seq we can cumulatively ACK.
|
||||
fn current_ack_seq(&self) -> u32 {
|
||||
self.expected_seq.wrapping_sub(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionHandle {
|
||||
pub upstream_id: u8,
|
||||
pub write: Arc<AsyncMutex<tokio::net::tcp::OwnedWriteHalf>>,
|
||||
pub write: Arc<AsyncMutex<OwnedWriteHalf>>,
|
||||
pub send_state: Arc<Mutex<SendState>>,
|
||||
pub recv_state: Arc<Mutex<RecvState>>,
|
||||
pub peer_mac: MacAddr,
|
||||
}
|
||||
|
||||
pub type SessionStore = Arc<Mutex<HashMap<u32, SessionHandle>>>;
|
||||
@@ -25,28 +145,83 @@ pub enum WriteError {
|
||||
Io,
|
||||
}
|
||||
|
||||
pub async fn write_to_session(
|
||||
/// Handle a DATA frame received from the tunnel. Delivers in-order payloads
|
||||
/// to the TCP socket and processes the ack_seq. Returns whether a pure ACK
|
||||
/// should be sent back (duplicate or out-of-order).
|
||||
pub async fn handle_data(
|
||||
store: &SessionStore,
|
||||
id: u32,
|
||||
session_id: u32,
|
||||
seq: u32,
|
||||
ack_seq: u32,
|
||||
payload: &[u8],
|
||||
) -> Result<(), WriteError> {
|
||||
let write = {
|
||||
tx: &TxChan,
|
||||
) -> Result<bool, WriteError> {
|
||||
let handle = {
|
||||
let store = store.lock().expect("store lock poisoned");
|
||||
store.get(&id).map(|h| h.write.clone())
|
||||
store.get(&session_id).cloned()
|
||||
};
|
||||
match write {
|
||||
Some(w) => {
|
||||
let mut w = w.lock().await;
|
||||
w.write_all(payload).await.map_err(|_| WriteError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(WriteError::UnknownSession),
|
||||
|
||||
let Some(handle) = handle else {
|
||||
return Err(WriteError::UnknownSession);
|
||||
};
|
||||
|
||||
// Process ack_seq to advance send window.
|
||||
{
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
ss.process_ack(ack_seq);
|
||||
}
|
||||
|
||||
// Process seq for in-order delivery.
|
||||
let need_ack;
|
||||
{
|
||||
let mut rs = handle.recv_state.lock().expect("recv_state poisoned");
|
||||
let (deliver, ack) = rs.process_data(seq, payload.to_vec());
|
||||
if !deliver.is_empty() {
|
||||
let mut w = handle.write.lock().await;
|
||||
for chunk in deliver {
|
||||
if w.write_all(&chunk).await.is_err() {
|
||||
return Err(WriteError::Io);
|
||||
}
|
||||
}
|
||||
}
|
||||
need_ack = ack;
|
||||
}
|
||||
|
||||
Ok(need_ack)
|
||||
}
|
||||
|
||||
/// Send a pure ACK (DATA frame with empty payload) for the given session.
|
||||
pub fn send_pure_ack(
|
||||
store: &SessionStore,
|
||||
session_id: u32,
|
||||
tx: &TxChan,
|
||||
) {
|
||||
let handle = {
|
||||
let store = store.lock().expect("store lock poisoned");
|
||||
store.get(&session_id).cloned()
|
||||
};
|
||||
let Some(handle) = handle else { return };
|
||||
|
||||
let (seq, ack_seq) = {
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
let rs = handle.recv_state.lock().expect("recv_state poisoned");
|
||||
let seq = ss.next_seq();
|
||||
(seq, rs.current_ack_seq())
|
||||
};
|
||||
|
||||
// Pure ACKs are not stored in the retransmit buffer (no payload to lose).
|
||||
let frame = Frame::Data {
|
||||
session_id,
|
||||
seq,
|
||||
ack_seq,
|
||||
payload: Vec::new(),
|
||||
};
|
||||
let _ = tx.try_send((handle.peer_mac, frame.encode()));
|
||||
}
|
||||
|
||||
/// Spawn the socket→tunnel pump: reads from the localhost TCP stream in
|
||||
/// 1480-byte chunks and emits DATA frames. On EOF/error sends CLOSE and
|
||||
/// removes the session from the store.
|
||||
/// 1480-byte chunks, tags each with seq, stores in retransmit buffer, and
|
||||
/// emits DATA frames. Also spawns the retransmit timer.
|
||||
pub fn spawn_pump(
|
||||
mut read: OwnedReadHalf,
|
||||
session_id: u32,
|
||||
@@ -54,21 +229,81 @@ pub fn spawn_pump(
|
||||
tx: TxChan,
|
||||
store: SessionStore,
|
||||
) {
|
||||
// Retransmit timer task.
|
||||
{
|
||||
let store = Arc::clone(&store);
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(RETRANSMIT_TICK);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let handle = {
|
||||
let store = store.lock().expect("store poisoned");
|
||||
store.get(&session_id).cloned()
|
||||
};
|
||||
let Some(handle) = handle else { break };
|
||||
|
||||
let (resend, should_close) = {
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
ss.check_retransmit()
|
||||
};
|
||||
|
||||
for frame_bytes in resend {
|
||||
let _ = tx.try_send((peer_mac, frame_bytes));
|
||||
}
|
||||
|
||||
if should_close {
|
||||
let close = Frame::Close {
|
||||
session_id,
|
||||
reason: Some(REASON_MAX_RETRIES),
|
||||
};
|
||||
let _ = tx.try_send((peer_mac, close.encode()));
|
||||
store.lock().expect("store poisoned").remove(&session_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Socket read pump.
|
||||
tokio::spawn(async move {
|
||||
let mut buf = vec![0u8; MAX_PAYLOAD];
|
||||
loop {
|
||||
match read.read(&mut buf).await {
|
||||
let n = match read.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
let frame = Frame::Data {
|
||||
session_id,
|
||||
payload: buf[..n].to_vec(),
|
||||
};
|
||||
if tx.send((peer_mac, frame.encode())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(n) => n,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let handle = {
|
||||
let store = store.lock().expect("store poisoned");
|
||||
store.get(&session_id).cloned()
|
||||
};
|
||||
let Some(handle) = handle else { break };
|
||||
|
||||
let (seq, ack_seq) = {
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
let rs = handle.recv_state.lock().expect("recv_state poisoned");
|
||||
let seq = ss.next_seq();
|
||||
(seq, rs.current_ack_seq())
|
||||
};
|
||||
|
||||
let frame = Frame::Data {
|
||||
session_id,
|
||||
seq,
|
||||
ack_seq,
|
||||
payload: buf[..n].to_vec(),
|
||||
};
|
||||
let frame_bytes = frame.encode();
|
||||
|
||||
// Store in retransmit buffer before sending.
|
||||
{
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
ss.record_sent(seq, frame_bytes.clone());
|
||||
}
|
||||
|
||||
if tx.send((peer_mac, frame_bytes)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let close = Frame::Close { session_id, reason: None };
|
||||
|
||||
Reference in New Issue
Block a user