Skip to content

Commit 133b5cd

Browse files
committed
client: add automatic tunnel re-registration on connection failures
When the remote TCP connection fails repeatedly (10 consecutive failures), the client now automatically re-registers with the server to get a fresh endpoint port, then restarts all connections. This handles the case where the server has cleaned up the client entry or the network path changed. Re-registration uses exponential backoff (2s to 60s) and respects the shutdown signal throughout. The public API (open_tunnel/ClientConfig) is unchanged.
1 parent b576486 commit 133b5cd

2 files changed

Lines changed: 303 additions & 61 deletions

File tree

client/src/lib.rs

Lines changed: 199 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
use std::sync::Arc;
2-
use tokio::sync::Semaphore;
1+
use std::sync::{
2+
atomic::{AtomicU32, Ordering},
3+
Arc,
4+
};
35

46
use anyhow::Result;
57
use serde::{Deserialize, Serialize};
68
use socket2::{SockRef, TcpKeepalive};
79
use tokio::io;
810
use tokio::net::TcpStream;
911
pub use tokio::sync::broadcast;
12+
use tokio::sync::{mpsc, Semaphore};
1013
use tokio::time::{sleep, Duration};
1114

1215
pub const PROXY_SERVER: &str = "https://your-domain.com";
@@ -18,6 +21,9 @@ const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(10);
1821
#[cfg(not(target_os = "windows"))]
1922
const TCP_KEEPALIVE_RETRIES: u32 = 5;
2023

24+
/// Number of consecutive remote connection failures before re-registering the tunnel endpoint.
25+
const MAX_CONSECUTIVE_FAILURES: u32 = 10;
26+
2127
#[derive(Debug, Serialize, Deserialize)]
2228
struct ProxyResponse {
2329
id: String,
@@ -45,7 +51,11 @@ pub struct ClientConfig {
4551
pub credential: Option<String>,
4652
}
4753

48-
/// Open tunnels directly between server and localhost
54+
/// Open tunnels directly between server and localhost.
55+
///
56+
/// Registers a tunnel endpoint, then spawns a supervisor that maintains
57+
/// connections and automatically re-registers if the endpoint becomes
58+
/// unreachable.
4959
pub async fn open_tunnel(config: ClientConfig) -> Result<String> {
5060
let ClientConfig {
5161
server,
@@ -56,65 +66,128 @@ pub async fn open_tunnel(config: ClientConfig) -> Result<String> {
5666
max_conn,
5767
credential,
5868
} = config;
59-
let tunnel_info = get_tunnel_endpoint(server, subdomain, credential).await?;
69+
let tunnel_info =
70+
get_tunnel_endpoint(server.clone(), subdomain.clone(), credential.clone()).await?;
71+
let url = tunnel_info.url.clone();
6072

61-
// TODO check the connect is failed and restart the proxy.
62-
tunnel_to_endpoint(
63-
tunnel_info.clone(),
73+
let supervisor_config = SupervisorConfig {
74+
server,
75+
subdomain,
76+
credential,
6477
local_host,
6578
local_port,
6679
shutdown_signal,
6780
max_conn,
68-
)
69-
.await;
81+
};
82+
tokio::spawn(tunnel_supervisor(supervisor_config, tunnel_info));
7083

71-
Ok(tunnel_info.url)
84+
Ok(url)
7285
}
7386

74-
async fn get_tunnel_endpoint(
87+
struct SupervisorConfig {
7588
server: Option<String>,
7689
subdomain: Option<String>,
7790
credential: Option<String>,
78-
) -> Result<TunnelServerInfo> {
79-
let server = server.as_deref().unwrap_or(PROXY_SERVER);
80-
let assigned_domain = subdomain.as_deref().unwrap_or("?new");
81-
let mut uri = format!("{}/{}", server, assigned_domain);
82-
if let Some(credential) = credential {
83-
uri = format!("{}?credential={}", uri, credential);
84-
}
85-
log::info!("Request for assign domain: {}", uri);
91+
local_host: Option<String>,
92+
local_port: u16,
93+
shutdown_signal: broadcast::Sender<()>,
94+
max_conn: u8,
95+
}
8696

87-
let resp = reqwest::get(uri).await?.json::<ProxyResponse>().await?;
88-
log::info!("Response from server: {:#?}", resp);
97+
// Runs the register → connect → detect-failures → re-register cycle.
98+
//
99+
// Each iteration ("round") spawns a pool of connections to the current endpoint.
100+
// If too many consecutive remote-connect failures occur (the server cleaned up
101+
// our listener port, or the network path changed), connection tasks signal via
102+
// `reregister_tx` and the supervisor requests a fresh endpoint from the API
103+
// server—using the same subdomain so the public tunnel URL stays stable.
104+
async fn tunnel_supervisor(config: SupervisorConfig, initial_info: TunnelServerInfo) {
105+
let mut current_info = initial_info;
106+
let mut shutdown_rx = config.shutdown_signal.subscribe();
89107

90-
let parts = resp.url.split("//").collect::<Vec<&str>>();
91-
let mut host = parts[1].split(':').collect::<Vec<&str>>()[0];
92-
host = match host.split_once('.') {
93-
Some((_, base)) => base,
94-
None => host,
95-
};
108+
loop {
109+
log::info!("Starting tunnel connections to {:?}", current_info);
96110

97-
let tunnel_info = TunnelServerInfo {
98-
host: host.to_string(),
99-
port: resp.port,
100-
max_conn_count: resp.max_conn_count,
101-
url: resp.url,
102-
};
111+
// Per-round shutdown channel: lets us stop this round's connections
112+
// without tearing down the whole supervisor.
113+
let (round_stop_tx, _) = broadcast::channel::<()>(1);
114+
let consecutive_failures = Arc::new(AtomicU32::new(0));
115+
let (reregister_tx, mut reregister_rx) = mpsc::channel::<()>(1);
103116

104-
Ok(tunnel_info)
117+
start_tunnel_connections(
118+
&current_info,
119+
config.local_host.clone(),
120+
config.local_port,
121+
round_stop_tx.clone(),
122+
config.max_conn,
123+
consecutive_failures,
124+
reregister_tx,
125+
);
126+
127+
// Block until either the connections ask for re-registration or we
128+
// are told to shut down entirely.
129+
tokio::select! {
130+
_ = reregister_rx.recv() => {
131+
log::warn!(
132+
"Re-registering tunnel after {} consecutive remote connection failures",
133+
MAX_CONSECUTIVE_FAILURES
134+
);
135+
let _ = round_stop_tx.send(());
136+
sleep(Duration::from_millis(500)).await;
137+
}
138+
_ = shutdown_rx.recv() => {
139+
let _ = round_stop_tx.send(());
140+
return;
141+
}
142+
}
143+
144+
// Re-register with exponential backoff (2 s → 4 s → … → 60 s cap).
145+
// The same subdomain is requested so the public URL doesn't change;
146+
// only the internal listener port is refreshed.
147+
let mut backoff = Duration::from_secs(2);
148+
loop {
149+
match get_tunnel_endpoint(
150+
config.server.clone(),
151+
config.subdomain.clone(),
152+
config.credential.clone(),
153+
)
154+
.await
155+
{
156+
Ok(info) => {
157+
log::info!("Re-registered tunnel endpoint: {:?}", info);
158+
current_info = info;
159+
break;
160+
}
161+
Err(err) => {
162+
log::error!(
163+
"Re-registration failed: {:?}, retrying in {:?}",
164+
err,
165+
backoff
166+
);
167+
tokio::select! {
168+
_ = sleep(backoff) => {
169+
backoff = (backoff * 2).min(Duration::from_secs(60));
170+
}
171+
_ = shutdown_rx.recv() => return,
172+
}
173+
}
174+
}
175+
}
176+
}
105177
}
106178

107-
async fn tunnel_to_endpoint(
108-
server: TunnelServerInfo,
179+
fn start_tunnel_connections(
180+
server: &TunnelServerInfo,
109181
local_host: Option<String>,
110182
local_port: u16,
111183
shutdown_signal: broadcast::Sender<()>,
112184
max_conn: u8,
185+
consecutive_failures: Arc<AtomicU32>,
186+
reregister_tx: mpsc::Sender<()>,
113187
) {
114-
log::info!("Tunnel server info: {:?}", server);
115-
let server_host = server.host;
188+
let server_host = server.host.clone();
116189
let server_port = server.port;
117-
let local_host = local_host.unwrap_or(LOCAL_HOST.to_string());
190+
let local_host = local_host.unwrap_or_else(|| LOCAL_HOST.to_string());
118191

119192
let count = std::cmp::min(server.max_conn_count, max_conn);
120193
log::info!("Max connection count: {}", count);
@@ -135,50 +208,82 @@ async fn tunnel_to_endpoint(
135208
};
136209
let server_host = server_host.clone();
137210
let local_host = local_host.clone();
138-
211+
let consecutive_failures = consecutive_failures.clone();
212+
let reregister_tx = reregister_tx.clone();
139213
let mut shutdown_receiver = shutdown_signal.subscribe();
140214

141215
tokio::spawn(async move {
142-
log::info!("Create a new proxy connection.");
143216
tokio::select! {
144-
res = handle_connection(server_host, server_port, local_host, local_port) => {
145-
match res {
146-
Ok(_) => log::info!("Connection result: {:?}", res),
147-
Err(err) => {
148-
log::error!("Failed to connect to proxy or local server: {:?}", err);
149-
sleep(Duration::from_secs(10)).await;
150-
}
151-
}
152-
}
217+
_ = tunnel_one_connection(
218+
&server_host, server_port,
219+
&local_host, local_port,
220+
&consecutive_failures, &reregister_tx,
221+
) => {}
153222
_ = shutdown_receiver.recv() => {
154-
log::info!("Shutting down the connection immediately");
223+
log::info!("Shutting down connection");
155224
}
156225
}
157226

158227
drop(permit);
159228
});
160229
}
161230
_ = shutdown_receiver.recv() => {
162-
log::info!("Shuttign down the loop immediately");
231+
log::info!("Shutting down the loop");
163232
return;
164233
}
165234
};
166235
}
167236
});
168237
}
169238

170-
async fn handle_connection(
171-
remote_host: String,
172-
remote_port: u16,
173-
local_host: String,
239+
// Handle one tunnel connection attempt.
240+
//
241+
// Only *remote* TCP-connect failures count toward re-registration: they
242+
// indicate the server's listener port is gone (cleanup, restart, etc.).
243+
// Local-connect or proxy errors (e.g. local server restarting) do not bump
244+
// the counter, avoiding spurious re-registration loops.
245+
//
246+
// A single successful remote connect resets the counter to zero, so
247+
// intermittent failures won't accumulate across otherwise-healthy rounds.
248+
async fn tunnel_one_connection(
249+
server_host: &str,
250+
server_port: u16,
251+
local_host: &str,
252+
local_port: u16,
253+
consecutive_failures: &AtomicU32,
254+
reregister_tx: &mpsc::Sender<()>,
255+
) {
256+
log::debug!("Connecting to remote: {}:{}", server_host, server_port);
257+
let remote_stream = match TcpStream::connect(format!("{server_host}:{server_port}")).await {
258+
Ok(stream) => {
259+
consecutive_failures.store(0, Ordering::Relaxed);
260+
stream
261+
}
262+
Err(err) => {
263+
let failures = consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1;
264+
log::error!("Remote connect failed ({failures} consecutive): {:?}", err);
265+
if failures >= MAX_CONSECUTIVE_FAILURES {
266+
let _ = reregister_tx.try_send(());
267+
}
268+
sleep(Duration::from_secs(10)).await;
269+
return;
270+
}
271+
};
272+
273+
if let Err(err) = proxy_through(remote_stream, local_host, local_port).await {
274+
log::error!("Proxy error: {:?}", err);
275+
sleep(Duration::from_secs(10)).await;
276+
}
277+
}
278+
279+
async fn proxy_through(
280+
mut remote_stream: TcpStream,
281+
local_host: &str,
174282
local_port: u16,
175283
) -> Result<()> {
176-
log::debug!("Connect to remote: {}, {}", remote_host, remote_port);
177-
let mut remote_stream = TcpStream::connect(format!("{}:{}", remote_host, remote_port)).await?;
178-
log::debug!("Connect to local: {}, {}", local_host, local_port);
179-
let mut local_stream = TcpStream::connect(format!("{}:{}", local_host, local_port)).await?;
284+
log::debug!("Connecting to local: {}:{}", local_host, local_port);
285+
let mut local_stream = TcpStream::connect(format!("{local_host}:{local_port}")).await?;
180286

181-
// configure keepalive on remote socket to early detect network issues and attempt to re-establish the connection.
182287
let ka = TcpKeepalive::new()
183288
.with_time(TCP_KEEPALIVE_TIME)
184289
.with_interval(TCP_KEEPALIVE_INTERVAL);
@@ -190,3 +295,36 @@ async fn handle_connection(
190295
io::copy_bidirectional(&mut remote_stream, &mut local_stream).await?;
191296
Ok(())
192297
}
298+
299+
async fn get_tunnel_endpoint(
300+
server: Option<String>,
301+
subdomain: Option<String>,
302+
credential: Option<String>,
303+
) -> Result<TunnelServerInfo> {
304+
let server = server.as_deref().unwrap_or(PROXY_SERVER);
305+
let assigned_domain = subdomain.as_deref().unwrap_or("?new");
306+
let mut uri = format!("{}/{}", server, assigned_domain);
307+
if let Some(credential) = credential {
308+
uri = format!("{}?credential={}", uri, credential);
309+
}
310+
log::info!("Request for assign domain: {}", uri);
311+
312+
let resp = reqwest::get(uri).await?.json::<ProxyResponse>().await?;
313+
log::info!("Response from server: {:#?}", resp);
314+
315+
let parts = resp.url.split("//").collect::<Vec<&str>>();
316+
let mut host = parts[1].split(':').collect::<Vec<&str>>()[0];
317+
host = match host.split_once('.') {
318+
Some((_, base)) => base,
319+
None => host,
320+
};
321+
322+
let tunnel_info = TunnelServerInfo {
323+
host: host.to_string(),
324+
port: resp.port,
325+
max_conn_count: resp.max_conn_count,
326+
url: resp.url,
327+
};
328+
329+
Ok(tunnel_info)
330+
}

0 commit comments

Comments
 (0)