Fix proxy setups (ip blocking, real IP); needs new chorus_is_behind_a_proxy config

This commit is contained in:
Mike Dilger 2024-07-13 11:15:19 +12:00
parent 37b1b3a0f3
commit a76be24d68
7 changed files with 83 additions and 10 deletions

View File

@ -34,6 +34,15 @@ port = 443
hostname = "localhost" 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 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. # If you are proxying via nginx, normally you will set this to false and allow nginx to handle TLS.

View File

@ -36,6 +36,14 @@ your relay host name.
Default is localhost 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 ### use_tls
If true, chorus will handle TLS, running over HTTPS. If false, chorus run over HTTP. 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 ### 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. If `use_tls` is false, this value is irrelevant.

View File

@ -4,6 +4,7 @@ data_directory = "./sample"
ip_address = "127.0.0.1" ip_address = "127.0.0.1"
port = 8080 port = 8080
hostname = "localhost" hostname = "localhost"
chorus_is_behind_a_proxy = false
use_tls = false use_tls = false
certchain_pem_path = "tls/fullchain.pem" certchain_pem_path = "tls/fullchain.pem"
key_pem_path = "tls/privkey.pem" key_pem_path = "tls/privkey.pem"

View File

@ -93,8 +93,10 @@ async fn main() -> Result<(), Error> {
(tcp_stream, hashed_peer) (tcp_stream, hashed_peer)
}; };
// Possibly IP block // Possibly IP block early
if GLOBALS.config.read().enable_ip_blocking { 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())?; let ip_data = chorus::get_ip_data(GLOBALS.store.get().unwrap(), hashed_peer.ip())?;
if ip_data.is_banned() { if ip_data.is_banned() {
log::debug!(target: "Client", log::debug!(target: "Client",

View File

@ -12,6 +12,7 @@ pub struct FriendlyConfig {
pub ip_address: String, pub ip_address: String,
pub port: u16, pub port: u16,
pub hostname: String, pub hostname: String,
pub chorus_is_behind_a_proxy: bool,
pub use_tls: bool, pub use_tls: bool,
pub certchain_pem_path: String, pub certchain_pem_path: String,
pub key_pem_path: String, pub key_pem_path: String,
@ -45,6 +46,7 @@ impl Default for FriendlyConfig {
ip_address: "127.0.0.1".to_string(), ip_address: "127.0.0.1".to_string(),
port: 443, port: 443,
hostname: "localhost".to_string(), hostname: "localhost".to_string(),
chorus_is_behind_a_proxy: false,
use_tls: true, use_tls: true,
certchain_pem_path: "/opt/chorus/etc/tls/fullchain.pem".to_string(), certchain_pem_path: "/opt/chorus/etc/tls/fullchain.pem".to_string(),
key_pem_path: "/opt/chorus/etc/tls/privkey.pem".to_string(), key_pem_path: "/opt/chorus/etc/tls/privkey.pem".to_string(),
@ -80,6 +82,7 @@ impl FriendlyConfig {
ip_address, ip_address,
port, port,
hostname, hostname,
chorus_is_behind_a_proxy,
use_tls, use_tls,
certchain_pem_path, certchain_pem_path,
key_pem_path, key_pem_path,
@ -135,6 +138,7 @@ impl FriendlyConfig {
ip_address, ip_address,
port, port,
hostname, hostname,
chorus_is_behind_a_proxy,
use_tls, use_tls,
certchain_pem_path, certchain_pem_path,
key_pem_path, key_pem_path,
@ -171,6 +175,7 @@ pub struct Config {
pub ip_address: String, pub ip_address: String,
pub port: u16, pub port: u16,
pub hostname: Host, pub hostname: Host,
pub chorus_is_behind_a_proxy: bool,
pub use_tls: bool, pub use_tls: bool,
pub certchain_pem_path: String, pub certchain_pem_path: String,
pub key_pem_path: String, pub key_pem_path: String,

View File

@ -31,6 +31,12 @@ pub enum ChorusError {
// Bad request // Bad request
BadRequest(&'static str), BadRequest(&'static str),
// Bad X-Real-Ip header
BadRealIpHeader(String),
// Bad X-Real-Ip header characters
BadRealIpHeaderCharacters,
// Event is banned // Event is banned
BannedEvent, BannedEvent,
@ -40,6 +46,9 @@ pub enum ChorusError {
// Base64 Decode Error // Base64 Decode Error
Base64Decode(base64::DecodeError), Base64Decode(base64::DecodeError),
// Blocked IP
BlockedIp,
// Channel Recv // Channel Recv
ChannelRecv(tokio::sync::broadcast::error::RecvError), ChannelRecv(tokio::sync::broadcast::error::RecvError),
@ -103,6 +112,9 @@ pub enum ChorusError {
// Pocket Types Error // Pocket Types Error
PocketType(pocket_types::Error), PocketType(pocket_types::Error),
// X-Real-Ip header is missing
RealIpHeaderMissing,
// Restricted // Restricted
Restricted, Restricted,
@ -146,9 +158,14 @@ impl std::fmt::Display for ChorusError {
ChorusError::AuthFailure(s) => write!(f, "AUTH failure: {s}"), ChorusError::AuthFailure(s) => write!(f, "AUTH failure: {s}"),
ChorusError::AuthRequired => write!(f, "AUTH required"), ChorusError::AuthRequired => write!(f, "AUTH required"),
ChorusError::BadRequest(s) => write!(f, "Bad Request: {s}"), 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::BannedEvent => write!(f, "Event is banned"),
ChorusError::BannedUser => write!(f, "User is banned"), ChorusError::BannedUser => write!(f, "User is banned"),
ChorusError::Base64Decode(e) => write!(f, "{e}"), ChorusError::Base64Decode(e) => write!(f, "{e}"),
ChorusError::BlockedIp => write!(f, "IP is temporarily blocked"),
ChorusError::ChannelRecv(e) => write!(f, "{e}"), ChorusError::ChannelRecv(e) => write!(f, "{e}"),
ChorusError::ChannelSend(e) => write!(f, "{e}"), ChorusError::ChannelSend(e) => write!(f, "{e}"),
ChorusError::Config(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::PocketDbHeed(e) => write!(f, "{e}"),
ChorusError::PocketType(e) => write!(f, "{e}"), ChorusError::PocketType(e) => write!(f, "{e}"),
ChorusError::ProtectedEvent => write!(f, "Protected event"), ChorusError::ProtectedEvent => write!(f, "Protected event"),
ChorusError::RealIpHeaderMissing => write!(f, "X-Real-Ip header is missing"),
ChorusError::Restricted => write!(f, "Restricted"), ChorusError::Restricted => write!(f, "Restricted"),
ChorusError::Rustls(e) => write!(f, "{e}"), ChorusError::Rustls(e) => write!(f, "{e}"),
ChorusError::TimedOut => write!(f, "Timed out"), ChorusError::TimedOut => write!(f, "Timed out"),
@ -226,9 +244,12 @@ impl ChorusError {
ChorusError::AuthFailure(_) => 0.25, ChorusError::AuthFailure(_) => 0.25,
ChorusError::AuthRequired => 0.0, ChorusError::AuthRequired => 0.0,
ChorusError::BadRequest(_) => 0.1, ChorusError::BadRequest(_) => 0.1,
ChorusError::BadRealIpHeader(_) => 0.0,
ChorusError::BadRealIpHeaderCharacters => 0.0,
ChorusError::BannedEvent => 0.1, ChorusError::BannedEvent => 0.1,
ChorusError::BannedUser => 0.2, ChorusError::BannedUser => 0.2,
ChorusError::Base64Decode(_) => 0.0, ChorusError::Base64Decode(_) => 0.0,
ChorusError::BlockedIp => 0.0,
ChorusError::ChannelRecv(_) => 0.0, ChorusError::ChannelRecv(_) => 0.0,
ChorusError::ChannelSend(_) => 0.0, ChorusError::ChannelSend(_) => 0.0,
ChorusError::Config(_) => 0.0, ChorusError::Config(_) => 0.0,
@ -250,6 +271,7 @@ impl ChorusError {
ChorusError::PocketDbHeed(_) => 0.0, ChorusError::PocketDbHeed(_) => 0.0,
ChorusError::PocketType(_) => 0.0, ChorusError::PocketType(_) => 0.0,
ChorusError::ProtectedEvent => 0.35, ChorusError::ProtectedEvent => 0.35,
ChorusError::RealIpHeaderMissing => 0.0,
ChorusError::Restricted => 0.1, ChorusError::Restricted => 0.1,
ChorusError::Rustls(_) => 0.0, ChorusError::Rustls(_) => 0.0,
ChorusError::TimedOut => 0.1, ChorusError::TimedOut => 0.1,

View File

@ -85,23 +85,49 @@ impl Service<Request<Incoming>> for ChorusService {
// This is called for each HTTP request made by the client // This is called for each HTTP request made by the client
// NOTE: it is not called for each websocket message once upgraded. // NOTE: it is not called for each websocket message once upgraded.
fn call(&self, req: Request<Incoming>) -> Self::Future { fn call(&self, req: Request<Incoming>) -> 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 let failvalue =
// that ip address instead (otherwise their log file will just say "127.0.0.1" |c: ChorusError| -> Self::Future { Box::pin(futures::future::ready(Err(c.into()))) };
// for every peer)
if peer.ip().is_loopback() { 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 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>() {
let hashed_ip = HashedIp::new(ipaddr); 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 })
} }
} }