mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(bungeecord): add BungeeGuard auth support (#3038)
* feat(bungeeguard): add BungeeGuard token authentication Add a BungeeGuard shared secret to the proxy configuration, validate the token during the BungeeCord login flow, reject connections with invalid tokens, and prevent players from bypassing the proxy to connect directly. * fix * fix * fix:ci
This commit is contained in:
@@ -89,8 +89,9 @@ and customizable experience. It prioritizes performance and player enjoyment whi
|
||||
- [x] Permissions
|
||||
- [x] Translations
|
||||
- Proxy
|
||||
- [x] Bungeecord
|
||||
- [x] Velocity
|
||||
- [x] [BungeeCord](https://github.com/SpigotMC/BungeeCord)
|
||||
- [x] [BungeeGuard](https://github.com/lucko/BungeeGuard)
|
||||
- [x] [Velocity](https://github.com/PaperMC/Velocity)
|
||||
|
||||
<!-- Check out our [Github Project](https://github.com/orgs/Pumpkin-MC/projects/3) to see current progress. -->
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ pub struct ProxyConfig {
|
||||
pub struct BungeeCordConfig {
|
||||
/// Whether `BungeeCord` support is enabled.
|
||||
pub enabled: bool,
|
||||
/// Shared secret for authenticating connections from the `BungeeCord`
|
||||
/// proxy, as provided by the `BungeeGuard` plugin. When set, the forwarded
|
||||
/// profile properties must contain a `bungeeguard-token` property holding
|
||||
/// this secret, otherwise the connection is rejected. This also blocks
|
||||
/// players connecting directly instead of through the proxy.
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
/// Configuration for Velocity proxy integration.
|
||||
|
||||
@@ -44,6 +44,7 @@ impl PendingConnection {
|
||||
&self.address,
|
||||
&self.server_address,
|
||||
login_start.name.into_string(),
|
||||
&proxy.bungeecord.secret,
|
||||
) {
|
||||
Ok((_ip, profile)) => {
|
||||
self.gameprofile = Some(profile.clone());
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
use std::{net::IpAddr, net::SocketAddr};
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::net::{GameProfile, offline_uuid};
|
||||
use pumpkin_protocol::Property;
|
||||
|
||||
/// The property name the `BungeeGuard` plugin uses to forward its shared
|
||||
/// secret inside the profile properties.
|
||||
const BUNGEEGUARD_TOKEN_PROPERTY: &str = "bungeeguard-token";
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BungeeCordError {
|
||||
@@ -15,6 +22,10 @@ pub enum BungeeCordError {
|
||||
FailedParseProperties,
|
||||
#[error("Failed to make offline UUID")]
|
||||
FailedMakeOfflineUUID,
|
||||
#[error("No BungeeGuard token in forwarded data")]
|
||||
MissingToken,
|
||||
#[error("Invalid BungeeGuard token")]
|
||||
InvalidToken,
|
||||
}
|
||||
|
||||
/// Attempts to login a player via `BungeeCord`.
|
||||
@@ -27,12 +38,19 @@ pub enum BungeeCordError {
|
||||
/// 2. UUID (if `ip_forward` is enabled on the `BungeeCord` server)
|
||||
/// 3. Game profile properties (if `ip_forward` and `online_mode` are enabled on the `BungeeCord` server)
|
||||
///
|
||||
/// If a `secret` is configured, the properties must contain a property named
|
||||
/// `bungeeguard-token` holding the secret, as injected by the `BungeeGuard`
|
||||
/// plugin. The token property is stripped from the profile, and a missing or
|
||||
/// mismatched token rejects the connection. This also blocks players
|
||||
/// connecting directly to this server, bypassing the proxy.
|
||||
///
|
||||
/// If any of the optional data is missing, the function will attempt to
|
||||
/// determine the player's information locally.
|
||||
pub fn bungeecord_login(
|
||||
client_address: &SocketAddr,
|
||||
server_address: &str,
|
||||
name: String,
|
||||
secret: &str,
|
||||
) -> Result<(IpAddr, GameProfile), BungeeCordError> {
|
||||
let mut parts = server_address.split('\0');
|
||||
|
||||
@@ -53,13 +71,62 @@ pub fn bungeecord_login(
|
||||
_ => offline_uuid(&name).map_err(|_| BungeeCordError::FailedMakeOfflineUUID)?,
|
||||
};
|
||||
|
||||
let properties = match parts.next() {
|
||||
let mut properties: Vec<Property> = match parts.next() {
|
||||
Some(json_str) if !json_str.is_empty() => {
|
||||
serde_json::from_str(json_str).map_err(|_| BungeeCordError::FailedParseProperties)?
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// The `BungeeGuard` plugin injects the shared secret as a property named
|
||||
// `bungeeguard-token` inside the forwarded profile properties. When a
|
||||
// secret is configured, that property must be present and hold the
|
||||
// secret; the property is then stripped so it never reaches the game
|
||||
// profile. This also blocks players connecting directly instead of
|
||||
// through the proxy.
|
||||
if !secret.is_empty() {
|
||||
let token_props: Vec<&Property> = properties
|
||||
.iter()
|
||||
.filter(|property| property.name.as_ref() == BUNGEEGUARD_TOKEN_PROPERTY)
|
||||
.collect();
|
||||
|
||||
match token_props.as_slice() {
|
||||
[token] if token.value.as_ref() == secret => {
|
||||
properties.retain(|property| property.name.as_ref() != BUNGEEGUARD_TOKEN_PROPERTY);
|
||||
}
|
||||
[] => {
|
||||
warn!(
|
||||
"Rejecting login: forwarded data has no `{}` property \
|
||||
({} parts, property names: {:?})",
|
||||
BUNGEEGUARD_TOKEN_PROPERTY,
|
||||
server_address.split('\0').count(),
|
||||
properties
|
||||
.iter()
|
||||
.map(|p| p.name.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
return Err(BungeeCordError::MissingToken);
|
||||
}
|
||||
_ => {
|
||||
// Log only SHA-256 hashes: one-way, so the secret never leaks,
|
||||
// but enough to tell a mismatch from duplicated tokens apart.
|
||||
let token_hashes: Vec<String> = token_props
|
||||
.iter()
|
||||
.map(|property| hex::encode(Sha256::digest(property.value.as_bytes())))
|
||||
.collect();
|
||||
warn!(
|
||||
"Rejecting login: expected exactly one matching `{}` property, \
|
||||
found {} (token hashes: {token_hashes:?}, configured secret \
|
||||
hash: {})",
|
||||
BUNGEEGUARD_TOKEN_PROPERTY,
|
||||
token_props.len(),
|
||||
hex::encode(Sha256::digest(secret.as_bytes()))
|
||||
);
|
||||
return Err(BungeeCordError::InvalidToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
ip,
|
||||
GameProfile {
|
||||
@@ -110,6 +177,7 @@ mod tests {
|
||||
&client_address,
|
||||
&handshake.server_address,
|
||||
"Steve".to_string(),
|
||||
"",
|
||||
)
|
||||
.expect("the forwarded address should produce a game profile");
|
||||
|
||||
@@ -129,4 +197,105 @@ mod tests {
|
||||
assert_eq!(&*properties[0].value, textures.as_str());
|
||||
assert_eq!(properties[0].signature.as_deref(), Some(signature.as_str()));
|
||||
}
|
||||
|
||||
const SECRET: &str = "bungeeguard-token";
|
||||
const FORWARDED_HOST: &str = concat!(
|
||||
// Split at the digits so the `\0` is not read as an octal escape.
|
||||
"mc.example.com\0",
|
||||
"192.0.2.10\0",
|
||||
"d8f4a1e0-0f1b-4c3a-9f2e-1a2b3c4d5e6f"
|
||||
);
|
||||
|
||||
fn client_address() -> SocketAddr {
|
||||
SocketAddr::from(([10, 0, 0, 1], 51234))
|
||||
}
|
||||
|
||||
/// The forwarded address with the given profile `properties` as its
|
||||
/// fourth part, as `BungeeCord` puts them on the wire.
|
||||
fn forwarded_address(properties: &str) -> String {
|
||||
format!("{FORWARDED_HOST}\0{properties}")
|
||||
}
|
||||
|
||||
/// The `BungeeGuard` token property alone, as the plugin injects it into
|
||||
/// the forwarded profile properties.
|
||||
fn token_property(token: &str) -> String {
|
||||
format!(r#"{{"name":"bungeeguard-token","value":"{token}","signature":""}}"#)
|
||||
}
|
||||
|
||||
/// A properties array containing the given property objects.
|
||||
fn properties_array(properties: &[&str]) -> String {
|
||||
format!("[{}]", properties.join(","))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_matching_bungeeguard_token() {
|
||||
let properties = format!(
|
||||
r#"[{{"name":"textures","value":"skin","signature":"sig"}},{{"name":"bungeeguard-token","value":"{SECRET}","signature":""}}]"#
|
||||
);
|
||||
let address = forwarded_address(&properties);
|
||||
|
||||
let (ip, profile) =
|
||||
bungeecord_login(&client_address(), &address, "Steve".to_string(), SECRET)
|
||||
.expect("a matching token should be accepted");
|
||||
|
||||
assert_eq!(ip, IpAddr::from([192, 0, 2, 10]));
|
||||
|
||||
// The token property is stripped so it never reaches the game profile.
|
||||
let properties = profile.properties.load();
|
||||
assert_eq!(properties.len(), 1);
|
||||
assert_eq!(&*properties[0].name, "textures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_bungeeguard_token() {
|
||||
let address =
|
||||
forwarded_address(r#"[{"name":"textures","value":"skin","signature":"sig"}]"#);
|
||||
|
||||
let result = bungeecord_login(&client_address(), &address, "Steve".to_string(), SECRET);
|
||||
|
||||
assert!(matches!(result, Err(BungeeCordError::MissingToken)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mismatched_bungeeguard_token() {
|
||||
let address = forwarded_address(&properties_array(&[&token_property("wrong-token")]));
|
||||
|
||||
let result = bungeecord_login(&client_address(), &address, "Steve".to_string(), SECRET);
|
||||
|
||||
assert!(matches!(result, Err(BungeeCordError::InvalidToken)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_bungeeguard_tokens() {
|
||||
let properties = properties_array(&[&token_property(SECRET), &token_property(SECRET)]);
|
||||
let address = forwarded_address(&properties);
|
||||
|
||||
let result = bungeecord_login(&client_address(), &address, "Steve".to_string(), SECRET);
|
||||
|
||||
assert!(matches!(result, Err(BungeeCordError::InvalidToken)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_direct_connection_when_secret_is_configured() {
|
||||
let result = bungeecord_login(
|
||||
&client_address(),
|
||||
"mc.example.com",
|
||||
"Steve".to_string(),
|
||||
SECRET,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(BungeeCordError::MissingToken)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_token_when_no_secret_is_configured() {
|
||||
let address = forwarded_address(&properties_array(&[&token_property(SECRET)]));
|
||||
|
||||
let result = bungeecord_login(&client_address(), &address, "Steve".to_string(), "");
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"an unconfigured secret must not reject logins"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user