Skip to content

Commit 847e406

Browse files
author
yosebyte
authored
Implement pairing link lifecycle management
1 parent 256ecab commit 847e406

12 files changed

Lines changed: 1089 additions & 530 deletions

File tree

docs/protocol.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,12 @@ without an application response. Network closure is initiated before detailed
337337
failure information is written to the Portal's local log. Service shutdown
338338
cancels the delay.
339339

340+
If a newly authenticated QUIC connection presents the `session_id` of an
341+
older active QUIC connection, the Portal replaces the older carrier. This
342+
permits recovery from a client-side path failure while the stale server-side
343+
connection is still waiting for its idle timeout. Existing flows on the older
344+
carrier are not migrated.
345+
340346
The Portal applies a process-wide pre-authentication admission limit shared by
341347
TCP and QUIC: at most 256 connections in total and 32 per IPv4 `/32` or IPv6
342348
`/64`. A validated QUIC attempt above either limit is silently ignored; an
@@ -429,7 +435,10 @@ Every frame begins with `version_u8(1) || type_u8`. Before receiving
429435
`OPEN_ACK`, every client payload uses `OPEN_DATA`; after the ACK, the client
430436
uses target-free `DATA`. The Portal treats repeated open metadata idempotently
431437
and forwards the first payload without an additional RTT. Target-to-client
432-
DATAGRAMs always use compact `DATA`.
438+
DATAGRAMs always use compact `DATA`. If the Portal receives target-free `DATA`
439+
for an unknown or expired flow, it returns `CLOSE`; the client then clears its
440+
ACK state and uses `OPEN_DATA` for the next payload, or recreates an asymmetric
441+
flow, to restore the target metadata.
433442

434443
The derived-order header below remains accepted for the existing symmetric
435444
implementation path but is not emitted by the upgraded reference client.

src/portal/conn.rs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ use std::sync::Arc;
1313
use quinn::{Connection, Incoming, VarInt};
1414
use tokio_util::sync::CancellationToken;
1515

16-
use crate::common::{quic_max_streams, rate_limit_bytes_per_second};
17-
use crate::protocol::Carrier;
18-
1916
use self::auth::{
2017
AuthenticationOutcome, authenticate_connection, authentication_deadline,
2118
authentication_failure_close,
@@ -34,6 +31,7 @@ use super::admission::UnauthenticatedGuard;
3431
use super::admission::{
3532
MAX_UNAUTHENTICATED_CONNECTIONS, MAX_UNAUTHENTICATED_PER_SOURCE, UnauthenticatedAdmission,
3633
};
34+
use crate::common::{quic_max_streams, rate_limit_bytes_per_second};
3735

3836
pub(super) async fn handle_incoming(
3937
portal: Arc<PortalInner>,
@@ -82,20 +80,14 @@ async fn handle_connection(
8280
conn.set_max_concurrent_bi_streams(VarInt::from_u32(quic_max_streams()));
8381
drop(admission);
8482
let session = authenticated.session;
85-
let _link_guard =
86-
match portal
87-
.pairing
88-
.register_link(session.session_id, Carrier::Udp, portal.stats.clone())
89-
{
90-
Ok(guard) => guard,
91-
Err(err) => {
92-
conn.close(VarInt::from_u32(1), b"duplicate session");
93-
portal.logger.error(format_args!(
94-
"portal::conn::handle_connection: failed to register link: {err}"
95-
));
96-
return;
97-
}
98-
};
83+
let link_replaced = CancellationToken::new();
84+
let link_guard = portal.pairing.register_quic_link(
85+
session.session_id,
86+
portal.stats.clone(),
87+
link_replaced.clone(),
88+
);
89+
session.set_quic_generation(link_guard.quic_generation());
90+
let _link_guard = link_guard;
9991

10092
let etar_bps = rate_limit_bytes_per_second(portal.etar_limit);
10193
if etar_bps > 0 {
@@ -113,6 +105,12 @@ async fn handle_connection(
113105
loop {
114106
tokio::select! {
115107
_ = shutdown.cancelled() => break,
108+
_ = link_replaced.cancelled() => {
109+
portal.logger.debug(format_args!(
110+
"portal::conn::handle_connection: authenticated QUIC carrier replaced"
111+
));
112+
break;
113+
},
116114
stream = conn.accept_bi() => {
117115
match stream {
118116
Ok((send, recv)) => {

src/portal/conn/relay_uot.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use crate::common::{handshake_timeout, udp_dial_timeout, udp_idle_timeout};
1313
use crate::portal::PortalInner;
1414
use crate::portal::pairing::PairedUdp;
1515
use crate::protocol::{
16-
Carrier, DATAGRAM_UDP_CLOSE, DATAGRAM_UDP_DATA, DATAGRAM_UDP_OPEN_ACK, encode_udp_compact,
17-
read_uot_packet, read_uot_setup_target, write_uot_packet_frame,
16+
Carrier, DATAGRAM_UDP_COMPACT_CLOSE, DATAGRAM_UDP_DATA, DATAGRAM_UDP_OPEN_ACK,
17+
encode_udp_compact, read_uot_packet, read_uot_setup_target, write_uot_packet_frame,
1818
};
1919

2020
use super::{SessionGuard, paired_exchange_path, symmetric_exchange_path};
@@ -159,6 +159,7 @@ pub(in crate::portal) async fn relay_paired_udp(portal: Arc<PortalInner>, paired
159159
downlink_carrier,
160160
uplink_path,
161161
downlink_path,
162+
compact_ack,
162163
} = paired;
163164
let socket = match portal
164165
.outbound
@@ -190,12 +191,7 @@ pub(in crate::portal) async fn relay_paired_udp(portal: Arc<PortalInner>, paired
190191
));
191192
portal.stats.add_session(true);
192193
let _done = SessionGuard::new(portal.clone(), true);
193-
if let Err(err) = send_udp_ack(&mut downlink, flow_id).await {
194-
portal.logger.debug(format_args!(
195-
"portal::conn::relay_paired_udp: failed to send ACK: {err}"
196-
));
197-
return;
198-
}
194+
let mut ack_sent = false;
199195
let mut target_buf = portal.buffers.get_udp_buffer();
200196
let mut last_used = Instant::now();
201197
loop {
@@ -217,6 +213,17 @@ pub(in crate::portal) async fn relay_paired_udp(portal: Arc<PortalInner>, paired
217213
}
218214
match socket.send(&payload).await {
219215
Ok(n) => {
216+
if !ack_sent {
217+
if let Err(err) =
218+
send_udp_ack(&mut downlink, flow_id, compact_ack.as_ref()).await
219+
{
220+
portal.logger.debug(format_args!(
221+
"portal::conn::relay_paired_udp: failed to send ACK: {err}"
222+
));
223+
break;
224+
}
225+
ack_sent = true;
226+
}
220227
portal.stats.udp_rx.fetch_add(n as u64, Ordering::Relaxed);
221228
match uplink_carrier {
222229
Carrier::Tcp => &portal.stats.up_tcp,
@@ -269,16 +276,24 @@ async fn read_paired_udp(
269276
async fn send_udp_ack(
270277
downlink: &mut crate::portal::pairing::UdpDown,
271278
flow_id: u64,
279+
compact_ack: Option<&crate::portal::pairing::CompactAck>,
272280
) -> anyhow::Result<()> {
273281
match downlink {
274282
crate::portal::pairing::UdpDown::Tcp(writer) => {
275283
writer.write_all(&[0, 0]).await?;
276284
}
277285
crate::portal::pairing::UdpDown::Quic(conn) => {
278-
let frame = encode_udp_compact(DATAGRAM_UDP_OPEN_ACK, flow_id, &[])?;
279-
conn.send_datagram(bytes::Bytes::from(frame))?;
286+
if compact_ack.is_none() {
287+
let frame = encode_udp_compact(DATAGRAM_UDP_OPEN_ACK, flow_id, &[])?;
288+
conn.send_datagram(bytes::Bytes::from(frame))?;
289+
}
280290
}
281291
}
292+
if let Some(ack) = compact_ack {
293+
let frame = encode_udp_compact(DATAGRAM_UDP_OPEN_ACK, flow_id, &[])?;
294+
ack.conn.send_datagram(bytes::Bytes::from(frame))?;
295+
ack.acked.store(true, Ordering::Release);
296+
}
282297
Ok(())
283298
}
284299

@@ -308,7 +323,7 @@ async fn send_udp_close(
308323
writer.write_all(&[0, 0]).await?;
309324
}
310325
crate::portal::pairing::UdpDown::Quic(conn) => {
311-
let frame = encode_udp_compact(DATAGRAM_UDP_CLOSE, flow_id, &[])?;
326+
let frame = encode_udp_compact(DATAGRAM_UDP_COMPACT_CLOSE, flow_id, &[])?;
312327
conn.send_datagram(bytes::Bytes::from(frame))?;
313328
}
314329
}

src/portal/conn/session.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod flow;
1010

1111
use std::collections::HashMap;
1212
use std::sync::Arc;
13-
use std::sync::atomic::{AtomicBool, Ordering};
13+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1414

1515
use bytes::Bytes;
1616
use quinn::{Connection, RecvStream, SendStream};
@@ -35,6 +35,7 @@ pub(super) struct PortalSession {
3535
portal: Arc<PortalInner>,
3636
conn: Connection,
3737
pub(super) session_id: SessionId,
38+
quic_generation: AtomicU64,
3839
udp_flows: Mutex<HashMap<UdpFlowKey, Arc<PortalUdpFlow>>>,
3940
compact_udp_flows: Mutex<HashMap<u64, CompactUdpState>>,
4041
udp_queue_budget: Arc<Semaphore>,
@@ -46,6 +47,7 @@ pub(super) struct CompactUdpState {
4647
pub(super) target: String,
4748
pub(super) downlink: Carrier,
4849
pub(super) sender: mpsc::Sender<Bytes>,
50+
pub(super) acked: Arc<AtomicBool>,
4951
}
5052

5153
impl PortalSession {
@@ -66,6 +68,7 @@ impl PortalSession {
6668
portal,
6769
conn,
6870
session_id,
71+
quic_generation: AtomicU64::new(0),
6972
udp_flows: Mutex::new(HashMap::new()),
7073
compact_udp_flows: Mutex::new(HashMap::new()),
7174
udp_queue_budget,
@@ -74,6 +77,14 @@ impl PortalSession {
7477
}
7578
}
7679

80+
pub(super) fn set_quic_generation(&self, generation: u64) {
81+
self.quic_generation.store(generation, Ordering::Release);
82+
}
83+
84+
pub(super) fn quic_generation(&self) -> u64 {
85+
self.quic_generation.load(Ordering::Acquire)
86+
}
87+
7788
/// Handles a bidirectional QUIC stream carrying one TCP target request.
7889
pub(super) async fn handle_stream(self: Arc<Self>, mut send: SendStream, recv: RecvStream) {
7990
let portal = &self.portal;
@@ -123,7 +134,10 @@ impl PortalSession {
123134
self.session_id,
124135
header,
125136
target_addr,
126-
self.link_path(),
137+
crate::portal::pairing::LinkHalf::quic(
138+
self.link_path(),
139+
self.quic_generation(),
140+
),
127141
Some(Box::pin(recv)),
128142
None,
129143
)
@@ -136,7 +150,10 @@ impl PortalSession {
136150
self.session_id,
137151
header,
138152
target_addr,
139-
self.link_path(),
153+
crate::portal::pairing::LinkHalf::quic(
154+
self.link_path(),
155+
self.quic_generation(),
156+
),
140157
None,
141158
Some(Box::pin(send)),
142159
)
@@ -166,9 +183,13 @@ impl PortalSession {
166183
self.session_id,
167184
header,
168185
target_addr,
169-
self.link_path(),
170-
None,
171-
Some(crate::portal::pairing::UdpDown::Quic(self.conn.clone())),
186+
crate::portal::pairing::LinkHalf::quic(
187+
self.link_path(),
188+
self.quic_generation(),
189+
),
190+
crate::portal::pairing::UdpHalf::Downlink(
191+
crate::portal::pairing::UdpDown::Quic(self.conn.clone()),
192+
),
172193
)
173194
.await;
174195
match result {

src/portal/conn/session_datagram.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
66
use std::collections::VecDeque;
77
use std::sync::Arc;
8+
use std::sync::atomic::AtomicBool;
89
use std::sync::atomic::Ordering;
910

1011
use bytes::Bytes;
1112
use tokio::sync::OwnedSemaphorePermit;
1213
use tokio_util::sync::CancellationToken;
1314

1415
use crate::protocol::{
15-
Carrier, CompactUdpFrame, DATAGRAM_UDP_CLOSE, DATAGRAM_UDP_REQUEST, DATAGRAM_UDP_RESPONSE,
16-
FlowHeader, FlowKind, FlowRole, decode_udp_compact, decode_udp_datagram_parts,
17-
encode_udp_compact, new_udp_datagram_header,
16+
Carrier, CompactUdpFrame, DATAGRAM_UDP_CLOSE, DATAGRAM_UDP_COMPACT_CLOSE,
17+
DATAGRAM_UDP_OPEN_ACK, DATAGRAM_UDP_REQUEST, DATAGRAM_UDP_RESPONSE, FlowHeader, FlowKind,
18+
FlowRole, decode_udp_compact, decode_udp_datagram_parts, encode_udp_compact,
19+
new_udp_datagram_header,
1820
};
1921

2022
use super::flow::{PortalUdpFlow, QueuedDatagram, UdpFlowKey};
@@ -112,11 +114,18 @@ impl PortalSession {
112114
(
113115
state.target == target && state.downlink == downlink,
114116
state.sender.clone(),
117+
state.acked.load(Ordering::Acquire),
115118
)
116119
});
117-
if let Some((valid, sender)) = existing {
120+
if let Some((valid, sender, acked)) = existing {
118121
if valid {
119122
let _ = sender.try_send(payload);
123+
if acked
124+
&& let Ok(frame) =
125+
encode_udp_compact(DATAGRAM_UDP_OPEN_ACK, flow_id, &[])
126+
{
127+
let _ = self.conn.send_datagram(Bytes::from(frame));
128+
}
120129
} else {
121130
drop(sender);
122131
self.reject_compact_flow(flow_id).await;
@@ -130,12 +139,14 @@ impl PortalSession {
130139
return;
131140
}
132141
let (sender, receiver) = tokio::sync::mpsc::channel(64);
142+
let acked = Arc::new(AtomicBool::new(false));
133143
self.compact_udp_flows.lock().await.insert(
134144
flow_id,
135145
CompactUdpState {
136146
target: target.clone(),
137147
downlink,
138148
sender: sender.clone(),
149+
acked: acked.clone(),
139150
},
140151
);
141152
let _ = sender.try_send(payload);
@@ -159,6 +170,10 @@ impl PortalSession {
159170
downlink_carrier: Carrier::Udp,
160171
uplink_path: path.clone(),
161172
downlink_path: path,
173+
compact_ack: Some(crate::portal::pairing::CompactAck {
174+
conn: self.conn.clone(),
175+
acked,
176+
}),
162177
})
163178
} else {
164179
let header = FlowHeader {
@@ -175,9 +190,17 @@ impl PortalSession {
175190
self.session_id,
176191
header,
177192
target,
178-
self.link_path(),
179-
Some(crate::portal::pairing::UdpUp::Quic(receiver)),
180-
None,
193+
crate::portal::pairing::LinkHalf::quic(
194+
self.link_path(),
195+
self.quic_generation(),
196+
),
197+
crate::portal::pairing::UdpHalf::Uplink {
198+
uplink: crate::portal::pairing::UdpUp::Quic(receiver),
199+
compact_ack: Some(crate::portal::pairing::CompactAck {
200+
conn: self.conn.clone(),
201+
acked,
202+
}),
203+
},
181204
)
182205
.await
183206
{
@@ -199,15 +222,17 @@ impl PortalSession {
199222
}
200223
}
201224
CompactUdpFrame::Data { flow_id, payload } => {
202-
if let Some(sender) = self
225+
let sender = self
203226
.compact_udp_flows
204227
.lock()
205228
.await
206229
.get(&flow_id)
207-
.map(|s| s.sender.clone())
208-
{
230+
.map(|s| s.sender.clone());
231+
if let Some(sender) = sender {
209232
let payload_offset = payload.as_ptr() as usize - data.as_ptr() as usize;
210233
let _ = sender.try_send(data.slice(payload_offset..));
234+
} else {
235+
self.reject_compact_flow(flow_id).await;
211236
}
212237
}
213238
CompactUdpFrame::Close { flow_id } => {
@@ -227,7 +252,7 @@ impl PortalSession {
227252
.pairing
228253
.cancel_udp(self.session_id, flow_id)
229254
.await;
230-
if let Ok(frame) = encode_udp_compact(DATAGRAM_UDP_CLOSE, flow_id, &[]) {
255+
if let Ok(frame) = encode_udp_compact(DATAGRAM_UDP_COMPACT_CLOSE, flow_id, &[]) {
231256
let _ = self.conn.send_datagram(Bytes::from(frame));
232257
}
233258
}

0 commit comments

Comments
 (0)