En alguna ocasión, bien porque necesitamos desarrollar una solución cliente / servidor, o bien porque queremos implementar otro servicio conocido, tenemos que programar un servidor TCP. Hay una serie de buenas prácticas que nos pueden ayudar a conseguir una mayor estabilidad frente a ataques o saturación del servicio.
Si participasteis en el primer wargame de SbD que organizamos hace unos meses, recordareis la primera prueba de networking que se trataba de un servidor que emulaba ser la configuración de un router. Estas buenas prácticas de las que hablamos fueron implementadas y, puede que gracias a ello, el servidor estuvo activo constantemente durante todo el concurso.
Está programado en ruby, y vamos a tomarlo como ejemplo. Hay que tener en cuenta que, al ser ruby un lenguaje de alto nivel, es posible que no necesitemos solucionar problemas que pueden darse en lenguajes de otro tipo, o de bajo nivel. Dicho ésto, ¡empezamos!
La estructura del programa es algo parecido a esto:
login
if correcto
bucle
menu
leer_opción
realizar_acción
fin
fin
Como de casualidad el servidor está a la escucha y cuando recibe una nueva conexión crea un hilo nuevo para servir a ese cliente y volver a ponerse a la escucha.
server = TCPServer.new("", srvport) loop do socket = server.accept if socket newclient(socket) end end
En este caso la función newclient() toma el socket y crea el hilo.
Uno de los problemas que podemos encontrar en este punto es que si se reciben muchas conexiones de forma simultánea, se crearán más hilos de los que el lenguaje o el sistema puede soportar, por lo que el programa morirá.
Ésto depende mucho del lenguaje de programación y de la versión. Por ejemplo, en versiones antiguas de ruby sucedía, pero en las versiones más modernas parecen haberlo controlado de alguna forma.
Las soluciones que podemos tomar son variadas. Personalmente la que más me convence es poner un pequeño tiempo de espera entre que se acepta la conexión y se crea el hilo. Nuestro código quedaría de la siguiente forma:
server = TCPServer.new("", srvport) loop do socket = server.accept sleep(0.05) if socket newclient(socket) end end
Aunque controlemos el número de conexiones al incio, puede que alguien con malas intenciones realice conexiones y luego las deje sin actividad. Ésto podría causar que se llegara al límite de conexiones que soporta el sistema, haciendo que quede saturado.
Para solventar este problema podemos usar timeouts por inactividad, de forma que si un usuario no muestra actividad durante X tiempo, se cierre el socket y el hilo muera. Para ello, hice una función alternativa para leer del socket.
def read_socket(s, thread, tout = 15, size = 500) begin Timeout::timeout(tout) do info = s.recv(size) return info end rescue Timeout::Error s.close thread.kill return false end end
La función recibe el socket y el hilo como parámetros, también podemos pasar el tiempo de timeout y el tamaño de lectura del socket. Si se supera el timeout se cierra el socket y se mata el hilo.
En nuesto caso particular hay otra cosa más con la que hay que contar. ¿Y si se crean conexiones de bots que entran en el bucle y realizan acciones para que no se les expulse por timeout?
Teniendo en cuenta esa posibilidad, la solución pasa por limitar el número de interacciones en el menú. En este caso estaba limitada a 50.
Tomando estas precauciones podemos conseguir un servicio bastante estable y resistente a ataques o saturación. ¿Conoces o sigues más medidas? ¡Compártelas en los comentarios!
Tomando estas precauciones podemos conseguir un servicio bastante estable y resistente a ataques o saturación. ¿Conoces o sigues más medidas? ¡Compártelas en los comentarios!