Skip to content

Commit d93b543

Browse files
committed
Merge branch 'pr17' into merge-pr17
# Conflicts: # src/config.rs # src/listener.rs # src/main.rs
2 parents e76c597 + 5e6bcb7 commit d93b543

5 files changed

Lines changed: 64 additions & 18 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You should get a Cloudflare IP (usually starts with `104.`, `172.67.`, `141.101.
4444

4545
```json
4646
{
47+
"graceful_shutdown_sec": 0,
4748
"listeners": [
4849
{
4950
"listen": "0.0.0.0:40443",
@@ -66,6 +67,10 @@ Replace `CLOUDFLARE_IP` with the IP from step 1. The `fake_sni` can be any domai
6667
| `keepalive_time_sec` | Seconds of idle before TCP keepalive probes begin (default: `11`) |
6768
| `keepalive_interval_sec` | Seconds between individual TCP keepalive probes (default: `2`) |
6869

70+
| Top-level Field | Description |
71+
|---|---|
72+
| `graceful_shutdown_sec` | Seconds to wait for active connections to finish after receiving a shutdown signal. `0` exits immediately (default: `0`) |
73+
6974
Multiple listeners are supported -- each maps to one upstream.
7075

7176
### Step 3: Edit your v2ray/xray config
@@ -145,6 +150,7 @@ nslookup myserver.example.com
145150

146151
```json
147152
{
153+
"graceful_shutdown_sec": 0,
148154
"listeners": [
149155
{
150156
"listen": "0.0.0.0:40443",
@@ -167,6 +173,10 @@ nslookup myserver.example.com
167173
| `keepalive_time_sec` | ثانیه‌های بی‌فعالیتی قبل از شروع پروب‌های TCP keepalive (پیش‌فرض: `11`) |
168174
| `keepalive_interval_sec` | فاصله زمانی بین پروب‌های TCP keepalive به ثانیه (پیش‌فرض: `2`) |
169175

176+
| فیلد سطح بالا | توضیح |
177+
|---|---|
178+
| `graceful_shutdown_sec` | مدت انتظار (به ثانیه) برای اتمام اتصالات فعال پس از دریافت سیگنال خاموشی. مقدار `0` بلافاصله خارج می‌شود (پیش‌فرض: `0`) |
179+
170180
### مرحله ۳: تغییر کانفیگ v2ray/xray
171181

172182
در کانفیگ VLESS/VMess خود این تغییرات را بدهید:

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@ fn default_buffer_size() -> usize {
2222
8
2323
}
2424

25+
fn default_graceful_shutdown_sec() -> u64 {
26+
0
27+
}
28+
2529
#[derive(Debug, Deserialize)]
2630
pub struct Config {
2731
pub idle_timeout: Option<u64>,
2832
#[serde(default = "default_buffer_size")]
2933
pub buffer_size: usize,
3034
pub listeners: Vec<ListenerConfig>,
35+
#[serde(default = "default_graceful_shutdown_sec")]
36+
pub graceful_shutdown_sec: u64,
3137
}
3238

3339
#[derive(Debug, Deserialize)]

src/listener.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::net::IpAddr;
22
use std::time::Duration;
33

44
use tokio::net::TcpListener;
5+
use tokio::task::JoinSet;
56
use tracing::{error, info, warn};
7+
use tokio_util::sync::CancellationToken;
68

79
use crate::config::ListenerConfig;
810
use crate::handler;
@@ -26,6 +28,7 @@ pub async fn run_listener(
2628
cmd_tx: std::sync::mpsc::Sender<SnifferCommand>,
2729
idle_timeout: Option<u64>,
2830
buffer_size: usize,
31+
token: CancellationToken,
2932
) {
3033
let listener = match TcpListener::bind(lc.listen).await {
3134
Ok(l) => {
@@ -38,8 +41,15 @@ pub async fn run_listener(
3841
}
3942
};
4043

44+
let mut tasks = JoinSet::new();
45+
4146
loop {
42-
match listener.accept().await {
47+
let accepted = tokio::select! {
48+
result = listener.accept() => result,
49+
_ = token.cancelled() => break,
50+
};
51+
52+
match accepted {
4353
Ok((stream, peer)) => {
4454
let upstream = lc.connect;
4555
let sni = lc.fake_sni.clone();
@@ -49,7 +59,7 @@ pub async fn run_listener(
4959
let handshake_timeout = lc.handshake_timeout_sec;
5060
let keepalive_time = lc.keepalive_time_sec;
5161
let keepalive_interval = lc.keepalive_interval_sec;
52-
tokio::spawn(async move {
62+
tasks.spawn(async move {
5363
tracing::debug!(peer = %peer, "accepted connection");
5464
handler::handle_connection(stream, upstream, sni, lip, tx, conn_timeout, handshake_timeout, keepalive_time, keepalive_interval, idle_timeout, buffer_size).await;
5565
});
@@ -65,4 +75,8 @@ pub async fn run_listener(
6575
}
6676
}
6777
}
78+
79+
info!(listen = %lc.listen, "stopped accepting, draining active connections");
80+
while tasks.join_next().await.is_some() {}
81+
info!(listen = %lc.listen, "all connections drained");
6882
}

src/main.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ mod sniffer;
1313
use std::net::IpAddr;
1414
use std::sync::atomic::AtomicBool;
1515
use std::sync::Arc;
16+
use std::time::Duration;
1617

1718
use tracing::{error, info};
1819
use tracing_subscriber::EnvFilter;
20+
use tokio_util::sync::CancellationToken;
1921

2022
fn main() {
2123
tracing_subscriber::fmt()
@@ -91,6 +93,7 @@ fn main() {
9193
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel::<proto::SnifferCommand>();
9294

9395
let stop = Arc::new(AtomicBool::new(false));
96+
let token = CancellationToken::new();
9497

9598
let sniffer_stop = stop.clone();
9699
let sniffer_local_ips = local_ips.clone();
@@ -103,24 +106,35 @@ fn main() {
103106
.expect("failed to spawn sniffer thread");
104107

105108
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
109+
let graceful_shutdown_sec = cfg.graceful_shutdown_sec;
106110
rt.block_on(async {
107-
let signal_stop = stop.clone();
108-
tokio::spawn(async move {
109-
shutdown::wait_for_signal(signal_stop).await;
110-
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
111-
std::process::exit(0);
112-
});
113-
114111
let mut handles = Vec::new();
115112
for lc in cfg.listeners {
116113
let tx = cmd_tx.clone();
117114
let lip = resolve_local_ip(lc.connect.ip()).unwrap_or(local_ips[0]);
118-
handles.push(tokio::spawn(listener::run_listener(lc, lip, tx, cfg.idle_timeout, cfg.buffer_size)));
115+
let tk = token.clone();
116+
handles.push(tokio::spawn(listener::run_listener(lc, lip, tx, cfg.idle_timeout, cfg.buffer_size, tk)));
119117
}
120118

121-
for h in handles {
122-
let _ = h.await;
119+
shutdown::wait_for_signal(stop, token).await;
120+
121+
if graceful_shutdown_sec == 0 {
122+
info!("graceful_shutdown_sec=0, exiting immediately");
123+
} else {
124+
info!("waiting up to {}s for active connections to drain", graceful_shutdown_sec);
125+
126+
let drain_all = async {
127+
for h in handles {
128+
let _ = h.await;
129+
}
130+
};
131+
132+
if tokio::time::timeout(Duration::from_secs(graceful_shutdown_sec), drain_all).await.is_err() {
133+
info!("drain timeout ({}s), forcing exit", graceful_shutdown_sec);
134+
}
123135
}
136+
137+
info!("shutdown complete");
124138
});
125139
}
126140

src/shutdown.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ use std::sync::atomic::{AtomicBool, Ordering};
22
use std::sync::Arc;
33

44
use tracing::info;
5+
use tokio_util::sync::CancellationToken;
56

6-
pub async fn wait_for_signal(stop: Arc<AtomicBool>) {
7-
let ctrl_c = tokio::signal::ctrl_c();
7+
pub async fn wait_for_signal(stop: Arc<AtomicBool>, token: CancellationToken) {
8+
wait_for_os_signal().await;
9+
stop.store(true, Ordering::Relaxed);
10+
token.cancel();
11+
}
812

13+
async fn wait_for_os_signal() {
14+
let ctrl_c = tokio::signal::ctrl_c();
915
#[cfg(unix)]
1016
{
1117
use tokio::signal::unix::{signal, SignalKind};
1218
let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM");
13-
1419
tokio::select! {
1520
_ = ctrl_c => {
1621
info!("received SIGINT, shutting down");
@@ -20,12 +25,9 @@ pub async fn wait_for_signal(stop: Arc<AtomicBool>) {
2025
}
2126
}
2227
}
23-
2428
#[cfg(not(unix))]
2529
{
2630
ctrl_c.await.expect("failed to listen for Ctrl+C");
2731
info!("received Ctrl+C, shutting down");
2832
}
29-
30-
stop.store(true, Ordering::Relaxed);
3133
}

0 commit comments

Comments
 (0)