🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2024-01-31 2:48:45.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2024-01-31 2:48:45.0 +00:00:00
commit
56d6b157e9c153dda14ed60164b60bcd030c290f [patch]
tree
d7efdcfbf47f501dd331a87aeb6c9ca0f28d35ab
parent
0f4c2633739a7a5911b75539742de3cac3f454ea
download
56d6b157e9c153dda14ed60164b60bcd030c290f.tar.gz

Implement IP cloaking



Diff

 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(-)

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<SocketAddr>,
    resolved_host: Option<String>,
    nick: Option<String>,
    user: Option<String>,
    real_name: Option<String>,
@@ -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),
@@ -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(),
@@ -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
@@ -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),
@@ -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<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)
}
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<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());
@@ -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
        ];