BGPSec – Implementación de Router con Apache MINA de RFC 8205 y subsiguientes

Este artículo describe el proyecto BGPSec – implementación de un Router utilizando Apache MINA, que soporta el protocolo BGPSec, recientemente publicado en las RFCs 8205 y siguientes.

¿Por qué BGPSec?

Las debilidades del protocolo BGP en cuanto a seguridad ya son ampliamente conocidas, habiendo numerosas RFCs y otras publicaciones que describen las problemáticas. Entre otras podemos mencionar:

  • No es un protocolo autenticado y básicamente se basa en la confianza entre los “Peers”.
  • Los mecanismos para verificar la validez de la información de ruteo son escasos.
  • Tampoco cuenta con un mecanismo para garantizar que quien envía la información efectivamente está autorizado a enviarla.

Un grupo de trabajo dentro de la IETF – Internet Engineering Task Force – ha desarrollado en total 39 RFCs que apuntan a solventar los diferentes problemas del protocolo BGP. Las útimas RFC presentadas, que conforman el nuevo protocolo BGPSec, apuntan a solucionar la segunda y tercera de las problemáticas que mencioné: se proporcionan mecanismos para garantizar que quien envía la información de ruteo está autorizado para enviarla, y establece un mecanismo para verificar la veracidad de la ruta informada.

Apache MINA

Apache MINA es un software que permite implementar rápidamente un Servidor, ya sea un Web server, FTP server, etc. y proporciona mecanismos para manejar fácilmente Sockets TCP, UDP, conexiones SSL/TLS, etc. Proporciona un conjunto de clases que permiten resolver la interpretación de un protocolo, pasaje de mensajes, múltiples Threads, máquina de estados, etc, de manera tal que permiten tener a la infraestructura de red y comunicaciones como una capa de abstracción que nos facilita la tarea de la implementación.

Para mi sorpresa, este software está escuetamente documentado, tal vez confiando en el viejo adagio “El código ES la documentación”, faltando buenos ejemplos de cómo explotar las funcionalidades del sistema, especialmente de las partes más poderosas y complejas.

El código fuente del software está disponible en GitHub.

Clases básicas

A continuación mostraremos cómo se lanza un servidor en Apache MINA y cómo se implementa un Codificador y Decodificador de protocolo, para transformar entradas recibidas por la red en mensajes, implementados como objetos dentro de nuestro sistema.

public class BgpServer 
{
	private void openListener() throws IOException
	{
		acceptor=new NioSocketAcceptor();
		acceptor.setReuseAddress(true);
		ProtocolCodecFilter pcf=new ProtocolCodecFilter(new BgpCoder(),new BgpDecoder());
		acceptor.getFilterChain().addLast("logger", new LoggingFilter());
		acceptor.getFilterChain().addLast("codec", pcf);
		acceptor.setHandler(createIoHandler());
		acceptor.setDefaultLocalAddress(new InetSocketAddress(179));
		acceptor.bind();
	}
}

Este método de la clase BgpServer muestra todos los componentes básicos de un servidor implementado con Apache MINA:

  • Un Socket para apertura del puerto en el que el BgpServer escuchará.
  • Un ProtocolCodecFilter, es un Filtro que se intercala en una cadena de Filtros sobre el Socket, y se encarga de proveer la infraestructura para convertir entre bytes recibidos por el Socket y mensajes en objetos propios del sistema.
  • Un BgpCoder, y un BgpDecoder, un codificador y un Decodificador respectivamente, de los mensajes del protocolo. Estas clases las debemos implementar nosotros de acuerdo a lo que queremos hacer con nuestro protocolo.
  • Un IoHandler, esta es la clase que se encarga de recibir los mensajes generados a partir del ProtocolCodecFilter, y procesarlos, de acuerdo a lo que queremos que haga nuestro sistema.

El IoHandler

Pasemos a hablar de la clase "central" de la infraestructura que implementamos para nuestro Server: el IoHandler.

public class BgpServer 
{
	private IoHandler createIoHandler() 
	{
		BgpHandler b=new BgpHandler(this.configuration);
		addTransitionListener(b);
		StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(BgpHandler.IDLE, b);

		return new StateMachineProxyBuilder().setStateContextLookup(
				new IoSessionStateContextLookup(new StateContextFactory() {
					public StateContext create() {
						return new BgpSession();
					}
				})).create(IoHandler.class, sm);
	}
}

Examinemos la compleja infraestructura creada aquí:

  • El BgpHandler propiamente dicho, al que le pasamos una configuración, decidida por nosotros.
  • Una máquina de Estados, que nos permite reaccionar de distinto modo a los mensajes recibidos en nuestro sistema de acuerdo a algún estado arbitrario en el que estemos.
  • Uno o más estados en los que nuestro sistema se encontrará, decididos por nosotros de manera arbitraria o de acuerdo a nuestro protocolo.
  • Una sesión, donde se almacenan los datos de quién es el cliente que se nos conecta, y podemos almacenar información que nos parezca relevante sobre esa sesión en particular. Cuando ese cliente nos vuelva a mandar un mensaje, tendremos la sesión a mano, y podremos reaccionar ante sus mensajes de acuerdo con el contexto ya establecido, ya sea por el estado en el que el Servidor se encuentra o por algún dato que hayamos almacenado.

El BgpHandler

Aquí es donde nos metemos de lleno en el protocolo BGP y su RFC 4271.

No vamos a reproducir completamente el código del BgpHandler aquí porque es muy extenso, el código completo se puede consultar en GitHub. Pero vamos a discutir los aspectos más relevantes:

  • Implementa la máquina de estados del protocolo.
  • Recibe los mensajes originados en Internet, y de acuerdo al estado en el que el Router se encuentra para la sesión establecida, reacciona ante tal mensaje generando una respuesta y comunicándola a la sesión.

Mostramos una parte del código del BgpHandler, para mostrar sus principales partes:

public class BgpHandler implements EventTransitionListener
{
	@State public static final String ROOT="Root";
	@State(ROOT) public static final String IDLE="Idle";
	@State(ROOT) public static final String CONNECT="Connect";
	@State(ROOT) public static final String ACTIVE="Active";
	@State(ROOT) public static final String OPEN_SENT="Open Sent";
	@State(ROOT) public static final String OPEN_CONFIRM="Open Confirm";
	@State(ROOT) public static final String ESTABLISHED="Established";
....
}

Aquí se observa la declaración de los estados del protocolo BGP:

  • Idle: el router está literalmente sin hacer nada. Cuando recibe una sesión, pasa al estado Connect.
  • Connect: el router admitió una sesión y recibió un mensaje inicial de apertura. Contesta ese mensaje y pasa al estado Open Sent.
  • Open Sent: el router ya tiene una sesión, envió un Open y ahora espera la confirmación mediante un mensaje Keepalive.
  • Open Confirm: el router recibe la confirmación del router remoto mediante el mensaje Keepalive y pasa al estado Established.
  • Established: el router tiene una sesión activa. Hay un KeepAliveTimer que envía periódicamente mensajes para indicar que sigue activo.

La belleza de esto es que cada sesión está en su propio estado, y el MINA maneja *automáticamente* este hecho: por cada sesión creada, recibe mensajes y de acuerdo al estado, ya sabe a qué método enviar el mensaje.

El MINA, para cada uno de los estados, envía los siguientes eventos:

  • Session Created
  • Session Closed
  • Session Opened
  • Message Received
  • Message Sent
  • Exception Caught
  • Y la belleza de la máquina de estados que definí, es que para cada uno de los estados, tiene que haber un método para capturar el evento enviado. De esta manera sé cómo reaccionar si recibo un mensaje en el estado Open Confirm, Established, etc.

    Veamos cómo se implementa:

    class BgpHandler implements EventTransitionListener
    {
    ......
    	@IoHandlerTransition(on=MESSAGE_SENT,in=CONNECT)
    	public void messageSentConnect(BgpSession bgpSession,IoSession ioSession,BgpMessage message)
    	{
    		// When a message is sent, I get here.
    	}
    	@IoHandlerTransition(on=MESSAGE_RECEIVED,in=OPEN_SENT)
    	public void bgpOpenSent(BgpSession bgpSession,IoSession ioSession,BgpMessage message)
    	{
    	}
    .....
    }
    

    Aquí se ve que por cada evento, tengo que especificar a qué estado corresponde con una Annotation.

    Y cuando quiero cambiar de estado, invoco:

    			StateControl.breakAndGotoNext(OPEN_CONFIRM);
    

    … pasándole el nombre del estado al que quiero pasar.

    Como se observa, cada uno de los métodos de un estado para un evento reciben como parámetro un BgpSession, que es un objeto que yo establezco y que guarda información que a mí me interese para este cliente, el IoSession, que es el objeto de MINA que representa la sesión establecida con un cliente en particular, y que guarda su IP, puerto, etc, y el mensaje recibido, si corresponde.

    ProtocolDecoder y ProtocolEncoder

    Otra de las bellezas de Apache MINA que ya establecimos antes, es la posibilidad de recibir mensajes de un protocolo binario y convertirlo en objetos, que después entregamos a la aplicación. Ya vimos cómo se crean las clases que sirven para este propósito, el BgpCoder y el BgpDecoder, que a su vez implementan ProtocolEncoder y ProtocolDecoder respectivamente.

    Miremos por ejemplo cómo funciona un ProtocolDecoder:

    public class BgpDecoder extends CumulativeProtocolDecoder
    {
    	@Override
    	protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception 
    	{
    		// First of all I have to configure who sent it to me. I made sure that when the session
    		// is created, the neighbor is set. 
    		BgpNeighbor neighbor=(BgpNeighbor) session.getAttribute("neighbor");
    		// There may be more than one message in this buffer. Use a while to read them,
    		// as long as the message is longer than the minimum size.
    		// Section 4. The smallest message that may be sent consists of a BGP header
    		// without a data portion.
    		if(in.remaining()>=19)
    		{
    			while(in.remaining()>=19)
    			{
    				BgpMessage message=null;
    				// RFC 4271 section 4.2. Open message format.
    				// All messages start with a 16-octet Marker which is all 1s, followed
    				// by a message length (unsigned int, 2 octets) and a message type (unsigned int, 1 octet)
    				byte[] marker=new byte[16];
    				in.get(marker);
    				int length=in.getUnsignedShort();
    				int message_type=in.getUnsigned();
    				switch(message_type)
    				{
    				case 1:
    					message=this.decodeOpen(session, in, length);
    					break;
    				case 2:
    					message=this.decodeUpdate(session, in, length);
    					break;
    				case 3:
    					message=this.decodeNotification(session, in, length);
    					break;
    				case 4:
    					message=this.decodeKeepalive(session, in, length);
    					break;
    				}
    				// Place the decoded message in the output
    				message.setNeighbor(neighbor);
    				out.write(message);
    			}
    			return true;
    		}
    		else
    			return false;
    	}
    }
    

    Como se ve aquí, en un ProtocolDecoder el método principal es doDecode. Recibe como parámetros la sesión establecida con el cliente, el buffer recibido, y un buffer de salida donde puedo escribir los objetos.

    Se puede observar que la clase BgpDecoder hereda de un ProtocolDecoder específico llamado CumulativeProtocolDecoder. Recordemos que esto es TCP y que un Stream o flujo de bytes está formado por uno o más paquetes. Por lo tanto un mensaje TCP puede estar dividido en uno o más paquetes, que podría recibir inmediatamente, o que puedo acumular – de ahí Cumulative – en un buffer antes de procesar. Hay protocolos en donde me interesa recibir los paquetes apenas lleguen, este es el caso de transmisiones de datos como HTTP o FTP, o protocolos orientados a caracteres como el SSH. Aquí en BGP prefiero esperar hasta que tenga un mensaje completo, un BGP Open completo, un Bgp Keepalive completo o lo que sea.

    Dentro del doDecode recibo un buffer, y una sesión, y una ruta de salida. En el Decoder, consumo el buffer y uso, o no, datos de la sesión para generar los objetos que luego deposito en la ruta de salida.

    Para el protocolo BGP, el mensaje más simple, el Keepalive, solamente se compone de un encabezado de 16 bytes, y 3 bytes que representan el tipo de mensaje y su largo. Por eso me fijo si recibí más de 19 bytes, y si hay más, repito el procesamiento hasta haber consumido todo. Los mensajes que son más cortos que 19 bytes los considero mal formados y los descarto.

    Entonces: en el proceso de MINA recibo Bytes, que consumo en el ProtocolDecoder, que convierto a objetos. Estos objetos son recibidos en el IoHandler, que reacciona ante ellos, genera objetos de respuesta, que son convertidos en bytes por el ProtocolEncoder. Muy bello, muy bello!!