carry transport proto end-to-end through OPEN/OPEN_ACK
OPEN and OPEN_ACK now carry proto:1 alongside upstream_id:1. The session is tagged with its transport proto and reliability semantics switch on the specific proto: - TCP (proto=1): full L2 reliability (seq, ack, retransmit, in-order) - UDP (proto=2, reserved): best-effort (no retransmit, no ordering, no pure ACKs — seq/ack_seq fields present but ignored) This is architecturally correct: instead of a generic 'reliable:bool' flag, each proto gets the semantics it needs. Today only TCP exists so every session is reliable, but the extension point is clean for when stateless protos are added. Updated: PROTOCOL.md, Rust frame.rs/session.rs/main.rs, C# Frame.cs/SessionManager.cs.
This commit is contained in:
+11
-11
@@ -44,8 +44,8 @@ pub struct UpstreamEntry {
|
||||
pub enum Frame {
|
||||
Discover,
|
||||
Manifest { hostname: String, entries: Vec<UpstreamEntry> },
|
||||
Open { upstream_id: u8 },
|
||||
OpenAck { session_id: u32, upstream_id: u8 },
|
||||
Open { upstream_id: u8, proto: u8 },
|
||||
OpenAck { session_id: u32, upstream_id: u8, proto: u8 },
|
||||
OpenNak { upstream_id: u8, reason: u8 },
|
||||
Data { session_id: u32, seq: u32, ack_seq: u32, payload: Vec<u8> },
|
||||
Close { session_id: u32, reason: Option<u8> },
|
||||
@@ -125,9 +125,9 @@ impl Frame {
|
||||
}
|
||||
build(TYPE_MANIFEST, 0, payload)
|
||||
}
|
||||
Frame::Open { upstream_id } => build(TYPE_OPEN, 0, vec![*upstream_id]),
|
||||
Frame::OpenAck { session_id, upstream_id } => {
|
||||
build(TYPE_OPEN_ACK, *session_id, vec![*upstream_id])
|
||||
Frame::Open { upstream_id, proto } => build(TYPE_OPEN, 0, vec![*upstream_id, *proto]),
|
||||
Frame::OpenAck { session_id, upstream_id, proto } => {
|
||||
build(TYPE_OPEN_ACK, *session_id, vec![*upstream_id, *proto])
|
||||
}
|
||||
Frame::OpenNak { upstream_id, reason } => {
|
||||
build(TYPE_OPEN_NAK, 0, vec![*upstream_id, *reason])
|
||||
@@ -225,16 +225,16 @@ impl Frame {
|
||||
Ok(Frame::Manifest { hostname, entries })
|
||||
}
|
||||
TYPE_OPEN => {
|
||||
if payload.len() != 1 {
|
||||
return Err(DecodeError::BadPayload("OPEN payload must be 1 byte"));
|
||||
if payload.len() != 2 {
|
||||
return Err(DecodeError::BadPayload("OPEN payload must be 2 bytes"));
|
||||
}
|
||||
Ok(Frame::Open { upstream_id: payload[0] })
|
||||
Ok(Frame::Open { upstream_id: payload[0], proto: payload[1] })
|
||||
}
|
||||
TYPE_OPEN_ACK => {
|
||||
if payload.len() != 1 {
|
||||
return Err(DecodeError::BadPayload("OPEN_ACK payload must be 1 byte"));
|
||||
if payload.len() != 2 {
|
||||
return Err(DecodeError::BadPayload("OPEN_ACK payload must be 2 bytes"));
|
||||
}
|
||||
Ok(Frame::OpenAck { session_id, upstream_id: payload[0] })
|
||||
Ok(Frame::OpenAck { session_id, upstream_id: payload[0], proto: payload[1] })
|
||||
}
|
||||
TYPE_OPEN_NAK => {
|
||||
if payload.len() != 2 {
|
||||
|
||||
+9
-6
@@ -147,10 +147,12 @@ async fn handle_frame(
|
||||
};
|
||||
let _ = tx.send((src, manifest.encode())).await;
|
||||
}
|
||||
Frame::Open { upstream_id } => {
|
||||
let port = table.get(upstream_id).map(|u| u.port);
|
||||
match port {
|
||||
Some(port) => {
|
||||
Frame::Open { upstream_id, proto } => {
|
||||
let upstream = table.get(upstream_id);
|
||||
match upstream {
|
||||
Some(upstream) => {
|
||||
let port = upstream.port;
|
||||
let proto = upstream.proto.as_u8();
|
||||
// Spawn so connect() doesn't block the rx loop.
|
||||
let tx = tx.clone();
|
||||
let store = Arc::clone(store);
|
||||
@@ -165,15 +167,16 @@ async fn handle_frame(
|
||||
sid,
|
||||
SessionHandle {
|
||||
upstream_id,
|
||||
proto,
|
||||
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 };
|
||||
let ack = Frame::OpenAck { session_id: sid, upstream_id, proto };
|
||||
let _ = tx.send((src, ack.encode())).await;
|
||||
spawn_pump(r, sid, src, tx, store);
|
||||
spawn_pump(r, sid, proto, src, tx, store);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("connect 127.0.0.1:{port} failed: {e}");
|
||||
|
||||
+34
-11
@@ -131,6 +131,7 @@ impl RecvState {
|
||||
#[derive(Clone)]
|
||||
pub struct SessionHandle {
|
||||
pub upstream_id: u8,
|
||||
pub proto: u8,
|
||||
pub write: Arc<AsyncMutex<OwnedWriteHalf>>,
|
||||
pub send_state: Arc<Mutex<SendState>>,
|
||||
pub recv_state: Arc<Mutex<RecvState>>,
|
||||
@@ -145,9 +146,9 @@ pub enum WriteError {
|
||||
Io,
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// Handle a DATA frame received from the tunnel. For TCP sessions, delivers
|
||||
/// in-order via the reliability layer. For stateless protos, delivers
|
||||
/// best-effort. Returns whether a pure ACK should be sent back.
|
||||
pub async fn handle_data(
|
||||
store: &SessionStore,
|
||||
session_id: u32,
|
||||
@@ -165,7 +166,18 @@ pub async fn handle_data(
|
||||
return Err(WriteError::UnknownSession);
|
||||
};
|
||||
|
||||
// Process ack_seq to advance send window.
|
||||
// Non-TCP protos: best-effort delivery, no reliability machinery.
|
||||
if handle.proto != crate::frame::PROTO_TCP {
|
||||
if !payload.is_empty() {
|
||||
let mut w = handle.write.lock().await;
|
||||
if w.write_all(payload).await.is_err() {
|
||||
return Err(WriteError::Io);
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// TCP: full reliability — process ack_seq to advance send window.
|
||||
{
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
ss.process_ack(ack_seq);
|
||||
@@ -202,6 +214,11 @@ pub fn send_pure_ack(
|
||||
};
|
||||
let Some(handle) = handle else { return };
|
||||
|
||||
// Pure ACKs are only meaningful for reliable (TCP) sessions.
|
||||
if handle.proto != crate::frame::PROTO_TCP {
|
||||
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");
|
||||
@@ -220,17 +237,21 @@ pub fn send_pure_ack(
|
||||
}
|
||||
|
||||
/// Spawn the socket→tunnel pump: reads from the localhost TCP stream in
|
||||
/// 1480-byte chunks, tags each with seq, stores in retransmit buffer, and
|
||||
/// emits DATA frames. Also spawns the retransmit timer.
|
||||
/// 1480-byte chunks, tags each with seq, and emits DATA frames. For TCP
|
||||
/// sessions, also stores in retransmit buffer and spawns the retransmit
|
||||
/// timer. For stateless protos, best-effort (no retransmit).
|
||||
pub fn spawn_pump(
|
||||
mut read: OwnedReadHalf,
|
||||
session_id: u32,
|
||||
proto: u8,
|
||||
peer_mac: MacAddr,
|
||||
tx: TxChan,
|
||||
store: SessionStore,
|
||||
) {
|
||||
// Retransmit timer task.
|
||||
{
|
||||
let is_tcp = proto == crate::frame::PROTO_TCP;
|
||||
|
||||
// Retransmit timer task (TCP only).
|
||||
if is_tcp {
|
||||
let store = Arc::clone(&store);
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -281,11 +302,13 @@ pub fn spawn_pump(
|
||||
};
|
||||
let Some(handle) = handle else { break };
|
||||
|
||||
let (seq, ack_seq) = {
|
||||
let (seq, ack_seq) = if is_tcp {
|
||||
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())
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
let frame = Frame::Data {
|
||||
@@ -296,8 +319,8 @@ pub fn spawn_pump(
|
||||
};
|
||||
let frame_bytes = frame.encode();
|
||||
|
||||
// Store in retransmit buffer before sending.
|
||||
{
|
||||
// Store in retransmit buffer before sending (TCP only).
|
||||
if is_tcp {
|
||||
let mut ss = handle.send_state.lock().expect("send_state poisoned");
|
||||
ss.record_sent(seq, frame_bytes.clone());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user