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"
# 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.

View File

@ -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.

View File

@ -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"

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -85,23 +85,49 @@ impl Service<Request<Incoming>> 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<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
// 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::<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 })
}
}