v0.4.18: protección anti-secuestro de ID en el relay de internet

relay.rs envía al relay una huella SHA-256 del client_token al registrar
el ID de 8 dígitos, en vez del token en texto plano. Permite que el relay
(gradio-relay, cambio complementario ya desplegado en relay.gradio.net)
rechace un re-registro de un ID activo cuando la huella no coincide,
cerrando la ventana para que un tercero que conozca el ID intercepte el
token de un cliente remoto suplantando a la estación real.
This commit is contained in:
2026-08-23 01:36:07 -05:00
parent 03084e2d5a
commit 70940420ff
5 changed files with 52 additions and 10 deletions
+8
View File
@@ -4,6 +4,14 @@ Todas las versiones publicadas de `gradio-player` (paquete .deb).
---
## [0.4.18] — 2026-08-23
### Seguridad
- **Protección contra secuestro de ID en el relay de internet**: `relay.rs` ahora envía al relay una huella SHA-256 del `client_token` configurado (nunca el token en sí) al registrarse con el ID de 8 dígitos. El servidor de relay (`gradio-relay`, desplegado en `relay.gradio.net`) rechaza un nuevo registro para un ID ya activo si la huella no coincide, evitando que un tercero que conozca/adivine el ID de una estación le robe el slot e intercepte el token de los clientes remotos que se conecten después. Compatible con estaciones que aún no actualizaron: si el slot activo nunca tuvo huella (versión anterior), el comportamiento es el mismo que antes de este fix — sin regresión, pero sin protección hasta actualizar.
---
## [0.4.17] — 2026-08-22
### Nuevo
Generated
+13 -1
View File
@@ -1591,7 +1591,7 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "radio-player"
version = "0.4.17"
version = "0.4.18"
dependencies = [
"anyhow",
"cairo-rs",
@@ -1614,6 +1614,7 @@ dependencies = [
"rand",
"serde",
"serde_json",
"sha2",
"symphonia",
"tokio",
"tokio-tungstenite",
@@ -1834,6 +1835,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "radio-player"
version = "0.4.17"
version = "0.4.18"
edition = "2021"
description = "G Radio Player — reproductor y herramientas de programación"
license = "GPL-3.0-or-later"
@@ -92,6 +92,7 @@ zip = "2"
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures-util = "0.3"
native-tls = "0.2"
sha2 = "0.10"
log = "0.4"
env_logger = "0.11"
+1 -1
View File
@@ -6619,7 +6619,7 @@ fn main() {
if gr_cfg_srv.relay_habilitado && gr_cfg_srv.relay_id.len() == 8
&& gr_cfg_srv.relay_id.chars().all(|c| c.is_ascii_digit())
{
relay::iniciar(gr_cfg_srv.relay_id, gr_cfg_srv.client_port, cliente_conectado.clone());
relay::iniciar(gr_cfg_srv.relay_id, gr_cfg_srv.client_port, cliente_conectado.clone(), gr_cfg_srv.client_token.clone());
}
let app = Application::builder()
+28 -7
View File
@@ -45,9 +45,27 @@ fn relay_url() -> String {
std::env::var("GRADIO_RELAY_URL").unwrap_or_else(|_| DEFAULT_RELAY_URL.to_string())
}
/// Calcula la huella (SHA-256 hex) del client_token para probar continuidad
/// de dueño ante el relay sin enviarle el token real. `None` si el token
/// está vacío (el relay trata eso como "sin protección", igual que antes
/// de tener este mecanismo).
fn secreto_de_token(client_token: &str) -> Option<String> {
if client_token.trim().is_empty() {
return None;
}
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(client_token.trim().as_bytes());
Some(format!("{:x}", hasher.finalize()))
}
/// Lanza el hilo de relay. No bloquea — retorna inmediatamente.
/// `id` — 8 dígitos numéricos, `puerto_local` — puerto del servidor TCP local.
pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectado) {
/// `id` — 8 dígitos numéricos, `puerto_local` — puerto del servidor TCP local,
/// `client_token` — token de autenticación configurado (se envía al relay
/// como huella SHA-256, nunca en texto plano) para evitar que un tercero
/// que conozca el ID le robe el slot a esta estación.
pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectado, client_token: String) {
let secreto = secreto_de_token(&client_token);
std::thread::Builder::new()
.name("gradio-relay".to_string())
.spawn(move || {
@@ -55,17 +73,17 @@ pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectad
.enable_all()
.build()
.expect("relay: no se pudo crear runtime tokio");
rt.block_on(bucle_relay(id, puerto_local, cliente_conectado));
rt.block_on(bucle_relay(id, puerto_local, cliente_conectado, secreto));
})
.expect("relay: no se pudo crear hilo");
}
// ─── Bucle de reconexión ──────────────────────────────────────────────────────
async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteConectado) {
async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteConectado, secreto: Option<String>) {
loop {
log::info!("relay: conectando con ID={}", id);
match conectar_y_operar(&id, puerto_local, cliente_conectado.clone()).await {
match conectar_y_operar(&id, puerto_local, cliente_conectado.clone(), secreto.clone()).await {
Ok(()) => {
log::info!("relay: sesión cerrada, reconectando en 1s...");
tokio::time::sleep(Duration::from_secs(1)).await;
@@ -80,7 +98,7 @@ async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteCo
// ─── Conexión principal ───────────────────────────────────────────────────────
async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: ClienteConectado) -> Result<()> {
async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: ClienteConectado, secreto: Option<String>) -> Result<()> {
// connect_async maneja TLS automáticamente para wss:// (feature native-tls)
let url = relay_url();
log::info!("relay: usando URL {}", url);
@@ -115,7 +133,10 @@ async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: Clien
});
// ── Handshake de registro ─────────────────────────────────────────────────
let msg = format!(r#"{{"tipo":"register","id":"{}"}}"#, id);
let msg = match &secreto {
Some(s) => format!(r#"{{"tipo":"register","id":"{}","secreto":"{}"}}"#, id, s),
None => format!(r#"{{"tipo":"register","id":"{}"}}"#, id),
};
out_tx.send(Message::Text(msg.into()))
.map_err(|_| anyhow::anyhow!("relay: canal de escritura cerrado antes de registrar"))?;