use futures_util::{SinkExt, StreamExt};
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use tokio::{
io,
net::{TcpListener, TcpStream},
};
use tokio_tungstenite::{WebSocketStream, accept_async, client_async};
use tracing::{debug, info, warn};
use crate::proxy::ProxyState;
pub async fn run_ws_proxy(
public: SocketAddr,
state: Arc<ProxyState>,
path_map: HashMap<String, String>,
) -> io::Result<()> {
let listener = TcpListener::bind(public).await?;
info!(event = "ws_proxy_listening", %public, "L7 WebSocket proxy accepting");
loop {
let (stream, peer) = match listener.accept().await {
Ok(v) => v,
Err(e) => {
warn!(error = %e, "ws proxy accept failed");
continue;
}
};
let state = Arc::clone(&state);
let map = path_map.clone();
tokio::spawn(async move {
if let Err(e) = handle_ws_client(stream, peer, state, &map).await {
debug!(error = %e, "ws proxy connection ended");
}
});
}
}
async fn handle_ws_client(
stream: TcpStream,
peer: SocketAddr,
state: Arc<ProxyState>,
path_map: &HashMap<String, String>,
) -> io::Result<()> {
let client_ip = peer.ip().to_string();
let mut dead: Vec<SocketAddr> = Vec::new();
let mut client_ws = accept_async(stream)
.await
.map_err(|e| io::Error::other(format!("ws accept: {e}")))?;
let route_key = path_map
.iter()
.find(|(k, _)| client_ip.contains(k.as_str()) || false)
.map(|(_, v)| v.clone())
.unwrap_or(client_ip.clone());
let mut backend_ws = loop {
let backend = match state.pick(&route_key, &dead) {
Some(b) => b,
None => {
debug!(%peer, "no healthy ws backend; closing client");
return Ok(());
}
};
let backend_url = format!("ws://{}/ws", backend.addr);
let tcp = match TcpStream::connect(backend.addr).await {
Ok(s) => s,
Err(e) => {
warn!(backend = %backend.addr, error = %e, "ws backend connect failed");
dead.push(backend.addr);
state.invalidate(&route_key);
continue;
}
};
match client_async(backend_url, tcp).await {
Ok((ws, _resp)) => break ws,
Err(e) => {
warn!(backend = %backend.addr, error = %e, "ws backend handshake failed");
dead.push(backend.addr);
state.invalidate(&route_key);
}
}
};
info!(%peer, backend = ?dead.last().unwrap_or(&peer), "ws client proxied");
relay_ws(&mut client_ws, &mut backend_ws).await;
let _ = client_ws.close(None).await;
let _ = backend_ws.close(None).await;
Ok(())
}
async fn relay_ws(
client: &mut WebSocketStream<TcpStream>,
backend: &mut WebSocketStream<TcpStream>,
) {
let (mut client_sink, mut client_stream) = client.split();
let (mut backend_sink, mut backend_stream) = backend.split();
let c2b = async {
while let Some(Ok(msg)) = client_stream.next().await {
if msg.is_close() {
break;
}
if backend_sink.send(msg).await.is_err() {
break;
}
}
};
let b2c = async {
while let Some(Ok(msg)) = backend_stream.next().await {
if client_sink.send(msg).await.is_err() {
break;
}
}
};
tokio::select! {
_ = c2b => debug!("ws client→backend stream ended"),
_ = b2c => debug!("ws backend→client stream ended"),
}
}
#[cfg(test)]
mod tests {
#[test]
fn relay_does_nothing_on_empty_streams() {
}
}