#![warn(clippy::pedantic)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)] use std::{ collections::HashMap, convert::Infallible, net::{Ipv6Addr, SocketAddr}, sync::Arc, time::Duration, }; use axum::{ http::StatusCode, routing::{get, post}, }; use clickhouse::{inserter::Inserter, Row}; use eyre::{eyre, Context}; use log::{error, info, warn}; use netty::{Handshake, ReadError}; use parking_lot::RwLock; use quinn::{ crypto::rustls::QuicServerConfig, rustls::pki_types::{CertificateDer, PrivateKeyDer}, ConnectionError, Endpoint, Incoming, ServerConfig, TransportConfig, }; use routing::RoutingTable; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, sync::Mutex, }; use crate::{ netty::{ReadExt, WriteExt}, proto::{ClientboundControlMessage, ServerboundControlMessage}, }; mod netty; mod proto; mod routing; mod unicode_madness; mod wordlist; fn get_certs() -> eyre::Result<(Vec>, PrivateKeyDer<'static>)> { let mut cert_file = std::io::BufReader::new(std::fs::File::open( std::env::var("QUICLIME_CERT_PATH").context("Reading QUICLIME_CERT_PATH")?, )?); let certs = rustls_pemfile::certs(&mut cert_file) .filter_map(Result::ok) .collect(); let mut key_file = std::io::BufReader::new(std::fs::File::open( std::env::var("QUICLIME_KEY_PATH").context("Reading QUICLIME_KEY_PATH")?, )?); let key = rustls_pemfile::private_key(&mut key_file)?.ok_or(eyre!("No private key?"))?; Ok((certs, key)) } async fn create_server_config() -> eyre::Result { let (cert_chain, key_der) = tokio::task::spawn_blocking(get_certs).await??; let mut rustls_config = quinn::rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(cert_chain, key_der)?; rustls_config.alpn_protocols = vec![b"quiclime".to_vec()]; let quic_rustls_config = QuicServerConfig::try_from(rustls_config)?; let mut config = ServerConfig::with_crypto(Arc::new(quic_rustls_config)); let mut transport = TransportConfig::default(); transport .max_concurrent_bidi_streams(1u32.into()) .max_concurrent_uni_streams(0u32.into()) .keep_alive_interval(Some(Duration::from_secs(5))); config.transport_config(Arc::new(transport)); Ok(config) } #[derive(Row, Serialize)] struct Connection { #[serde(with = "clickhouse::serde::time::datetime")] established: OffsetDateTime, region: &'static str, client: Ipv6Addr, intent: &'static str, outcome: &'static str, target: String, } #[tokio::main] async fn main() -> eyre::Result<()> { env_logger::init(); // JUSTIFICATION: this lives until the end of the entire program let endpoint = Box::leak(Box::new(Endpoint::server( create_server_config().await?, std::env::var("QUICLIME_BIND_ADDR_QUIC") .context("Reading QUICLIME_BIND_ADDR_QUIC")? .parse()?, )?)); // JUSTIFICATION: this lives until the end of the entire program let routing_table = Box::leak(Box::new(routing::RoutingTable::new( std::env::var("QUICLIME_BASE_DOMAIN").context("Reading QUICLIME_BASE_DOMAIN")?, ))); let client = clickhouse::Client::default() .with_url(std::env::var("CLICKHOUSE_URL").context("Reading CLICKHOUSE_URL")?) .with_user(std::env::var("CLICKHOUSE_USER").context("Reading CLICKHOUSE_USER")?) .with_password( tokio::fs::read_to_string( std::env::var("CLICKHOUSE_PASSWORD_PATH") .context("Reading CLICKHOUSE_PASSWORD_PATH")?, ) .await .context("Reading from CLICKHOUSE_PASSWORD_PATH")?, ) .with_database(std::env::var("CLICKHOUSE_DB").context("Reading CLICKHOUSE_DB")?); let inserter: clickhouse::inserter::Inserter = client .inserter(&std::env::var("CLICKHOUSE_TABLE").context("Reading CLICKHOUSE_TABLE")?)? .with_timeouts(Some(Duration::from_secs(5)), Some(Duration::from_secs(20))) .with_max_bytes(50_000_000) .with_max_rows(750_000) .with_period(Some(Duration::from_secs(15))); let inserter = Arc::new(Mutex::new(inserter)); let blocklist = Arc::new(RwLock::new(HashMap::new())); #[allow(unreachable_code)] tokio::try_join!( listen_quic(endpoint, routing_table), listen_control(endpoint, routing_table), listen_minecraft(routing_table, inserter.clone(), blocklist.clone()), send_commits(inserter), refresh_bl(blocklist) )?; Ok(()) } async fn try_handle_quic(connection: Incoming, routing_table: &RoutingTable) -> eyre::Result<()> { let connection = connection.await?; info!( "QUIClime connection established to: {}", connection.remote_address() ); let (mut send_control, mut recv_control) = connection.accept_bi().await?; info!("Control channel open: {}", connection.remote_address()); let mut handle = loop { let mut buf = vec![0u8; recv_control.read_u8().await? as _]; recv_control.read_exact(&mut buf).await?; if let Ok(parsed) = serde_json::from_slice(&buf) { match parsed { ServerboundControlMessage::RequestDomainAssignment => { let handle = routing_table.register(); info!( "Domain assigned to {}: {}", connection.remote_address(), handle.domain() ); let response = serde_json::to_vec(&ClientboundControlMessage::DomainAssignmentComplete { domain: handle.domain().to_string(), })?; send_control.write_all(&[response.len() as u8]).await?; send_control.write_all(&response).await?; break handle; } } } let response = serde_json::to_vec(&ClientboundControlMessage::UnknownMessage)?; send_control.write_all(&[response.len() as u8]).await?; send_control.write_all(&response).await?; }; tokio::select! { e = connection.closed() => { match e { ConnectionError::ConnectionClosed(_) | ConnectionError::ApplicationClosed(_) | ConnectionError::LocallyClosed => Ok(()), e => Err(e.into()), } }, r = async { while let Some(remote) = handle.next().await { match remote { routing::RouterRequest::RouteRequest(remote) => { let pair = connection.open_bi().await; if let Err(ConnectionError::ApplicationClosed(_)) = pair { break; } else if let Err(ConnectionError::ConnectionClosed(_)) = pair { break; } remote.send(pair?).map_err(|e| eyre!("{:?}", e))?; } routing::RouterRequest::BroadcastRequest(message) => { let response = serde_json::to_vec(&ClientboundControlMessage::RequestMessageBroadcast { message, })?; send_control.write_all(&[response.len() as u8]).await?; send_control.write_all(&response).await?; } } } Ok(()) } => r } } async fn handle_quic(connection: Incoming, routing_table: &RoutingTable) { if let Err(e) = try_handle_quic(connection, routing_table).await { error!("Error handling QUIClime connection: {:#}", e); } info!("Finished handling QUIClime connection"); } async fn listen_quic( endpoint: &'static Endpoint, routing_table: &'static RoutingTable, ) -> eyre::Result { while let Some(connection) = endpoint.accept().await { tokio::spawn(handle_quic(connection, routing_table)); } Err(eyre!("quiclime endpoint closed")) } async fn listen_control( endpoint: &'static Endpoint, routing_table: &'static RoutingTable, ) -> eyre::Result { let app = axum::Router::new() .route( "/metrics", get(|| async { format!("host_count {}", routing_table.size()) }), ) .route( "/reload-certs", post(|| async { match create_server_config().await { Ok(config) => { endpoint.set_server_config(Some(config)); (StatusCode::OK, "Success".to_string()) } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), } }), ) .route( "/broadcast", post(move |body: String| async move { routing_table.broadcast(&body) }), ) .route( "/stop", post(|| async { endpoint.close(0u32.into(), b"e4mc closing"); }), ); axum::Server::bind( &std::env::var("QUICLIME_BIND_ADDR_WEB") .context("Reading QUICLIME_BIND_ADDR_WEB")? .parse()?, ) .serve(app.into_make_service()) .await?; Err(eyre!("control endpoint closed")) } async fn try_handle_minecraft( mut connection: TcpStream, routing_table: &'static RoutingTable, inserter: Arc>>, blocklist: Arc>>, ) -> eyre::Result<()> { let established = OffsetDateTime::now_utc(); let peer = connection.peer_addr()?; info!("Minecraft client connected from: {}", peer); let handshake = netty::read_packet(&mut connection, 512).await; if let Err(ReadError::LegacyServerListPing) = handshake { connection .write_all(include_bytes!("legacy_serverlistping_response.bin")) .await?; return Ok(()); } let handshake = Handshake::new(&handshake?)?; let Some(address) = handshake.normalized_address() else { return politely_disconnect(connection, handshake).await; }; let target = address.clone(); let trace = |outcome| { tokio::task::spawn(async move { if let Err(e) = inserter.lock().await.write(&Connection { established, region: routing_table.base_domain(), client: match peer.ip() { std::net::IpAddr::V4(ipv4_addr) => ipv4_addr.to_ipv6_mapped(), std::net::IpAddr::V6(ipv6_addr) => ipv6_addr, }, intent: match handshake.next_state { netty::HandshakeType::Status => "status", netty::HandshakeType::Login => "login", }, outcome, target, }) { error!("Failed to send telemetry: {e:?}"); } }); }; if routing_table.ratelimit(peer.ip()) { trace("ratelimited"); return disconnect( connection, handshake, include_str!("./serverlistping_response_rate.json"), include_str!("./disconnect_response_rate.json"), ) .await; } let bl_status = blocklist .read() .get(&match peer.ip() { std::net::IpAddr::V4(ipv4_addr) => ipv4_addr.to_ipv6_mapped(), std::net::IpAddr::V6(ipv6_addr) => ipv6_addr, }) .copied(); if bl_status == Some(BlocklistStatus::ShadowBanned) { trace("shadowbanned"); return politely_disconnect(connection, handshake).await; } else if bl_status == Some(BlocklistStatus::Blocked) { trace("blocked"); return disconnect( connection, handshake, include_str!("./serverlistping_response_blocked.json"), include_str!("./disconnect_response_blocked.json"), ) .await; } else if bl_status == Some(BlocklistStatus::PingMasked) && handshake.next_state == netty::HandshakeType::Status { trace("ping_masked"); return disconnect( connection, handshake, include_str!("./serverlistping_response_masked.json"), include_str!("./disconnect_response_blocked.json"), ) .await; } let routing_result = routing_table.route(&address).await; let routing_ok = routing_result.is_some(); trace(if routing_ok { "ok" } else { "bad_domain" }); let Some((mut send_host, mut recv_host)) = routing_result else { return politely_disconnect(connection, handshake).await; }; handshake.send(&mut send_host).await?; let (mut recv_client, mut send_client) = connection.split(); tokio::select! { _ = tokio::io::copy(&mut recv_client, &mut send_host) => (), _ = tokio::io::copy(&mut recv_host, &mut send_client) => () } _ = connection.shutdown().await; _ = send_host.finish(); _ = recv_host.stop(0u32.into()); _ = send_host.stopped().await; info!("Minecraft client disconnected from: {}", peer); Ok(()) } async fn politely_disconnect(connection: TcpStream, handshake: Handshake) -> eyre::Result<()> { disconnect( connection, handshake, include_str!("./serverlistping_response.json"), include_str!("./disconnect_response.json"), ) .await } async fn disconnect( mut connection: TcpStream, handshake: Handshake, slp_resp: &str, dc_resp: &str, ) -> eyre::Result<()> { match handshake.next_state { netty::HandshakeType::Status => { let packet = netty::read_packet(&mut connection, 1).await?; let mut packet = packet.as_slice(); let id = packet.read_varint()?; if id != 0 { return Err(eyre!( "Packet isn't a Status Request(0x00), but {:#04x}", id )); } let mut buf = vec![]; buf.write_varint(0).await?; buf.write_string(slp_resp).await?; connection.write_varint(buf.len() as i32).await?; connection.write_all(&buf).await?; let packet = netty::read_packet(&mut connection, 9).await?; let mut packet = packet.as_slice(); let id = packet.read_varint()?; if id != 1 { return Err(eyre!("Packet isn't a Ping Request(0x01), but {:#04x}", id)); } let payload = packet.read_long()?; let mut buf = Vec::with_capacity(1 + 8); buf.write_varint(1).await?; buf.write_u64(payload).await?; connection.write_varint(buf.len() as i32).await?; connection.write_all(&buf).await?; } netty::HandshakeType::Login => { let _ = netty::read_packet(&mut connection, 128).await?; let mut buf = vec![]; buf.write_varint(0).await?; buf.write_string(dc_resp).await?; connection.write_varint(buf.len() as i32).await?; connection.write_all(&buf).await?; } } Ok(()) } async fn handle_minecraft( connection: TcpStream, routing_table: &'static RoutingTable, inserter: Arc>>, blocklist: Arc>>, ) { if let Err(e) = try_handle_minecraft(connection, routing_table, inserter, blocklist).await { error!("Error handling Minecraft connection: {:#}", e); } } async fn listen_minecraft( routing_table: &'static RoutingTable, inserter: Arc>>, blocklist: Arc>>, ) -> eyre::Result { let server = tokio::net::TcpListener::bind( std::env::var("QUICLIME_BIND_ADDR_MC") .context("Reading QUICLIME_BIND_ADDR_MC")? .parse::()?, ) .await?; loop { match server.accept().await { Ok((connection, _)) => { tokio::spawn(handle_minecraft( connection, routing_table, inserter.clone(), blocklist.clone(), )); } Err(e) => { error!("Error accepting minecraft connection: {:#}", e); } } } } async fn send_commits(inserter: Arc>>) -> eyre::Result { loop { if let Err(e) = inserter.lock().await.commit().await { error!("Error committing: {e:?}"); } tokio::time::sleep(Duration::from_secs(5)).await; } } #[derive(Deserialize, Clone, Copy, PartialEq, Eq)] enum BlocklistStatus { ShadowBanned, Blocked, PingMasked, } async fn refresh_bl( blocklist: Arc>>, ) -> eyre::Result { let client = reqwest::Client::new(); let url = std::env::var("BLOCKLIST_URL").context("Reading BLOCKLIST_URL")?; loop { tokio::time::sleep(Duration::from_secs(10)).await; let Ok(resp) = client.get(&url).send().await else { error!("Failed to fetch blocklist!"); continue; }; let Ok(json) = resp.json::>().await else { error!("Failed to fetch blocklist!"); continue; }; *blocklist.write() = json; } }