From 56d6b157e9c153dda14ed60164b60bcd030c290f Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Wed, 31 Jan 2024 02:48:45 +0000 Subject: [PATCH] Implement IP cloaking --- Cargo.lock | 2 + Cargo.toml | 2 + migrations/2023010814480_initial-schema.sql | 5 ++ src/connection.rs | 78 +++++++++++---------- src/keys.rs | 28 ++++++++ src/lib.rs | 1 + src/main.rs | 11 ++- src/server/response.rs | 4 +- 8 files changed, 89 insertions(+), 42 deletions(-) create mode 100644 src/keys.rs diff --git a/Cargo.lock b/Cargo.lock index 6ce7754..160b8e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2049,12 +2049,14 @@ dependencies = [ "clap", "const_format", "futures", + "hex", "hickory-resolver", "irc-proto", "itertools", "rand", "serde", "serde-humantime", + "sha2", "sqlx", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 0bbe178..5bfd9cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,12 @@ const_format = "0.2" chrono = "0.4" clap = { version = "4.1", features = ["cargo", "derive", "std", "suggestions", "color"] } futures = "0.3" +hex = "0.4" hickory-resolver = { version = "0.24", features = ["tokio-runtime", "system-config"] } rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde-humantime = "0.1" +sha2 = "0.10 " sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "any"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } diff --git a/migrations/2023010814480_initial-schema.sql b/migrations/2023010814480_initial-schema.sql index 6051d28..bb3258a 100644 --- a/migrations/2023010814480_initial-schema.sql +++ b/migrations/2023010814480_initial-schema.sql @@ -1,3 +1,8 @@ +CREATE TABLE keys ( + name VARCHAR(255) PRIMARY KEY, + enckey VARCHAR(255) NOT NULL +); + CREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255) NOT NULL, diff --git a/src/connection.rs b/src/connection.rs index 1f59ecd..883d650 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -20,6 +20,7 @@ use hickory_resolver::TokioAsyncResolver; use irc_proto::{ error::ProtocolError, CapSubCommand, Command, IrcCodec, Message, Prefix, Response, }; +use sha2::digest::{FixedOutput, Update}; use tokio::{ io::{ReadHalf, WriteHalf}, net::TcpStream, @@ -33,6 +34,7 @@ use crate::{ sasl::{AuthStrategy, ConnectionSuccess, SaslSuccess}, }, host_mask::HostMask, + keys::Keys, persistence::{events::ReserveNick, Persistence}, }; @@ -46,7 +48,6 @@ pub struct UserId(pub i64); #[derive(Default)] pub struct ConnectionRequest { host: Option, - resolved_host: Option, nick: Option, user: Option, real_name: Option, @@ -70,28 +71,9 @@ pub struct InitiatedConnection { } impl InitiatedConnection { - #[must_use] - pub fn to_nick(&self) -> Prefix { - Prefix::Nickname( - self.nick.to_string(), - self.user.to_string(), - self.cloak.to_string(), - ) - } - - #[must_use] - pub fn to_host_mask(&self) -> HostMask<'_> { - HostMask::new(&self.nick, &self.user, &self.cloak) - } -} - -impl TryFrom for InitiatedConnection { - type Error = ConnectionRequest; - - fn try_from(value: ConnectionRequest) -> Result { + pub fn new(value: ConnectionRequest, keys: &Keys) -> Result { let ConnectionRequest { host: Some(host), - resolved_host, nick: Some(nick), user: Some(user), real_name: Some(real_name), @@ -102,10 +84,17 @@ impl TryFrom for InitiatedConnection { return Err(value); }; + let cloak = sha2::Sha256::default() + .chain(host.ip().to_canonical().to_string()) + .chain(keys.ip_salt) + .finalize_fixed(); + let mut cloak = hex::encode(cloak); + cloak.truncate(12); + Ok(Self { host, - resolved_host: resolved_host.clone(), - cloak: resolved_host.unwrap_or_else(|| "xxx".to_string()), + resolved_host: None, + cloak: format!("cloaked-{cloak}"), nick, user, mode: UserMode::empty(), @@ -116,6 +105,20 @@ impl TryFrom for InitiatedConnection { at: Utc::now(), }) } + + #[must_use] + pub fn to_nick(&self) -> Prefix { + Prefix::Nickname( + self.nick.to_string(), + self.user.to_string(), + self.cloak.to_string(), + ) + } + + #[must_use] + pub fn to_host_mask(&self) -> HostMask<'_> { + HostMask::new(&self.nick, &self.user, &self.cloak) + } } /// Currently just awaits client preamble (nick, user), but can be expanded to negotiate @@ -128,6 +131,7 @@ pub async fn negotiate_client_connection( persistence: &Addr, database: sqlx::Pool, resolver: &TokioAsyncResolver, + keys: &Keys, ) -> Result, ProtocolError> { let mut request = ConnectionRequest { host: Some(host), @@ -210,19 +214,7 @@ pub async fn negotiate_client_connection( } }; - if let Ok(Ok(v)) = tokio::time::timeout( - Duration::from_millis(250), - resolver.reverse_lookup(host.ip()), - ) - .await - { - request.resolved_host = v - .iter() - .next() - .map(|v| v.to_utf8().trim_end_matches('.').to_string()); - } - - match InitiatedConnection::try_from(std::mem::take(&mut request)) { + match InitiatedConnection::new(std::mem::take(&mut request), keys) { Ok(v) => break Some(v), Err(v) => { // connection isn't fully initiated yet... @@ -233,10 +225,22 @@ pub async fn negotiate_client_connection( // if the user closed the connection before the connection was fully established, // return back early - let Some(initiated) = initiated else { + let Some(mut initiated) = initiated else { return Ok(None); }; + if let Ok(Ok(v)) = tokio::time::timeout( + Duration::from_millis(250), + resolver.reverse_lookup(host.ip().to_canonical()), + ) + .await + { + initiated.resolved_host = v + .iter() + .next() + .map(|v| v.to_utf8().trim_end_matches('.').to_string()); + } + write .send(ConnectionSuccess(initiated.clone()).into_message()) .await?; diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..f88cbf9 --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,28 @@ +use sqlx::{Any, Pool}; + +#[derive(Copy, Clone)] +pub struct Keys { + pub ip_salt: [u8; 32], +} + +impl Keys { + pub async fn new(pool: &Pool) -> Result { + Ok(Self { + ip_salt: fetch_or_create(pool, "ip_salt").await?.try_into().unwrap(), + }) + } +} + +async fn fetch_or_create(pool: &Pool, name: &str) -> Result, sqlx::Error> { + sqlx::query_as( + "INSERT INTO keys (name, enckey) + VALUES (?, ?) + ON CONFLICT(name) DO UPDATE SET enckey = enckey + RETURNING enckey", + ) + .bind(name) + .bind(rand::random::<[u8; 32]>().to_vec()) + .fetch_one(pool) + .await + .map(|(v,)| v) +} diff --git a/src/lib.rs b/src/lib.rs index b583bd1..b6c93ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod config; pub mod connection; pub mod database; pub mod host_mask; +pub mod keys; pub mod messages; pub mod persistence; pub mod server; diff --git a/src/main.rs b/src/main.rs index fc9ef05..a99ece4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,8 @@ use irc_proto::{Command, IrcCodec, Message}; use rand::seq::SliceRandom; use sqlx::migrate::Migrator; use titanircd::{ - client::Client, config::Args, connection, messages::UserConnected, persistence::Persistence, - server::Server, + client::Client, config::Args, connection, keys::Keys, messages::UserConnected, + persistence::Persistence, server::Server, }; use tokio::{ io::WriteHalf, @@ -60,6 +60,8 @@ async fn main() -> anyhow::Result<()> { MIGRATOR.run(&database).await?; + let keys = Arc::new(Keys::new(&database).await?); + let listen_address = opts.config.listen_address; let client_threads = opts.config.client_threads; @@ -94,6 +96,7 @@ async fn main() -> anyhow::Result<()> { persistence_addr, server, client_threads, + keys, )); info!("Server listening on {}", listen_address); @@ -112,6 +115,7 @@ async fn start_tcp_acceptor_loop( persistence: Addr, server: Addr, client_threads: usize, + keys: Arc, ) { let client_arbiters = Arc::new(build_arbiters(client_threads)); let resolver = Arc::new(AsyncResolver::tokio_from_system_conf().unwrap()); @@ -127,6 +131,7 @@ async fn start_tcp_acceptor_loop( let client_arbiters = client_arbiters.clone(); let persistence = persistence.clone(); let resolver = resolver.clone(); + let keys = keys.clone(); actix_rt::spawn(async move { // split the stream into its read and write halves and setup codecs @@ -136,7 +141,7 @@ async fn start_tcp_acceptor_loop( // ensure we have all the details required to actually connect the client to the server // (ie. we have a nick, user, etc) - let connection = match connection::negotiate_client_connection(&mut read, &mut write, addr, &persistence, database, &resolver).await { + let connection = match connection::negotiate_client_connection(&mut read, &mut write, addr, &persistence, database, &resolver, &keys).await { Ok(Some(v)) => v, Ok(None) => { error!("Failed to fully handshake with client, dropping connection"); diff --git a/src/server/response.rs b/src/server/response.rs index 5f1e700..2e5d3f4 100644 --- a/src/server/response.rs +++ b/src/server/response.rs @@ -92,8 +92,8 @@ impl IntoProtocol for Whois { "is connecting from {}@{} {}", conn.user, conn.resolved_host - .unwrap_or_else(|| conn.host.ip().to_string()), - conn.host.ip() + .unwrap_or_else(|| conn.host.ip().to_canonical().to_string()), + conn.host.ip().to_canonical() ) ), // RPL_WHOISHOST ];