Fix launcher verification session check and socket IP resolution

This commit is contained in:
SCOPEDD committed 2026-09-29 02:03:08 -04:00
1 parent 522e3a5717
commit c7aa563dac
6 files changed
+185 -29

No files matched your search

+131 -15
View File
@@ -219,6 +219,93 @@ pub async fn login(
Ok(Json(check_login(&state, &server, user.as_ref(), req.ip.as_deref(), &brand).await?))
}
fn canonical_ip(ip: std::net::IpAddr) -> std::net::IpAddr {
match ip {
std::net::IpAddr::V4(v4) => std::net::IpAddr::V4(v4),
std::net::IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
std::net::IpAddr::V4(v4)
} else {
std::net::IpAddr::V6(v6)
}
}
}
}
fn is_private_or_local(ip: &std::net::IpAddr) -> bool {
match canonical_ip(*ip) {
std::net::IpAddr::V4(v4) => v4.is_loopback() || v4.is_private() || v4.is_link_local(),
std::net::IpAddr::V6(v6) => v6.is_loopback(),
}
}
pub fn ips_match(sess_str: &str, req_str: &str) -> bool {
let s = sess_str.trim();
let r = req_str.trim();
if s.eq_ignore_ascii_case(r) {
return true;
}
let (Ok(ip_a), Ok(ip_b)) = (s.parse::<std::net::IpAddr>(), r.parse::<std::net::IpAddr>()) else {
return false;
};
let a = canonical_ip(ip_a);
let b = canonical_ip(ip_b);
if a == b {
return true;
}
if a.is_loopback() && b.is_loopback() {
return true;
}
if is_private_or_local(&a) && is_private_or_local(&b) {
return true;
}
if is_private_or_local(&a) {
return true;
}
match (a, b) {
(std::net::IpAddr::V4(v4_a), std::net::IpAddr::V4(v4_b)) => {
v4_a.octets()[0..3] == v4_b.octets()[0..3]
}
(std::net::IpAddr::V6(v6_a), std::net::IpAddr::V6(v6_b)) => {
v6_a.segments()[0..4] == v6_b.segments()[0..4]
}
_ => false,
}
}
async fn verify_launcher_ip(
state: &AppState,
user_id: Option<i64>,
req_ip: Option<&str>,
since: &str,
) -> AppResult<bool> {
let req_ip = match req_ip {
Some(ip) if !ip.trim().is_empty() => ip.trim(),
_ => return Ok(false),
};
let sessions: Vec<String> = match user_id {
Some(uid) => {
sqlx::query_scalar("SELECT ip FROM launcher_sessions WHERE user_id = ? AND created_at >= ? ORDER BY id DESC LIMIT 50")
.bind(uid)
.bind(since)
.fetch_all(&state.db)
.await?
}
None => {
sqlx::query_scalar("SELECT ip FROM launcher_sessions WHERE user_id IS NULL AND created_at >= ? ORDER BY id DESC LIMIT 50")
.bind(since)
.fetch_all(&state.db)
.await?
}
};
for sess_ip in sessions {
if ips_match(&sess_ip, req_ip) {
return Ok(true);
}
}
Ok(false)
}
async fn check_login(
state: &AppState,
server: &ServerRow,
@@ -227,11 +314,18 @@ async fn check_login(
brand: &str,
) -> AppResult<LoginVerdict> {
let Some(user) = user else {
return Ok(if server.access == "all" && !server.require_launcher {
LoginVerdict { allowed: true, message: None, account: None }
if server.access == "all" {
if server.require_launcher {
let since = ago(chrono::Duration::hours(LAUNCHER_WINDOW_HOURS));
let launched = verify_launcher_ip(state, None, ip, &since).await?;
if !launched {
return Ok(LoginVerdict::deny(format!("Please join through the {brand} launcher.")));
}
}
return Ok(LoginVerdict { allowed: true, message: None, account: None });
} else {
LoginVerdict::deny(format!("You need a {brand} account to join this server.\nCreate one in the {brand} launcher."))
});
return Ok(LoginVerdict::deny(format!("You need a {brand} account to join this server.\nCreate one in the {brand} launcher.")));
}
};
match user.status.as_str() {
"active" => {}
@@ -253,17 +347,7 @@ async fn check_login(
}
if server.require_launcher {
let since = ago(chrono::Duration::hours(LAUNCHER_WINDOW_HOURS));
let launched: bool = match ip {
Some(ip) => {
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM launcher_sessions WHERE user_id = ? AND ip = ? AND created_at >= ?)")
.bind(user.id)
.bind(ip.trim())
.bind(&since)
.fetch_one(&state.db)
.await?
}
None => true,
};
let launched = verify_launcher_ip(state, Some(user.id), ip, &since).await?;
if !launched {
return Ok(LoginVerdict::deny(format!("Please join through the {brand} launcher.")));
}
@@ -683,3 +767,35 @@ pub async fn live_summary(state: &AppState) -> AppResult<Value> {
"servers": servers.into_iter().map(view).collect::<Vec<_>>(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ips_match() {
// Exact match
assert!(ips_match("1.2.3.4", "1.2.3.4"));
assert!(ips_match("203.0.113.9", "203.0.113.9"));
// Loopback IPv4 & IPv6
assert!(ips_match("127.0.0.1", "::1"));
assert!(ips_match("::1", "127.0.0.1"));
assert!(ips_match("127.0.0.1", "127.0.0.1"));
// IPv4-mapped IPv6
assert!(ips_match("::ffff:192.168.1.10", "192.168.1.10"));
assert!(ips_match("192.168.1.10", "::ffff:192.168.1.10"));
// Docker bridge / private gateway recorded
assert!(ips_match("172.18.0.1", "192.168.1.50"));
assert!(ips_match("10.0.0.1", "10.0.0.2"));
// Subnet /24 match
assert!(ips_match("203.0.113.5", "203.0.113.9"));
// Different public networks do not match
assert!(!ips_match("198.51.100.1", "203.0.113.9"));
assert!(!ips_match("8.8.8.8", "1.1.1.1"));
}
}