viernes, 17 de agosto de 2007

Hotplug en Xen

Xen es un sistema de paravirtualización (enlace en inglés), es decir, una manera de correr varios sistemas operativos en una máquina física, cada sistema creyendo que corre en una máquina real cuando lo hace en una máquina virtual con las características que hayamos definido.

Es paravirtualización, y no virtualización, porque los sistemas que van a correr en las máquinas virtuales deben ser modificados para correr sobre Xen. No obstante, las últimas versiones de Xen ofrecen virtualización real si se utilizan sobre procesadores de última generación, con instrucciones específicas.

El caso es que la máquina virtual no utiliza discos duros reales, sino elementos que desde la máquina real se le exportan como tales. Dichos elementos pueden ser un disco real, una partición o un fichero, y desde la máquina virtual se verán como un disco duro o una partición.

Entre las virtudes de Xen está la posibilidad de añadir o eliminar tales elementos de la máquina virtual en caliente, como si se enchufaran o desenchufaran de ella.

Y ahora, la almendra del asunto: Quiero que cuando enchufe un dispositivo en la máquina real (llamada "el hierro"), se enchufe automáticamente a la máquina virtual.

Esta solución la desarrollé yo, a partir de una idea de Kuko "Maestro Xen" Armas, cuando, teniendo otras cosas mejores que hacer en la empresa (Grupo CPD), me empeñé en sacar adelante la idea en vez de darlo por imposible.

El primer paso es detectar que se ha enchufado un dispositivo determinado al hierro. De eso se encarga udev. El segundo paso es introducir ese dispositivo en la máquina virtual. De eso se encarga xm, uno de los comandos de Xen.

Y en medio, hay que decidir a qué máquina virtual, de las varias que puede haber corriendo, se debe enchufar el dispositivo introducido. De esto se encarga XenHotplug, la solución que he desarrollado.

XenHotplug se compone de reglas de udev, guiones shell llamados desde esas reglas y un directorio de registro. Uno de esos guiones shell utiliza la firma de discos que presentaba en el artículo anterior.

Reglas de udev



Debemos crear un fichero de reglas de udev. Residirá en el directorio /etc/udev/rules.d y lo podemos llamar, por ejemplo, 10-local.rules. Ese nombre hará que se ejecute después de ciertas reglas fundamentales (las 5-early.rules) pero antes que el resto de reglas predefinidas.

En dicho fichero detectaremos la introducción o eliminación en el sistema de un dispositivo determinado y diremos qué hay que hacer con él:

SUBSYSTEM=="block", SYSFS{product}=="Mass Storage Device", \
SYSFS{manufacturer}=="Prolific Technology Inc.", \
SYSFS{start}=="63", ACTION=="add", \
PROGRAM="/usr/local/sbin/identify.sh %P", \
SYMLINK+="externalusbdisk%c", \
RUN+="/usr/local/sbin/action.sh %c %k"

SUBSYSTEM=="block", ACTION=="remove", \
RUN+="/usr/local/sbin/action.sh 0 %k"


La primera regla detecta la introducción en el sistema (ACTION=="add") de un dispositivo de bloque (SUBSYSTEM=="block") que puede ser cualquier cosa que se comporte como un disco duro externo o un disquete externo, que sea de la clase de dispositivos tipo disco (SYSFS{product}=="Mass Storage Device"), de una determinada marca (SYSFS{manufacturer}=="Prolific Technology Inc.").

Esto detectará la introducción de cualquier dispositivo de bloque, es decir, tanto el disco externo como sus particiones. Hay que tener en cuenta que al enchufar el disco, se genera un evento de introducción del disco completo, pero además el núcleo (kernel) del sistema revisa el disco y detecta sus particiones, que son añadidas al sistema, además del propio disco. Por eso, cuando enchufamos un disco particionado, aparecen los dispositivos correspondientes a las particiones, y no tenemos que crearlos a mano.

La regla, además, distingue si se trata del evento correspondiente al disco completo o a la partición, ya que la pieza SYSFS{start}=="63" sólo la cumple la primera partición. De esta manera la regla no casa con el disco completo, ni con las restantes particiones de un disco, sino sólo con la primera.

La siguiente pieza de la regla (PROGRAM="/usr/local/sbin/identify.sh %P") llama a un programa (guión de shell) de identificación del disco, que nos dirá de qué disco se trata exactamente. Así podremos distinguir los discos de esa marca y modelo que queremos que se enchufen a la máquina virtual de los que no. El programa se llama con un argumento (%P) que indica qué nombre de dispositivo se ha asignado al disco completo.

El programa proporciona un número de dispositivo, si se trata de un disco conocido (para lo que usamos la firma del disco) o la cadena NOID si se trata de un disco no identificado. En cualquiera de los dos casos, se crea un enlace simbólico /dev/externalusbdiskX (donde X es el número proporcionado antes), es decir, puede aparecer un enlace como /dev/externalusbdisk2 para el disco identificado como 2, o /dev/externalusbdiskNOID para un disco no identificado, gracias a la parte de la regla que dice SYMLINK+="externalusbdisk%c".

Finalmente, se ejecuta otro programa (guión de shell), con el dispositivo y el enlace simbólico ya creados, al que se pasan como argumentos el mismo número o NOID establecido antes y el nombre real del dispositivo (RUN+="/usr/local/sbin/action.sh %c %k"). Este programa borrará el enlace /dev/externalusbdiskNOID, si se trata de un dispositivo no identificado, o añadirá la partición a la máquina virtual y la registrará, si se trata de un dispositivo identificado.

La segunda regla detecta la retirada del sistema (ACTION=="remove") de cualquier (no podemos pedir mucha información acerca de un dispositivo ya retirado, ¿no?) dispositivo de bloque (SUBSYSTEM=="block"), y ejecuta el mismo programa que en el caso anterior, que en esta ocasión detectará si el dispositivo está registrado como insertado en la máquina virtual y, en tal caso, lo eliminará de la misma y lo desregistrará.

Guiones shell



Ahora veremos los programas que son llamados desde udev. El primero es identify.sh:

#!/bin/bash
# fichero /usr/local/sbin/identify.sh de XenHotplug

disp=$1
gen_id=`cat /usr/local/lib/identify.in`
disk_id=`dd if=/dev/$1 count=3 bs=1 skip=440 2> /dev/null`
disk_sn=`dd if=/dev/$1 count=1 bs=1 skip=443 2> /dev/null`
disk_sn_f=`echo -n $disk_sn|od -A n -N 1 --format=u`
if [ "$disk_id" = "$gen_id" ] ; then
echo $disk_sn_f
else
echo NOID
fi


Como se puede ver, el programa guarda en la variable $disp el nombre del dispositivo padre que la regla de udev le pasa como argumento, a continuación guarda en la variable $gen_id el contenido del fichero /usr/local/lib/identify.in, que es la firma de disco que empleamos para reconocerlos, y a continuación lee datos del propio disco.

Primero lee los bytes que deberían tener la firma de disco y los almacena en la variable $disk_id, y luego lee el byte de número de serie y lo almacena en la variable $disk_sn. Este número de serie, que es un byte con un valor entre 0 y 255 y que por lo tanto puede representar un carácter cualquiera, hemos de transformarlo en una cadena de texto para poderlo comparar, por eso lo formateamos como tal y lo almacenamos en la variable disk_sn_f.

Finalmente, si la firma del disco es la que esperamos, devolveremos el número de serie, que la regla udev empleará como ya hemos visto, y si el disco no es uno de los que buscamos, devolveremos la cadena NOID.

Obviamente, el contenido de /usr/local/lib/identify.in es la firma binaria que esperamos:

# hexdump /usr/local/lib/identify.in -e "\"%x\""
eeeeee


El segundo programa, action.sh, es el encargado de registrar y desregistrar los dispositivos, y de insertarlos y eliminarlos de la máquina virtual, así como de borrar el enlace simbólico /dev/externalusbdiskNOID en su caso:

#!/bin/bash
# fichero /usr/local/sbin/action.sh de XenHotplug

virt=mimaquinavirtual

if [ "$ACTION" = "add" ] ; then
if [ "$1" = NOID ] ; then
rm -f /dev/externalusbdisk$1
else
mkdir /var/xenhotplug/$1
echo $2 > /var/xenhotplug/$1/devname
/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/prelist
/usr/sbin/xm block-attach $virt phy:externalusbdisk$1 hdb$1 \
w 2>&1
/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/postlist
diff -u /var/xenhotplug/$1/{pre,post}list > \
/var/xenhotplug/$1/difflist
cat /var/xenhotplug/$1/difflist | grep ^+ | tail -n -1 | \
awk '{print \$1}'|awk -F + '{print \$2}' > \
/var/xenhotplug/$1/vdev
fi
else
if [ "$ACTION" = "remove" ] ; then
for file in /var/xenhotplug/* ; do
filecont=`cat $file/devname`
if [ "$filecont" = "$2" ] ; then
vdev=`cat $file/vdev`
/usr/local/sbin/assure-detach.sh $virt $vdev 2>&1
rm -rf $file
fi
done
fi
fi


El código actual no es exactamente ese, sino que incluye varias líneas de depurado y de registro que aquí no se muestran por simplicidad.


Es obvio que se trata de código aún no muy pulido, como en el guión shell anterior, no obstante hace su trabajo bastante bien. Eso sí: asume que la máquina virtual se llama mimaquinavirtual y que es la única máquina en la que insertamos dispositivos por esta vía, y asume que está permanentemente encendida.

Bueno, al grano. En primer lugar, vemos que hay dos grandes bloques de código, el primero se ejecuta si la variable de entorno $ACTION tiene el valor add y el segundo si tiene el valor remove. Esta variable de entorno la establece udev para el programa que ejecuta cuando se produce un evento.

El primer bloque, correspondiente a add, se subdivide en dos partes. Por un lado, si el programa se llama con un primer argumento de NOID, que recordemos es lo que ocurre cuando se introduce un disco no firmado, el programa borra el enlace simbólico espúreo y sale. Por otro lado, si el programa se llama con cualquier otro argumento supondremos que se trata del número de disco y entraremos en harina.

En primer lugar crearemos el directorio donde registramos este disco (mkdir /var/xenhotplug/$1), lo registramos en un fichero en ese directorio (echo $2 > /var/xenhotplug/$1/devname, hallamos la lista de dispositivos conectados a la máquina virtual (/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/prelist), insertamos el disco en la máquina virtual (/usr/sbin/xm block-attach $virt phy:externalusbdisk$1 hdb$1 w 2>&1) y volvemos a hallar la lista de dispositivos conectados (/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/postlist). Esto hay que hacerlo así porque no es posible determinar el número de dispositivo que Xen asigna a cada dispositivo insertado.

De hecho sí es posible, ya que el número asignado es una combinación de los números mayor y menor de dispositivo, pero aparte de complicado sólo vale cuando la máquina virtual es Linux.


Debido a ello debemos determinar el número de dispositivo asignado, para lo que hallamos la diferencia entre las dos listas de dispositivos (diff -u /var/xenhotplug/$1/{pre,post}list > /var/xenhotplug/$1/difflist), que es la línea correspondiente al dispositivo añadido, y extraemos de ella el citado número (cat /var/xenhotplug/$1/difflist |grep ^+|tail -n -1|awk '{print \$1}'|awk -F + '{print \$2}' > /var/xenhotplug/$1/vdev) y lo registramos también. Trabajo hecho.

Por otro lado, el segundo bloque, que se ejecuta cuando se retira un dispositivo, parece un poco más sencillo. Revisa a través de todos los dispositivos registrados en busca del que acaba de desaparecer (for file in /var/xenhotplug/* ; do ... done), poniendo temporalmente el nombre de cada uno en la variable $filecont (filecont=`cat $file/devname`) y comparándolo con el que buscamos (if [ "$filecont" = "$2" ] ; then ... fi). Cuando lo encuentra, registra temporalmente en la variable $vdev el número de dispositivo buscado (vdev=`cat $file/vdev`), llama a otro programa que se asegura de desconectar ese dispositivo de la máquina virtual (/usr/local/sbin/assure-detach.sh $virt $vdev 2>&1) y desregistra el dispositivo (rm -rf $file).

Directorio de registro



Bueno, el funcionamiento del directorio de registro quedó ya explicado arriba, con el fichero que lo usa, pero en resumen: el directorio /var/xenhotplug se llena con otros directorios, uno por cada dispositivo registrado. En el interior de estos se generan varios ficheros, de los cuales sólo son importantes dos, /var/xenhotplug/X/vdev, que contiene el número de dispositivo virtual Xen, y /var/xenhotplug/X/devname, que contiene el nombre de dispositivo del hierro que se utilizó para añadir a la máquina virtual. Crear uno de estos directorios con esos dos ficheros es lo que llamamos registrar un dispositivo, y borrarlo es lo que llamamos desregistrarlo.

assure-detach.sh



Estoooo... no lo voy a presentar. No está acabado. Y de hecho no funciona bien (aún). Pero básicamente lo que hace es llamar a /usr/sbin/xm block-detach $virt $vdev. El problema que tiene en este momento es que si el dispositivo está en uso en la máquina virtual, ésta ignora la orden de eliminarlo. Estoy trabajando en ello.

Firmar discos duros

PUBLICADA, PERO PENDIENTE DE MEJORAR EL FORMATO (no habrá cambios de contenido, salvo correcciones)


¿Cómo distinguir entre sí dos discos duros?

Evidentemente, la pregunta no es cómo distinguirlos por fuera, para eso basta con una pegatina. Sino cómo distinguirlos desde el ordenador.

La respuesta típica sería usar el número de serie. Una aplicación sencilla como smartctl nos da tan interesante dato, que además es único para cada disco.

Vale. Pero es que tengo el disco en una de esas cajas USB tan monas, que me dejan usar como disco externo USB un disco IDE.

Pues la respuesta general sería que lo llevo chungo. No obstante, aquí viene la solución. La he desarrollado cuando me surgió la necesidad de distinguir entre sí discos externos que utilizamos en Grupo CPD (la empresa para la que trabajo) para hacer copias de seguridad con Espiral (un programa que no uso) o con Dirvish (que yo sí uso, es la base de Espiral, y está basado en el viejo y eficaz rsync).

Un disco particionable almacena su tabla de particiones en el sector de arranque, ese lugar tan conocido llamado a veces MBR, objetivo de virus y elementos indeseables semejantes.

El sector de arranque de un disco particionable cualquiera, independientemente de que se utilice en Windows o Linux (no me comprometo con otros sistemas), es el primer sector del disco duro, es decir, los primeros 512 bytes, y tiene una estructura muy definida.

Los primeros 440 bytes (del 0 al 439) contienen el cargador de arranque del sistema. Es un programa muy pequeño que se carga en memoria al arrancar el ordenador, y se encarga de arrancar el ordenador desde una de las particiones disponibles. Ejemplos de programas de este tipo son el MBR de MS-DOS, LILO y la primera etapa (stage 1) de GRUB.

Los 64 bytes del 446 a 509 contienen la tabla de particiones. Y los dos últimos (510 y 511) contienen una firma especial, los números 55h y AAh, que identifican el sector completo como un sector de arranque.

Y ¿qué pasa con los bytes 440 al 445 de los que no he hablado?

Algunos sistemas, como Windows NT y sus sucesores, los emplean como un modo de identificar cada disco. El fdisk de Linux, además, no los toca. No me comprometo con otros programas como LILO, GRUB o el fdisk de MS-DOS/Windows.

Así, los 4 bytes del 440 al 443 son llamados "Número de serie de disco de Windows NT", y los otros dos, 444 y 445, no se emplean.

Personalmente empleo esos 4 bytes para firmar los discos IDE en cajas USB en algunos de mis sistemas. El primer disco contiene los bytes EE EE EE 01 (hexadecimal, en orden de disco, no olvidemos que la arquitectura Intel x86 almacena las palabras de 2 bytes con el byte de menor peso delante). El segundo disco contiene los bytes EE EE EE 02, y así sucesivamente. Aún no he llegado a tener 255 discos externos...

¿Y cómo lo hago? Fácil de lo fácil. Me descargo el sector de arranque del disco que quiero tocar, por ejemplo con dd:
dd if=/dev/sdb of=sdb_mbr count=1 bs=512

lo edito con un editor hexadecimal:
khexedit sdb_mbr

y lo vuelvo a meter en el disco:
dd if=sdb_mbr of=/dev/sdb

Y ya tengo mi disco firmado.

Para beneficio de los usuarios del editor hexadecimal, y ya que las direcciones suelen venir en hexadecimal, las repito en ese formato:






Código0 a 4390000h a 01B7h
Número de serie de disco de Windows NT440 a 44301B8h a 01BBh
Sin uso444 a 44501BCh a 01BDh
Tabla de particiones446 a 50901BEh a 01FDh
Firma mágica AA55h510 a 51101FEh a 01FFh

lunes, 13 de agosto de 2007

Certificados X509 para Apache 2 (I): Clave de la Autoridad Certificadora

PUBLICADA, PERO PENDIENTE DE MEJORAR EL FORMATO (no habrá cambios de contenido, salvo correcciones)



Bueno, a ver cómo van las cosas. Llegas un día y decides que quieres cambiar de protocolo: tu servidor Apache, con la costumbre de escuchar por el puerto 80 (HTTP), quieres que se ponga a escuchar en el puerto 443 (SSL) para usar eso que llaman el "protocolo HTTPS".

En una relación cifrada cliente-servidor como esa normalmente hay tres partes implicadas, no dos. Está el cliente, que no tiene motivos para confiar en el servidor, está el servidor, que no tiene motivos para confiar en el cliente, y está la Autoridad Certificadora. Así, con mayúsculas. Y la idea es que tanto el cliente como el servidor confían en la Autoridad Certificadora. Así, si el servidor presenta al cliente un documento de identidad certificado por la Autoridad Certificadora, el cliente puede estar seguro de que el servidor es quien realmente dice ser, y no un impostor.

Por eso, el papel de la Autoridad Certificadora es tan importante. Si he de meter mi número de tarjeta de crédito para hacer un pago en una página, quiero estar seguro de que el servidor que lo va a recibir es quien dice ser, y estoy seguro si mi navegador me dice que lo es.

Mi navegador me lo dice si el servidor se identifica mediante un certificado firmado por una Autoridad Certificadora en la que confíe. Y los navegadores confían en unas pocas Autoridades Certificadoras que tienen un negocio enorme con la venta de certificados. Evidentemente, los navegadores no confiarán en los certificados de cualquier autoridad certificadora, ya que cualquier ladrón informatizado que falsee una página puede falsear también su certificado creando una autoridad certificadora falsa.

Así que si no quieres pagar los 1500 (sí, mil quinientos) dólares que Verisign[1] u otros piratas semejantes cobran por un certificado, o si simplemente tienes algún motivo para convertirte en tu propia CA (Certificating Authority - Autoridad Certificadora), puedes hacerlo sin demasiados problemas (igual que lo haría el falsificador). Ten en cuenta que un servidor con SSL no funcionará sin un certificado.

Sólo necesitas tener instalado el paquete openssl, que proporciona las piezas y programas necesarios.

Crear la clave maestra de la CA


Lo primero es pensar en dónde almacenaremos todos los ficheros sensibles que vamos a crear. Lo ideal sería que creáramos un directorio sólo para esas cosas, con permisos adecuados, y que nos metiéramos en él antes de empezar a trabajar.

EDITO: He mejorado estas órdenes en una nueva entrada.

Nota: los comandos introducidos a partir de ahora son válidos en un sistema operativo de tipo UNIX, en particular yo los he probado en Linux, en la distribución Debian.

Antes de crear la clave crearemos un directorio llamado private para almacenar en él la clave privada:
mkdir private

A continuación creamos la clave:
openssl genrsa -des3 -out private/cakey.pem 2048

El comando openssl es la base de todo nuestro trabajo. Tiene un buen número de subcomandos para diferentes tareas. En esta ocasión utilizamos el subcomando genrsa para, precisamente, generar una clave RSA. Las opciones indican, por ese orden, que la clave generada estará protegida mediante un cifrado 3DES con contraseña (-3des) y que se almacenará en el directorio private en el fichero cakey.pem (-out private/cakey.pem), mientras que el argumento 2048 indica que la clave generada tendrá 2048 bits de longitud. La clave se almacenará en formato PEM, por eso ponemos la extensión como .pem, mientras que el nombre cakey indica, literalmente, que se trata de la clave de la CA. Esa es, además, la razón (aparte de que lo recomienda la documentación) de que la guardemos en el directorio private.

En la documentación oficial de OpenSSL se detalla que el subcomando genrsa es obsoleto y se debería usar en su lugar el subcomando genpkey, en cuyo caso la línea de órdenes sería:
openssl genpkey -des3 -outform PEM -out private/cakey.pem -pkeyopt rsa_keygen_bits:2048

Ojo: No he probado esta línea.

Bueno, en cualquier caso, como hemos pedido que la clave quede protegida mediante contraseña, tendremos que introducir la contraseña elegida, y volverla a introducir para confirmar que la hemos escrito bien:
openssl genrsa -des3 -out private/cakey.pem 2048
Generating RSA private key, 2048 bit long modulus
..........................................+++
.............................................+++
e is 65537 (0x10001)
Enter pass phrase for private/cakey.pem:
Verifying - Enter pass phrase for private/cakey.pem:

Como se puede ver, el generador nos indica que está creando una clave privada RSA de 2048 bits, con el exponente 65537, lo que es un dato técnico que no nos importa ahora.

¿Y qué son todos esos puntitos y cruces? Pues muy sencillo: hay que encontrar dos números primos, y se prueban números al azar a ver si lo son. Cada puntito es un número probado que pasa una prueba de primalidad sencilla, y cada cruz es un número que pasa una prueba mayor. El salto de línea es un número que pasa todas las pruebas, es decir, que es uno de los dos números primos que estamos buscando.

Ojo: no pierdas nunca la contraseña que has introducido para cifrar la clave privada de la CA. Sin esa contraseña, toda la estructura de la CA queda inutilizada.

Seguiré en otro artículo con la creación del certificado raíz, la creación de la clave del servidor y la creación del certificado del servidor.

Comienza la desbitácora

Bueno, aquí estamos.

He creado esta desbitácora (bitácora de desarrollo, igual que devlog de developer log) para ir apuntando todo lo que vaya descubriendo sobre la informática y el desarrollo de aplicaciones informáticas... y matemáticas.

No busquen aquí pensamientos ni cosas parecidas. Para eso está la bitácora madre: Envite.