En artículos anteriores expliqué cómo instalar y configurar VirtualBox en FreeBSD 10. VirtualBox es una excelente plataforma de virtualización para sistemas de escritorio y entornos de desarrollo. Sin embargo, cuando se necesita configurar sistema FreeBSD donde hospedar servidores virtuales, probablemente deseemos optar por una alternativa más adecuada a nuestras necesidades: QEMU+libvirt. Sobre todo si, como administradores de sistemas, venimos de entornos de virtualización sobre GNU/Linux y estamos más familiarizados con libvirt y su conjunto de herramientas.

Esto no significa que VirtualBox sea una plataforma inadecuada para utilizar como host de máquinas virtuales. Es perfectamente posible configurar un hipervisor VirtualBox headless (sin entorno gráfico) y gestionarlo totalmente utilizando el front-end VBoxManage (tarea que tengo pendiente para un futuro artículo). Además, las posibilidades que ofrece esta herramienta de línea de comandos son mucho más amplias y completas que las que ofrece la interfaz gráfica, al punto que una gran parte de las funcionalidades avanzadas de VirtualBox están sólo disponibles a través de línea de comandos (VBoxManage).

Sin embargo, quienes hemos hecho experiencia con QEMU+libvirt conocemos todas sus ventajas. Por ello, en este artículo voy a explicar paso a paso cómo instalar y configurar QEMU y libvirt, cómo crear una máquina virtual desde línea de comandos con virsh, y como configurar una red NAT utilizando el firewall ipfw provisto por FreeBSD.

Instalación y configuración de QEMU+libvirt

Recientemente se ha actualizado el port QEMU a la versión 2.4.1, lo que supone un avance importante en cuanto a características y evolución del emulador. Por ello (si no se ha hecho recientemente) es recomendable actualizar el sistema y la colección de ports antes de comenzar con la instalación:

# freebsd-update fetch && freebsd-update install
# portsnap fetch && portsnap update

Para que libvirt tenga soporte para QEMU, la instalación debe realizarse desde el port (el paquete no soporta QEMU actualmente):

Compilar e instalar libvirt utilizando el gestor de ports portmaster:

# portmaster devel/libvirt

Este se encargará a su vez de compilar e instalar cualquier por que sea dependencia de libvirt.

Durante la configuración del port, se debe habilitar el soporte para QEMU:

Al habilitar el soporte para QEMU, portmaster instalará automáticamente el port qemu-devel (actualmente en la versión 2.5.0).

A fin de poder utilizar las interfaces bridge y tap, las cuales proveen las interfaces de red para las máquinas virtuales QEMU, es necesario habilitar los módulos if_bridge, if_tap y aio el kernel de FreeBSD. Para ello, editar el archivo /boot/loader.conf:

root@hal9000:/usr/ports/devel/libvirt # nano /boot/loader.conf

Y agregar las siguientes líneas:

# QEMU
if_bridge_load="YES"
if_tap_load="YES"
aio_load="YES"

Sin embargo, para evitar reiniciar el sistema, es posible cargar los módulos manualmente:

root@hal9000:/usr/ports/devel/libvirt # kldload if_bridge
root@hal9000:/usr/ports/devel/libvirt # kldload if_tap
root@hal9000:/usr/ports/devel/libvirt # kldload aio

El demonio libvirtd es quien se encarga de proveer acceso al sistema de virtualización para gestionar y configurar tanto los componentes del host como las máquinas virtuales (guests). Es necesario habilitar este servicio en la configuración de FreeBSD:

root@hal9000:/usr/ports/devel/libvirt # nano /etc/rc.conf

Agregar las siguientes líneas:

# Libvirt
libvirtd_enable="YES"
virtlogd_enable="YES"

Por defecto, el port libvirt no posee una configuración de red habilitada, aunque posee disponible una definición de red "default" de ejemplo. Para habilitar esta red virtual, copiar el archivo de configuración dentro del directorio /usr/local/etc/libvirt/qemu/networks y crear el link simbólico necesario para que se levante automáticamente al iniciar el servicio:

root@hal9000:~ # cp /usr/local/share/examples/libvirt/networks/default.xml /usr/local/etc/libvirt/qemu/networks
root@hal9000:~ # ln -s ../default.xml /usr/local/etc/libvirt/qemu/networks/autostart/default.xml

El link simbólico debe quedar apuntando correctamente, tal como se observa a continuación:

root@hal9000:~ # ll /usr/local/etc/libvirt/qemu/networks/autostart/default.xml
lrwxr-xr-x  1 root  wheel  14 Feb 16 13:08 /usr/local/etc/libvirt/qemu/networks/autostart/default.xml@ -> ../default.xml
root@hal9000:~ # ll /usr/local/etc/libvirt/qemu/networks/
total 8
drwxr-xr-x  2 root  wheel  512 Feb 16 13:08 autostart/
-rw-r--r--  1 root  wheel  280 Feb 16 13:08 default.xml

A continuación se debe configurar el servicio libvirt, para ello editar el archivo de configuración del demonio:

root@hal9000:~ # nano /usr/local/etc/libvirt/libvirtd.conf

Configurar el demonio para que escuche peticiones en un socket UNIX:

unix_sock_group = "libvirt"
unix_sock_ro_perms = "0777"
unix_sock_rw_perms = "0770"
unix_sock_dir = "/var/run/libvirt"

De acuerdo a la configuración, es necesario agregar el grupo "libvirt":

root@hal9000:~ # pw group add libvirt

Es posible configurar el demonio para que acepte conexiones entrantes en un puerto TCP/IP utilizando TLS, tal como explica la siguiente página de la documentación de libvirt.

En este punto es posible iniciar libvirt:

root@hal9000:~ # service libvirtd start
Starting libvirtd.
root@hal9000:~ # service virtlogd start

Se debe iniciar tanto libvirtd como virtlogd.

Es posible verificar que el proceso haya iniciado utilizando ps:

root@hal9000:/usr/ports/devel/libvirt # ps aux | grep libvirt
root       34433   0.0  0.3  160060   24464  -  I    10:15AM   0:00.05 /usr/local/sbin/libvirtd --daemon -
root       34481   0.0  0.0   18844    2440  2  S+   10:16AM   0:00.00 grep libvirt

Es recomendable además verificar que libvirtd haya levantado la interfaz bridge "virbr0" correctamente:

root@hal9000:/usr/ports/devel/libvirt # ifconfig | grep virbr
virbr0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        member: virbr0-nic flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
virbr0-nic: flags=8942<BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
        inet6 fe80::5054:ff:fef2:2571%virbr0-nic prefixlen 64 tentative scopeid 0x5 

También verificar que el demonio haya abierto el socket UNIX donde aceptará conexiones entrantes:

root@hal9000:~ # netstat | grep libvirt
fffff80168eb05a0 stream      0      0 fffff80059a17ce8        0        0        0 /var/run/libvirt/libvirt-sock-ro
fffff8010ef07960 stream      0      0 fffff801c018b760        0        0        0 /var/run/libvirt/libvirt-sock

Actualmente está disponible el port para virt-manager (gestor gráfico de hosts y máquinas virtuales para diferentes plataformas), el cual funciona correctamente con bhyve (el hipervisor desarrollado por la comunidad FreeBSD). Sin embargo, virt-manager no funciona con QEMU en FreeBSD, a causa de una dependencia con grupos de control (cgroups) de Linux. Esto significa que se deben gestionar tanto el hipervisor como las máquinas virtuales utilizando la herramienta virsh. Tampoco se puede utilizar la herramienta virt-install provista por virt-manager, por lo cual se deben crear las máquinas virtuales desde archivos XML utilizando el comando virsh define.

Personalmente siempre preferí (y me parece una metodología de trabajo más adecuada, organizada y flexible) definir las máquinas virtuales en archivos XML (para luego crearlas con virsh define) por lo que no me afecta la carencia de virt-install. Esto permite guardar la configuración de las máquinas virtuales, incluso luego de que hayan sido borradas, y mantener plantillas de máquinas virtuales para, por ejemplo, clonarlas.

Además de verificar el correcto inicio de libvirt, es posible comprobar que posee soporte para el hipervisor QEMU:

root@hal9000:/usr/ports/devel/libvirt # virsh -V
Virsh command line tool of libvirt 1.3.1
See web site at http://libvirt.org/

Compiled with support for:
 Hypervisors: QEMU/KVM VMWare PHYP VirtualBox ESX Bhyve Test
 Networking: Remote Network Bridging
 Storage: Dir SCSI
 Miscellaneous: Daemon Secrets Debug Readline Modular

Notar que además de soportar QEMU, esta instalación de libvirt soporta hipervisores VMware (ESX/ESXi), VirtualBox y bhyve, entre otros. Esto significa que es posible gestionar dichos hipervisores en hosts remotos utilizando el conjunto de herramientas provistas por libvirt en esta instalación en FreeBSD, incluyendo virsh.

Luego de comprobar que libvirt haya iniciado correctamente, es posible conectarse al hipervisor QEMU con virsh:

root@hal9000:/usr/ports/devel/libvirt # virsh -c qemu:///system
Welcome to virsh, the virtualization interactive terminal.

Type:  'help' for help with commands
       'quit' to quit

virsh # exit

Al no haber arrojado errores, significa que se conectó con el hipervisor (en este caso significa que libvirt soporta QEMU y ha iniciado el emulador) de forma exitosa.

Creación de máquinas virtuales con virsh

El siguiente paso consiste en crear una máquina virtual, para ello se debe crear un archivo XML con la configuración de su hardware virtual. El siguiente archivo define una máquina virtual llamada "Alpine" con 1 GB de memoria RAM, 1 CPU de arquitectura x86_64 (provista por el emulador QEMU cuyo ejecutable se encuentra localizado en la ruta /usr/local/bin/qemu-system-x86_64), 1 unidad de CDROM con la imagen booteable de Alpine Linux cargada (desde la ubicación /home/emi/Downloads/alpine-3.3.1-x86_64.iso), una interfaz de red conectada a la red por defecto ("default", que ha sido definida luego de la instalación de libvirt), y un dispositivo gráfico de tipo VNC escuchando en el puerto 7777:

<domain type='qemu'>
  <name>Alpine</name>
  <memory>1024000</memory>
  <vcpu>1</vcpu>
  <os>
    <type arch='x86_64' machine='pc'>hvm</type>
    <boot dev='cdrom'/>
  </os>
  <devices>
    <emulator>/usr/local/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='cdrom'>
      <source file='/home/emi/Downloads/alpine-3.3.1-x86_64.iso'/>
      <target dev='hdc'/>
      <readonly/>
    </disk>
    <interface type='network'>
      <source network='default'/>
    </interface>
    <graphics type='vnc' port='7777'/>
  </devices>
</domain>

 

Al tratarse de un LiveCD no se le ha asignado un disco rígido, pero es posible agregarle uno fácilmente dentro de la sección de dispositivos (<devices>). Para ello remitirse a la documentación oficial de libvirt respecto al formato XML para definir dominios. Recordar que en la jerga libvirt, los guests (máquinas virtuales) se denominan "dominios".

Guardar el archivo XML y luego crear la máquina virtual a partir del mismo ejecutando virsh define:

root@hal9000:~ # virsh define alpine.xml 
Domain Alpine defined from alpine.xml

Al listar todas las máquinas virtuales, se observa la nueva "Alpine" en estado "apagada":

root@hal9000:~ # virsh list --all
 Id    Name                           State
----------------------------------------------------
 -     Alpine                         shut off

Para iniciarla, ejecutar virsh start:

root@hal9000:~ # virsh start Alpine
Domain Alpine started

Acceso a la consola gráfica mediante VNC

Tal como se ha definido la máquina virtual, es posible acceder a la consola gráfica de la misma utilizando un cliente VNC desde el host local. Claro está que en este caso se trata de un host FreeBSD con entorno gráfico (KDE), pero en caso de tratarse de un host headless (sin entorno gráfico, un servidor) existen varias soluciones.

Primero es posible configurar la máquina virtual para que el servidor VNC acepte conexiones desde el exterior (no sólo desde localhost). Para ello se debe configurar la interfaz gráfica de la siguiente forma, tal como explica el artículo Cómo acceder a la consola gráfica de una máquina virtual KVM en un host sin entorno gráfico (según el método 1):

<graphics type='vnc' port='7777' listen='0.0.0.0' passwd='1234'>

Según esta configuración, estaría aceptando conexiones en todas las interfaces de red del host, y la contraseña para acceder a la consola gráfica sería "1234".

Una alternativa más segura, sería implementar un túnel con SSH. Por ejemplo, si el servidor VNC acepta conexiones en el puerto 7777 sólo desde localhost (en el host "hal9000"), sería posible crear un túnel para acceder al servidor VNC desde el host "pepe" de la siguiente forma:

pepe@pepe:~ $ ssh -p 22 -L 8888:localhost:7777 emi@hal9000

Luego sería posible conectarse en el host "pepe" utilizando un cliente VNC a localhost:8888.

Por último, se podría configurar la máquina virtual para que utilice el protocolo "spice", tal como se explica en el artículo Acceso remoto a máquinas virtuales KVM utilizando el cliente SPICE.

Existen muchos clientes VNC, por ejemplo KRDC:

Luego de iniciar la máquina virtual, se accede mediante VNC a la consola gráfica:

Se observa que el modelo de procesador que ve la máquina virtual es un "QEMU Virtual CPU version 2.5+":

Networking, NAT y acceso a Internet desde las máquinas virtuales

La red "default" provee una conexión para las máquinas virtuales y ofrece direcciones IP de forma dinámica mediante DHCP en la red 192.168.122.0/24. Es posible comprobar, una vez que inicia el servicio libvirt, que la interfaz virbr0 tiene asignada la dirección IP 192.168.122.1:

root@hal9000:~ # ifconfig virbr0
virbr0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        ether 02:73:9c:da:e2:00
        inet 192.168.122.1 netmask 0xffffff00 broadcast 192.168.122.255 
        nd6 options=1<PERFORMNUD>
        id 00:00:00:00:00:00 priority 32768 hellotime 2 fwddelay 4
        maxage 20 holdcnt 6 proto rstp maxaddr 2000 timeout 1200
        root id 00:00:00:00:00:00 priority 32768 ifcost 0 port 0
        member: virbr0-nic flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
                ifmaxaddr 0 port 5 priority 128 path cost 2000000

Además es posible conectarse desde el guest al host y viceversa:

Y desde el host es posible determinar los leases DHCP con el siguiente comando:

root@hal9000:~ # virsh net-dhcp-leases --network default
 Expiry Time          MAC address        Protocol  IP address                Hostname        Client ID or DUID
-------------------------------------------------------------------------------------------------------------------
 2016-02-19 11:49:02  52:54:00:f3:e3:c7  ipv4      192.168.122.42/24         -               01:52:54:00:f3:e3:c7

Sin embargo, si se desea que los guests tengan acceso a Internet, será necesario implementar un mecanismo de NAT (Network Address Translation). De esto debe encargarse el host FreeBSD, pues (a diferencia de VirtualBox) tanto el emulador QEMU como libvirt no lo incluyen entre sus funcionalidades.

Tal como mencioné en el artículo Implementando traffic shaping en FreeBSD con dummynet, la implementación de NAT dependerá del firewall escogido. Cada uno implementa NAT a su manera.

Una vez más voy a optar por IPFW, tal como hice al implementar packet shaping. De acuerdo al manual de FreeBSD, es posible implementar NAT con IPFW mediante reglas divert utilizando el demonio natd. Sin embargo, existe la posibilidad de realizar NAT a nivel kernel, tal como explica la página de manual de ipfw. Personalmente opté por implementar NAT a nivel kernel (in-kernel), tal como hace Linux con iptables, por seguridad, para evitar tener un servicio más, y para minimizar el uso de recursos. La ventaja que tiene natd sobre la implementación in-kernel es la posibilidad de crear reglas de forma dinámica.

Para contar con la capacidad de implementar NAT a nivel kernel utilizando IPFW, será necesario recompilar el kernel de FreeBSD habilitando las siguientes opciones:

# IPFW
options    IPFIREWALL                   # enables IPFW
options    IPFIREWALL_VERBOSE           # enables logging for rules with log keyword
options    IPFIREWALL_VERBOSE_LIMIT=5   # limits number of logged packets per-entry
options    IPFIREWALL_DEFAULT_TO_ACCEPT # sets default policy to pass what is not explicitly denied
options    DUMMYNET                     # ipfw packet shaping support
options    IPFIREWALL_NAT               # ipfw in-kernel nat support
options    LIBALIAS                     # required for in-kernel NAT / replacement for NATd

Luego de compilar el kernel, es necesario habilitar IPFW en el archivo /etc/rc.conf, al igual que el ruteo de paquetes:

root@hal9000:~ # nano /etc/rc.conf
# Firewall
firewall_enable="YES"
gateway_enable="YES"

Además es necesario configurar el firewall para que acepte todo por defecto y limite el número de paquetes logueados por entrada (para evitar ataques DoS)

root@hal9000:~ # nano /boot/loader.conf
#firewall
net.inet.ip.fw.default_to_accept="1"
net.inet.ip.fw.verbose_limit="5"

Cabe aclarar que esta configuración es redundante si se ha compilado el kernel con las opciones IPFIREWALL_VERBOSE_LIMIT=5 y IPFIREWALL_DEFAULT_TO_ACCEPT.

Al reiniciar el sistema con el nuevo kernel, se pueden listar las reglas que incluye IPFW por defecto:

root@hal9000:~ # ipfw list
00100 allow ip from any to any via lo0
00200 deny ip from any to 127.0.0.0/8
00300 deny ip from 127.0.0.0/8 to any
00400 deny ip from any to ::1
00500 deny ip from ::1 to any
00600 allow ipv6-icmp from :: to ff02::/16
00700 allow ipv6-icmp from fe80::/10 to fe80::/10
00800 allow ipv6-icmp from fe80::/10 to ff02::/16
00900 allow ipv6-icmp from any to any ip6 icmp6types 1
01000 allow ipv6-icmp from any to any ip6 icmp6types 2,135,136
65535 allow ip from any to any

Desde la máquina virtual, se observa que no hay acceso a Internet (a pesar de que sí se llega hasta el host, tal como muestran las capturas anteriores):

Suponiendo que la salida a Internet es a través de la interfaz em0, se deberá crear una configuración de nat para que enmascare el tráfico saliente:

root@hal9000:~ # ipfw nat 1 config if em0 log deny_in same_ports reset
ipfw nat 1 config if em0 log deny_in same_ports reset
root@hal9000:~ # ipfw nat 1 show config
ipfw nat 1 config if em0 log deny_in same_ports reset

Cada configuración de nat debe tener un número (1 en el ejemplo anterior).

Luego se deben agregar las reglas del firewall para que todo el tráfico entrante y saliente sea "nateado":

root@hal9000:~ # ipfw add 2000 nat 1 ip from any to any via em0 in
02000 nat 1 ip from any to any via em0 in
root@hal9000:~ # ipfw add 2001 nat 1 ip from any to any via em0 out
02001 nat 1 ip from any to any via em0 out
root@hal9000:~ # ipfw list 2000 2001
02000 nat 1 ip from any to any via em0 in
02001 nat 1 ip from any to any via em0 out

Una vez agregadas las reglas se puede comprobar que no se haya roto nada (algo frecuente al probar configuraciones de firewall).

root@hal9000:~ # ping -c 1 www.google.com
PING www.google.com (173.194.42.51): 56 data bytes
64 bytes from 173.194.42.51: icmp_seq=0 ttl=53 time=15.077 ms

--- www.google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 15.077/15.077/15.077/0.000 ms

Finalmente se puede comprobar que el guest accede a Internet, lo que indica que las reglas de nat están siendo aplicadas correctamente:

En caso de necesitar implementar port forwarding (permitir el acceso externo hacia un determinado puerto de un guest), esto debe hacerse directamente en la configuración de la nat. Es posible reconfigurar una nat existente.

El reenvío de puertos (port forwarding) se realiza a través de la directiva redirect_port:

ipfw nat 1 config if em0 log deny_in same_ports reset redirect_port tcp 192.168.122.42:80 80

En este ejemplo se está abriendo el puerto 80 en el host (en la IP pública, externa o accesible desde hosts remotos) y redirigiendo el tráfico hacia el puerto 80 del guest accesible mediante la dirección IP 192.168.122.42.

Referencias

Qemu on FreeBSD as host

libvirt: Domain XML format

libvirt: Firewall and network filtering in libvirt

FreeBSD - System Manager's Manual - IPFW(8)

FreeBSD - System Manager's Manual - IPFW(8) - NETWORK ADDRESS TRANSLATION (NAT)

Alpine Linux - Configure Networking

man livbirtd
man qemu
man ipfw


Tal vez pueda interesarte


Compartí este artículo