Do not log IP addresses -- hash IP addresses with SHA256:

We need to match repeat requests as being from the same IP address so we can keep
IP address reputations. If someone has a suspect IP address, they can hash it and
then find it in our logs, so this is not perfect protection. But if they are
trying to get IP addresses they won't find any.
This commit is contained in:
Mike Dilger 2024-03-17 14:10:38 +13:00
parent b80f14d56b
commit 585dda0cb8
7 changed files with 125 additions and 32 deletions

7
Cargo.lock generated
View File

@ -59,6 +59,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -168,6 +174,7 @@ dependencies = [
name = "chorus-lib" name = "chorus-lib"
version = "1.1.1" version = "1.1.1"
dependencies = [ dependencies = [
"base64 0.22.0",
"futures", "futures",
"heed", "heed",
"hyper", "hyper",

View File

@ -7,7 +7,7 @@ use crate::globals::GLOBALS;
use crate::tls::MaybeTlsStream; use crate::tls::MaybeTlsStream;
use chorus_lib::config::{Config, FriendlyConfig}; use chorus_lib::config::{Config, FriendlyConfig};
use chorus_lib::error::{ChorusError, Error}; use chorus_lib::error::{ChorusError, Error};
use chorus_lib::ip::SessionExit; use chorus_lib::ip::{HashedIp, HashedPeer, SessionExit};
use chorus_lib::reply::NostrReply; use chorus_lib::reply::NostrReply;
use chorus_lib::store::Store; use chorus_lib::store::Store;
use chorus_lib::types::{OwnedFilter, Pubkey}; use chorus_lib::types::{OwnedFilter, Pubkey};
@ -22,7 +22,7 @@ use std::error::Error as StdError;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::future::Future; use std::future::Future;
use std::io::Read; use std::io::Read;
use std::net::{IpAddr, SocketAddr}; use std::net::IpAddr;
use std::pin::Pin; use std::pin::Pin;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
@ -119,13 +119,17 @@ async fn main() -> Result<(), Error> {
// Accepts network connections and spawn a task to serve each one // Accepts network connections and spawn a task to serve each one
v = listener.accept() => { v = listener.accept() => {
let (tcp_stream, peer_addr) = v?; let (tcp_stream, hashed_peer) = {
let ipaddr = peer_addr.ip(); let (tcp_stream, peer_addr) = v?;
let hashed_peer = HashedPeer::new(peer_addr);
(tcp_stream, hashed_peer)
};
let ip_data = GLOBALS.store.get().unwrap().get_ip_data(ipaddr)?; let ip_data = GLOBALS.store.get().unwrap().get_ip_data(hashed_peer.ip())?;
if ip_data.is_banned() { if ip_data.is_banned() {
log::debug!(target: "Client", log::debug!(target: "Client",
"{peer_addr}: Blocking reconnection until {}", "{}: Blocking reconnection until {}",
hashed_peer.ip(),
ip_data.ban_until); ip_data.ban_until);
continue; continue;
} }
@ -136,20 +140,20 @@ async fn main() -> Result<(), Error> {
match tls_acceptor_clone.accept(tcp_stream).await { match tls_acceptor_clone.accept(tcp_stream).await {
Err(e) => log::error!( Err(e) => log::error!(
target: "Client", target: "Client",
"{}: {}", peer_addr, e "{}: {}", hashed_peer, e
), ),
Ok(tls_stream) => { Ok(tls_stream) => {
if let Err(e) = serve(MaybeTlsStream::Rustls(tls_stream), peer_addr).await { if let Err(e) = serve(MaybeTlsStream::Rustls(tls_stream), hashed_peer).await {
log::error!( log::error!(
target: "Client", target: "Client",
"{}: {}", peer_addr, e "{}: {}", hashed_peer, e
); );
} }
} }
} }
}); });
} else { } else {
serve(MaybeTlsStream::Plain(tcp_stream), peer_addr).await?; serve(MaybeTlsStream::Plain(tcp_stream), hashed_peer).await?;
} }
} }
}; };
@ -197,9 +201,9 @@ async fn main() -> Result<(), Error> {
} }
// Serve a single network connection // Serve a single network connection
async fn serve(stream: MaybeTlsStream<TcpStream>, peer_addr: SocketAddr) -> Result<(), Error> { async fn serve(stream: MaybeTlsStream<TcpStream>, peer: HashedPeer) -> Result<(), Error> {
// Serve the network stream with our http server and our HttpService // Serve the network stream with our http server and our HttpService
let service = HttpService { peer: peer_addr }; let service = HttpService { peer };
let connection = GLOBALS let connection = GLOBALS
.http_server .http_server
@ -214,12 +218,12 @@ async fn serve(stream: MaybeTlsStream<TcpStream>, peer_addr: SocketAddr) -> Resu
// do nothing // do nothing
} else { } else {
// Print in detail // Print in detail
log::error!(target: "Client", "{}: {:?}", peer_addr, src); log::error!(target: "Client", "{}: {:?}", peer, src);
} }
} else { } else {
// Print in less detail // Print in less detail
let e: Error = he.into(); let e: Error = he.into();
log::error!(target: "Client", "{}: {}", peer_addr, e); log::error!(target: "Client", "{}: {}", peer, e);
} }
} }
}); });
@ -229,7 +233,7 @@ async fn serve(stream: MaybeTlsStream<TcpStream>, peer_addr: SocketAddr) -> Resu
// This is our per-connection HTTP service // This is our per-connection HTTP service
struct HttpService { struct HttpService {
peer: SocketAddr, peer: HashedPeer,
} }
impl Service<Request<Body>> for HttpService { impl Service<Request<Body>> for HttpService {
@ -253,7 +257,8 @@ impl Service<Request<Body>> for HttpService {
if let Some(rip) = req.headers().get("x-real-ip") { if let Some(rip) = req.headers().get("x-real-ip") {
if let Ok(ripstr) = rip.to_str() { if let Ok(ripstr) = rip.to_str() {
if let Ok(ipaddr) = ripstr.parse::<IpAddr>() { if let Ok(ipaddr) = ripstr.parse::<IpAddr>() {
peer.set_ip(ipaddr); let hashed_ip = HashedIp::new(ipaddr);
peer = HashedPeer::from_parts(hashed_ip, peer.port());
} }
} }
} }
@ -264,7 +269,7 @@ impl Service<Request<Body>> for HttpService {
} }
async fn handle_http_request( async fn handle_http_request(
peer: SocketAddr, peer: HashedPeer,
mut request: Request<Body>, mut request: Request<Body>,
) -> Result<Response<Body>, Error> { ) -> Result<Response<Body>, Error> {
let ua = match request.headers().get("user-agent") { let ua = match request.headers().get("user-agent") {
@ -393,7 +398,7 @@ async fn handle_http_request(
} }
struct WebSocketService { struct WebSocketService {
pub peer: SocketAddr, pub peer: HashedPeer,
pub subscriptions: HashMap<String, Vec<OwnedFilter>>, pub subscriptions: HashMap<String, Vec<OwnedFilter>>,
pub buffer: Vec<u8>, pub buffer: Vec<u8>,
pub websocket: WebSocketStream<Upgraded>, pub websocket: WebSocketStream<Upgraded>,

View File

@ -1,10 +1,10 @@
use crate::globals::GLOBALS; use crate::globals::GLOBALS;
use chorus_lib::config::Config; use chorus_lib::config::Config;
use chorus_lib::error::Error; use chorus_lib::error::Error;
use chorus_lib::ip::HashedPeer;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use std::net::SocketAddr;
pub async fn serve_http(peer: SocketAddr, request: Request<Body>) -> Result<Response<Body>, Error> { pub async fn serve_http(peer: HashedPeer, request: Request<Body>) -> Result<Response<Body>, Error> {
log::debug!(target: "Client", "{}: HTTP request for {}", peer, request.uri()); log::debug!(target: "Client", "{}: HTTP request for {}", peer, request.uri());
let response = Response::builder() let response = Response::builder()
.header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Origin", "*")
@ -15,7 +15,7 @@ pub async fn serve_http(peer: SocketAddr, request: Request<Body>) -> Result<Resp
Ok(response) Ok(response)
} }
pub async fn serve_nip11(peer: SocketAddr) -> Result<Response<Body>, Error> { pub async fn serve_nip11(peer: HashedPeer) -> Result<Response<Body>, Error> {
log::debug!(target: "Client", "{}: sent NIP-11", peer); log::debug!(target: "Client", "{}: sent NIP-11", peer);
let rid = { let rid = {
let config = &*GLOBALS.config.read(); let config = &*GLOBALS.config.read();

View File

@ -8,6 +8,7 @@ repository = "https://github.com/mikedilger/chorus"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
base64 = "0.22"
futures = "0.3" futures = "0.3"
heed = { git = "https://github.com/meilisearch/heed", rev = "64fd6fec293c0dee94855b8267557ce03e7ce5d8" } heed = { git = "https://github.com/meilisearch/heed", rev = "64fd6fec293c0dee94855b8267557ce03e7ce5d8" }
hyper = { version = "0.14", features = [ "http1", "server", "runtime", "stream" ] } hyper = { version = "0.14", features = [ "http1", "server", "runtime", "stream" ] }

View File

@ -1,5 +1,61 @@
use crate::types::Time; use crate::types::Time;
use speedy::{Readable, Writable}; use speedy::{Readable, Writable};
use std::net::{IpAddr, SocketAddr};
#[derive(Debug, Clone, Copy)]
pub struct HashedIp(pub [u8; 20], bool);
impl std::fmt::Display for HashedIp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
unsafe { write!(f, "{}", std::str::from_utf8_unchecked(self.0.as_slice())) }
}
}
impl HashedIp {
pub fn new(ip_addr: IpAddr) -> HashedIp {
use base64::prelude::*;
use secp256k1::hashes::{sha256, Hash};
let bytes = ip_addr.write_to_vec().unwrap();
let hashvalue: sha256::Hash = Hash::hash(&bytes);
let tag = BASE64_STANDARD.encode(&hashvalue.as_byte_array()[0..16]);
HashedIp(
tag.as_bytes()[..20].try_into().unwrap(),
ip_addr.is_loopback(),
)
}
pub fn is_loopback(&self) -> bool {
self.1
}
}
#[derive(Debug, Clone, Copy)]
pub struct HashedPeer(pub HashedIp, pub u16);
impl std::fmt::Display for HashedPeer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.0, self.1)
}
}
impl HashedPeer {
pub fn new(peer_addr: SocketAddr) -> HashedPeer {
let hashed_ip = HashedIp::new(peer_addr.ip());
HashedPeer(hashed_ip, peer_addr.port())
}
pub fn from_parts(hashed_ip: HashedIp, port: u16) -> HashedPeer {
HashedPeer(hashed_ip, port)
}
pub fn ip(&self) -> HashedIp {
self.0
}
pub fn port(&self) -> u16 {
self.1
}
}
// Single-session exit condition // Single-session exit condition
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@ -92,3 +148,17 @@ impl IpData {
} }
} }
} }
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_hashed_ip() {
let ipaddr: std::net::IpAddr = "127.0.0.1".parse().unwrap();
println!("HashedIP={}", HashedIp::new(ipaddr).unwrap());
let socketaddr = std::net::SocketAddr::new(ipaddr, 80);
println!("HashedPEER={}", HashedPeer::new(socketaddr).unwrap());
}
}

View File

@ -2,7 +2,7 @@ use super::Store;
use crate::error::Error; use crate::error::Error;
use heed::RwTxn; use heed::RwTxn;
pub const CURRENT_MIGRATION_LEVEL: u32 = 2; pub const CURRENT_MIGRATION_LEVEL: u32 = 3;
impl Store { impl Store {
pub fn migrate(&self) -> Result<(), Error> { pub fn migrate(&self) -> Result<(), Error> {
@ -39,6 +39,7 @@ impl Store {
match level { match level {
1 => self.migrate_to_1(txn)?, 1 => self.migrate_to_1(txn)?,
2 => self.migrate_to_2(txn)?, 2 => self.migrate_to_2(txn)?,
3 => self.migrate_to_3(txn)?,
_ => panic!("Unknown migration level {level}"), _ => panic!("Unknown migration level {level}"),
} }
@ -102,4 +103,10 @@ impl Store {
Ok(()) Ok(())
} }
// Clear IP data (we are hashing now)
fn migrate_to_3(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
self.ip_data.clear(txn)?;
Ok(())
}
} }

View File

@ -5,7 +5,7 @@ mod migrations;
use crate::config::Config; use crate::config::Config;
use crate::error::{ChorusError, Error}; use crate::error::{ChorusError, Error};
use crate::ip::IpData; use crate::ip::{HashedIp, IpData};
use crate::types::{Event, Filter, Id, Kind, Pubkey, Time}; use crate::types::{Event, Filter, Id, Kind, Pubkey, Time};
use heed::byteorder::BigEndian; use heed::byteorder::BigEndian;
use heed::types::{OwnedType, UnalignedSlice, Unit, U64}; use heed::types::{OwnedType, UnalignedSlice, Unit, U64};
@ -13,7 +13,6 @@ use heed::{Database, Env, EnvFlags, EnvOpenOptions, RwTxn};
use speedy::{Readable, Writable}; use speedy::{Readable, Writable};
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fs; use std::fs;
use std::net::IpAddr;
use std::ops::Bound; use std::ops::Bound;
#[derive(Debug)] #[derive(Debug)]
@ -297,8 +296,12 @@ impl Store {
} }
/// Find all events that match the filter /// Find all events that match the filter
pub fn find_events<F>(&self, filter: Filter, screen: F, config: &Config) pub fn find_events<F>(
-> Result<Vec<Event>, Error> &self,
filter: Filter,
screen: F,
config: &Config,
) -> Result<Vec<Event>, Error>
where where
F: Fn(&Event) -> bool, F: Fn(&Event) -> bool,
{ {
@ -909,21 +912,21 @@ impl Store {
Ok(()) Ok(())
} }
pub fn get_ip_data(&self, ip: IpAddr) -> Result<IpData, Error> { pub fn get_ip_data(&self, ip: HashedIp) -> Result<IpData, Error> {
let key = ip.write_to_vec()?; let key = &ip.0;
let txn = self.env.read_txn()?; let txn = self.env.read_txn()?;
let bytes = match self.ip_data.get(&txn, &key)? { let bytes = match self.ip_data.get(&txn, key)? {
Some(b) => b, Some(b) => b,
None => return Ok(Default::default()), None => return Ok(Default::default()),
}; };
Ok(IpData::read_from_buffer(bytes)?) Ok(IpData::read_from_buffer(bytes)?)
} }
pub fn update_ip_data(&self, ip: IpAddr, data: &IpData) -> Result<(), Error> { pub fn update_ip_data(&self, ip: HashedIp, data: &IpData) -> Result<(), Error> {
let key = ip.write_to_vec()?; let key = &ip.0;
let mut txn = self.env.write_txn()?; let mut txn = self.env.write_txn()?;
let bytes = data.write_to_vec()?; let bytes = data.write_to_vec()?;
self.ip_data.put(&mut txn, &key, &bytes)?; self.ip_data.put(&mut txn, key, &bytes)?;
txn.commit()?; txn.commit()?;
Ok(()) Ok(())
} }