Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed reverse proxy IP forwading #50

Merged
merged 6 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pocket-relay"
version = "0.5.7"
version = "0.5.8"
description = "Pocket Relay Server"
readme = "README.md"
keywords = ["EA", "PocketRelay", "MassEffect"]
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ RUN apk add curl
WORKDIR /app

# Download server executable
RUN curl -LJ -o pocket-relay-linux https://github.com/PocketRelay/Server/releases/download/v0.5.7/pocket-relay-linux
RUN curl -LJ -o pocket-relay-linux https://github.com/PocketRelay/Server/releases/download/v0.5.8/pocket-relay-linux

# Make the server executable
RUN chmod +x ./pocket-relay-linux
Expand Down
18 changes: 0 additions & 18 deletions default.json

This file was deleted.

8 changes: 7 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ version: "3"
services:
pocket-relay:
container_name: pocket-relay
restart: unless-stopped
ports:
# Server port
- 80:80/tcp
image: jacobtread/pocket-relay:latest
image: jacobtread/pocket-relay:latest
volumes:
# Bind the server config to a local config.json file
- ./config.json:/app/config.json
# Binding the server data to a local data folder
- ./data:/app/data
4 changes: 4 additions & 0 deletions examples/nginx/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"port": 80,
"reverse_proxy": true
}
20 changes: 20 additions & 0 deletions examples/nginx/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: "3"
services:
server:
restart: unless-stopped
container_name: pocket-relay
image: jacobtread/pocket-relay:latest
volumes:
# Bind the server config to a local config.json file
- ./config.json:/app/config.json
# Binding the server data to a local data folder
- ./data:/app/data
nginx:
restart: unless-stopped
image: nginx
ports:
- "80:80/tcp"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- server
21 changes: 21 additions & 0 deletions examples/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
events {}

http {
server {
listen 80;

server_name localhost;

location / {
proxy_pass http://server:80;

# Provide server with real IP address of clients
proxy_set_header X-Real-IP $remote_addr;

# Upgrade websocket connections
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
}
}
}
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::Deserialize;
use std::{env, fs::read_to_string, path::Path};

pub struct RuntimeConfig {
pub reverse_proxy: bool,
pub galaxy_at_war: GalaxyAtWarConfig,
pub menu_message: String,
pub dashboard: DashboardConfig,
Expand Down Expand Up @@ -58,6 +59,7 @@ pub struct ServicesConfig {
#[serde(default)]
pub struct Config {
pub port: Port,
pub reverse_proxy: bool,
pub dashboard: DashboardConfig,
pub menu_message: String,
pub galaxy_at_war: GalaxyAtWarConfig,
Expand All @@ -69,6 +71,7 @@ impl Default for Config {
fn default() -> Self {
Self {
port: 80,
reverse_proxy: false,
dashboard: Default::default(),
menu_message: "<font color='#B2B2B2'>Pocket Relay</font> - <font color='#FFFF66'>Logged as: {n}</font>".to_string(),
galaxy_at_war: Default::default(),
Expand Down
75 changes: 75 additions & 0 deletions src/middleware/ip_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use std::net::SocketAddr;

use axum::{
async_trait,
body::boxed,
extract::{rejection::ExtensionRejection, ConnectInfo, FromRequestParts},
http::request::Parts,
response::{IntoResponse, Response},
Extension,
};
use hyper::{HeaderMap, StatusCode};
use log::warn;
use thiserror::Error;

use crate::state::App;

/// Middleware for extracting the server public address
pub struct IpAddress(pub SocketAddr);

const REAL_IP_HEADER: &str = "X-Real-IP";

#[async_trait]
impl<S> FromRequestParts<S> for IpAddress
where
S: Send + Sync,
{
type Rejection = IpAddressError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let reverse_proxy = App::config().reverse_proxy;
if reverse_proxy {
let ip = match extract_ip_header(&parts.headers) {
Some(ip) => ip,
None => {
warn!("Failed to extract X-Real-IP header from connecting client. If you are NOT using a reverse proxy\n\
disable the `reverse_proxy` config property, otherwise check that your reverse proxy is configured\n\
correctly according the guide. (Closing connection with error)");
return Err(IpAddressError::InvalidOrMissing);
}
};
return Ok(Self(ip));
}
let value = Extension::<ConnectInfo<SocketAddr>>::from_request_parts(parts, state).await?;
Ok(Self(value.0 .0))
}
}

fn extract_ip_header(headers: &HeaderMap) -> Option<SocketAddr> {
let header = headers.get(REAL_IP_HEADER)?;
let value = header.to_str().ok()?;
value.parse().ok()
}

/// Error type used by the token checking middleware to handle
/// different errors and create error respones based on them
#[derive(Debug, Error)]
pub enum IpAddressError {
#[error(transparent)]
ConnectInfo(#[from] ExtensionRejection),
#[error("X-Real-IP header is invalid or missing")]
InvalidOrMissing,
}

/// IntoResponse implementation for TokenError to allow it to be
/// used within the result type as a error response
impl IntoResponse for IpAddressError {
#[inline]
fn into_response(self) -> Response {
let status: StatusCode = match self {
IpAddressError::ConnectInfo(err) => return err.into_response(),
_ => StatusCode::BAD_REQUEST,
};
(status, boxed(self.to_string())).into_response()
}
}
2 changes: 2 additions & 0 deletions src/middleware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ pub mod auth;
pub mod blaze_upgrade;
/// Middleware functions related to CORS implementation
pub mod cors;
/// IP address extraction middleware
pub mod ip_address;
/// XML response types
pub mod xml;
13 changes: 3 additions & 10 deletions src/routes/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@

use crate::{
database::entities::players::PlayerRole,
middleware::{auth::AdminAuth, blaze_upgrade::BlazeUpgrade},
middleware::{auth::AdminAuth, blaze_upgrade::BlazeUpgrade, ip_address::IpAddress},
session::Session,
state::{self, App},
utils::logging::LOG_FILE_NAME,
};
use axum::{
body::Empty,
extract::ConnectInfo,
http::{header, HeaderValue, StatusCode},
response::{IntoResponse, Response},
Json,
Expand All @@ -19,10 +18,7 @@ use blaze_pk::packet::PacketCodec;
use interlink::service::Service;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use std::{
net::SocketAddr,
sync::atomic::{AtomicU32, Ordering},
};
use std::sync::atomic::{AtomicU32, Ordering};
use tokio::{fs::read_to_string, io::split};
use tokio_util::codec::{FramedRead, FramedWrite};

Expand Down Expand Up @@ -75,10 +71,7 @@ pub async fn dashboard_details() -> Json<DashboardDetails> {
/// Handles upgrading connections from the Pocket Relay Client tool
/// from HTTP over to the Blaze protocol for proxing the game traffic
/// as blaze sessions using HTTP Upgrade
pub async fn upgrade(
ConnectInfo(socket_addr): ConnectInfo<SocketAddr>,
upgrade: BlazeUpgrade,
) -> Response {
pub async fn upgrade(IpAddress(socket_addr): IpAddress, upgrade: BlazeUpgrade) -> Response {
// TODO: Socket address extraction for forwarded reverse proxy

tokio::spawn(async move {
Expand Down
1 change: 1 addition & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl App {

// Config data persisted to runtime
let runtime_config = RuntimeConfig {
reverse_proxy: config.reverse_proxy,
galaxy_at_war: config.galaxy_at_war,
menu_message,
dashboard: config.dashboard,
Expand Down