Protocolo del laboratorio 2 de Sistemas Operativos (CIIC4050).
Cada pareja de estudiantes escribe un cliente en Python que corre en una Raspberry Pi y habla con este servidor por un socket TCP crudo.
1. TCP no tiene mensajes, tiene bytes. Lo que envías con un sendall()
puede llegar partido en varios recv(), y varios mensajes seguidos pueden
llegar pegados en uno solo. Por eso cada mensaje de este protocolo es un
objeto JSON en una línea, terminado en \n, y hay que leer hasta
encontrar el salto de línea. Un cliente que haga
json.loads(sock.recv(4096)) funciona en tus pruebas y falla en el
laboratorio.
2. Identidad y autenticación no son lo mismo. Tu Raspberry se
identifica con una huella de hardware, que viaja en claro y cualquiera
podría copiar. Se autentica demostrando que conoce un secreto, sin
enviarlo: el servidor manda un nonce y tú devuelves
HMAC-SHA256(device_secret, nonce).
3. Son parejas. Cada Raspberry admite como máximo 2 estudiantes activos, y ambos deben ser de la misma sección.
conectar TCP :9000
<- hello (trae el nonce y los límites del servidor)
device.register (una sola vez por Raspberry, con el código pegado a ella)
-> device.registered (device_id + device_secret: GUÁRDALOS)
session.open (HMAC del nonce)
-> session.ok (session_token)
student.enroll x2 (tú y tu pareja)
data.push xN (los datos del laboratorio)
session.close
Todo mensaje, en las dos direcciones, tiene la misma forma:
{"v": 1, "id": "c7", "action": "student.enroll", "token": "…", "payload": {}}
| campo | quién | para qué |
|---|---|---|
v |
los dos | versión del protocolo; hoy 1 |
id |
cliente | id de correlación; el servidor lo devuelve en su respuesta |
action |
los dos | qué mensaje es |
token |
cliente | session_token; obligatorio salvo en device.register, session.open y session.resume |
payload |
los dos | el cuerpo, distinto para cada acción |
El campo id existe porque sobre un flujo puedes enviar tres mensajes sin
esperar respuesta, y necesitas saber cuál respuesta corresponde a cuál.
El socket crudo contra el que trabajas. Un objeto JSON por línea,
terminado en \n.
Va sin TLS, a propósito. El HMAC demuestra quién eres, pero no cifra nada: cualquiera en la misma red puede leer lo que envías. Esa distinción entre autenticación y confidencialidad es parte del laboratorio.
El puerto es el 9500 y no el 9000 porque en ese servidor el 9000 ya está ocupado por otro servicio.
Puedes ver el saludo sin escribir una línea de código:
nc solab.ivanvasquez.tech 9500
Para quien levante el servidor en su propia máquina siguiendo el README del repositorio. Mismo protocolo, otro host y puerto.
WebSocket de solo lectura que alimenta el tablero en vivo. Requiere el token de administración del docente: los estudiantes no lo usan.
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
El servidor saluda en cuanto aceptas la conexión, sin que pidas nada.
No envías nada para recibirlo: llega solo. Trae el nonce que necesitas
para session.open, y los límites del servidor para que no los adivines.
El nonce es de esta conexión. Si te reconectas, el nonce es otro y
la prueba anterior ya no sirve. Eso es lo que impide que alguien capture
tu prueba y la reutilice.
Available only on servers:
Accepts the following message:
{
"v": 1,
"action": "hello",
"payload": {
"server": "solab",
"protocol_version": 1,
"nonce": "0104115f8f2fd20a18e2f4c37f9c39f8",
"docs_url": "http://localhost:8080/docs",
"heartbeat_seconds": 30,
"max_line_bytes": 65536
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Canjea el código pegado a tu Raspberry por unas credenciales permanentes.
El enrollment_code está impreso en una etiqueta pegada a tu Raspberry y
es de un solo uso. Sin ese papel no puedes registrar nada: es lo que
impide que alguien registre "una Raspberry" desde su portátil.
El device_secret que recibes se muestra una sola vez. Guárdalo en
/etc/solab/device.json con permisos 600 antes de hacer nada más. Si
lo pierdes, tienes que pedirle al docente un código nuevo.
La huella de hardware que envías no te autentica: viaja en claro y es falsificable. Le sirve al servidor para detectar que la misma Raspberry se registró dos veces, o que una imagen se reinstaló.
Available only on servers:
Accepts the following message:
Canjea el enrollment code por credenciales.
{
"v": 1,
"id": "c1",
"action": "device.register",
"payload": {
"enrollment_code": "SOLAB-A7K3-2Q9F",
"fingerprint": "326295b6f104a9a78c61bb9dfdcdabe1909c44ec5874fd840aa01303051eff7a",
"cpu_serial": "10000000abcd1234",
"hostname": "rpi-equipo-07"
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Prueba que conoces el device_secret sin enviarlo.
Calcula HMAC-SHA256(device_secret, nonce) en hexadecimal minúscula y
envíalo como proof. En Python, con la biblioteca estándar:
import hmac, hashlib
proof = hmac.new(secret.encode(), nonce.encode(), hashlib.sha256).hexdigest()
El secret se usa tal como viene en tu device.json, como cadena: no
hay que pasarlo por bytes.fromhex().
A cambio recibes un session_token, que va en el campo token del sobre
de todos los mensajes siguientes.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "c2",
"action": "session.open",
"payload": {
"device_id": "ce756e20-bfc9-4649-92d9-949c9b68bb7e",
"proof": "d2b1a4c7e8f09a3b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3"
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Vuelve a usar un token vigente sin repetir el HMAC.
Las conexiones se caen. session.resume te devuelve a donde estabas sin
tener que releer el secreto del disco. El token sigue siendo el mismo:
reanudar no lo rota.
Si el token venció (TOKEN_EXPIRED) o fue revocado, toca session.open
otra vez.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "c2",
"action": "session.resume",
"payload": {
"token": "a1b2c3d4e5f6…"
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Matricula un estudiante en esta Raspberry.
Cada integrante de la pareja se matricula por separado. Se aplican tres reglas, y cada una tiene su código de error:
| regla | si la rompes |
|---|---|
| máximo 2 estudiantes activos por Raspberry | DEVICE_FULL |
| un estudiante, una sola pareja activa | STUDENT_ALREADY_ENROLLED |
| los 2 de una Raspberry, misma sección | SECTION_MISMATCH |
Las secciones válidas son 070, 071, 100 y 101; cualquier otra da
INVALID_SECTION.
Repetir la misma matrícula no es un error: si reintentas tras un timeout, recibes el estado actual igual que la primera vez.
Available only on servers:
Accepts the following message:
Matricula un estudiante en esta Raspberry.
{
"v": 1,
"id": "c3",
"action": "student.enroll",
"token": "a1b2c3…",
"payload": {
"student_id": "802181234",
"full_name": "Ada Lovelace",
"section": "071"
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Quita a un estudiante de esta Raspberry.
Solo desde la Raspberry donde estás matriculado. Si te equivocaste de Pi, libérate desde aquella, no desde esta.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "string",
"action": "string",
"token": "string",
"payload": {
"student_id": "string"
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Quién soy, quién está matriculado, cuántos puestos quedan.
La acción que más vas a usar mientras depuras. No lleva payload.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "string",
"action": "string",
"token": "string",
"payload": {}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Envía una medición al servidor.
kind nombra el tipo de dato y data es un objeto JSON libre: el
servidor no valida su contenido.
seq es tu contador. Reenviar un seq que ya enviaste en la misma
sesión no duplica ni falla: el ack te responde stored: false. Eso hace
que reintentar tras un timeout sea seguro.
Hay un límite de mensajes por minuto; si lo pasas recibes RATE_LIMITED.
No es un castigo: es para que un bucle sin sleep no tumbe el servidor
del resto del laboratorio.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "c5",
"action": "data.push",
"token": "a1b2c3…",
"payload": {
"kind": "demo",
"seq": 42,
"ts": "2026-09-21T14:03:00Z",
"data": {
"lo_que_sea": 123
}
}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Evita que el servidor cierre por inactividad.
El servidor cierra las conexiones sin tráfico (mira
heartbeat_seconds y el timeout en el saludo). Si tu cliente va a estar
callado un rato, manda ping.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "string",
"action": "string",
"token": "string",
"payload": {}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Revoca el token antes de desconectarte.
Opcional, pero es buena costumbre y deja el tablero del docente limpio.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "string",
"action": "string",
"token": "string",
"payload": {}
}
Todo el protocolo va por la misma conexión TCP. No hay rutas ni
endpoints: lo que distingue un mensaje de otro es el campo action.
Respuesta de error, con un código estable contra el que puedes programar.
Cualquier acción puede responder error en lugar de su respuesta normal.
El id es el de tu mensaje, así que sabes cuál falló.
Programa contra code, no contra detail: el código es contrato y no
cambia; el detalle está escrito para que lo leas tú y puede mejorar.
Available only on servers:
Accepts the following message:
{
"v": 1,
"id": "c3",
"action": "error",
"payload": {
"code": "DEVICE_FULL",
"title": "Dispositivo lleno",
"detail": "la Raspberry RPI-07 ya tiene 2 estudiantes (802181234, 802185678); el laboratorio es en parejas"
}
}
Canjea el enrollment code por credenciales.
Credenciales permanentes. El secreto se muestra una sola vez.
Sesión abierta. Trae el token y el estado de la Raspberry.
Matricula un estudiante en esta Raspberry.
Estado completo de la Raspberry. Es la respuesta de device.status y también la de student.enroll (como student.enrolled) y student.leave (como student.left).
Confirmación. stored=false significa que ese seq ya estaba.