1- use std:: sync:: Arc ;
2- use tokio:: sync:: Semaphore ;
1+ use std:: sync:: {
2+ atomic:: { AtomicU32 , Ordering } ,
3+ Arc ,
4+ } ;
35
46use anyhow:: Result ;
57use serde:: { Deserialize , Serialize } ;
68use socket2:: { SockRef , TcpKeepalive } ;
79use tokio:: io;
810use tokio:: net:: TcpStream ;
911pub use tokio:: sync:: broadcast;
12+ use tokio:: sync:: { mpsc, Semaphore } ;
1013use tokio:: time:: { sleep, Duration } ;
1114
1215pub 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" ) ) ]
1922const 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 ) ]
2228struct 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.
4959pub 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