Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac22617849 | |||
| e5a91f298c |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-26 - 4.14.2 - fix(hub-core)
|
||||
improve stream shutdown handling and connection cleanup in hub and edge
|
||||
|
||||
- Cancel edge upload loops immediately when the hub closes a stream instead of waiting for the window stall timeout.
|
||||
- Reduce stalled stream timeouts from 120s to 55s to detect broken connections faster.
|
||||
- Allow hub writer tasks to shut down gracefully before aborting to avoid unnecessary TCP resets.
|
||||
- Enable TCP keepalive on hub upstream connections to detect silent SmartProxy failures.
|
||||
- Remove leaked QUIC UDP session entries when setup fails or sessions end.
|
||||
- Rename npmextra.json to .smartconfig.json and update package packaging references.
|
||||
|
||||
## 2026-03-21 - 4.14.1 - fix(remoteingress edge/hub crash recovery)
|
||||
prevent duplicate crash recovery listeners and reset saved runtime state on shutdown
|
||||
|
||||
|
||||
21
license.md
Normal file
21
license.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/remoteingress",
|
||||
"version": "4.14.1",
|
||||
"version": "4.14.2",
|
||||
"private": false,
|
||||
"description": "Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -14,17 +14,17 @@
|
||||
"buildDocs": "(tsdoc)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tsrust": "^1.3.0",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tsrust": "^1.3.2",
|
||||
"@git.zone/tstest": "^3.6.0",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^25.3.0"
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartrust": "^1.2.1"
|
||||
"@push.rocks/smartrust": "^1.3.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -47,7 +47,7 @@
|
||||
"dist_rust/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"keywords": [
|
||||
|
||||
2725
pnpm-lock.yaml
generated
2725
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -189,7 +189,7 @@ Tokens are base64url-encoded — safe for environment variables, CLI arguments,
|
||||
|
||||
| Method / Property | Description |
|
||||
|-------------------|-------------|
|
||||
| `start(config?)` | Start the hub. Config: `{ tunnelPort?: number, targetHost?: string }`. Listens on both TCP and UDP (QUIC) on the tunnel port. |
|
||||
| `start(config?)` | Start the hub. Config: `{ tunnelPort?, targetHost?, tls?: { certPem?, keyPem? } }`. Listens on both TCP and UDP (QUIC) on the tunnel port. |
|
||||
| `stop()` | Graceful shutdown. |
|
||||
| `updateAllowedEdges(edges)` | Set authorized edges. Each: `{ id, secret, listenPorts?, listenPortsUdp?, stunIntervalSecs? }`. Port changes are pushed to connected edges in real time. |
|
||||
| `getStatus()` | Returns `{ running, tunnelPort, connectedEdges: [...] }`. |
|
||||
@@ -221,6 +221,10 @@ Tokens are base64url-encoded — safe for environment variables, CLI arguments,
|
||||
interface IHubConfig {
|
||||
tunnelPort?: number; // default: 8443
|
||||
targetHost?: string; // default: '127.0.0.1'
|
||||
tls?: {
|
||||
certPem?: string; // PEM-encoded TLS certificate
|
||||
keyPem?: string; // PEM-encoded TLS private key
|
||||
};
|
||||
}
|
||||
|
||||
interface IEdgeConfig {
|
||||
@@ -372,7 +376,7 @@ await edge.start({ token });
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license.md) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ struct EdgeStreamState {
|
||||
send_window: Arc<AtomicU32>,
|
||||
/// Notifier to wake the client reader when the window opens.
|
||||
window_notify: Arc<Notify>,
|
||||
/// Per-stream cancellation token — cancelled on FRAME_CLOSE_BACK to promptly
|
||||
/// terminate the upload loop instead of waiting for the window stall timeout.
|
||||
cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
/// Edge configuration (hub-host + credentials only; ports come from hub).
|
||||
@@ -399,7 +402,11 @@ async fn handle_edge_frame(
|
||||
}
|
||||
FRAME_CLOSE_BACK => {
|
||||
let mut writers = client_writers.lock().await;
|
||||
writers.remove(&frame.stream_id);
|
||||
if let Some(state) = writers.remove(&frame.stream_id) {
|
||||
// Cancel the stream's token so the upload loop exits promptly
|
||||
// instead of waiting for the window stall timeout.
|
||||
state.cancel_token.cancel();
|
||||
}
|
||||
}
|
||||
FRAME_CONFIG => {
|
||||
if let Ok(update) = serde_json::from_slice::<ConfigUpdate>(&frame.payload) {
|
||||
@@ -1012,6 +1019,7 @@ async fn handle_client_connection(
|
||||
back_tx,
|
||||
send_window: Arc::clone(&send_window),
|
||||
window_notify: Arc::clone(&window_notify),
|
||||
cancel_token: client_token.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1093,8 +1101,8 @@ async fn handle_client_connection(
|
||||
tokio::select! {
|
||||
_ = notified => continue,
|
||||
_ = client_token.cancelled() => break,
|
||||
_ = tokio::time::sleep(Duration::from_secs(120)) => {
|
||||
log::warn!("Stream {} upload stalled (window empty for 120s)", stream_id);
|
||||
_ = tokio::time::sleep(Duration::from_secs(55)) => {
|
||||
log::warn!("Stream {} upload stalled (window empty for 55s)", stream_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +475,12 @@ async fn handle_hub_frame(
|
||||
})??;
|
||||
|
||||
upstream.set_nodelay(true)?;
|
||||
// TCP keepalive detects silent failures on the hub→SmartProxy connection
|
||||
let ka = socket2::TcpKeepalive::new()
|
||||
.with_time(Duration::from_secs(30));
|
||||
#[cfg(target_os = "linux")]
|
||||
let ka = ka.with_interval(Duration::from_secs(10));
|
||||
let _ = socket2::SockRef::from(&upstream).set_tcp_keepalive(&ka);
|
||||
upstream.write_all(proxy_header.as_bytes()).await?;
|
||||
|
||||
let (mut up_read, mut up_write) =
|
||||
@@ -485,7 +491,7 @@ async fn handle_hub_frame(
|
||||
let writer_token = stream_token.clone();
|
||||
let wub_tx = writer_tx.clone();
|
||||
let stream_counter_w = Arc::clone(&stream_counter);
|
||||
let writer_for_edge_data = tokio::spawn(async move {
|
||||
let mut writer_for_edge_data = tokio::spawn(async move {
|
||||
let mut consumed_since_update: u32 = 0;
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -569,8 +575,8 @@ async fn handle_hub_frame(
|
||||
tokio::select! {
|
||||
_ = notified => continue,
|
||||
_ = stream_token.cancelled() => break,
|
||||
_ = tokio::time::sleep(Duration::from_secs(120)) => {
|
||||
log::warn!("Stream {} download stalled (window empty for 120s)", stream_id);
|
||||
_ = tokio::time::sleep(Duration::from_secs(55)) => {
|
||||
log::warn!("Stream {} download stalled (window empty for 55s)", stream_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -633,7 +639,11 @@ async fn handle_hub_frame(
|
||||
}
|
||||
}
|
||||
|
||||
writer_for_edge_data.abort();
|
||||
// Give the writer task 2s to shut down gracefully (sends TCP FIN
|
||||
// via up_write.shutdown()) before force-aborting (which causes RST).
|
||||
if tokio::time::timeout(Duration::from_secs(2), &mut writer_for_edge_data).await.is_err() {
|
||||
writer_for_edge_data.abort();
|
||||
}
|
||||
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
|
||||
}
|
||||
.await;
|
||||
@@ -1379,6 +1389,7 @@ async fn handle_edge_connection_quic(
|
||||
let session_token = dgram_token.child_token();
|
||||
let (tx, mut rx) = mpsc::channel::<Bytes>(256);
|
||||
let proxy_v2_data: Vec<u8> = proxy_data.to_vec();
|
||||
let cleanup_sessions = sessions.clone();
|
||||
|
||||
{
|
||||
let mut s = sessions.lock().await;
|
||||
@@ -1390,17 +1401,20 @@ async fn handle_edge_connection_quic(
|
||||
Ok(s) => Arc::new(s),
|
||||
Err(e) => {
|
||||
log::error!("QUIC UDP session {} bind failed: {}", session_id, e);
|
||||
cleanup_sessions.lock().await.remove(&session_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = upstream.connect((target.as_str(), dest_port)).await {
|
||||
log::error!("QUIC UDP session {} connect failed: {}", session_id, e);
|
||||
cleanup_sessions.lock().await.remove(&session_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send PROXY v2 header as first datagram so SmartProxy knows the original client
|
||||
if let Err(e) = upstream.send(&proxy_v2_data).await {
|
||||
log::error!("QUIC UDP session {} failed to send PROXY v2 header: {}", session_id, e);
|
||||
cleanup_sessions.lock().await.remove(&session_id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1443,6 +1457,8 @@ async fn handle_edge_connection_quic(
|
||||
}
|
||||
}
|
||||
recv_handle.abort();
|
||||
// Clean up session entry to prevent memory leak
|
||||
cleanup_sessions.lock().await.remove(&session_id);
|
||||
});
|
||||
|
||||
continue;
|
||||
@@ -1590,6 +1606,12 @@ async fn handle_quic_stream(
|
||||
};
|
||||
|
||||
let _ = upstream.set_nodelay(true);
|
||||
// TCP keepalive detects silent failures on the hub→SmartProxy connection
|
||||
let ka = socket2::TcpKeepalive::new()
|
||||
.with_time(Duration::from_secs(30));
|
||||
#[cfg(target_os = "linux")]
|
||||
let ka = ka.with_interval(Duration::from_secs(10));
|
||||
let _ = socket2::SockRef::from(&upstream).set_tcp_keepalive(&ka);
|
||||
// Send PROXY header to SmartProxy
|
||||
if let Err(e) = upstream.write_all(proxy_header.as_bytes()).await {
|
||||
log::error!("QUIC stream {} failed to write PROXY header to upstream: {}", stream_id, e);
|
||||
@@ -1600,7 +1622,7 @@ async fn handle_quic_stream(
|
||||
|
||||
// Task: QUIC -> upstream (edge data to SmartProxy)
|
||||
let writer_token = stream_token.clone();
|
||||
let writer_task = tokio::spawn(async move {
|
||||
let mut writer_task = tokio::spawn(async move {
|
||||
let mut buf = vec![0u8; 32768];
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -1651,7 +1673,11 @@ async fn handle_quic_stream(
|
||||
|
||||
// Gracefully close the QUIC send stream
|
||||
let _ = quic_send.finish();
|
||||
writer_task.abort();
|
||||
// Give the writer task 2s to shut down gracefully (sends TCP FIN
|
||||
// via up_write.shutdown()) before force-aborting (which causes RST).
|
||||
if tokio::time::timeout(Duration::from_secs(2), &mut writer_task).await.is_err() {
|
||||
writer_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/remoteingress',
|
||||
version: '4.14.1',
|
||||
version: '4.14.2',
|
||||
description: 'Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.'
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user