feat(server): add PROXY protocol v2 support for real client IP handling and connection ACLs

This commit is contained in:
2026-03-29 17:40:55 +00:00
parent e31086d0c2
commit 229db4be38
9 changed files with 592 additions and 404 deletions

View File

@@ -49,6 +49,11 @@ pub struct ServerConfig {
pub quic_idle_timeout_secs: Option<u64>,
/// Pre-registered clients for IK authentication.
pub clients: Option<Vec<ClientEntry>>,
/// Enable PROXY protocol v2 parsing on incoming WebSocket connections.
/// SECURITY: Must be false when accepting direct client connections.
pub proxy_protocol: Option<bool>,
/// Server-level IP block list — applied at TCP accept, before Noise handshake.
pub connection_ip_block_list: Option<Vec<String>>,
}
/// Information about a connected client.
@@ -70,6 +75,8 @@ pub struct ClientInfo {
pub authenticated_key: String,
/// Registered client ID from the client registry.
pub registered_client_id: String,
/// Real client IP:port (from PROXY protocol header or direct TCP connection).
pub remote_addr: Option<String>,
}
/// Server statistics.
@@ -562,8 +569,8 @@ impl VpnServer {
}
}
/// WebSocket listener — accepts TCP connections, upgrades to WS, then hands off
/// to the transport-agnostic `handle_client_connection`.
/// WebSocket listener — accepts TCP connections, optionally parses PROXY protocol v2,
/// upgrades to WS, then hands off to `handle_client_connection`.
async fn run_ws_listener(
state: Arc<ServerState>,
listen_addr: String,
@@ -576,17 +583,51 @@ async fn run_ws_listener(
tokio::select! {
accept = listener.accept() => {
match accept {
Ok((stream, addr)) => {
info!("New connection from {}", addr);
Ok((mut tcp_stream, tcp_addr)) => {
info!("New connection from {}", tcp_addr);
let state = state.clone();
tokio::spawn(async move {
match transport::accept_connection(stream).await {
// Phase 0: Parse PROXY protocol v2 header if enabled
let remote_addr = if state.config.proxy_protocol.unwrap_or(false) {
match crate::proxy_protocol::read_proxy_header(&mut tcp_stream).await {
Ok(header) if header.is_local => {
info!("PP v2 LOCAL probe from {}", tcp_addr);
return; // Health check — close gracefully
}
Ok(header) => {
info!("PP v2: real client {} (via {})", header.src_addr, tcp_addr);
Some(header.src_addr)
}
Err(e) => {
warn!("PP v2 parse failed from {}: {}", tcp_addr, e);
return; // Drop connection
}
}
} else {
Some(tcp_addr) // Direct connection — use TCP SocketAddr
};
// Phase 1: Server-level connection IP block list (pre-handshake)
if let (Some(ref block_list), Some(ref addr)) = (&state.config.connection_ip_block_list, &remote_addr) {
if !block_list.is_empty() {
if let std::net::IpAddr::V4(v4) = addr.ip() {
if acl::is_connection_blocked(v4, block_list) {
warn!("Connection blocked by server IP block list: {}", addr);
return;
}
}
}
}
// Phase 2: WebSocket upgrade + VPN handshake
match transport::accept_connection(tcp_stream).await {
Ok(ws) => {
let (sink, stream) = transport_trait::split_ws(ws);
if let Err(e) = handle_client_connection(
state,
Box::new(sink),
Box::new(stream),
remote_addr,
).await {
warn!("Client connection error: {}", e);
}
@@ -662,6 +703,7 @@ async fn run_quic_listener(
state,
Box::new(sink),
Box::new(stream),
Some(remote),
).await {
warn!("QUIC client error: {}", e);
}
@@ -700,6 +742,7 @@ async fn handle_client_connection(
state: Arc<ServerState>,
mut sink: Box<dyn TransportSink>,
mut stream: Box<dyn TransportStream>,
remote_addr: Option<std::net::SocketAddr>,
) -> Result<()> {
let server_private_key = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
@@ -779,6 +822,24 @@ async fn handle_client_connection(
let mut noise_transport = responder.into_transport_mode()?;
// Connection-level ACL: check real client IP against per-client ipAllowList/ipBlockList
if let (Some(ref sec), Some(ref addr)) = (&client_security, &remote_addr) {
if let std::net::IpAddr::V4(v4) = addr.ip() {
if !acl::is_source_allowed(
v4,
sec.ip_allow_list.as_deref(),
sec.ip_block_list.as_deref(),
) {
warn!("Connection-level ACL denied client {} from IP {}", registered_client_id, addr);
let disconnect_frame = Frame { packet_type: PacketType::Disconnect, payload: Vec::new() };
let mut frame_bytes = BytesMut::new();
<FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, disconnect_frame, &mut frame_bytes)?;
let _ = sink.send_reliable(frame_bytes.to_vec()).await;
anyhow::bail!("Connection denied: source IP {} not allowed for client {}", addr, registered_client_id);
}
}
}
// Use the registered client ID as the connection ID
let client_id = registered_client_id.clone();
@@ -811,6 +872,7 @@ async fn handle_client_connection(
burst_bytes: burst,
authenticated_key: client_pub_key_b64.clone(),
registered_client_id: registered_client_id.clone(),
remote_addr: remote_addr.map(|a| a.to_string()),
};
state.clients.write().await.insert(client_id.clone(), client_info);
@@ -845,7 +907,9 @@ async fn handle_client_connection(
<FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, encrypted_info, &mut frame_bytes)?;
sink.send_reliable(frame_bytes.to_vec()).await?;
info!("Client {} ({}) connected with IP {}", registered_client_id, &client_pub_key_b64[..8], assigned_ip);
info!("Client {} ({}) connected with IP {} from {}",
registered_client_id, &client_pub_key_b64[..8], assigned_ip,
remote_addr.map(|a| a.to_string()).unwrap_or_else(|| "unknown".to_string()));
// Main packet loop with dead-peer detection
let mut last_activity = tokio::time::Instant::now();