From a76be24d682fa652c8a594d0e4db8363b18df4a2 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 13 Jul 2024 11:15:19 +1200 Subject: [PATCH] Fix proxy setups (ip blocking, real IP); needs new chorus_is_behind_a_proxy config --- contrib/chorus.toml | 9 +++++++++ docs/CONFIG.md | 10 +++++++++- sample/sample.config.toml | 1 + src/bin/chorus.rs | 6 ++++-- src/config.rs | 5 +++++ src/error.rs | 22 +++++++++++++++++++++ src/lib.rs | 40 ++++++++++++++++++++++++++++++++------- 7 files changed, 83 insertions(+), 10 deletions(-) diff --git a/contrib/chorus.toml b/contrib/chorus.toml index d52cd1d..5bf2721 100644 --- a/contrib/chorus.toml +++ b/contrib/chorus.toml @@ -34,6 +34,15 @@ port = 443 hostname = "localhost" +# If chorus is behind a proxy like nginx, set this to true. In this case chorus will look for and +# trust the `X-Real-Ip` HTTP request header to get the real IP of the client. This header MUST exist +# or the connection will not be served. +# +# Default is false. +# +chorus_is_behind_a_proxy = false + + # If true, chorus will handle TLS, running over HTTPS. If false, chorus run over HTTP. # # If you are proxying via nginx, normally you will set this to false and allow nginx to handle TLS. diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 098a405..e3ea2a4 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -36,6 +36,14 @@ your relay host name. Default is localhost +### chorus_is_behind_a_proxy + +If chorus is behind a proxy like nginx, set this to true. In this case chorus will look for and +trust the `X-Real-Ip` HTTP request header to get the real IP of the client. This header MUST exist +or the connection will not be served. + +Default is false. + ### use_tls If true, chorus will handle TLS, running over HTTPS. If false, chorus run over HTTP. @@ -58,7 +66,7 @@ systemd service copies letsencrypt TLS certificates into this position on start. ### key_pem_path -This is the path to yoru TLS private key file. +This is the path to your TLS private key file. If `use_tls` is false, this value is irrelevant. diff --git a/sample/sample.config.toml b/sample/sample.config.toml index d2e4ffa..1f339bd 100644 --- a/sample/sample.config.toml +++ b/sample/sample.config.toml @@ -4,6 +4,7 @@ data_directory = "./sample" ip_address = "127.0.0.1" port = 8080 hostname = "localhost" +chorus_is_behind_a_proxy = false use_tls = false certchain_pem_path = "tls/fullchain.pem" key_pem_path = "tls/privkey.pem" diff --git a/src/bin/chorus.rs b/src/bin/chorus.rs index 2f4edd2..0df19a5 100644 --- a/src/bin/chorus.rs +++ b/src/bin/chorus.rs @@ -93,8 +93,10 @@ async fn main() -> Result<(), Error> { (tcp_stream, hashed_peer) }; - // Possibly IP block - if GLOBALS.config.read().enable_ip_blocking { + // Possibly IP block early + if ! GLOBALS.config.read().chorus_is_behind_a_proxy + && GLOBALS.config.read().enable_ip_blocking + { let ip_data = chorus::get_ip_data(GLOBALS.store.get().unwrap(), hashed_peer.ip())?; if ip_data.is_banned() { log::debug!(target: "Client", diff --git a/src/config.rs b/src/config.rs index 1ad63fb..11206b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ pub struct FriendlyConfig { pub ip_address: String, pub port: u16, pub hostname: String, + pub chorus_is_behind_a_proxy: bool, pub use_tls: bool, pub certchain_pem_path: String, pub key_pem_path: String, @@ -45,6 +46,7 @@ impl Default for FriendlyConfig { ip_address: "127.0.0.1".to_string(), port: 443, hostname: "localhost".to_string(), + chorus_is_behind_a_proxy: false, use_tls: true, certchain_pem_path: "/opt/chorus/etc/tls/fullchain.pem".to_string(), key_pem_path: "/opt/chorus/etc/tls/privkey.pem".to_string(), @@ -80,6 +82,7 @@ impl FriendlyConfig { ip_address, port, hostname, + chorus_is_behind_a_proxy, use_tls, certchain_pem_path, key_pem_path, @@ -135,6 +138,7 @@ impl FriendlyConfig { ip_address, port, hostname, + chorus_is_behind_a_proxy, use_tls, certchain_pem_path, key_pem_path, @@ -171,6 +175,7 @@ pub struct Config { pub ip_address: String, pub port: u16, pub hostname: Host, + pub chorus_is_behind_a_proxy: bool, pub use_tls: bool, pub certchain_pem_path: String, pub key_pem_path: String, diff --git a/src/error.rs b/src/error.rs index 26a5cef..122059a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,6 +31,12 @@ pub enum ChorusError { // Bad request BadRequest(&'static str), + // Bad X-Real-Ip header + BadRealIpHeader(String), + + // Bad X-Real-Ip header characters + BadRealIpHeaderCharacters, + // Event is banned BannedEvent, @@ -40,6 +46,9 @@ pub enum ChorusError { // Base64 Decode Error Base64Decode(base64::DecodeError), + // Blocked IP + BlockedIp, + // Channel Recv ChannelRecv(tokio::sync::broadcast::error::RecvError), @@ -103,6 +112,9 @@ pub enum ChorusError { // Pocket Types Error PocketType(pocket_types::Error), + // X-Real-Ip header is missing + RealIpHeaderMissing, + // Restricted Restricted, @@ -146,9 +158,14 @@ impl std::fmt::Display for ChorusError { ChorusError::AuthFailure(s) => write!(f, "AUTH failure: {s}"), ChorusError::AuthRequired => write!(f, "AUTH required"), ChorusError::BadRequest(s) => write!(f, "Bad Request: {s}"), + ChorusError::BadRealIpHeader(s) => write!(f, "Bad X-Real-Ip header: {s}"), + ChorusError::BadRealIpHeaderCharacters => { + write!(f, "Bad X-Real-Ip header (non utf-8 characters)") + } ChorusError::BannedEvent => write!(f, "Event is banned"), ChorusError::BannedUser => write!(f, "User is banned"), ChorusError::Base64Decode(e) => write!(f, "{e}"), + ChorusError::BlockedIp => write!(f, "IP is temporarily blocked"), ChorusError::ChannelRecv(e) => write!(f, "{e}"), ChorusError::ChannelSend(e) => write!(f, "{e}"), ChorusError::Config(e) => write!(f, "{e}"), @@ -170,6 +187,7 @@ impl std::fmt::Display for ChorusError { ChorusError::PocketDbHeed(e) => write!(f, "{e}"), ChorusError::PocketType(e) => write!(f, "{e}"), ChorusError::ProtectedEvent => write!(f, "Protected event"), + ChorusError::RealIpHeaderMissing => write!(f, "X-Real-Ip header is missing"), ChorusError::Restricted => write!(f, "Restricted"), ChorusError::Rustls(e) => write!(f, "{e}"), ChorusError::TimedOut => write!(f, "Timed out"), @@ -226,9 +244,12 @@ impl ChorusError { ChorusError::AuthFailure(_) => 0.25, ChorusError::AuthRequired => 0.0, ChorusError::BadRequest(_) => 0.1, + ChorusError::BadRealIpHeader(_) => 0.0, + ChorusError::BadRealIpHeaderCharacters => 0.0, ChorusError::BannedEvent => 0.1, ChorusError::BannedUser => 0.2, ChorusError::Base64Decode(_) => 0.0, + ChorusError::BlockedIp => 0.0, ChorusError::ChannelRecv(_) => 0.0, ChorusError::ChannelSend(_) => 0.0, ChorusError::Config(_) => 0.0, @@ -250,6 +271,7 @@ impl ChorusError { ChorusError::PocketDbHeed(_) => 0.0, ChorusError::PocketType(_) => 0.0, ChorusError::ProtectedEvent => 0.35, + ChorusError::RealIpHeaderMissing => 0.0, ChorusError::Restricted => 0.1, ChorusError::Rustls(_) => 0.0, ChorusError::TimedOut => 0.1, diff --git a/src/lib.rs b/src/lib.rs index 3d0fdbf..781b461 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,23 +85,49 @@ impl Service> for ChorusService { // This is called for each HTTP request made by the client // NOTE: it is not called for each websocket message once upgraded. fn call(&self, req: Request) -> Self::Future { - let mut peer = self.peer; + let mut hashed_peer = self.peer; - // If chorus is behind a proxy that sets an "X-Real-Ip" header, we use - // that ip address instead (otherwise their log file will just say "127.0.0.1" - // for every peer) - if peer.ip().is_loopback() { + let failvalue = + |c: ChorusError| -> Self::Future { Box::pin(futures::future::ready(Err(c.into()))) }; + + if GLOBALS.config.read().chorus_is_behind_a_proxy { + // If chorus is behind a proxy that sets an "X-Real-Ip" header, we use + // that ip address instead (otherwise their log file will just give the proxy IP + // for every peer) + // + // This header must be found and be valid for us to proceed if let Some(rip) = req.headers().get("x-real-ip") { if let Ok(ripstr) = rip.to_str() { if let Ok(ipaddr) = ripstr.parse::() { let hashed_ip = HashedIp::new(ipaddr); - peer = HashedPeer::from_parts(hashed_ip, peer.port()); + hashed_peer = HashedPeer::from_parts(hashed_ip, hashed_peer.port()); + } else { + return failvalue(ChorusError::BadRealIpHeader(ripstr.to_owned())); + } + } else { + return failvalue(ChorusError::BadRealIpHeaderCharacters); + } + } else { + return failvalue(ChorusError::RealIpHeaderMissing); + } + + // Possibly IP block late (if behind a proxy) + if GLOBALS.config.read().enable_ip_blocking { + if let Ok(ip_data) = + crate::get_ip_data(GLOBALS.store.get().unwrap(), hashed_peer.ip()) + { + if ip_data.is_banned() { + log::debug!(target: "Client", + "{}: Blocking reconnection until {}", + hashed_peer.ip(), + ip_data.ban_until); + return failvalue(ChorusError::BlockedIp); } } } } - Box::pin(async move { handle_http_request(peer, req).await }) + Box::pin(async move { handle_http_request(hashed_peer, req).await }) } }