Skip to content

Commit

Permalink
Implement IP cloaking
Browse files Browse the repository at this point in the history
  • Loading branch information
w4 committed Jan 31, 2024
1 parent 0f4c263 commit 56d6b15
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 42 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
5 changes: 5 additions & 0 deletions migrations/2023010814480_initial-schema.sql
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
78 changes: 41 additions & 37 deletions src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,6 +34,7 @@ use crate::{
sasl::{AuthStrategy, ConnectionSuccess, SaslSuccess},
},
host_mask::HostMask,
keys::Keys,
persistence::{events::ReserveNick, Persistence},
};

Expand All @@ -46,7 +48,6 @@ pub struct UserId(pub i64);
#[derive(Default)]
pub struct ConnectionRequest {
host: Option<SocketAddr>,
resolved_host: Option<String>,
nick: Option<String>,
user: Option<String>,
real_name: Option<String>,
Expand All @@ -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<ConnectionRequest> for InitiatedConnection {
type Error = ConnectionRequest;

fn try_from(value: ConnectionRequest) -> Result<Self, Self::Error> {
pub fn new(value: ConnectionRequest, keys: &Keys) -> Result<Self, ConnectionRequest> {
let ConnectionRequest {
host: Some(host),
resolved_host,
nick: Some(nick),
user: Some(user),
real_name: Some(real_name),
Expand All @@ -102,10 +84,17 @@ impl TryFrom<ConnectionRequest> 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(),
Expand All @@ -116,6 +105,20 @@ impl TryFrom<ConnectionRequest> 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
Expand All @@ -128,6 +131,7 @@ pub async fn negotiate_client_connection(
persistence: &Addr<Persistence>,
database: sqlx::Pool<sqlx::Any>,
resolver: &TokioAsyncResolver,
keys: &Keys,
) -> Result<Option<InitiatedConnection>, ProtocolError> {
let mut request = ConnectionRequest {
host: Some(host),
Expand Down Expand Up @@ -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...
Expand All @@ -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?;
Expand Down
28 changes: 28 additions & 0 deletions src/keys.rs
Original file line number Diff line number Diff line change
@@ -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<Any>) -> Result<Self, sqlx::Error> {
Ok(Self {
ip_salt: fetch_or_create(pool, "ip_salt").await?.try_into().unwrap(),
})
}
}

async fn fetch_or_create(pool: &Pool<Any>, name: &str) -> Result<Vec<u8>, 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)
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 8 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -94,6 +96,7 @@ async fn main() -> anyhow::Result<()> {
persistence_addr,
server,
client_threads,
keys,
));

info!("Server listening on {}", listen_address);
Expand All @@ -112,6 +115,7 @@ async fn start_tcp_acceptor_loop(
persistence: Addr<Persistence>,
server: Addr<Server>,
client_threads: usize,
keys: Arc<Keys>,
) {
let client_arbiters = Arc::new(build_arbiters(client_threads));
let resolver = Arc::new(AsyncResolver::tokio_from_system_conf().unwrap());
Expand All @@ -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
Expand All @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions src/server/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
];
Expand Down

0 comments on commit 56d6b15

Please sign in to comment.