OpenBSD en alta disponibilidad (High Availability)

© 31 de julio de 2024 por Adrián Alí
Licenciado bajo los terminos de Creative Commons Attribution 4.0 International
Este trabajo fue desarrollado integramente en OpenBSD Release 7.5
Contacto: adrianali arroba fortix.com.ar

Índice

1. Introducción
2. Que es CARP
2.1. Sobre los programadores de CARP
2.2. Conceptos y parámetros de configuración del protocolo CARP
2.3. Entendiendo CARP con un ejemplo sencillo
2.3.1. Secuencia de comandos en el host "B" del grupo de redundancia
2.3.2. Secuencia de comandos en el host "A" del grupo de redundancia
2.4. Posibles estados de la interfaz del protocolo CARP
2.5. Que tener en cuenta en la elección del Master
2.6. Guardar la configuración de CARP de forma persistente en el sistema operativo
2.7. Monitoreo y logs
2.7.1. Analizar tráfico CARP con tcpdump
2.7.2. Cambios de estado y consulta en logs
2.7.3. Incrementar verbosidad de logs
2.7.4. Generar conmutación por error usando contador de degradación de CARP
3. Que es pfsync
3.1. Sobre los programadores de pfsync
3.2. Estados en PF
3.2.1. Ejemplo de seguimiento de un estado
3.2.2. Como impacta los cambios de reglas, flush de estados en conexiones ya establecidas
3.3. Modos de conexión entre hosts pfsync
3.3.1. Conexión con protocolo multicast
3.3.2. Conexión con protocolo unicast
3.4. Segurizar conexión pfsync
4. Ejemplo práctico del uso de CARP y pfsync
4.1. Configuración inicial de los hosts
4.2. Descripción general de los pasos de configuración
4.2.1. Configuración de variables de estados en el kernel (sysctl)
4.2.2. Configuración de las interfaces de red de la VM
4.2.3. Configuración de las interfaces del protocolo CARP
4.2.4. Configuración de túnel VPN Wireguard para segurizar pfsync
4.2.5. Configuración de la interfaz del protocolo pfsync
4.2.6. Configuración de PF en función de los estados a sincronizar
4.2.7. Probar la configuración de HA en OpenBSD
4.2.7.a. Primer test host "A" en estado Master
4.2.7.b. Segundo test degradar host "A" al estado de Backup
4.2.7.c. Tercer test volver host "A" al estado de Master
5. Sincronización de archivos entre hosts
5.1. Unison
5.1.1. Sobre los programadores de Unison
5.2. Instalar Unison
5.3. Diseñar esquema de sincronización
5.4. Configurar Unison
5.4.1. Usuario para ejecutar la sincronización
5.4.1.a. Creación de usuario
5.4.1.b. Generación del par de llaves ssh
5.4.1.c. Método de autenticación ssh por publickey entre ambos hosts
5.4.2. Configurar en Unison una tarea de sincronización
5.4.3. Script de arranque RC para Unison
6. Lista de referencias

1. Introducción

Surgió la necesidad en mi trabajo de realizar una implementación de OpenBSD en alta disponibilidad, la llamada en ingles High Availability, abreviada como HA, como vengo usando OpenBSD hace unos años se que el mejor lugar para encontrar información es la documentación oficial, así que rápidamente me dirigí ahí y en un minuto ya estaba leyendo la sección:

OpenBSD PF - Firewall Redundancy (CARP and pfsync)

del manual oficial:

OpenBSD PF - User's Guide

Por otro lado, mientras aprendo un tema nuevo me gusta ir haciendo anotaciones en español de lo que voy leyendo y probando en la computadora, ayuda a que mi cerebro retenga mejor ese tema nuevo que si solamente lo leo en ingles y lo implemento. Entonces pensé en no ser tan egoísta y hacer un post en español en mi sitio que quizás pueda servir a otro hispanohablante.

A diferencia de la documentación oficial, en este documento vamos a realizar varios ejercicios de configuración para al final lograr una implementación completa y funcional de HA sobre OpenBSD. La idea no es repetir lo que ya dice la documentación oficial, sino sumar ejercicios que nos lleven a conclusiones que nos permitan aprender estos temas más rápidamente.

Al ser un apasionado de Unix y del software que corre en él me gusta leer las historias que hay por detrás del desarrollo de un programa, por ello también encontraras en este documento algunas referencias para que el que comparte esta pasión.

Ademas de lo anterior también voy a traducir algunos párrafos de la documentación oficial, los cuales creo necesarios para que no haya dudas sobre la interpretación de conceptos base.

Ya que voy a traducir partes del manual oficial de CARP del sitio de OpenBSD y pegarlo en mi propio sitio voy a copiar la licencia del mismo:

file: faq/pf/carp.html

Copyright (c) 2005-2007 Joel Knight <enabled@myrealbox.com>

Permission to use, copy, modify, and distribute this 
documentation for any purpose with or without fee is 
hereby granted, provided that the above copyright notice 
and this permission notice appear in all copies.

THE DOCUMENTATION IS PROVIDED "AS IS" AND THE AUTHOR 
DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS 
DOCUMENTATION INCLUDING ALL IMPLIED WARRANTIES OF 
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR 
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING 
FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING 
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 
THIS DOCUMENTATION

2. Que es CARP

CARP es un protocolo que hace posible tener una misma dirección IP en dos o más hosts en una misma red LAN. Existen otros protocolos que realizan funciones similares, como:

  1. HSRP
  2. GLBP
  3. VRRP

Los dos primeros son propiedad de la empresa CISCO y si bien el tercero fue desarrollado por la IETF está también bajo las patentes de la empresa CISCO. Por ese motivo el proyecto OpenBSD decide desarrollar CARP, para no estar bajo el dominio de las molestas patentes de CISCO. En el proyecto OpenBSD realmente se preocupan para que el software sea realmente libre y eso significa no solo tener una licencia libre, si no también estar libre de las ataduras de las patentes. Hay que tener en cuenta que de cada protocolo puede haber varias implementaciones, por ejemplo de VRRP en el mundo Linux podemos nombrar la más popular que es keepalived. Pero también tenemos uvrrpd y vrrpd.

Con CARP también tenemos varias implementaciones, la original o inicial, que es la que se desarrolló en el proyecto OpenBSD, que es la que aprenderemos a configurar en este manual. También tenemos las implementaciones de CARP en los proyectos NetBSD y FreeBSD, estas implementaciones heredaron código de la original y en el caso de la implementación de FreeBSD la misma fue significativamente reescrita en este último. Encontramos ademas una implementación en el espacio de usuario de CARP en Linux llamada UCARP, esta implementación está mínimamente relacionada a nivel de código con la original, se hizo llamar portable CARP y aparentemente podía correr en varios sistemas operativos:

"The software has been successfully tested on Linux 2.4, Linux 2.6, MacOS X, OpenBSD, MirBSD and NetBSD."[6]

Fue usada y venía en los repositorios de paquetes de varias distribuciones Linux, ejemplo de página de man de UCARP en Ubuntu:

https://manpages.ubuntu.com/manpages/bionic/man8/ucarp.8.html

Esta implementación fue cerrada el 24 de abril de 2019, el propietario del repositorio y desarrollador principal decidió archivar el repositorio, dejándolo solo lectura, lo que en otras palabras significa que cancelo el desarrollo de UCARP.

Para aclarar aún más la función de CARP en OpenBSD vamos a empezar leyendo el primer punto del manual oficial:

"Introducción a CARP

CARP es un protocolo y sus siglas significan Common Address Redundancy Protocol. Su objetivo principal es permitir que varios hosts en el mismo segmento de red compartan una dirección IP. CARP es una alternativa segura y libre a VRRP Virtual Router Redundancy Protocol (VRRP) y al protocolo Hot Standby Router Protocol (HSRP). CARP funciona permitiendo que un grupo de hosts en el mismo segmento de red compartan una dirección IP. Este grupo de hosts se denomina "grupo de redundancia". Al grupo de redundancia se le asigna una dirección IP que se comparte entre los miembros del grupo. Dentro del grupo, un host se designa como "Master" y el resto como "Backups". El Master host es el que actualmente "posee" la IP compartida; responde a cualquier tráfico o solicitud ARP dirigida hacia esa dirección IP. Cada host puede pertenecer a más de un grupo de redundancia a la vez.

Un uso común de CARP es crear un grupo de firewalls redundantes. La IP virtual asignada al grupo de redundancia se configura en las computadoras clientes como puerta de enlace predeterminada. En caso de que el firewall maestro sufra una falla o se desconecte, la IP se moverá a uno de los firewalls de respaldo y el servicio continuará sin verse afectado.

CARP es compatible con IPv4 e IPv6."[1]

También me resulta interesante citar este par de párrafos relacionados a la historia de este protocolo sacado de la Wikipedia:

"A finales de la década de 1990, el Grupo de Trabajo de Ingeniería de Internet (IETF) comenzó a trabajar en un protocolo para la redundancia de enrutadores. En 1997, Cisco informó al IETF que tenía patentes en esta área y, en 1998, señaló su patente sobre HSRP. No obstante, el IETF continuó trabajando en el VRRP. Después de algún debate, el grupo de trabajo VRRP del IETF decidió aprobar el estándar, a pesar de su dependencia de técnicas patentadas, siempre y cuando Cisco pusiera la patente a disposición de terceros bajo términos de licencia razonables y no discriminatorios. Debido a que VRRP solucionó problemas con el protocolo HSRP, Cisco comenzó a utilizar VRRP en su lugar, sin dejar de reclamarlo como propio."[5]

"Cisco informó a los desarrolladores de OpenBSD que haría cumplir su patente sobre HSRP. La posición de Cisco pudo deberse a su demanda con Alcatel. Como los términos de la licencia de Cisco impedían la implementación de VRRP de código abierto, los desarrolladores de OpenBSD comenzaron a desarrollar CARP. OpenBSD se centra en la seguridad. Diseñaron CARP para utilizar criptografía. Esto hizo que CARP fuera fundamentalmente diferente de VRRP y aseguró que CARP no infringiera la patente de Cisco. CARP estuvo disponible en octubre de 2003. Posteriormente, se integró en FreeBSD (lanzado por primera vez en mayo de 2005 con FreeBSD 5.4), NetBSD y Linux (UCARP). Si bien la patente estadounidense de Cisco expiró en 2014, los dos protocolos incompatibles continúan coexistiendo."[5]

Si se tiene más curiosidad sobre CARP y sus inicios recomiendo leer la historia completa con el relato de lo que pasó entre IETF, Cisco y OpenBSD y porque esto dio nacimiento a CARP:

http://www.openbsd.org/lyrics.html#35

2.1. Sobre los programadores de CARP

La idea de nombrar a los programadores es hacer un reconocimiento a su excelente trabajo, seguramente también colaboraron muchas personas no solo codificando, sino haciendo pruebas, documentando, etc. a los cuales va todo nuestro agradecimiento ya que hacen nuestra vida más fácil administrando OpenBSD.

Como podemos leer en el encabezado del archivo de código de CARP:

netinet/ip_carp.c

figuran estos tres programadores:

* Copyright (c) 2002 Michael Shalayeff. All rights reserved.
* Copyright (c) 2003 Ryan McBride. All rights reserved.
* Copyright (c) 2006-2008 Marco Pfatschbacher. All rights reserved.

y en el encabezado del archivo de código principal de pfsync:

net/if_pfsync.c

figuran:

Copyright (c) 2002 Michael Shalayeff
Copyright (c) 2008 David Gwynne <dlg@openbsd.org>

Al escribir este manual nos enteramos que Michael "Mickey" Shalayeff falleció en el 2017. En su momento se envió este mensaje de correo avisando en la lista del New York City *BSD User Group (NYC*BUG):

[talk] mickey@ OpenBSD RIP

copio aquí el mensaje original:

[talk] mickey@ OpenBSD RIP
George Rosamond george at ceetonetechnology.com
Sat Aug 12 23:59:00 EDT 2017

Mickey Shalayeff passed recently.

He was a longtime OpenBSD developer and had his 
fingerprints throughout the operating system. He lived in 
Brooklyn for many years and was involved in 
infrastructures around NYC. He was at Calyx (.com) when 
Nick Merrill got the NSL.

He did a great NYC*BUG meeting for us many years ago.

I'll give more details later, and there's so many crazy 
stories about Mickey.

He'll be sorely missed. He was talented and eternally 
devoted to good code. Personally, I'll miss the long 
rambling emails we had so many times hitting tech, 
politics and food.

He was the only person I ever met who put salt and pepper 
in coffee. He communicated in mickeyese. Unintelligible 
to many, but hysterical to read and listen to.

mickey.lucifer.net is filled with some of his gems, 
including his take on the alleged OpenBSD IPSec backdoor.

http://images.kd85.com/mickey/IMAG0484.jpg

RIP mickey@

su repositorio github con algunos de sus trabajos:

McIkye en github

Se puede contar su nombre en 257 archivos de código en el kernel de OpenBSD, una gran perdida para este proyecto y para el software FLOSS en general.

2.2. Conceptos y parámetros de configuración del protocolo CARP

Vamos a leer algo de teoría antes de realizar configuraciones, para ello copio la traducción del punto "Configurando CARP" del manual oficial:

"Configurando CARP

Cada grupo de redundancia está representado por una interfaz de red virtual carp(4). Como tal, CARP se configura usando ifconfig(8).


# ifconfig carpN create
# ifconfig carpN vhid vhid [pass password] [carpdev carpdev] \
   [advbase advbase] [advskew advskew] [state state] [group|-group group] \
   ipaddress netmask mask

  • carpN El nombre de la interfaz virtual carp(4) donde N es un número entero que representa el número de la interfaz (por ejemplo, carp10).

  • vhid La identificación del host virtual (Virtual Host ID). Este es un número único que se utiliza para identificar el grupo de redundancia ante otros hosts del grupo y para ante otros grupos en la misma red. Los valores aceptables son del 1 al 255. Debe ser el mismo en todos los miembros del grupo.

  • password La contraseña de autenticación que se utilizará al hablar con otros hosts habilitados para CARP en este grupo de redundancia. Este debe ser el mismo para todos los miembros del grupo.

  • carpdev Este parámetro opcional especifica la interfaz de red física que pertenece a este grupo de redundancia. De forma predeterminada, CARP intentará determinar qué interfaz usar buscando una interfaz física que esté en la misma subred que la combinación de dirección IP y máscara proporcionada a la interfaz carp(4).

  • advbase Este parámetro opcional especifica con qué frecuencia, en segundos, anunciar que somos miembros del grupo de redundancia. El valor predeterminado es 1 segundo. Los valores aceptables son del 1 al 255.

  • advskew Este parámetro opcional especifica cuánto sesgar la base de datos al enviar anuncios CARP. Manipulando advskew, se puede elegir el anfitrión CARP maestro. Cuanto mayor sea el número, menos preferido será el anfitrión a la hora de elegir un maestro. El valor predeterminado es 0. Los valores aceptables son de 0 a 254.

  • state Fuerza una interfaz carp(4) a un estado determinado. Los estados válidos son Init, Backup y Master.

  • group, -group Agregue o elimine una interfaz carp(4) a un determinado grupo de interfaces. De forma predeterminada, todas las interfaces de CARP se agregan al grupo carp. Cada grupo tiene un contador que afecta a todas las interfaces de CARP que pertenecen a ese grupo. Si una interfaz física habilitada para CARP deja de funcionar, CARP aumentará el contador de degradación en 1 en los grupos de interfaces de los que es miembro la interfaz carp(4), lo que de hecho provocará que todos los miembros del grupo conmuten por error juntos.

  • ipaddress Esta es la dirección IP compartida asignada al grupo de redundancia. Esta dirección no tiene que estar en la misma subred que la dirección IP en la interfaz física (si está presente). Sin embargo, esta dirección debe ser la misma en todos los hosts del grupo.

  • mask La máscara de subred de la IP compartida.

Se puede controlar más comportamiento de CARP mediante sysctl(8).

  • net.inet.carp.allow Aceptar paquetes CARP entrantes o no. El valor predeterminado es 1 (sí).

  • net.inet.carp.preempt Permitir que un host dentro de un grupo de redundancia, que tengan mejores valores en advbase y advskew sea seleccionado como Maestro. net.inet.carp.preempt viene por defecto en 0 (deshabilitado) de forma predeterminada.

  • net.inet.carp.log Cambio de estado, paquetes defectuosos y otros errores. Este valor puede estar entre 0 y 7, correspondiente a las prioridades de syslog(3). El valor predeterminado es 2 (solo mostrar cambios de estado)."[1]

2.3. Entendiendo CARP con un ejemplo sencillo

Antes de pasar al ejemplo leamos el siguiente párrafo del manual oficial:

"Como funciona CARP (traducido del ingles tal cual el manual oficial)

El Master host del grupo envía anuncios sistemáticamente a la red local para que los Backup hosts sepan que todavía está activo. Si los Backup hosts no escuchan un anuncio del Master host durante un período de tiempo determinado, uno de ellos asumirá las funciones del Master (cualquier Backup host que tenga los valores de advbase y advskew configurados más bajos). Es posible que existan varios grupos CARP en el mismo segmento de red. Los anuncios CARP contienen el ID del virtual host que permite a los miembros del grupo identificar a qué grupo de redundancia pertenece el anuncio. Para evitar que un usuario malintencionado en el segmento de la red falsifique los anuncios CARP, en cada grupo se puede configurar un password. Cada paquete CARP enviado al grupo está protegido por un HMAC SHA1."[1]

Ahora que ya leimos como funciona CARP veamos un ejemplo sencillo, tenemos dos host con las siguientes IPs para management:

  • obsda: 10.0.0.100/24
  • obsdb: 10.0.0.200/24

y una IP de LAN que van a compartir con CARP: 192.168.46.254/24

el ejemplo será implementando usando maquinas virtuales, notar que por eso las interfaces de red son del tipo "vio".

Diagrama:

10.0.0.200/24  .---.192.168.46.254/24.---.
           vio0| B |<--------------->| A |vio0
               '---'      carp0      '---'  10.0.0.100/24

2.3.1. Secuencia de comandos en el host "B" del grupo de redundancia

Primero consultamos con el comando ifconfig(8) el estado de las interfaces de red:

obsdb ~ root # ifconfig -A
lo0: flags=2008049<UP,LOOPBACK,RUNNING,MULTICAST,LRO> mtu 32768
	index 3 priority 0 llprio 3
	groups: lo
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
	inet 127.0.0.1 netmask 0xff000000
vio0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:79
	index 1 priority 0 llprio 3
	groups: egress
	media: Ethernet autoselect
	status: active
	inet 10.0.0.200 netmask 0xffffff00 broadcast 10.0.0.255
enc0: flags=0<>
	index 2 priority 0 llprio 3
	groups: enc
	status: active
pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33136
	index 5 priority 0 llprio 3
	groups: pflog
obsdb ~ root #

Por defecto, por lo menos en OpenBSD 7.5 que es la versión que usé para escribir este manual, CARP se encuentra activo por default en el kernel, lo consultamos con el comando:

obsdb ~ root # sysctl net.inet.carp.allow
net.inet.carp.allow=1
obsdb ~ root #

por cualquier actualización a futuro, que traiga cambios en este comportamiento por defecto, podemos dejar persistente que siempre este activo CARP de la siguiente forma:

obsdb ~ root # echo 'net.inet.carp.allow=1' >> /etc/sysctl.conf
obsdb ~ root #

otro parámetro que nos interesa configurar es:

obsdb ~ root # sysctl net.inet.carp.preempt
net.inet.carp.preempt=0
obsdb ~ root #

que viene deshabilitado por defecto, con este parámetro hacemos que el Master sea el host con mejores valores en advbase y advskew. Lo configuamos en caliente:

obsdb ~ root # sysctl net.inet.carp.preempt=1
net.inet.carp.preempt: 0 -> 1
obsdb ~ root #

y dejamos persistente la configuración:

obsdb ~ root # echo 'net.inet.carp.preempt=1' >> /etc/sysctl.conf
obsdb ~ root #

para que no haya duda hemos agregado al archivo:

/etc/sysctl.conf

estos dos valores:

net.inet.carp.allow=1
net.inet.carp.preempt=1

ahora vamos a proceder a crear la interfaz para CARP:

obsdb ~ root # ifconfig carp0 create
obsdb ~ root #

y por último vamos a proceder a configurarlo:

obsdb ~ root # ifconfig carp0 vhid 86 pass 4PAmAmVfRaRG carpdev vio0 advskew 20 192.168.46.254 netmask 255.255.255.0
obsdb ~ root #

ahora consultamos con el comando ifconfig(8) como quedó configurado:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:56
	index 6 priority 15 llprio 3
	carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 20
	groups: carp
	status: master
	inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #

Como vemos en el campo status: al ser el host "B" el primero en levantar en el grupo de redundancia, éste queda con el estado de Master, aunque las transición es primero Init, luego backup y Master si corresponde. Ver más adelante una explicación de los estados de las interfaces del protocolo CARP.

Para una explicación detallada de las opciones del comando ifconfig(8) para la creación de la interfaz del protocolo CARP, leer la sección CARP en la página del manual:

https://man.openbsd.org/ifconfig#CARP

Para borrar la interfaz CARP el comando sería:

obsdb ~ root # ifconfig carp0 destroy
obsdb ~ root #

2.3.2. Secuencia de comandos en el host "A" del grupo de redundancia

Primero consultamos con el comando ifconfig(8) el estado de las interfaces de red:

obsda ~ root # ifconfig -A
lo0: flags=2008049<UP,LOOPBACK,RUNNING,MULTICAST,LRO> mtu 32768
    index 3 priority 0 llprio 3
    groups: lo
    inet6 ::1 prefixlen 128
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
    inet 127.0.0.1 netmask 0xff000000
vio0: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
    lladdr fe:e1:bb:d1:a9:b9
    index 1 priority 0 llprio 3
    groups: egress
    media: Ethernet autoselect
    status: active
    inet 10.0.0.100 netmask 0xffffff00 broadcast 10.0.0.255
enc0: flags=0<>
    index 2 priority 0 llprio 3
    groups: enc
    status: active
pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33136
    index 5 priority 0 llprio 3
    groups: pflog
obsda ~ root #

Como hicimos en el host "B" verificamos que CARP este habilitado en el kernel y dejamos persistente esa configuración:

obsda ~ root # sysctl net.inet.carp.allow
net.inet.carp.allow=1
obsda ~ root #
obsda ~ root # echo 'net.inet.carp.allow=1' >> /etc/sysctl.conf
obsda ~ root #

ademas dejamos persistente esta otra configuración:

obsda ~ root #
obsda ~ root # sysctl net.inet.carp.preempt=1
net.inet.carp.preempt: 0 -> 1
obsda ~ root #
obsda ~ root # echo 'net.inet.carp.preempt=1' >> /etc/sysctl.conf
obsda ~ root #

ahora creamos la interfaz del protocolo CARP:

obsda ~ root #
obsda ~ root # ifconfig carp0 create
obsda ~ root #

vemos que si bien esta creada, no esta configurada, vemos que su estado es invalid:

obsda ~ root # ifconfig carp0
carp0: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:00:00:00:00
    index 6 priority 15 llprio 3
    groups: carp
    status: invalid
obsda ~ root #

ahora la configuramos:

obsda ~ root #
obsda ~ root # ifconfig carp0 vhid 86 pass 4PAmAmVfRaRG carpdev vio0 advskew 10 192.168.46.254 netmask 255.255.255.0
obsda ~ root #

y consultamos como quedo la configuración:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 6 priority 15 llprio 3
    carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 10
    groups: carp
    status: master
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

como vemos levantó y con el estado de Master, eso porque tiene el parámetro advskew en 10 o sea menor que el host "B" que lo tiene configurado en 20. Al levantar el host "A" como Master el host "B" pasó al estado Backup:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 6 priority 15 llprio 3
    carp: BACKUP carpdev vio0 vhid 86 advbase 1 advskew 20
    groups: carp
    status: backup
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #

estos cambios se registran en los logs del sistema, en el archivo:

/var/log/messages

ejemplo:

obsdb ~ root # tail -f /var/log/messages | grep carp
Mar 26 01:29:47 obsdb /bsd: carp0: state transition: BACKUP -> MASTER
Mar 26 01:45:12 obsdb /bsd: carp0: state transition: MASTER -> BACKUP
Mar 26 01:57:51 obsdb /bsd: carp0: state transition: BACKUP -> MASTER
Mar 26 01:58:55 obsdb /bsd: carp0: state transition: BACKUP -> MASTER
Mar 26 02:01:16 obsdb /bsd: carp0: state transition: MASTER -> BACKUP

2.4. Posibles estados de la interfaz del protocolo CARP

  • Invalid: no configurado aún.
  • Backup: Backup del grupo, porque se descubrió un Master activo.
  • Master: Master del grupo.
  • Init: iniciando, pasa a modo Backup inicialmente y en ese modo busca un Master, si lo encuentra queda en Backup sino pasa a modo Master.

2.5. Que tener en cuenta en la elección del Master

La elección del host Master en el grupo de redundancia depende de los siguientes dos parámetros que ya los vimos en la documentación oficial, copio la traducción de la misma:

"

  • advbase Este parámetro opcional especifica con qué frecuencia, en segundos, anunciar que somos miembros del grupo de redundancia. El valor predeterminado es 1 segundo. Los valores aceptables son del 1 al 255.

  • advskew Este parámetro opcional especifica cuánto sesgar la base de datos al enviar anuncios CARP. Manipulando advskew, se puede elegir el anfitrión CARP maestro. Cuanto mayor sea el número, menos preferido será el anfitrión a la hora de elegir un maestro. El valor predeterminado es 0. Los valores aceptables son de 0 a 254.

"[1]

Solo agregar que en la elección del Master el parámetro advbase tiene preferencia sobre advskew, ejemplo:

Host A:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 7 priority 15 llprio 3
    carp: BACKUP carpdev vio0 vhid 86 advbase 2 advskew 10
    groups: carp
    status: backup
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

Host B:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 4 priority 15 llprio 3
    carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 20
    groups: carp
    status: master
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #

fijarse que en el host "A" si bien se tiene el parámetro advskew con el valor 10 o sea con más preferencia que en el host "B" que tiene 20, este último tiene el parámetro advbase en 1 segundo, o sea con más preferencia que en el host "A" que lo tiene en 2 segundos. Por este motivo el host "B" gana la elección de Master.

2.6. Guardar la configuración de CARP de forma persistente en el sistema operativo

Para conseguir que la configuración del punto anterior sea tomada al inicio del sistema operativo o sea en tiempo de boot time, debemos guardar la misma en un archivo de configuración hostname.if(5), los cuales nos posibilitan configurar distintos tipos de interfaces de red de forma persistente. Entonces en el host "A":

  • obsda

vamos a crear el siguiente archivo:

/etc/hostname.carp0

con el siguiente contenido:

inet 192.168.46.254 255.255.255.0 192.168.46.255 vhid 86 advskew 10 carpdev vio0 pass 4PAmAmVfRaRG

Ahora en el host "B":

  • obsdb

creamos el mismo archivo, recordar que en este host, para forzar que el mismo tenga el estado de Backup, ponemos un valor de advskew en 20 y no en 10 como en el host "A". Archivo de configuración:

inet 192.168.46.254 255.255.255.0 192.168.46.255 vhid 86 advskew 20 carpdev vio0 pass 4PAmAmVfRaRG

recordar que en el punto tres de este manual también dejamos fija la configuración de CARP en el kernel:

obsda ~ aali $ cat /etc/sysctl.conf
net.inet.carp.allow=1
net.inet.carp.preempt=1
obsda ~ aali $

esta configuración es la misma en ambos hosts.

NOTA: Los archivos hostname.if(5) no solo se procesan en el tiempo de boot time, también podemos cargar la configuración que almacenan con el comando netstart(8) de la siguiente forma:

sh /etc/netstart carp0

2.7. Monitoreo y logs

En este punto veremos como obtener información del funcionamiento de CARP, aprenderemos como incrementar la información enviada a syslogd(8), ademas de usar herramientas de análisis de tráfico como el comando tcpdump.

2.7.1. Analizar tráfico CARP con tcpdump

NOTA importante: El comando tcpdump (man de tcpdump en OpenBSD) en OpenBSD pertenece a la base del sistema y es diferente al tcpdump (man de tcpdump de los Linux) que viene en los Linux, la diferencia radica en que si bien tienen un ancestro común son ramas diferentes de desarrollo. El tcpdump originalmente fue desarrollado por:

"Van Jacobson van@ee.lbl.gov, Craig Leres leres@ee.lbl.gov, and Steven McCanne mccanne@ee.lbl.gov, all of the Lawrence Berkeley Laboratory, University of California, Berkeley, CA."[8]

Cuando la linea de comando o salida del mismo sea diferente entre OpenBSD y Linux lo haré notar.

Con tcpdump podemos descubrir en el tráfico de red que host está con el estado de Master y enviando anuncios CARP. Estos anuncios son enviados vía multicast, o sea a una dirección multicast específica, podemos averiguar cual es esa dirección con el siguiente comando en OpenBSD:

puffypc ~ root # tcpdump -i em0 -v net 224.0.0.0/4
tcpdump: listening on em0, link-type EN10MB
18:36:31.600845 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 22128, len 56)
18:36:32.641089 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 16996, len 56)
18:36:33.681033 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 32614, len 56)
...

En Linux de forma similar:

tuxpc ~ root # tcpdump -i eth0 net 224.0.0.0/4
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
18:37:12.748078 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36
18:37:13.778081 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36
18:37:14.818120 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36
...

como vemos la dirección multicast destino donde se envían los anuncios es:

vrrp.mcast.net

la cual tiene la IP:

puffypc ~ root # host vrrp.mcast.net
vrrp.mcast.net has address 224.0.0.18
puffypc ~ root #

entonces para analizar el tráfico CARP en nuestra red podemos ejecutar un comando tcpdump más específico, cosa que en la salida no nos aparezca otro tráfico multicast, ejemplo en OpenBSD:

puffypc ~ root # tcpdump -i em0 -v host 224.0.0.18
tcpdump: listening on em0, link-type EN10MB
18:39:05.450948 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 11373, len 56)
18:39:06.491096 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 51322, len 56)
18:39:07.531042 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 40209, len 56)
...

En Linux de forma similar:

tuxpc ~ root # tcpdump -i eth0 host 224.0.0.18
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
18:39:17.469809 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36
18:39:18.509947 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36
18:39:19.549671 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36
...

Tener en cuenta que la dirección multicast es vrrp.mcast.net, VRRP y CARP usan la misma, el nombre (vrrp) fue reservado haciendo referencia solo al protocolo VRRP, pero es valida para CARP.

Como se dijo anteriormente, lo presentado por el comando tcpdump va a depender de la implementación de tcpdump que estemos usando, por ejemplo en OpenBSD la linea se ve así:

18:39:05.450948 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 11373, len 56)

en Linux, el mismo tráfico se ve así:

18:39:17.469809 IP 10.0.0.100 > vrrp.mcast.net: VRRPv2, Advertisement, vrid 86, prio 10, authtype none, intvl 1s, length 36

es fácil cotejar los nombres de los parámetros que representan lo mismo en ambas implementaciones:

OpenBSD     Linux
-------     -----
vhid        vrid
advbase     intvl
advskew     prio

Los parámetros y sus valores presentados por tcpdump, son fáciles de reconocer en la linea de configuración de ifconfig(8) cuando creamos la interfaz del protocolo CARP, por ejemplo, esta linea de tcpdump(OpenBSD):

18:39:05.450948 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=1 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 11373, len 56)

El primer campo "18:39:05.450948" es el timestamp. Después nos informa el protocolo "carp". Luego tenemos el origen y el destino con "10.0.0.100 > vrrp.mcast.net". El tipo de mensaje "CARPv2-advertise 36". El ID del grupo de redundancia con "vhid=86". Con "advbase" nos informa cada cuantos segundos se envía el anuncio. Y con "advskew=10" que es como la prioridad de VRRP.

Ejercicio: como podemos ver en las salidas anteriores en el timestamp de cada linea, se envía un anuncio por segundo, recordar que este valor de un segundo es un valor por defecto que lo podemos cambiar con el argumento "advbase". Por ejemplo, vamos a realizar este cambio en ambos host, primeramente en el host "B", que esta con el estado de "Backup", haremos un "destroy" de la interfaz carp0 y la volvemos a crear con el parámetro "advbase" configurado en 2 segundos, comandos:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 6 priority 15 llprio 3
    carp: BACKUP carpdev vio0 vhid 86 advbase 1 advskew 20
    groups: carp
    status: backup
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #
obsdb ~ root # ifconfig carp0 destroy
obsdb ~ root #
obsdb ~ root # ifconfig carp0 vhid 86 pass 4PAmAmVfRaRG carpdev vio0 advskew 20 advbase 2 192.168.46.254 netmask 255.255.255.0
obsdb ~ root #
obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 7 priority 15 llprio 3
    carp: BACKUP carpdev vio0 vhid 86 advbase 2 advskew 20
    groups: carp
    status: backup
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #

como vemos en el listado de comandos, con ifconfig(8) usamos ahora el argumento advbase con el valor de 2. En comandos anteriores no lo usamos puesto que queríamos que la frecuencia de los anuncios esté con el valor por defecto que es 1 segundo. Ahora procedemos a cambiar en el Master:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 6 priority 15 llprio 3
    carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 10
    groups: carp
    status: master
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #
obsda ~ root # ifconfig carp0 destroy
obsda ~ root #
obsda ~ root # ifconfig carp0 vhid 86 pass 4PAmAmVfRaRG carpdev vio0 advskew 10 advbase 2 192.168.46.254 netmask 255.255.255.0
obsda ~ root #
obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 7 priority 15 llprio 3
    carp: MASTER carpdev vio0 vhid 86 advbase 2 advskew 10
    groups: carp
    status: master
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

como podemos apreciar en el listado anterior el argumento advbase de la interfaz de red carp0 pasó a valer 2 segundos. Ahora vemos con tcpdump(OpenBSD) la salida:

puffypc ~ root # tcpdump -i em0 -v host 224.0.0.18
tcpdump: listening on em0, link-type EN10MB
18:49:01.900924 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=2 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 24989, len 56)
18:49:03.940959 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=2 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 34382, len 56)
18:49:05.980893 carp 10.0.0.100 > vrrp.mcast.net: CARPv2-advertise 36: vhid=86 advbase=2 advskew=10 demote=0 (DF) [tos 0x10] (ttl 255, id 16994, len 56)
...

fijarse en el timestamp que ahora entre cada linea hay una diferencia de 2 segundos.

2.7.2. Cambios de estado y consulta en logs

En una instalación estándar de OpenBSD los logs de CARP están en el archivo:

/var/log/messages

En ambos servidores, el host "A" y el B vamos a dejar corriendo el comando tail de forma interactiva así vamos viendo como cambian de estado las interfaces del protocolo CARP, el comando sería:

tail -f /var/log/messages | grep carp

Ahora que ya estamos viendo de forma interactiva el archivo de log, podemos enterarnos al instante como cambia el estado de las interfaces de CARP, para realizar estos cambios podemos usar el parámetro state, forzando a una interface CARP a cambiar de estado. Ejemplo, tenemos el host "A" del grupo de redundancia 86 (vhid) en estado de Master:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 7 priority 15 llprio 3
    carp: MASTER carpdev vio0 vhid 86 advbase 2 advskew 10
    groups: carp
    status: master
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

y el host "B", del mismo grupo en estado de Backup:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 7 priority 15 llprio 3
    carp: BACKUP carpdev vio0 vhid 86 advbase 2 advskew 20
    groups: carp
    status: backup
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #

ahora procedemos a cambiar el estado de la interface CARP del host "A" a estado de Backup:

obsda ~ root # ifconfig carp0 state backup

resultado:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    lladdr 00:00:5e:00:01:56
    index 7 priority 15 llprio 3
    carp: BACKUP carpdev vio0 vhid 86 advbase 2 advskew 10
    groups: carp
    status: backup
    inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

NOTA: Tener en cuenta que el estado de Backup no dura más que unos pocos segundos en el host "A", puesto que éste tiene más prioridad en el parámetro advskew que el host "B".

log en host "A":

May  4 18:52:06 obsda /bsd: carp0: state transition: MASTER -> BACKUP

log en host "B":

May  4 18:52:10 obsdb /bsd: carp0: state transition: BACKUP -> MASTER

Como dijimos este estado no dura más de unos segundos, lo comprobamos examinando los logs en el host "B":

May  4 18:52:10 obsdb /bsd: carp0: state transition: MASTER -> BACKUP

notamos que el cambio al estado original fue casi inmediato. En el host "A", la salida de log cuando volvió al estado de Master fue:

May  4 18:52:10 obsda /bsd: carp0: state transition: BACKUP -> MASTER

2.7.3. Incrementar verbosidad de logs

Como vimos en el punto 2 de este manual con sysctl(8) podemos cambiar el nivel de verbosidad de CARP en el kernel, usando la variable:

net.inet.carp.log

descripción sacada del manual oficial:

"net.inet.carp.log Cambio de estado, paquetes defectuosos y otros errores. Este valor puede estar entre 0 y 7, correspondiente a las prioridades de syslog(3). El valor predeterminado es 2 (solo mostrar cambios de estado)."[1]

entonces la variable puede ser configurada con los valores de 0 a 7, valores que tienen la siguiente correspondencia como vemos en el man de syslog(3):

The level (ORed with the facility) is selected from the
 following list, ordered by decreasing importance:

LOG_EMERG   : A panic condition.  This is normally broadcast 
to all users.

LOG_ALERT   : A condition that should be corrected 
immediately, such as a corrupted system database.

LOG_CRIT    : Critical conditions, e.g., hard device 
errors.

LOG_ERR     : Errors.

LOG_WARNING : Warning messages.

LOG_NOTICE  : Conditions that are not error conditions, 
but should possibly be handled specially.

LOG_INFO    : Informational messages.

LOG_DEBUG   : Messages that contain information normally 
of use only when debugging a program.

si consultamos:

/usr/include/syslog.h

vemos más claros los valores:

#define LOG_EMERG   0 /* system is unusable */
#define LOG_ALERT   1 /* action must be taken immediately */
#define LOG_CRIT    2 /* critical conditions */
#define LOG_ERR     3 /* error conditions */
#define LOG_WARNING 4 /* warning conditions */
#define LOG_NOTICE  5 /* normal but significant condition */
#define LOG_INFO    6 /* informational */
#define LOG_DEBUG   7 /* debug-level messages */

entonces como expresa el manual, el valor por defecto de esta variable es 2 y como nosotros aún no lo hemos cambiado, ese es el valor actual en ambos hosts:

Host A:

obsda ~ root # sysctl net.inet.carp.log
net.inet.carp.log=2
obsda ~ root #

Host B:

obsdb ~ root # sysctl net.inet.carp.log
net.inet.carp.log=2
obsdb ~ root #

con este valor por defecto, si procedemos a bajar la interface carp0 del host "A":

obsda ~ root # ifconfig carp0 down

en el archivo de log:

/var/log/messages

de este host no veríamos ninguna salida, solo se informará en el host "B", el cual cambia al estado de Master:

May  4 19:03:05 obsdb /bsd: carp0: state transition: BACKUP -> MASTER

ahora volvemos a levantar la interface carp0 en el host "A":

obsda ~ root # ifconfig carp0 up

lo que produce en ambos hosts los siguientes mensajes:

Host A:

May  4 19:05:13 obsda /bsd: carp0: state transition: BACKUP -> MASTER

Host B:

May  4 19:05:13 obsdb /bsd: carp0: state transition: MASTER -> BACKUP

Ahora vamos a configurar la variable:

net.inet.carp.log

con el valor máximo, este valor es 7, para ello ejecutamos en ambos hosts:

Host A:

obsda ~ root # sysctl net.inet.carp.log=7
net.inet.carp.log: 2 -> 7
obsda ~ root #

Host B:

obsdb ~ root # sysctl net.inet.carp.log=7
net.inet.carp.log: 2 -> 7
obsdb ~ root #

ahora al bajar la interface carp0 en el host "A":

obsda ~ root # ifconfig carp0 down

vemos que si aparece una linea informativa en el log del host "A":

May  4 19:07:08 obsda /bsd: carp0: state transition: MASTER -> INIT

la cual antes no aparecia. En el host "B" el log es:

May  4 19:07:08 obsdb /bsd: carp0: state transition: BACKUP -> MASTER

Ahora volvemos a levantar la interface carp0 en el host "A":

obsda ~ root # ifconfig carp0 up

lo que muestra en el log del host "A":

May  4 19:10:13 obsda /bsd: carp0: state transition: INIT -> BACKUP
May  4 19:10:14 obsda /bsd: carp0: state transition: BACKUP -> MASTER

como podemos ver aparece una linea extra de información que avisa que estamos pasando del estado "INIT" al estado "BACKUP", para luego pasar al estado "MASTER". En el host "B" la linea de log es:

May  4 19:10:14 obsdb /bsd: carp0: state transition: MASTER -> BACKUP

Conclusión, como vimos elevando el nivel de log aparecen mensajes en el archivo de log que no aparecerían con el valor por defecto. Existen más mensajes que podrían aparecer en el log pero son relacionados a errores en el protocolo y el tráfico de red, ellos se pueden consultar en el código fuente:

/usr/src/sys/netinet/ip_carp.c

mi concejo es elevar el nivel de log a LOG_DEBUG o sea 7, para estar atentos a cualquier eventualidad, si lo consideramos molesto lo bajamos. Para dejar persistente esta configuración agregamos la siguiente linea al siguiente archivo en ambos hosts:

obsda ~ root # grep net.inet.carp.log /etc/sysctl.conf
net.inet.carp.log=7
obsda ~ root #

2.7.4. Generar conmutación por error usando contador de degradación de CARP

En ejemplos anteriores hemos forzado el cambio del Master bajando la interface carp0 en el host "A", esto hace que el host "B" tome ese lugar. Luego levantamos la interface carp0 para que en base a los valores de advbase y advskew el host "A" vuelva a ser elegido Master del grupo. Si bien es valido generar lo que se denomina failover o conmutación por error bajando una interface de red, existe una forma más acorde de hacerlo en la implementación del protocolo CARP en OpenBSD. Podemos lograr realizar una conmutación por error en un grupo de redundancia CARP usando el argumento "-g" del comando ifconfig(8), copio párrafo del man donde explica el uso:

INTERFACE GROUPS
 ifconfig -g group-name [[-]carpdemote [number]]

 The following options are available for interface groups:

 -g group-name
   Specify the group.

 carpdemote [number]
   Increase carp(4) demotion counter for given interface 
   group by number. Acceptable values are 0 to 128.  If 
   number is omitted, it is increased by 1.  The maximum 
   value for a demotion counter is 255.

 -carpdemote [number]
   Decrease carp(4) demotion counter for given interface 
   group by number. Acceptable values are 0 to 128.  If 
   number is omitted, it is decreased by 1.

Todas las interfaces CARP están por defecto en el grupo "carp", como vemos en la linea:

groups: carp

de la salida del comando:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:56
	index 7 priority 15 llprio 3
	carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 10
	groups: carp
	status: master
	inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

Podemos consultar el contador de degradación CARP de la interface con el comando:

obsda ~ root # ifconfig -g carp
carp: carp demote count 0
obsda ~ root #

en el host "B" el valor esta también por defecto como en el host "A":

obsdb ~ root # ifconfig -g carp
carp: carp demote count 0
obsdb ~ root #

ahora vamos hacer que el host "B" tome el lugar de Master incrementando el contador de degradación en el host "A", vamos a incrementar el contador en 1 con el siguiente comando:

obsda ~ root # ifconfig -g carp carpdemote 1
obsda ~ root #
obsda ~ root # ifconfig -g carp
carp: carp demote count 1
obsda ~ root #

vemos en logs de los host:

Host A:

May  6 22:19:49 obsda /bsd: carp0: state transition: MASTER -> BACKUP

Host B:

May  6 22:20:52 obsdb /bsd: carp0: state transition: BACKUP -> MASTER

o si consultamos las interfaces:

Host A:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:56
	index 7 priority 15 llprio 3
	carp: BACKUP carpdev vio0 vhid 86 advbase 1 advskew 10
	groups: carp
	status: backup
	inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #

Host B:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:56
	index 4 priority 15 llprio 3
	carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 20
	groups: carp
	status: master
	inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #

hemos logrado hacer un failover o conmutación por error sin necesidad de bajar una interface. Ahora volvemos a poner en cero el contador en el host "A":

obsda ~ root # ifconfig -g carp -carpdemote 1
obsda ~ root #
obsda ~ root # ifconfig -g carp
carp: carp demote count 0
obsda ~ root #

con esto el host "A" vuelve a ser Master:

Host A:

May  6 22:26:38 obsda /bsd: carp0: state transition: BACKUP -> MASTER

Host B:

May  6 22:27:39 obsdb /bsd: carp0: state transition: MASTER -> BACKUP

3. Que es pfsync

Según explica el manual oficial pfsync es:

"La interfaz de red pfsync(4) expone ciertos cambios realizados en la tabla de estado de pf(4). Al monitorear esta interfaz con tcpdump(8), se pueden observar los cambios en la tabla de estado en tiempo real. Además, la interfaz pfsync puede enviar estos mensajes de cambio de estado a la red para que otros nodos que ejecutan PF puedan fusionar los cambios en sus propias tablas de estado. Asimismo, pfsync también puede escuchar en la red los mensajes entrantes."[1]

Entonces con pfsync podemos compartir entre dos hosts el estado de conexiones que se están manejando con PF. Los "Estados" de PF son como las "Conexiones" de Conntrack de IPTables, recordar que PF es un firewall de "Estado", eso significa que se hace un seguimiento de cada conexión, PF entiende que un paquete es parte de una conexión que ya fue aceptada con anterioridad, entonces las reglas de filtrado ya no avalúan los siguientes paquetes que pertenecen a conexiones aceptadas.

A modo de explicación muy general en PF si un paquete es aceptado queda registrada esa conexión con el estado de "Pass", si un paquete es denegado se bloquea "Block" y no se genera un estado. Esto lo podemos monitorear a nivel de logs con la herramienta tcpdump sobre la interfaz de red pflog0, en esta interfaz se registra solo una vez el paso de una conexión, puesto que las conexiones relacionadas a un estado de Pass ya quedan con el estado impuesto y los siguientes paquetes de la misma ya no se registran en esta interfaz.

3.1. Sobre los programadores de pfsync

El programador inicial del proyecto pfsync es Michael "Mickey" Shalayeff en este caso el mismo del proyecto CARP, ver:

2.1. Sobre los programadores de CARP

3.2. Estados en PF

Si el lector ya conoce sobre el manejo de estados sobre PF puede saltear este punto, si no es recomendable leer y practicar el par de ejercicios, es indispensable entender que son los estados en PF para implementar pfsync.

3.2.1. Ejemplo de seguimiento de un Estado

En este ejemplo veremos el seguimiento de un Estado de "pass" de una conexión generada por un comando ping. En el servidor tendremos una sola regla que acepta todo el tráfico y genera logs de todo, como se muestra en el archivo de configuración de PF:

/etc/pf.conf

tendremos el siguiente contenido:

# $OpenBSD: pf.conf,v 1.55 2017/12/03 20:40:04 sthen Exp $
#
# See pf.conf(5) and /etc/examples/pf.conf

pass log all

la regla anterior ya cargada en el sistema se muestra como:

server ~ root # pfctl -sr
pass log all flags S/SA
server ~ root #

ahora ejecutamos un comando tcpdump sobre la interface pflog0 así:

server ~ root # tcpdump -i pflog0 -n icmp
tcpdump: WARNING: snaplen raised from 116 to 160
tcpdump: listening on pflog0, link-type PFLOG

ahora probamos desde un cliente en la red enviar un ping al servidor:

client ~ root # ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: icmp_seq=0 ttl=255 time=0.296 ms
64 bytes from 10.0.0.1: icmp_seq=1 ttl=255 time=0.294 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=255 time=0.285 ms
...

como se puede ver en la salida del comando tcpdump, solo se registra el primer paquete que es el que genera el estado de "pass", o sea se genera un tipo de conexión que también podemos llamar "establecida":

server ~ root # tcpdump -i pflog0 -n icmp
tcpdump: WARNING: snaplen raised from 116 to 160
tcpdump: listening on pflog0, link-type PFLOG

23:15:52.453781 10.0.0.100 > 10.0.0.1: icmp: echo request

notar que solamente aparece una linea en la salida del comando tcpdump, que es la que genera el nuevo estado, luego de generarlo los paquetes posteriores de esta conexión ya no son analizados por las reglas de PF, estos paquetes ya pasan directo.

Ahora si cortamos el comando ping y lo relanzamos, vemos que se genera un nuevo estado porque aparece en la salida del comando tcpdump una nueva linea:

server ~ root # tcpdump -i pflog0 -n icmp
tcpdump: WARNING: snaplen raised from 116 to 160
tcpdump: listening on pflog0, link-type PFLOG

23:15:52.453781 10.0.0.100 > 10.0.0.1: icmp: echo request
23:26:22.303932 10.0.0.100 > 10.0.0.1: icmp: echo request

También podemos consultar con PF los estados que tenemos con protocolo icmp en el servidor:

server ~ root # pfctl -vss | grep -A1 icmp
all icmp 10.0.0.1:8 <- 10.0.0.100:25440       0:0
   age 00:01:38, expires in 00:00:09, 98:98 pkts, 8232:8232 bytes, rule 0
server ~ root #

el estado expuesto para icmp tiene un vencimiento de 10 segundos (fijarse que aparece en 00:00:09), como son conexiones del comando ping que van y vuelven en 1 segundo aproximadamente ese valor de vencimiento esta siempre rotando entre 10 y 9. Ahora si cortamos el comando ping ese valor entra a caer desde los 10 segundos hasta que llega a 0 y entonces el estado desaparece del sistema. Ejemplo, si cortamos el comando ping, lanzamos otro y rápidamente consultamos los estados, veremos dos estados icmp, uno del ping que cortamos y uno del ping nuevo:

server ~ root # pfctl -vss | grep -A1 icmp
all icmp 10.0.0.1:8 <- 10.0.0.100:25440       0:0
   age 00:04:02, expires in 00:00:02, 235:235 pkts, 19740:19740 bytes, rule 0
--
all icmp 10.0.0.1:8 <- 10.0.0.100:52322       0:0
   age 00:00:06, expires in 00:00:09, 6:6 pkts, 504:504 bytes, rule 0
server ~ root #

Si cortamos todos los comandos ping y esperemos más de 10 segundos veremos que en el sistema no queda rastros de un estado icmp:

server ~ root # pfctl -vss | grep -A1 icmp
server ~ root #

3.2.2. Como impacta los cambios de reglas, flush de estados en conexiones ya establecidas

Tenemos en el archivo de configuración de Packet Filter:

/etc/pf.conf

solo la siguiente rule:

pass log all

que cargada en el sistema se lee como:

server ~ root # pfctl -sr
pass log all flags S/SA
server ~ root #

lo que significa que dejamos pasar todo el tráfico pero registrando el mismo en logs. Ahora ejecutamos el comando tcpdump sobre la interface pflog0 de esta forma:

server ~ root # tcpdump -i pflog0 -n -p icmp

Y probamos ejecutando un ping contra el mismo servidor desde un cliente:

client ~ root # ping -c5 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: icmp_seq=0 ttl=255 time=18.760 ms
64 bytes from 10.0.0.1: icmp_seq=1 ttl=255 time=0.237 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=255 time=0.248 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=255 time=0.387 ms
64 bytes from 10.0.0.1: icmp_seq=4 ttl=255 time=0.530 ms
--- 10.0.0.1 ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.237/4.032/18.760/7.365 ms
client ~ root #

El ping anterior genera un estado que puede verse en el comando tcpdump que ejecutamos anteriormente:

server ~ root # tcpdump -i pflog0 -n -p icmp
10:54:34.203846 10.0.0.100 > 10.0.0.1: icmp: echo request

como puede verse los cinco ping han generado solo una linea en pflog0, esta linea representa el primer paquete, el que crea el nuevo estado, ya con el mismo creado o sea ya cuando la conexión esta establecida, a los siguiente paquetes PF ya los deja pasar sin analizar puesto que ya tienen un estado de Pass.

Ahora que pasaría si en vez de limitar la ejecución a un número fijo de pings como en el ejemplo anterior, los dejamos corriendo indefinidamente y mientras el mismo se está ejecutando levantamos una regla de bloqueo del protocolo icmp, realicemos ese ejercicio:

client ~ root # ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: icmp_seq=0 ttl=255 time=0.281 ms
64 bytes from 10.0.0.1: icmp_seq=1 ttl=255 time=0.294 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=255 time=0.220 ms
...

como ya vimos eso me genera un log de estado en pflog:

11:10:09.543915 10.0.0.100 > 10.0.0.1: icmp: echo request

si queremos consultar el estado podemos ejecutar este comando de PF:

server ~ root # pfctl -vss | grep -A1 icmp
all icmp 10.0.0.1:8 <- 10.0.0.100:8172       0:0
   age 00:02:20, expires in 00:00:09, 140:140 pkts, 11760:11760 bytes, rule 0
server ~ root #

también si consultamos las rules con el argumento "-v" vemos que la columna "state" tiene muchos estados, 52 en esta caso, uno de ellos generado por el ping:

server ~ root # pfctl -vsr
pass log all flags S/SA
[ Evaluations: 949 Packets: 1806 Bytes: 668500 States: 52 ]
[ Inserted: uid 0 pid 33621 State Creations: 151 ]
server ~ root #

Ahora en el archivo:

pf.conf

vamos agregar la siguiente regla:

block on vport0 proto icmp

lo que ya cargado se ve en el sistema de esta forma:

server ~ root # pfctl -sr
pass log all flags S/SA
block drop on vport0 proto icmp all
server ~ root #

entonces como vemos tenemos ya cargada una rule que bloquea icmp, sin embargo si consultamos la pantalla donde se esta ejecutando el comando ping desde el host cliente, vemos que ese ping sigue llegando a destino. Esto pasa por lo que dijimos anteriormente, las reglas de Pass y Block se analizan la primera vez, en el caso de Pass generan un estado, en el caso de Block no. En nuestro caso que el ping nació cuando no existía el "Block" pero si el "Pass" este ping tiene un estado y que por más que carguemos una nueva regla ese estado sigue siendo valido, por ese motivo los paquetes icmp del comando ping siguen llegando a destino. Si queremos que se actualicen los estados en base a las nuevas reglas podemos ejecutar el comando:

pfctl -Fs

para hacer un flush de los estados, lo hacemos en la siguiente secuencia de comandos:

server ~ root #
server ~ root # pfctl -vss | grep -A1 icmp
all icmp 10.0.0.1:8 <- 10.0.0.100:8172       0:0
 age 00:09:27, expires in 00:00:09, 567:567 pkts, 47628:47628 bytes
server ~ root #
server ~ root # pfctl -Fs
59 states cleared
server ~ root #
server ~ root # pfctl -vss | grep -A1 icmp
server ~ root #

notar que no solo desapareció el estado icmp anterior, sino que no se generó uno nuevo, esto porque como ya sabemos block no genera estados. También logramos que el comando ping que se estaba ejecutando desde el cliente ya no pueda alcanzar el destino vía protocolo icmp.

3.3. Modos de conexión entre hosts pfsync

Existen dos modos de conexión entre hosts pfsync:

  • Vía protocolo multicast
  • Vía protocolo unicast

vamos a realizar las dos tipos de configuraciones, primeramente usando el comando ifconfig(8), leer la sección ifconfig(8)#PFSYNC, sintaxis:

ifconfig pfsync-interface [[-]defer] [maxupd n] [[-]syncdev iface] [[-]syncpeer peer_address]

3.3.1. Conexión con protocolo multicast

Vamos a levantar en ambos host una conexión pfsync con protocolo multicast, esto tiene la ventaja que los nodos se auto descubren en la red, pero existe un inconveniente con este tipo de configuración y es que el protocolo pfsync viaja en texto plano. Para asegurarlo en el modo multicast lo podemos hacer a nivel físico, una forma sería si es que tenemos dos hosts es destinar en ambos una placa de red dedicada a este tráfico y unir ambos con un cable cruzado. Otra forma sería usar una VLAN específica para pfsync. Si no podemos segurizar la conexión multicast debemos analizar el modo unicast, que se explica en el siguiente punto.

Lista de comandos para modo multicast:

Host B:

obsdb ~ root #
obsdb ~ root # ifconfig pfsync0 syncdev vio2 up
obsdb ~ root #
obsdb ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent vio2
	pfsync: syncdev: vio2 maxupd: 128 defer: off
	groups: carp pfsync
obsdb ~ root #

Host A:

obsda ~ root #
obsda ~ root # ifconfig pfsync0 syncdev vio2 up
obsda ~ root #
obsda ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent vio2
	pfsync: syncdev: vio2 maxupd: 128 defer: off
	groups: carp pfsync
obsda ~ root #

ahora si ejecutamos este comando tcpdump en ambos host sobre la interfaz pfsync0, vemos la salida:

Host A:

obsda ~ root # tcpdump -i pfsync0 -n -p
tcpdump: listening on pfsync0, link-type PFSYNC
22:42:59.694245 PFSYNCv69 len 128
22:43:00.714287 PFSYNCv69 len 128
22:43:01.754351 PFSYNCv69 len 128
22:43:03.094329 PFSYNCv69 len 128
^C
4 packets received by filter
0 packets dropped by kernel
obsda ~ root #

Host B:

obsdb ~ root # tcpdump -i pfsync0 -n -p
tcpdump: listening on pfsync0, link-type PFSYNC
22:43:12.334987 PFSYNCv69 len 128
22:43:13.334885 PFSYNCv69 len 128
22:43:14.354986 PFSYNCv69 len 128
22:43:15.595078 PFSYNCv69 len 128
^C
4 packets received by filter
0 packets dropped by kernel
obsdb ~ root #

vemos que pfsync esta funcionando. Este tráfico al ser multicast podemos mirarlo con algo como:

obsda ~ root # tcpdump -i vio0 -v -n net 224.0.0.0/4
tcpdump: listening on vio0, link-type EN10MB
22:53:06.444251 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 17036, len 128)
22:53:07.444075 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 1024, len 128)
22:53:08.125512 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 124
    act UPD ST COMP count 1
    ...

como vemos estamos analizando todo el espectro multicast con tcpdump, puesto que pusimos la subred entera:

224.0.0.0/4

Por otro lado vemos que en las lineas que muestra el comando esta la dirección multicast del protocolo pfsync, es la dirección:

224.0.0.240

entonces para que el tráfico a analizar no se mezcle con otro tráfico multicast podemos mejorar el comando ejecutándolo de esta forma:

obsda ~ root # tcpdump -i vio0 -v host 224.0.0.240
tcpdump: listening on vio0, link-type EN10MB
22:58:47.582328 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 10673, len 128)
22:58:48.592265 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 3103, len 128)
22:58:48.612223 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 288
    act INS ST count 1
    ...
 (DF) [tos 0x10] (ttl 255, id 19810, len 308)
22:58:49.802285 0.0.0.0 > 224.0.0.240: PFSYNCv6 len 108
    act UPD ST COMP count 1
    ...

ahora en el siguiente punto vamos hacer el ejercicio de configurar pfsync en modo unicast, pero antes vamos a dar de baja la interfaz pfsync0 que creamos en ambos hosts, ejecutar el siguiente comando en ambos hosts:

ifconfig pfsync0 destroy

3.3.2. Conexión con protocolo unicast

En este punto vamos hacer una configuración unicast de pfsync, similar a como hicimos en el punto anterior, pero ahora usando el argumento "syncpeer" para especificar al otro host con pfsync. Este modo tiene la desventaja que los host no se auto descubren por la red, pero tiene la ventaja que podemos canalizar el tráfico por un túnel VPN, primero veamos un ejemplo sin túnel VPN, quedaría así:

10.1.1.200/24  .---.                 .---.
           vio2| B |<--------------->| A |vio2
               '---'     pfsync0     '---'  10.1.1.100/24

Recordar leer la página del manual en ifconfig(8)#PFSYNC, sintaxis:

ifconfig pfsync-interface [[-]defer] [maxupd n] [[-]syncdev iface] [[-]syncpeer peer_address]

Los comandos manuales en ambos hosts serían:

Host B:

obsdb ~ root #
obsdb ~ root # ifconfig pfsync0 syncdev vio2 syncpeer 10.1.1.100 up
obsdb ~ root #
obsdb ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent vio2
	pfsync: syncdev: vio2 syncpeer: 10.1.1.100 maxupd: 128 defer: off
	groups: carp pfsync
obsdb ~ root #

Host A:

obsda ~ root #
obsda ~ root # ifconfig pfsync0 syncdev vio2 syncpeer 10.1.1.200 up
obsda ~ root #
obsda ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent vio2
	pfsync: syncdev: vio2 syncpeer: 10.1.1.200 maxupd: 128 defer: off
	groups: carp pfsync
obsda ~ root #

fijarse como en estos comandos ahora especificamos el peer en la conexión, el mismo también se lo puede visualizar cuando ejecutamos ifconfig(8) para consultar la configuración de la interfaz. Como dijimos previamente la ventaja de este modo de configuración de pfsync con unicast es que podemos canalizar el tráfico a través de un túnel VPN, la documentación habla de usar IPSEC, en este manual vamos a usar el más moderno Wireguard.

3.4. Segurizar conexión pfsync

Para segurizar la conexión de pfsync con protocolo unicast vamos a utilizar un túnel VPN Wireguard. Como se vio en el punto:

3.3.2. Conexión con protocolo unicast

podemos montar la sincronización de estados de pf con pfsync vía la conexión de dos puntos, especificados con direcciones IPs. Como ya se mencionó el problema es que ese tráfico de sincronización de estados viaja en texto plano.

NOTA: No es la función de este manual realizar una explicación exhaustiva sobre posibles configuraciones de túneles VPN con Wireguard. Si bien los pasos de la configuración propuesta en este ejercicio son sencillos y se los describe uno por uno más adelante, se recomienda leer alguna documentación introductoria sobre Wireguard.

NOTA: Para este ejercicio tenemos que instalar las utilidades Wireguard con el siguiente comando:

pkg_add -mv wireguard-tools

En el ejercicio tenemos dos servidores virtuales que se interconectan a través de las interfaces vio0 que están con IPs de la subred:

10.0.0.0/24

diagrama:

10.0.0.200/24  .---.                 .---.
           vio0| B |<--------------->| A |vio0
               '---'                 '---'  10.0.0.100/24

consultamos esta configuración en el sistema:

Host A:

obsda ~ root # ifconfig vio0
vio0: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
	lladdr fe:e1:bb:d1:ae:d9
	index 1 priority 0 llprio 3
	groups: egress
	media: Ethernet autoselect
	status: active
	inet 10.0.0.100 netmask 0xffffff00 broadcast 10.0.0.255
obsda ~ root #

Host B:

obsdb ~ root # ifconfig vio0
vio0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:78
	index 1 priority 0 llprio 3
	groups: egress
	media: Ethernet autoselect
	status: active
	inet 10.0.0.200 netmask 0xffffff00 broadcast 10.0.0.255
obsdb ~ root #

ahora vamos a proceder a levantar el túnel VPN Wireguard. En ambos servidores vamos a crear las keys necesarias (public y secret) con los siguientes comandos, ejemplo en host "A":

obsda ~ root # cd /etc
obsda /etc root #
obsda /etc root # mkdir wireguard
obsda /etc root #
obsda /etc root # chmod 700 wireguard/
obsda /etc root #
obsda /etc root # cd wireguard/
obsda /etc/wireguard root #
obsda /etc/wireguard root # wg genkey > secret.key
Warning: writing to world accessible file.
Consider setting the umask to 077 and trying again.
obsda /etc/wireguard root #
obsda /etc/wireguard root # chmod 600 secret.key
obsda /etc/wireguard root #
obsda /etc/wireguard root # ls -la secret.key
-rw-------  1 root  wheel  45 May 26 09:09 secret.key
obsda /etc/wireguard root #
obsda /etc/wireguard root # wg pubkey < secret.key > public.key
obsda /etc/wireguard root #
obsda /etc/wireguard root # ls -la public.key
-rw-r--r--  1 root  wheel  45 May 26 09:10 public.key
obsda /etc/wireguard root #

repetir los mismos comandos en host "B". Una vez que los dos hosts tienen las keys necesarias pasamos a configurar el túnel vía archivos de hostname.if(5). Vamos a crear el siguiente archivo:

/etc/hostname.wg1

en el host "A".

NOTA: fijarse que vamos a usar la interfaz wg1 para la sincronización de pfsync, esto es para dejar la wg0 libre, imaginando que ya podría estar siendo usada para otra utilidad con Wireguard. Así como usamos la wg1 podríamos usar la wg2 o incluso la wg0 si estaría libre, esto queda a disponibilidad de cada diseño particular de red.

El contenido del archivo será similar a:

wgport 51830 wgkey 'CIqY6QylkTAizY0TXd3tNBLJyPYVNaOCpLGEwFM/oUw='
10.1.1.100/24

# obsdb
wgpeer X+islmYJ263cJjh2h/x+pHGgVCAzthzQDYYmb14YEAw= wgendpoint 10.0.0.200 51830 wgaip 10.1.1.200/32

up

NOTA: fijarse que no hemos usado el puerto estándar de Wireguard que es el 51820, así como no usamos la interfaz wg0 tampoco usamos el puerto estándar por considerar que ya podría estar siendo usado.

Ahora en el host "B" creamos de forma similar el archivo:

obsdb ~ root # cat /etc/hostname.wg1
wgport 51830 wgkey '0LDS09MpEFdw8d+5yyiMDs10LBkkX2j2nx3Gumsap0g='
10.1.1.200/24

# obsda
wgpeer sB2kGR9mFaeR190jZ1lNs5SDUYLP5LFA0VgX1iWsJzs= wgendpoint 10.0.0.100 51830 wgaip 10.1.1.100/32

up

NOTA importante: Recordar poner en los archivos de configuración sus propias keys, tanto la pública como la privada.

Ahora vamos agregar una configuración bastante sencilla y permisiva en PF de ambos host. En el archivo de configuración del mismo:

/etc/pf.conf

pondremos las siguientes lineas:

block log all
set skip on lo
pass on vio0
pass on wg1

y cargamos en el sistema esas reglas con el comando:

pfctl -f /etc/pf.conf

ahora en ambos host cargamos el túnel VPN Wireguard con el comando netstart(8) que ya explicamos anteriormente:

sh /etc/netstart wg1

comprobamos que se haya cargado la configuración:

Host A:

obsda ~ root # ifconfig wg1
wg1: flags=80c3<UP,BROADCAST,RUNNING,NOARP,MULTICAST> mtu 1420
	index 6 priority 0 llprio 3
	wgport 51830
	wgpubkey sB2kGR9mFaeR190jZ1lNs5SDUYLP5LFA0VgX1iWsJzs=
	wgpeer X+islmYJ263cJjh2h/x+pHGgVCAzthzQDYYmb14YEAw=
		wgendpoint 10.0.0.200 51830
		tx: 0, rx: 0
		wgaip 10.1.1.200/32
	groups: wg
	inet 10.1.1.100 netmask 0xffffff00 broadcast 10.1.1.255
obsda ~ root #

Host B:

obsdb ~ root # ifconfig wg1
wg1: flags=80c3<UP,BROADCAST,RUNNING,NOARP,MULTICAST> mtu 1420
	index 6 priority 0 llprio 3
	wgport 51830
	wgpubkey X+islmYJ263cJjh2h/x+pHGgVCAzthzQDYYmb14YEAw=
	wgpeer sB2kGR9mFaeR190jZ1lNs5SDUYLP5LFA0VgX1iWsJzs=
		wgendpoint 10.0.0.100 51830
		tx: 0, rx: 0
		wgaip 10.1.1.100/32
	groups: wg
	inet 10.1.1.200 netmask 0xffffff00 broadcast 10.1.1.255
obsdb ~ root #

probamos un ping desde el host "B" hacia el host "A":

obsdb ~ root # ping -c3 10.1.1.100
PING 10.1.1.100 (10.1.1.100): 56 data bytes
64 bytes from 10.1.1.100: icmp_seq=0 ttl=255 time=0.959 ms
64 bytes from 10.1.1.100: icmp_seq=1 ttl=255 time=1.701 ms
64 bytes from 10.1.1.100: icmp_seq=2 ttl=255 time=1.923 ms

--- 10.1.1.100 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.959/1.528/1.923/0.412 ms
obsdb ~ root #

y desde el host "A" hacia el host "B":

obsda ~ root # ping -c3 10.1.1.200
PING 10.1.1.200 (10.1.1.200): 56 data bytes
64 bytes from 10.1.1.200: icmp_seq=0 ttl=255 time=6.384 ms
64 bytes from 10.1.1.200: icmp_seq=1 ttl=255 time=1.434 ms
64 bytes from 10.1.1.200: icmp_seq=2 ttl=255 time=1.132 ms

--- 10.1.1.200 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.132/2.984/6.384/2.408 ms
obsda ~ root #

como vemos tenemos conectividad a través del túnel.

Ahora vamos a montar pfsync sobre Wireguard o sea la interfaz pfsync0 sobre wg1, comandos:

Host B:

obsdb ~ root # ifconfig pfsync0 syncdev wg1 syncpeer 10.1.1.100 up
obsdb ~ root #
obsdb ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent wg1
	pfsync: syncdev: wg1 syncpeer: 10.1.1.100 maxupd: 128 defer: off
	groups: carp pfsync
obsdb ~ root #

Host A:

obsda ~ root # ifconfig pfsync0 syncdev wg1 syncpeer 10.1.1.200 up
obsda ~ root #
obsda ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent wg1
	pfsync: syncdev: wg1 syncpeer: 10.1.1.200 maxupd: 128 defer: off
	groups: carp pfsync
obsda ~ root #

En el siguiente y último punto en donde desarrollaremos un ejemplo más práctico, veremos como dejar persistentes en el sistema estas configuraciones.

4. Ejemplo práctico del uso de CARP y pfsync

Ahora que ya nos ejercitamos con CARP, los estados de PF y levantamos pfsync segurizado con Wireguard, vamos a proponer un ejemplo más real del uso de CARP con pfsync para lograr la tan apreciada HA.

En este ejercicio proponemos levantar la configuración necesaria para obtener un router/firewall en HA que separa dos redes. Estas dos redes LAN pueden cumplir la función que usted necesite en su organización, una puede ser una red LAN de usuarios y la otra de servidores o ambas ser redes de usuarios pero de distintas oficinas por ejemplo. La idea detrás es que tenemos dos redes separadas por un firewall en donde solo dejamos pasar entre ambas el tráfico permitido.

Como en los ejemplos anteriores, vamos a tener dos host cumpliendo la función de firewall en HA, el obsda en estado Master por defecto y el obsdb en estado Backup por defecto. Como son maquinas virtuales no vamos a escatimar en interfaces de red para armar el diseño, el mismo se podría armar con menos interfaces, pero como son gratis en la virtualización mejor tener una por función, para auditar el tráfico más fácilmente en el uso diario.

El diseño en cuanto a interfaces de red para ambos host sería:

obsda
  |____vio0.....(10.0.0.100/24) MGMT y montar Wireguard
  |____vio1.....Montar el pseudo dispositivo carp0
  |____vio2.....Montar el pseudo dispositivo carp1
  |____carp0....(192.168.46.254/24 - vio1) LAN1
  |____carp1....(192.168.47.254/24 - vio2) LAN2
  |____wg1......(10.1.1.100/24 - vio0) túnel VPN Wireguard
  |____pfsync0..(wg1) Sincronizar conexiones de PF

obsdb
  |____vio0.....(10.0.0.200/24) MGMT y montar Wireguard
  |____vio1.....Montar el pseudo dispositivo carp0
  |____vio2.....Montar el pseudo dispositivo carp1
  |____carp0....(192.168.46.254/24 - vio1) LAN1
  |____carp1....(192.168.47.254/24 - vio2) LAN2
  |____wg1......(10.1.1.200/24 - vio0) túnel VPN Wireguard
  |____pfsync0..(wg1) Sincronizar conexiones de PF

Diagrama de conectividad:

                      .-------------.
                      |             |
,---------------------| Workstation |<----.
|                     |     MGMT    |     |
|                     '-------------'     |
|                       10.0.0.1/24       |
|                                         |
|                                         |
|     .-------------.                     |
|     |             |                     |
|     | Workstation |<--------.           |
|     |    LAN 1    |          \          |
|     '-------------'           \         |
|    192.168.46.102/24           \        |
|                                 |       |
|                                 v       |
|                               .---.     |
|                             /       \   |
|                            |  LAN 1  |  |
|                             \       /   |
|                              '-----'    |
|                                 ^       |
|                                 |       |
|10.0.0.100/24                    |       |10.0.0.200/24
'---->vio0 .---.                  |       '-->vio0 .---.
           |   |                  v                |   |
           |   |          192.168.46.254/24        |   |
           |   |vio1 <-----------------------> vio1|   |
           | F |                carp0              | F |
           | I |                                   | I |
           | R |                                   | R |
           | E |                      10.1.1.200/24| E |
           | W |wg1 <---------pfsync0---------->wg1| W |
           | A |10.1.1.100/24                      | A |
           | L |                                   | L |
           | L |                                   | L |
           |   |          192.168.47.254/24        |   |
           | A |vio2 <-----------------------> vio2| B |
           '---'                carp1              '---'
                                  ^
                                  |
                                  |
                                  v
                                .---.
                              /       \
                             |  LAN 2  |
                              \       /
                               '-----'
                                  ^
                                  |
                                   \    192.168.47.102/24
                                    \     .-------------.
                                     \    |             |
                                      '-->| Workstation |
                                          |    LAN 2    |
                                          '-------------'

4.1. Configuración inicial de los hosts

Como se dijo anteriormente vamos a necesitar para el ejercicio dos maquinas virtuales para poder albergar los dos host, los mismos serían:

  • Host A:
Nombre de host...........................: obsda
Número de interfaces de red virtuales....: 3
Dirección IP en vio0.....................: 10.0.0.100/24
RAM......................................: 1536MB

si estamos virtualizando con VMM de OpenBSD la definición de la VM sería:

switch "uplink_veb0" {
    interface veb0
}

vm "obsda" {
    memory 1536M
    enable
    disk /datos/vmm/obsda/obsda.qcow2
    interface { switch "uplink_veb0" }
    interface { switch "uplink_veb0" }
    interface { switch "uplink_veb0" }
}

Recordar cambiar el path del disco a conveniencia. Con realizar una instalación estándar de OpenBSD es suficiente. Una vez instalado OpenBSD y configurado el entorno a nuestro gusto recordar instalar ademas este paquete:

pkg_add -mv wireguard-tools

similarmente la configuración del host "B" sería:

  • Host B:
Nombre de host...........................: obsdb
Número de interfaces de red virtuales....: 3
Dirección IP en vio0.....................: 10.0.0.200/24
RAM......................................: 1536MB

una vez que arranquen ambos host el comando:

ifconfig -A

debería mostrar algo similar a:

  • Host A:

comando:

obsda ~ root # ifconfig -A
lo0: flags=2008049<UP,LOOPBACK,RUNNING,MULTICAST,LRO> mtu 32768
	index 5 priority 0 llprio 3
	groups: lo
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5
	inet 127.0.0.1 netmask 0xff000000
vio0: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
	lladdr fe:e1:bb:d1:90:c9
	index 1 priority 0 llprio 3
	groups: egress
	media: Ethernet autoselect
	status: active
	inet 10.0.0.100 netmask 0xffffff00 broadcast 10.0.0.255
vio1: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
	lladdr fe:e1:bb:d2:76:c8
	index 2 priority 0 llprio 3
	media: Ethernet autoselect
	status: no carrier
vio2: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
	lladdr fe:e1:bb:d3:27:db
	index 3 priority 0 llprio 3
	media: Ethernet autoselect
	status: no carrier
enc0: flags=0<>
	index 4 priority 0 llprio 3
	groups: enc
	status: active
pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33136
	index 6 priority 0 llprio 3
	groups: pflog
obsda ~ root #

comando route:

obsda ~ root # route -n show -inet -gateway
Routing tables

Internet:
Destination Gateway     Flags  Refs   Use  Mtu   Prio Iface
default     10.0.0.1     UGS     5     15   -     8   vio0
224/4       127.0.0.1    URS     0    444  32768  8   lo0
10.0.0/24   10.0.0.100   UCn     1      0    -    4   vio0
10.0.0.255  10.0.0.100   UHb     0      0    -    1   vio0
127/8       127.0.0.1    UGRS    0      0  32768  8   lo0
127.0.0.1   127.0.0.1    UHhl    1      2  32768  1   lo0
obsda ~ root #
  • Host B:

comando:

obsdb ~ root # ifconfig -A
lo0: flags=2008049<UP,LOOPBACK,RUNNING,MULTICAST,LRO> mtu 32768
	index 5 priority 0 llprio 3
	groups: lo
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5
	inet 127.0.0.1 netmask 0xff000000
vio0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:78
	index 1 priority 0 llprio 3
	groups: egress
	media: Ethernet autoselect
	status: active
	inet 10.0.0.200 netmask 0xffffff00 broadcast 10.0.0.255
vio1: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:79
	index 2 priority 0 llprio 3
	media: Ethernet autoselect
	status: no carrier
vio2: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:80
	index 3 priority 0 llprio 3
	media: Ethernet autoselect
	status: no carrier
enc0: flags=0<>
	index 4 priority 0 llprio 3
	groups: enc
	status: active
pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33136
	index 6 priority 0 llprio 3
	groups: pflog
obsdb ~ root #

comando route:

obsdb ~ root # route -n show -inet -gateway
Routing tables

Internet:
Destination Gateway     Flags Refs  Use   Mtu  Prio Iface
default     10.0.0.1    UGS      5   11     -     8 vio0
224/4       127.0.0.1   URS      0    1 32768     8 lo0
10.0.0/24   10.0.0.200  UCn      1    0     -     4 vio0
10.0.0.255  10.0.0.200  UHb      0    0     -     1 vio0
127/8       127.0.0.1   UGRS     0    0 32768     8 lo0
127.0.0.1   127.0.0.1   UHhl     1    2 32768     1 lo0
obsdb ~ root # 

4.2. Descripción general de los pasos de configuración

Vamos a realizar las configuraciones progresivamente y de forma persistente en archivos de configuración de OpenBSD. Cuando sea el caso ademas vamos a probar el resultado de las mismas. Al final del ejercicio tenemos que lograr tener conexiones persistentes entre las workstations de ambas redes (LAN1, LAN2) y alternar los modos Master y Backup entre los hosts de firewall sin que las conexiones se corten.

4.2.1. Configuración de variables de estados en el kernel (sysctl)

En la sección de configuración de CARP en este manual ya vimos que hacen las siguientes variables del kernel:

  • net.inet.carp.allow
  • net.inet.carp.preempt
  • net.inet.carp.log

ademas de las anteriores variables, vamos a configurar la siguiente, para que permita el forward entre interfaces de red:

  • net.inet.ip.forwarding

podemos configurar estas variables al vuelo de la siguiente forma:

obsda ~ root # sysctl net.inet.carp.allow=1
net.inet.carp.allow: 1 -> 1
obsda ~ root #
obsda ~ root # sysctl net.inet.carp.preempt=1
net.inet.carp.preempt: 1 -> 1
obsda ~ root #
obsda ~ root # sysctl net.inet.carp.log=7
net.inet.carp.log: 7 -> 7
obsda ~ root #
obsda ~ root # sysctl net.inet.ip.forwarding=1
net.inet.ip.forwarding: 1 -> 1
obsda ~ root #

para dejar configuradas estas variables de forma persistente, agregamos las mismas al siguiente archivo:

obsda ~ root # cat /etc/sysctl.conf
net.inet.carp.allow=1
net.inet.carp.preempt=1
net.inet.carp.log=7
net.inet.ip.forwarding=1
obsda ~ root #

ambos hosts tienen que tener la misma configuración. Si necesita mayor información leer las siguientes páginas de man:

4.2.2. Configuración de las interfaces de red de la VM

En este punto vamos a ver la configuración de las interfaces de red de la VM (Virtual Machine o Máquina Virtual), como ya se dijo en el punto:

4.1. Configuración inicial de los hosts

la principal interface que tenemos en este diseño de red es la:

  • vio0

puesto que se usará para MGMT, entonces es muy importante asegurarnos el ingreso por la misma. Ademas en esta interface vamos a montar el túnel VPN Wireguard, necesario para segurizar pfsync. La misma tiene una dirección IP configurada, que en cada host es:

Host A:

10.0.0.100/24

Host B:

10.0.0.200/24

entonces los archivos de configuración que tenemos que crear en cada host son:

Host A:

obsda ~ root # cat /etc/hostname.vio0
inet 10.0.0.100 0xffffff00
obsda ~ root #

Host B:

obsdb ~ root # cat /etc/hostname.vio0
inet 10.0.0.200 0xffffff00
obsdb ~ root #

también cada VM tendrá dos interfaces de red extras:

  • vio1
  • vio2

para montar sobre ellas las interfaces CARP. La configuración de estas es la misma para ambos host, archivo:

/etc/hostname.vio1

con el contenido:

up

archivo:

/etc/hostname.vio2

con el contenido:

up

verificar que una vez que el sistema reinicie estas interfaces esten levantadas:

Host A:

obsda ~ root # ifconfig vio1
vio1: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
	lladdr fe:e1:bb:d2:76:c8
	index 2 priority 0 llprio 3
	media: Ethernet autoselect
	status: active
obsda ~ root #
obsda ~ root # ifconfig vio2
vio2: flags=8b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST> mtu 1500
	lladdr fe:e1:bb:d3:27:db
	index 3 priority 0 llprio 3
	media: Ethernet autoselect
	status: active
obsda ~ root #

Host B:

obsdb ~ root # ifconfig vio1
vio1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:79
	index 2 priority 0 llprio 3
	media: Ethernet autoselect
	status: active
obsdb ~ root #
obsdb ~ root # ifconfig vio2
vio2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 12:e4:07:63:53:80
	index 3 priority 0 llprio 3
	media: Ethernet autoselect
	status: active
obsdb ~ root #

4.2.3. Configuración de las interfaces del protocolo CARP

Como se vio en el inicio del punto:

4. Ejemplo práctico del uso de CARP y pfsync

en los routers/firewalls en HA vamos a tener dos interfaces para el protocolo CARP, una con una IP de la subred que vive en LAN1 y la otra con una IP de la subred que vive en LAN2. Las direcciones IP serán:

carp0......(192.168.46.254/24 - vio1) Interface hacia LAN1         
carp1......(192.168.47.254/24 - vio2) Interface hacia LAN2

Estas direcciones IP asignadas a las interfaces del protocolo CARP, pueden ser usadas en los clientes como puertas de enlace o simplemente como router para encaminar una subred o IP.

Host A:

crear el siguiente archivo:

/etc/hostname.carp0

con el contenido:

inet 192.168.46.254 255.255.255.0 192.168.46.255 vhid 86 advskew 10 carpdev vio0 pass 4PAmAmVfRaRG

y el archivo:

/etc/hostname.carp1

con el contenido:

inet 192.168.47.254 255.255.255.0 192.168.47.255 vhid 87 advskew 10 carpdev vio1 pass PavkTgmtMZU4

Host B:

crear el siguiente archivo:

/etc/hostname.carp0

con el contenido:

inet 192.168.46.254 255.255.255.0 192.168.46.255 vhid 86 advskew 20 carpdev vio0 pass 4PAmAmVfRaRG

y el archivo:

/etc/hostname.carp1

con el contenido:

inet 192.168.47.254 255.255.255.0 192.168.47.255 vhid 87 advskew 20 carpdev vio1 pass PavkTgmtMZU4

lo único en lo que difieren los archivos del host "A" con los del host "B" es en el parámetro advskew, que fue explicado en el punto:

2.5. Que tener en cuenta en la elección del Master

Una vez creados los archivos, reiniciamos y gracias a los mismos se levantaran automáticamente las interfaces CARP, que quedaran funcionando como se explicó en el punto que se trató CARP. El resultado sería el siguiente:

Host A:

obsda ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:56
	index 6 priority 15 llprio 3
	carp: MASTER carpdev vio0 vhid 86 advbase 1 advskew 10
	groups: carp
	status: master
	inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsda ~ root #
obsda ~ root # ifconfig carp1
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:57
	index 7 priority 15 llprio 3
	carp: MASTER carpdev vio1 vhid 87 advbase 1 advskew 10
	groups: carp
	status: master
	inet 192.168.47.254 netmask 0xffffff00 broadcast 192.168.47.255
obsda ~ root #

Host B:

obsdb ~ root # ifconfig carp0
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:56
	index 6 priority 15 llprio 3
	carp: BACKUP carpdev vio0 vhid 86 advbase 1 advskew 20
	groups: carp
	status: backup
	inet 192.168.46.254 netmask 0xffffff00 broadcast 192.168.46.255
obsdb ~ root #
obsdb ~ root # ifconfig carp1
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	lladdr 00:00:5e:00:01:57
	index 7 priority 15 llprio 3
	carp: BACKUP carpdev vio1 vhid 87 advbase 1 advskew 20
	groups: carp
	status: backup
	inet 192.168.47.254 netmask 0xffffff00 broadcast 192.168.47.255
obsdb ~ root #

4.2.4. Configuración de túnel VPN Wireguard para segurizar pfsync

Anteriormente en el punto:

3.4. Segurizar conexión pfsync

se trató como realizar la configuración de forma persistente para levantar un túnel VPN Wireguard, logrando que se establezca automáticamente al iniciar el host. Todo esto con el fin de que el tráfico pfsync entre los hosts se transfiera encapsulado por dicho túnel. Ahora escribiremos un resumen de lo ya tratado. Recordemos que lo primero que tenemos que hacer es instalar en ambos host el port:

pkg_add -mv wireguard-tools

luego ejecutar en cada host los siguientes comandos para la generación de las llaves pública y privada. Ejemplo en host "A":

obsda ~ root # cd /etc
obsda /etc root #
obsda /etc root # mkdir wireguard
obsda /etc root #
obsda /etc root # chmod 700 wireguard/
obsda /etc root #
obsda /etc root # cd wireguard/
obsda /etc/wireguard root #
obsda /etc/wireguard root # wg genkey > secret.key
Warning: writing to world accessible file.
Consider setting the umask to 077 and trying again.
obsda /etc/wireguard root #
obsda /etc/wireguard root # chmod 600 secret.key
obsda /etc/wireguard root #
obsda /etc/wireguard root # ls -la secret.key
-rw-------  1 root  wheel  45 May 26 09:09 secret.key
obsda /etc/wireguard root #
obsda /etc/wireguard root # wg pubkey < secret.key > public.key
obsda /etc/wireguard root #
obsda /etc/wireguard root # ls -la public.key
-rw-r--r--  1 root  wheel  45 May 26 09:10 public.key
obsda /etc/wireguard root #

también debemos crear un archivo de configuración en cada host para que al iniciar levante el túnel VPN automáticamente. Ejemplo de archivo en el host "A":

/etc/hostname.wg1

con el contenido:

wgport 51830 wgkey 'CIqY6QylkTAizY0TXd3tNBLJyPYVNaOCpLGEwFM/oUw='
10.1.1.100/24

# peer obsdb
wgpeer X+islmYJ263cJjh2h/x+pHGgVCAzthzQDYYmb14YEAw= wgendpoint 10.0.0.200 51830 wgaip 10.1.1.200/32

up

y ejemplo de archivo en el host "B":

/etc/hostname.wg1

con el contenido:

wgport 51830 wgkey '0LDS09MpEFdw8d+5yyiMDs10LBkkX2j2nx3Gumsap0g='
10.1.1.200/24

# peer obsda
wgpeer sB2kGR9mFaeR190jZ1lNs5SDUYLP5LFA0VgX1iWsJzs= wgendpoint 10.0.0.100 51830 wgaip 10.1.1.100/32

up

para mayor detalles de las opciones y comandos de configuración consultar el punto:

3.4. Segurizar conexión pfsync

Una vez que hayamos reiniciado para verificar esta configuración, veremos la siguiente interfaz wg1:

Host A:

obsda ~ root # ifconfig wg1
wg1: flags=80c3<UP,BROADCAST,RUNNING,NOARP,MULTICAST> mtu 1420
	index 8 priority 0 llprio 3
	wgport 51830
	wgpubkey sB2kGR9mFaeR190jZ1lNs5SDUYLP5LFA0VgX1iWsJzs=
	wgpeer X+islmYJ263cJjh2h/x+pHGgVCAzthzQDYYmb14YEAw=
		wgendpoint 10.0.0.200 51830
		tx: 436, rx: 380
		last handshake: 29 seconds ago
		wgaip 10.1.1.200/32
	groups: wg
	inet 10.1.1.100 netmask 0xffffff00 broadcast 10.1.1.255
obsda ~ root #

Host B:

obsdb ~ root # ifconfig wg1
wg1: flags=80c3<UP,BROADCAST,RUNNING,NOARP,MULTICAST> mtu 1420
	index 8 priority 0 llprio 3
	wgport 51830
	wgpubkey X+islmYJ263cJjh2h/x+pHGgVCAzthzQDYYmb14YEAw=
	wgpeer sB2kGR9mFaeR190jZ1lNs5SDUYLP5LFA0VgX1iWsJzs=
		wgendpoint 10.0.0.100 51830
		tx: 380, rx: 436
		last handshake: 46 seconds ago
		wgaip 10.1.1.100/32
	groups: wg
	inet 10.1.1.200 netmask 0xffffff00 broadcast 10.1.1.255
obsdb ~ root #

si ejecutamos un ping por el túnel desde el host "A":

obsda ~ root # ping -c3 10.1.1.200
PING 10.1.1.200 (10.1.1.200): 56 data bytes
64 bytes from 10.1.1.200: icmp_seq=0 ttl=255 time=1.915 ms
64 bytes from 10.1.1.200: icmp_seq=1 ttl=255 time=1.202 ms
64 bytes from 10.1.1.200: icmp_seq=2 ttl=255 time=1.305 ms

--- 10.1.1.200 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.202/1.474/1.915/0.315 ms
obsda ~ root #

comprobamos que responde, ahora ejecutamos desde el host "B":

obsdb ~ root # ping -c3 10.1.1.100
PING 10.1.1.100 (10.1.1.100): 56 data bytes
64 bytes from 10.1.1.100: icmp_seq=0 ttl=255 time=1.226 ms
64 bytes from 10.1.1.100: icmp_seq=1 ttl=255 time=1.221 ms
64 bytes from 10.1.1.100: icmp_seq=2 ttl=255 time=1.637 ms

--- 10.1.1.100 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.221/1.361/1.637/0.195 ms
obsdb ~ root #

hemos comprobado que el túnel esta funcionando correctamente. Ahora podemos levantar pfsync sobre el mismo.

4.2.5. Configuración de la interfaz del protocolo pfsync

En los puntos:

3.3.1. Conexión con protocolo multicast
3.3.2. Conexión con protocolo unicast

hemos configurado la interfaz pfsync(4) usando el comando ifconfig(8), ahora vamos a usar el formato de archivo hostname.if(5) para que dicha configuración quede persistente ante reinicios del sistema. Recordar que para segurizar la conexión de pfsync, vamos a realizar la misma con protocolo unicast vía túnel VPN Wireguard. Los archivos de configuración son:

Host A:

/etc/hostname.pfsync0

contenido:

syncdev wg1
syncpeer 10.1.1.200
up

Host B:

/etc/hostname.pfsync0

contenido:

syncdev wg1
syncpeer 10.1.1.100
up

al reiniciar veremos la interfaz pfsync0 en cada host, por otro lado recordar que en vez de reiniciar también podemos ejecutar el comando netstart(8) de la siguiente forma:

sh /etc/netstart pfsync0

ahora consultamos las interfaces pfsync en ambos hosts:

Host A:

obsda ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent wg1
	pfsync: syncdev: wg1 syncpeer: 10.1.1.200 maxupd: 128 defer: off
	groups: carp pfsync
obsda ~ root #

Host B:

obsdb ~ root # ifconfig pfsync0
pfsync0: flags=41<UP,RUNNING> mtu 1500
	index 8 priority 0 llprio 3
	encap: parent wg1
	pfsync: syncdev: wg1 syncpeer: 10.1.1.100 maxupd: 128 defer: off
	groups: carp pfsync
obsdb ~ root #

si quisiéramos verificar que entre los hosts están compartiendo estados PF, podemos ejecutar por ejemplo en el host "B" el siguiente comando tcpdump, fijarse que lo ejecutamos sobre la interfaz que transporta pfsync que es la wg1 (interface de Wireguard) y que ponemos de filtro que solo muestre el tráfico del source host 10.1.1.100 que es el host "A".

tcpdump -c 1 -i wg1 -n src host 10.1.1.100

ejemplo de salida:

obsdb ~ root # tcpdump -c 1 -i wg1 -n src host 10.1.1.100
tcpdump: listening on wg1, link-type LOOP
22:39:31.172576 10.1.1.100: PFSYNCv6 len 276
    act UPD ST COMP count 3
    ...
 (DF) [tos 0x10]
obsdb ~ root #

con esta salida comprobamos que pfsync esta funcionando.

4.2.6. Configuración de PF en función de los estados a sincronizar

Ahora que ya tenemos todas las interfaces listas podemos sincronizar "estados" entre los dos hosts de firewall:

  • obsda

  • obsdb

recordar que para hacerlo, debemos definir explícitamente con reglas de PF que conexiones vamos a dejar pasar generando un estado que luego será sincronizado con el otro peer. Una configuración inicial de PF podría ser:

Host A: /etc/pf.conf

contenido:

#	$OpenBSD: pf.conf,v 1.55 2017/12/03 20:40:04 sthen Exp $
#
# See pf.conf(5) and /etc/examples/pf.conf

block log all

set skip on lo

pass out on vio0
pass in  on vio0 proto icmp
pass in  on vio0 inet proto tcp from any to 10.0.0.100 port 22
pass in  on vio0 inet proto udp from 10.0.0.200 port 51830 to 10.0.0.100 port 51830

pass out on vio1
pass in  on vio1 proto icmp
pass in  on vio1 proto carp
pass in  on vio1 inet proto tcp from 192.168.46.0/24 to 192.168.46.254

pass out on vio2
pass in  on vio2 proto icmp
pass in  on vio2 proto carp
pass in  on vio2 inet proto tcp from 192.168.47.0/24 to 192.168.47.254

pass on wg1 proto pfsync
pass on wg1 proto icmp

match in  on vio1 inet proto tcp from any to 192.168.46.254 port 28232 rdr-to 192.168.47.102 port 22
match out on vio2 inet proto tcp from any to 192.168.47.102 nat-to 192.168.47.254

Host B: /etc/pf.conf

contenido:

#	$OpenBSD: pf.conf,v 1.55 2017/12/03 20:40:04 sthen Exp $
#
# See pf.conf(5) and /etc/examples/pf.conf

block log all

set skip on lo

pass out on vio0
pass in  on vio0 proto icmp
pass in  on vio0 inet proto tcp from any to 10.0.0.200 port 22
pass in  on vio0 inet proto udp from 10.0.0.100 port 51830 to 10.0.0.200 port 51830

pass out on vio1
pass in  on vio1 proto icmp
pass in  on vio1 proto carp
pass in  on vio1 inet proto tcp from 192.168.46.0/24 to 192.168.46.254

pass out on vio2
pass in  on vio2 proto icmp
pass in  on vio2 proto carp
pass in  on vio2 inet proto tcp from 192.168.47.0/24 to 192.168.47.254

pass on wg1 proto pfsync
pass on wg1 proto icmp

match in  on vio1 inet proto tcp from any to 192.168.46.254 port 28232 rdr-to 192.168.47.102 port 22
match out on vio2 inet proto tcp from any to 192.168.47.102 nat-to 192.168.47.254

Como podemos ver la configuración de reglas de filtrado es casi idéntica en ambos hosts. Vamos a describir los distintos bloques y que se quiere lograr con cada uno. Tomaremos de ejemplo la configuración del host "A", como se dijo la configuración del host "B" es idéntica excepto que cambian la IPs de los hosts en tres reglas, empezamos por:

block log all

con la regla anterior decimos que bloqueamos y generamos logs de todo el tráfico que ingresa al firewall, terminara pasando solamente lo que coincida con una regla que este por debajo de esta.

set skip on lo

es recomendable no aplicar reglas de filtrado en la interfaz de loopback, por eso realizamos la configuración anterior.

pass in  on vio0 proto icmp
pass in  on vio0 inet proto tcp from any to 10.0.0.100 port 22
pass in  on vio0 inet proto udp from 10.0.0.200 port 51830 to 10.0.0.100 port 51830

las reglas anteriores la aplicamos en la interfaz de MGMT, también usada para montar el túnel VPN Wireguard. En la primera dejamos pasar ida y vuelta (Input/Output) el protocolo ICMP, o sea que nos tiene que responder ping sin problemas. En la segunda regla habilitamos conexiones ssh a la IP 10.0.0.100 puerto 22. En la última y tercer regla dejamos pasar el tráfico UDP de Wireguard.

pass out on vio1
pass in  on vio1 proto icmp
pass in  on vio1 proto carp
pass in  on vio1 inet proto tcp from 192.168.46.0/24 to 192.168.46.254

pass out on vio2
pass in  on vio2 proto icmp
pass in  on vio2 proto carp
pass in  on vio2 inet proto tcp from 192.168.47.0/24 to 192.168.47.254

las reglas para las interfaces vio1 y vio2 son similares puesto que son las destinadas para montar los pseudo dispositivos:

carp0 - 192.168.46.254

y

carp1 - 192.168.47.254

La primer regla de cada interfaz deja salir (output) todo tipo de tráfico. La segunda regla deja entrar protocolo ICMP. La tercer regla deja entrar protocolo CARP. La última deja entrar todo tipo de tráfico desde la subred de confianza que le corresponde, 192.168.46.0/24 para vio1/carp0 y 192.168.47.0/24 para vio2/carp1.

pass on wg1 proto pfsync
pass on wg1 proto icmp

en estas dos reglas dejamos pasar por el túnel VPN Wireguard el tráfico de protocolos ICMP y pfsync.

match in  on vio1 inet proto tcp from any to 192.168.46.254 port 28232 rdr-to 192.168.47.102 port 22
match out on vio2 inet proto tcp from any to 192.168.47.102 nat-to 192.168.47.254

las últimas dos reglas vendrían a ser un ejemplo para probar conexiones persistentes como las de SSH que atraviesan el firewall, generando estados a compartir por pfsync. Estas reglas nos permitirán probar el beneficio de contar con HA en el último ejercicio de esta manual.

4.2.7. Probar la configuración de HA en OpenBSD

Para comprobar que la configuración de HA que realizamos en los puntos anteriores funciona, vamos a ejecutar un ejercicio en donde generamos una conexión con el protocolo SSH, esta conexión se realiza desde una Workstation en la LAN 1 hacia otra Workstation en la LAN 2. Estas Workstations pueden tener instalado cualquier sistema operativo que permita tener un cliente y un servidor del protocolo SSH. Entonces la conexión persistente SSH con la cual abriremos una sesión desde el cliente al servidor sería:

Workstation1(client)<---SSH--->Workstation2(server)

Recordar el diagrama de conectividad en donde observamos que ambas Workstations están en subredes diferentes:

Workstation1 IP:192.168.46.102/24 subred:192.168.46.0/24
Workstation2 IP:192.168.47.102/24 subred:192.168.47.0/24

como vimos en el punto anterior las últimas dos reglas de filtrado:

match in  on vio1 inet proto tcp from any to 192.168.46.254 port 28232 rdr-to 192.168.47.102 port 22
match out on vio2 inet proto tcp from any to 192.168.47.102 nat-to 192.168.47.254

sirven para que las conexiones con protocolo SSH dirigidas a la IP 192.168.46.254, puerto 28232, sean redirigidas a la IP 192.168.47.102 con el puerto 22. Como se puede apreciar estamos usando NAT para pasar una conexión SSH desde una IP origen de una subred a una IP destino de otra subred. Con rdr-to cambiamos IP y puertos destinos y con nat-to cambiamos IP origen.

NOTA: El ejercicio podría también realizarse haciendo un forwarding entre las IPs de ambas subredes, ejemplo:

pass in on vport1 from 192.168.46.102 to 192.168.47.102    
pass in on vport2 from 192.168.47.102 to 192.168.46.102

Sea de una forma u otra la idea es generar conexiones que PF las registre para que las mismas produzcan "estados" que sean replicados con pfsync entre los hosts de firewall.

A continuación vamos a establecer una conexión SSH contra la Workstation 2, vamos a probar alternar el estado de los hosts CARP (obsda, obsdb) entre Master y Backup, ya sea degradando el host "A" o reiniciando el mismo. La sesión SSH no debe ni cortarse ni colgarse, debe seguir como si nada hubiese pasado.

4.2.7.a. Primer test host "A" en estado Master

El primer test va a ser realizado con el firewall en HA en estado óptimo, host "A" como Master y host "B" como Backup. Vamos usar unos comandos en ambos hosts para comprobar como atraviesa la conexión y como se replican los estados de PF, los comandos son:

pfctl -vsr | grep -A 2 192.168.47.102
pfctl -ss | grep 192.168.47.102:22
tcpdump -i vio1 -n -p src host 192.168.46.102 and dst port 28232
tcpdump -i vio2 -n -p src host 192.168.47.254 and dst host 192.168.47.102 and dst port 22

Por supuesto que también debemos ejecutar en la Workstation 1, un comando para abrir una sesión ssh contra la Workstation 2. Si el sistema operativo de la Workstation 1 es un Unix Like como algún *BSD o un Linux ejecutamos algo como:

ssh -p 28232 192.168.46.254

Entonces abrimos dos sesiones en el host "A" y dejamos corriendo los comandos tcpdump, en una tercera sesión vamos a ejecutar con pfctl los comandos correspondientes. Al realizar la conexión por SSH desde la Workstation 1, los comandos tcpdump nos muestran:

obsda ~ root # tcpdump -i vio1 -n -p src host 192.168.46.102 and dst port 28232
tcpdump: listening on vio1, link-type EN10MB
11:01:20.575489 192.168.46.102.38408 > 192.168.46.254.28232: S 3825979887:3825979887(0) win 65535 <mss 1460,sackOK,timestamp 2530648975 0,nop,wscale 10> (DF)
11:01:20.635651 192.168.46.102.38408 > 192.168.46.254.28232: . ack 1083415399 win 86 <nop,nop,timestamp 2530649071 3639759073> (DF)
11:01:20.636425 192.168.46.102.38408 > 192.168.46.254.28232: P 0:22(22) ack 1 win 86 <nop,nop,timestamp 2530649071 3639759073> (DF)
11:01:20.759861 192.168.46.102.38408 > 192.168.46.254.28232: . ack 22 win 86 <nop,nop,timestamp 2530649195 3639759263> (DF)
11:01:20.765525 192.168.46.102.38408 > 192.168.46.254.28232: P 22:1046(1024) ack 22 win 86 <nop,nop,timestamp 2530649199 3639759263> (DF)
11:01:20.785791 192.168.46.102.38408 > 192.168.46.254.28232: P 1046:1094(48) ack 1142 win 88 <nop,nop,timestamp 2530649221 3639759283> (DF)
...

y:

obsda ~ root # tcpdump -i vio2 -n -p src host 192.168.47.254 and dst host 192.168.47.102 and dst port 22
tcpdump: listening on vio2, link-type EN10MB
11:01:20.576045 192.168.47.254.65160 > 192.168.47.102.22: S 3825979887:3825979887(0) win 65535 <mss 1460,sackOK,timestamp 2530648975 0,nop,wscale 10> (DF)
11:01:20.635670 192.168.47.254.65160 > 192.168.47.102.22: . ack 1083415399 win 86 <nop,nop,timestamp 2530649071 3639759073> (DF)
11:01:20.636445 192.168.47.254.65160 > 192.168.47.102.22: P 0:22(22) ack 1 win 86 <nop,nop,timestamp 2530649071 3639759073> (DF)
11:01:20.759882 192.168.47.254.65160 > 192.168.47.102.22: . ack 22 win 86 <nop,nop,timestamp 2530649195 3639759263> (DF)
11:01:20.765548 192.168.47.254.65160 > 192.168.47.102.22: P 22:1046(1024) ack 22 win 86 <nop,nop,timestamp 2530649199 3639759263> (DF)
11:01:20.785810 192.168.47.254.65160 > 192.168.47.102.22: P 1046:1094(48) ack 1142 win 88 <nop,nop,timestamp 2530649221 3639759283> (DF)
...

la salida es la esperada. El primer tcpdump muestra como se recibe en el host "A" la conexión desde la Workstation 1, se puede ver el inicio de la conexión con la flag "S" que es "SYN" o sea inicio de conexión, después vemos el tráfico habitual, fijarse que el puerto destino es el 28232. El segundo tcpdump muestra la conexión desde otra óptica, desde el mismo firewall ya haciendo el source NAT y teniendo ya como destino la Workstation 2, en este caso el puerto destino ya es el 22.

Con el primer comando pfctl observamos:

obsda ~ root # pfctl -vsr | grep -A 2 192.168.47.102
match in on vio1 inet proto tcp from any to 192.168.46.254 port = 28232 rdr-to 192.168.47.102 port 22
[ Evaluations: 38133 Packets: 224 Bytes: 24210 States: 1 ]
[ Inserted: uid 0 pid 42880 State Creations: 0 ]
--
match out on vio2 inet proto tcp from any to 192.168.47.102 nat-to 192.168.47.254
[ Evaluations: 23346 Packets: 817 Bytes: 83896 States: 1 ]
[ Inserted: uid 0 pid 42880 State Creations: 0 ]
obsda ~ root #

que las reglas de destination NAT "rdr-to" y source NAT "nat-to" tuvieron movimiento, notar la cantidad de paquetes y bytes procesados, pero por sobre todo notar el campo "States" que en ambas reglas aparece con un número 1, eso significa que estas reglas generaron un estado, que es lo que queríamos aparte de sus funciones de Destination NAT y Source NAT, ya que estos estados son los que tendrían que estar en este momento sincronizados gracias a pfsync en el host "B". El siguiente comando pfctl muestra:

obsda ~ root # pfctl -ss | grep 192.168.47.102:22
all tcp 192.168.47.102:22 (192.168.46.254:28232) <- 192.168.46.102:38408 ESTABLISHED:ESTABLISHED
all tcp 192.168.47.254:65160 (192.168.46.102:38408) -> 192.168.47.102:22 ESTABLISHED:ESTABLISHED
obsda ~ root #

que en el host "A" tenemos dos estados persistentes que son las dos conexiones SSH que esta administrando el firewall, una desde la Workstation 1 hacia 192.168.46.254 que como ya vimos es redirigida hacia la Workstation 2 con IP 192.168.47.102. Ahora lo más importante de este ejercicio es comprobar que dichos estados fueron sincronizados al host "B", ejecutamos el mismo comando en ese host:

obsdb ~ root # pfctl -ss | grep 192.168.47.102:22
all tcp 192.168.47.102:22 (192.168.46.254:28232) <- 192.168.46.102:38408 ESTABLISHED:ESTABLISHED
all tcp 192.168.47.254:65160 (192.168.46.102:38408) -> 192.168.47.102:22 ESTABLISHED:ESTABLISHED
obsdb ~ root #

como observamos en la salida del comando en el host "B", los estados si fueron sincronizados a este Host.

4.2.7.b. Segundo test degradar host "A" al estado de Backup

Para este test vamos aplicar lo aprendido en el punto:

2.7.4. Generar conmutación por error usando contador de degradación de CARP

en pocas palabras vamos a degradar el host "A" para que cambie al estado de Backup cosa que el host "B" pase al estado Master, todo ello con la conexión ssh en curso. Si todo esta bien configurado la conexión ssh no tiene que cerrarse ni colgarse, no se tiene que notar nada en la misma, tiene que seguir funcionando de forma normal. Por supuesto podemos realizar el mismo ejercicio apagando o reiniciando el host "A". Procedamos abriendo en ambos Host una sesión en donde dejamos corriendo este comando:

tail -f /var/log/messages

ahora en el host "A" ejecutamos el siguiente comando para proceder a degradarlo al estado de Backup:

obsda ~ root # ifconfig -g carp carpdemote 1

vemos en el log del host "A":

Jun 17 12:03:13 obsda /bsd: carp1: state transition: MASTER -> BACKUP
Jun 17 12:03:13 obsda /bsd: carp0: state transition: MASTER -> BACKUP

que pasó al estado de Backup y en el host "B" vemos:

Jun 17 12:02:57 obsdb /bsd: carp1: state transition: BACKUP -> MASTER
Jun 17 12:02:57 obsdb /bsd: carp0: state transition: BACKUP -> MASTER

que pasó al estado de Master. Lo primero que podemos comprobar es que la sesión ssh en la Workstation 1 no se cortó ni se clavó, sigue respondiendo como si nada hubiera cambiado. Lo otro que podemos ver es que los dos comandos tcpdump que ejecutamos en el punto anterior en el host "A", ya no muestran tráfico de red al movernos por la sesión ssh de la Workstation 1. Pero si ejecutamos esos mismos comandos tcpdump en el host "B", veremos que con solo apretar la tecla enter dentro de la sesión ssh de la Workstation 1, ahora el tráfico de red pasa por ese host:

obsdb ~ root # tcpdump -i vio1 -n -p src host 192.168.46.102 and dst port 28232
tcpdump: listening on vio1, link-type EN10MB
12:07:14.285776 192.168.46.102.38408 > 192.168.46.254.28232: P 3825982698:3825982734(36) ack 1083419388 win 93 <nop,nop,timestamp 2534619389 3643703703> (DF)
12:07:14.289078 192.168.46.102.38408 > 192.168.46.254.28232: . ack 37 win 93 <nop,nop,timestamp 2534619417 3643729523> (DF)
12:07:14.289110 192.168.46.102.38408 > 192.168.46.254.28232: . ack 73 win 93 <nop,nop,timestamp 2534619417 3643729523> (DF)
12:07:44.682321 192.168.46.102.38408 > 192.168.46.254.28232: P 36:72(36) ack 73 win 93 <nop,nop,timestamp 2534649783 3643729523> (DF)
...

y:

obsdb ~ root # tcpdump -i vio2 -n -p src host 192.168.47.254 and dst host 192.168.47.102 and dst port 22
tcpdump: listening on vio2, link-type EN10MB
12:07:14.285898 192.168.47.254.65160 > 192.168.47.102.22: P 3825982698:3825982734(36) ack 1083419388 win 93 <nop,nop,timestamp 2534619389 3643703703> (DF)
12:07:14.289284 192.168.47.254.65160 > 192.168.47.102.22: . ack 37 win 93 <nop,nop,timestamp 2534619417 3643729523> (DF)
12:07:14.289287 192.168.47.254.65160 > 192.168.47.102.22: . ack 73 win 93 <nop,nop,timestamp 2534619417 3643729523> (DF)
12:07:44.682433 192.168.47.254.65160 > 192.168.47.102.22: P 36:72(36) ack 73 win 93 <nop,nop,timestamp 2534649783 3643729523> (DF)
...

todo funciona según lo esperado.

4.2.7.c. Tercer test volver host "A" al estado de Master

Ahora vamos a devolver al estado de Master al host "A" con el siguiente comando:

obsda ~ root # ifconfig -g carp -carpdemote 1

vemos en el log del host "A":

Jun 17 12:11:28 obsda /bsd: carp1: state transition: BACKUP -> MASTER
Jun 17 12:11:28 obsda /bsd: carp0: state transition: BACKUP -> MASTER

en el host "B":

Jun 17 12:11:11 obsdb /bsd: carp1: state transition: MASTER -> BACKUP
Jun 17 12:11:11 obsdb /bsd: carp0: state transition: MASTER -> BACKUP

ahora nuevamente comprobamos que la conexión ssh no se cortó ni se clavó y ademas vemos que los comandos tcpdump en el host "A" volvieron a tener tráfico y en cambio los del host "B" ya no.

5. Sincronización de archivos entre hosts

No podíamos dar por concluido este manual sin ofrecer una recomendación para la sincronización de archivos entre los hosts que están en HA. Habitualmente en una configuración real de un router/firewall en HA vamos a tener archivos que cambian regularmente, estos archivos pueden estar relacionados a reglas de firewall, rutas o cualquier archivo con configuraciones que cambien periódicamente. No ganaríamos nada en tener dos instancias de un router/firewall funcionando en la modalidad HA, si una de ellas esta desactualizada en cuanto a archivos de configuración diarios.

Habitualmente estaríamos realizando configuraciones en el Master, que seria el host "A", pero que pasaría si no sincronizamos esos archivos en donde hacemos configuraciones al host "B" en estado de Backup, pasaría que cuando este tome el lugar de Master nos encontraríamos con el firewall, ruteo y otras configuraciones desactualizadas, entonces ese host "B" no nos sería útil como Master.

Tampoco nos serviría una copia o sincronización, ya sea manual o automática, que sea desde una sola dirección. Habitualmente la haríamos desde el host "A" que es el Master default hacia el host "B" que es el de Backup. Tendríamos un problema cuando el host "B" tome el lugar de Master, no podríamos hacer ningún cambio en los archivos de configuración, ya que los mismos no se propagarán automáticamente hacia el host "A" cuando este reviva y tome el lugar de Master nuevamente. En este caso deberíamos recordar que cambios hicimos y pasarlos manualmente.

Por todo lo comentado anteriormente necesitamos algo que haga la sincronización de forma automática cosa de evitar olvidos y que sea bidireccional.

5.1. Unison

NOTA: En este manual veremos como solucionar el problema de la sincronización bidireccional con el programa Unison, pero esto no significa que no haya otras opciones, la elección de Unison y la forma de funcionamiento que elegimos obedece a ser lo menos invasivo posible a la base del sistema OpenBSD, si bien Unison se instala a través de un port, solo se instala uno el cual aparte no tiene dependencias. Otras opciones para evaluar:

  • Syncthing
  • Rclone

NOTA: Unison puede enterarse de cambios en archivos y carpetas a través de eventos del kernel. Unison usa para ello un programa helper para monitorear los cambios. Este programa helper mira estos cambios a través de la API inotify() en Linux. La compatibilidad con los BSDs se logra usando libinotify-kqueue, que es una capa por arriba de lo nativo de BSD que es kqueue(). Entonces libinotify-kqueue sirve para hacer compatibles programas que usan inotify en BSD. El problema es que el port de Unison en OpenBSD no tiene la opción para construir el helper unison-fsmonitor, desconozco el porque, por lo tanto, en este manual vamos a configurar Unison para evalúe cada cierto tiempo si hay cambios para sincronizar, dicha evaluación se configura para que se haga cada tantos segundos, minutos, etc.

5.1.1. Sobre los programadores de Unison

El principal programador de Unison es Benjamin C. Pierce, su sitio web personal:

https://www.cis.upenn.edu/~bcpierce/

entrada en la wikipedia:

https://en.wikipedia.org/wiki/Benjamin_C._Pierce

en github:

https://github.com/bcpierce00

No solo es creador del programa Unison, si no también un prolífico escritor de documentos relacionados a las ciencias de la computación:

https://www.cis.upenn.edu/~bcpierce/papers/index.shtml

5.2. Instalar Unison

Vamos a instalar Unison en OpenBSD en formato paquete binario con el siguiente comando:

pkg_add -mv unison

la instalación la vamos a realizar en ambos host del cluster de HA, sin embargo como veremos en el siguiente punto, la sincronización siempre se va a disparar desde el host Master por defecto que es el A. Instalamos en ambos porque siempre es aconsejable tener los mismos igualados en cuanto al software instalado. Comandos de instalación:

Host A:

obsda ~ root # pkg_add -mv unison
Update candidates: quirks-7.14 -> quirks-7.14
quirks-7.14 signed on 2024-07-10T21:22:44Z
unison-2.53.4: ok
New and changed readme(s):
    /usr/local/share/doc/pkg-readmes/unison
obsda ~ root #

Host B:

obsdb ~ root # pkg_add -mv unison
Update candidates: quirks-7.14 -> quirks-7.14
quirks-7.14 signed on 2024-07-10T21:26:59Z
unison-2.53.4: ok
New and changed readme(s):
    /usr/local/share/doc/pkg-readmes/unison
obsdb ~ root #

en OpenBSD siempre que instalamos un paquete es recomendable leer el archivo de novedades, en este caso el archivo es:

/usr/local/share/doc/pkg-readmes/unison

5.3. Diseñar esquema de sincronización

Vamos a diseñar un esquema de sincronización lo más sencillo posible, las condiciones serían:

  1. La sincronización se debe realizar cada un minuto.
  2. La sincronización la dispara el usuario syncha.
  3. El comando de sincronización se disparará desde el host Master default que en nuestro caso es el "A".
  4. La sincronización es de forma bidireccional "Host A <--> Host B". Si hay conflictos se resuelve con preferencia para los archivos más nuevos en su timestamp.
  5. Independientemente del estado de CARP (Maestro o Backup) en los hosts, la sincronización se realiza de la misma forma establecida en el punto 4.
  6. Si ambos host están encendidos pero no tienen comunicación entre ellos, una vez que la misma vuelva se realiza la sincronización.
  7. Si un host se apaga se sincronizará una vez que se encienda.

5.4. Configurar Unison

Necesitamos que el proceso de Unison se levante automáticamente al encender el host "A", que es el Master default y que ademas ejecute cada 60 segundos la sincronización, todo esto se debe realizar con un usuario dedicado a tal fin. A continuación analizamos los pasos para realizar esta configuración.

5.4.1. Usuario para ejecutar la sincronización

Vamos a crear el usuario "syncha" en ambos host para realizar la sincronización, en este manual vamos a proponer un ejemplo de sincronización de archivos de Packet Filter (PF), o sea que son archivos que tienen reglas de filtrado, como es lógico a estos archivos solo puede acceder para leerlos el usuario root y usuarios del grupo wheel, por tal motivo al usuario syncha lo vamos a poner de agregado en ese grupo. Teniendo en cuenta que la sincronización podría llegar a usar algo más de recursos de lo normal, también vamos a agregar al usuario en la clase staff. Procedamos con los pasos de creación del usuario.

5.4.1.a. Creación de usuario

El comando que vamos a usar para la creación del usuario syncha es:

adduser

la primera vez que ejecutamos este comando nos hace unas preguntas para crear un archivo de perfil para la creación de usuarios, dicho archivo es:

/etc/adduser.conf

a continuación un ejemplo de como ejecutar este comando en el host "A":

obsda ~ aali $ doas adduser
Couldn't find /etc/adduser.conf: creating a new adduser configuration file
Reading /etc/shells
Enter your default shell: bash csh git-shell ksh nologin sh
[ksh]:
Your default shell is: ksh -> /bin/ksh
Default login class: authpf bgpd daemon default pbuild staff syncthing unbound xenodm
[default]:
Enter your default HOME partition: [/home]:
Copy dotfiles from: /etc/skel no [/etc/skel]:
Send welcome message?: /path/file default no [no]:
Do not send message(s)
Prompt for passwords by default (y/n) [y]:
Default encryption method for passwords: auto blowfish [auto]:
Use option ''-silent'' if you don't want to see all warnings and questions.

Reading /etc/shells
Check /etc/master.passwd
Check /etc/group

Ok, let's go.
Don't worry about mistakes. There will be a chance later to correct any input.
Enter username []: syncha
Enter full name []: syncha
Enter shell bash csh git-shell ksh nologin sh [ksh]:
Uid [1001]:
Login group syncha [syncha]:
Login group is ''syncha''. Invite syncha into other groups: guest no
[no]: wheel
Login class authpf bgpd daemon default pbuild staff syncthing unbound xenodm
[default]: staff
Enter password []:
Enter password again []:

Name:	     syncha
Password:    ****
Fullname:    syncha
Uid:	     1001
Gid:	     1001 (syncha)
Groups:	     syncha wheel
Login Class: staff
HOME:	     /home/syncha
Shell:	     /bin/ksh
OK? (y/n) [y]: y
Added user ''syncha''
Copy files from /etc/skel to /home/syncha
Add another user? (y/n) [y]: n
Goodbye!
obsda ~ aali $

Como ya dijimos inicialmente en este punto, vamos a crear el usuario syncha en ambos hosts, recordar ejecutar este comando de la misma forma en el host "B". Una vez que hayamos creado el usuario en ambos host probamos el ingreso:

workstation ~ aali $ ssh syncha@10.0.0.100
syncha@10.0.0.100's password:
Last login: Thu Jul 11 22:17:12 2024 from 10.0.0.1
OpenBSD 7.5 (GENERIC) #79: Wed Mar 20 15:33:49 MDT 2024

Welcome to OpenBSD: The proactively secure Unix-like operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the latest
version of the code.  With bug reports, please try to ensure that
enough information to reproduce the problem is enclosed, and if a
known fix for it exists, include that as well.

obsda$
obsda$ id
uid=1001(syncha) gid=1001(syncha) groups=1001(syncha), 0(wheel)
obsda$

ingresamos perfecto.

5.4.1.b. Generación del par de llaves ssh

Ahora vamos a generar el par de llaves ssh (pública y privada) con el siguiente comando:

obsda$ ssh-keygen
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/syncha/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/syncha/.ssh/id_ed25519
Your public key has been saved in /home/syncha/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:vSDboWRNwjb0AvKGhgK4yAkTN3HmzUm7lViB+cTB2CM syncha@obsda.fortix.com.ar
The key's randomart image is:
+--[ED25519 256]--+
|+.=.+ oB+o       |
|=o O BE**.       |
|*o+ + %+=.       |
|++ . . O..       |
|      = S .      |
|     o = o .     |
|      o . .      |
|                 |
|                 |
+----[SHA256]-----+
obsda$

Cuando se nos pregunte por una passphrase, no vamos a cargar ninguna, para pasar por alto esta pregunta solo presionamos la tecla enter. Esto es necesario puesto que si protegeríamos la private key con una passphrase, cada vez que se requiera una validación nos solicitaría la misma, usar de esta forma la validación SSH no es posible con procedimientos automáticos como el que necesitamos configurar. En el momento de escribir este manual se usa el algoritmo de firma digital de curva de Edwards (EdDSA) ed25519. Recordar ejecutar el comando anterior en ambos hosts. Esto genera los archivos:

obsda$ ls -la .ssh/
total 16
drwx------  2 syncha  syncha  512 Jul 11 22:48 .
drwxr-xr-x  3 syncha  syncha  512 Jul 11 22:33 ..
-rw-------  1 syncha  syncha    0 Jul 11 22:33 authorized_keys
-rw-------  1 syncha  syncha  419 Jul 11 22:47 id_ed25519
-rw-r--r--  1 syncha  syncha  108 Jul 11 22:47 id_ed25519.pub
obsda$
5.4.1.c. Método de autenticación ssh por publickey entre ambos hosts

Ahora vamos agregar confianza entre ambos hosts, dejando que el usuario syncha pueda iniciar sesión en ambos sentidos:

hostA(syncha)<--SSH-->(syncha)hostB

El método de autenticación que vamos a usar no será "password" sino "publickey", con esto vamos a lograr que cuando se ejecute la sincronización no nos pida un password de forma interactiva, lo cual sería ineficiente puesto que tendríamos que disparar la sincronización de forma manual en una sesión y no de forma automática con una tarea en el planificador Cron. Para lograr esto debemos copiar la llave pública del host "A" en el host "B" y viceversa, esta copia de la llave pública se ubica en el archivo:

~/.ssh/authorized_keys

podemos realizar esto de varias formas, manualmente es una, por ejemplo si empezamos con el host "A", abrimos una sesión en el mismo, editamos el archivo:

~/.ssh/id_ed25519.pub

y copiamos su contenido, el cual pegamos en otra sesión, en otra pantalla en donde estemos editando en el host "B" el archivo:

authorized_keys

Luego hacemos lo mismo pero desde el host "B" al host "A". Otra forma de realizar esto es instalar el script "ssh-copy-id" con el comando:

obsda ~ root # pkg_add -mv ssh-copy-id
Update candidates: quirks-7.14 -> quirks-7.14
quirks-7.14 signed on 2024-07-12T15:17:11Z
ssh-copy-id-8.8pl1: ok
obsda ~ root #

con este script ya instalado en el sistema, la copia se realiza simplemente ejecutando:

Host A:

obsda$ ssh-copy-id -i .ssh/id_ed25519.pub obsdb.fortix.com.ar
/usr/local/bin/ssh-copy-id: INFO: Source of key(s) to be installed: ".ssh/id_ed25519.pub"
The authenticity of host 'obsdb.fortix.com.ar (10.0.0.200)' can't be established.
ED25519 key fingerprint is SHA256:lE9I56sG7MqG3JDBJspftJqWfFquizrt0uH1IZ5PLYA.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/local/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/local/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
syncha@obsdb.fortix.com.ar's password:

Number of key(s) added:        1

Now try logging into the machine, with:   "ssh 'obsdb.fortix.com.ar'"
and check to make sure that only the key(s) you wanted were added.

obsda$

Host B:

obsdb$ ssh-copy-id -i .ssh/id_ed25519.pub obsda.fortix.com.ar
/usr/local/bin/ssh-copy-id: INFO: Source of key(s) to be installed: ".ssh/id_ed25519.pub"
The authenticity of host 'obsda.fortix.com.ar (10.0.0.100)' can't be established.
ED25519 key fingerprint is SHA256:ksgShTKABatQvzLMbzuTdLbj4S/gFrrAFlOvYLXMJlU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/local/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/local/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
syncha@obsda.fortix.com.ar's password:

Number of key(s) added:        1

Now try logging into the machine, with:   "ssh 'obsda.fortix.com.ar'"
and check to make sure that only the key(s) you wanted were added.

obsdb$

ahora probamos ingresar desde un host al otro.

Desde host "A" hacia el host "B":

obsda$ ssh obsdb.fortix.com.ar
Last login: Sun Jul 14 19:24:33 2024 from 10.0.0.1
OpenBSD 7.5 (GENERIC.MP) #138: Wed Mar 20 19:42:15 MDT 2024

Welcome to OpenBSD: The proactively secure Unix-like operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the latest
version of the code.  With bug reports, please try to ensure that
enough information to reproduce the problem is enclosed, and if a
known fix for it exists, include that as well.

obsdb$

Desde host "B" hacia el host "A":

obsdb$ ssh obsda.fortix.com.ar
Last login: Sun Jul 14 19:40:55 2024 from 10.0.0.1
OpenBSD 7.5 (GENERIC) #79: Wed Mar 20 15:33:49 MDT 2024

Welcome to OpenBSD: The proactively secure Unix-like operating system.

Please use the sendbug(1) utility to report bugs in the system.
Before reporting a bug, please try to reproduce it with the latest
version of the code.  With bug reports, please try to ensure that
enough information to reproduce the problem is enclosed, and if a
known fix for it exists, include that as well.

obsda$

ambos accesos ssh con publickey funcionan.

5.4.2. Configurar en Unison una tarea de sincronización

Podemos ejecutar Unison de dos formas, una es cargar los argumentos necesarios en la linea de comandos, ejemplo:

unison -logfile /var/log/unison.log -prefer newer -auto -batch -repeat 60 /etc/pf/clients/ ssh://obsdb//etc/pf/clients/

o generar un archivo tipo perfil por cada sincronización, para luego en la linea de comandos ejecutar Unison con el perfil que necesitamos usar, ejemplo:

unison syncha

Entonces vamos a crear un archivo de perfil, el mismo se ubica en:

~/.unison/syncha.prf

con el siguiente contenido:

label = syncha
prefer = newer
auto = true
batch = true
repeat = 60
root = /etc/pf/clients/
root = ssh://obsdb//etc/pf/clients/
logfile = /var/log/unison.log
sshargs = -oIdentityFile=/home/syncha/.ssh/id_ed25519
#debug=all

notar que los archivos de perfiles se guardan por defecto en el directorio:

~/.unison/

Ejecutemos la primer sincronización y veamos el resultado:

obsda$ unison syncha
Unison 2.53.4 (ocaml 4.14.1): Contacting server...
Connected [//obsda.fortix.com.ar//etc/pf/clients -> //obsdb.fortix.com.ar//etc/pf/clients]

Looking for changes
Warning: No archive files were found for these roots, whose canonical names are:
	/etc/pf/clients
	//obsdb.fortix.com.ar//etc/pf/clients
This can happen either
because this is the first time you have synchronized these roots,
or because you have upgraded Unison to a new version with a different
archive format.

Update detection may take a while on this run if the replicas are
large.

Unison will assume that the 'last synchronized state' of both replicas
was completely empty.  This means that any files that are different
will be reported as conflicts, and any files that exist only on one
replica will be judged as new and propagated to the other replica.
If the two replicas are identical, then no changes will be reported.

If you see this message repeatedly, it may be because one of your machines
is getting its address from DHCP, which is causing its host name to change
between synchronizations.  See the documentation for the UNISONLOCALHOSTNAME
environment variable for advice on how to correct this.


  Waiting for changes from server
Reconciling changes
file     ---->            mysql01.conf
file     ---->            mysql02.conf
file     ---->            owncloud01.conf
file     ---->            proxy01.conf
file     ---->            proxy02.conf
file     ---->            proxy03.conf
file     ---->            webserver01.conf
file     ---->            webserver02.conf
file     ---->            webserver03.conf
file     ---->            webserver04.conf
file     ---->            webserver05.conf

11 items will be synced, 0 skipped
1.6 KiB to be synced from local to obsdb.fortix.com.ar
0 B to be synced from obsdb.fortix.com.ar to local
Propagating updates
Unison 2.53.4 (ocaml 4.14.1) started propagating changes at 22:25:47.85 on 14 Jul 2024
[BGN] Copying mysql01.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying mysql02.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying owncloud01.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying proxy01.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying proxy02.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying proxy03.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying webserver01.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying webserver02.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying webserver03.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying webserver04.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[BGN] Copying webserver05.conf from /etc/pf/clients to //obsdb.fortix.com.ar//etc/pf/clients
[END] Copying mysql01.conf
[END] Copying mysql02.conf
[END] Copying owncloud01.conf
[END] Copying proxy01.conf
[END] Copying proxy02.conf
[END] Copying proxy03.conf
[END] Copying webserver01.conf
[END] Copying webserver02.conf
[END] Copying webserver03.conf
[END] Copying webserver04.conf
[END] Copying webserver05.conf
Unison 2.53.4 (ocaml 4.14.1) finished propagating changes at 22:25:47.96 on 14 Jul 2024, 0.106 s
Saving synchronizer state
Synchronization complete at 22:25:47  (11 items transferred, 0 skipped, 0 failed)

Sleeping for 60 seconds...

como observamos la sincronización funcionó con éxito. Un detalle a tener en cuenta es que al ser la primera vez que sincronizamos estas carpetas, se nos informa lo siguiente:

Looking for changes
Warning: No archive files were found for these roots, whose canonical names are:
	/etc/pf/clients
	//obsdb.fortix.com.ar//etc/pf/clients
This can happen either
because this is the first time you have synchronized these roots,
or because you have upgraded Unison to a new version with a different
archive format.

este mensaje aparece porque es la primera vez que sincronizamos estas carpetas, en las próximas sincronizaciones ya no aparecerá. Tener en cuenta que aparte de los perfiles de sincronización, en el directorio "~/.unison/" también se guardan archivos de control de las sincronizaciones, con los cuales Unison puede hacer el seguimiento de las mismas, entonces en el normal funcionamiento nunca deberíamos borrar el directorio ~/.unison/ ni los archivos que el programa genera adentro. Ejemplo de como quedo el directorio en ambos hosts con la primer sincronización:

Host A:

obsda$ cd .unison/
obsda$ ls -la
total 20
drwx------  2 syncha  syncha   512 Jul 14 22:25 .
drwxr-xr-x  5 syncha  syncha   512 Jul 14 22:24 ..
-rw-------  1 syncha  syncha  1740 Jul 14 22:25 ar5e8abc5af841f5a0ade604c2f0956860
-rw-------  1 syncha  syncha   862 Jul 14 22:25 fp5e8abc5af841f5a0ade604c2f0956860
-rw-r--r--  1 syncha  syncha   301 Jul 14 22:15 syncha.prf
obsda$

Host B:

obsdb$ cd .unison/
obsdb$ ls -la
total 16
drwx------  2 syncha  syncha   512 Jul 14 22:27 .
drwxr-xr-x  5 syncha  syncha   512 Jul 14 22:03 ..
-rw-------  1 syncha  syncha  1718 Jul 14 22:25 ar8723b97164c76ecba6400f55a6e69858
-rw-------  1 syncha  syncha    34 Jul 14 22:25 fp8723b97164c76ecba6400f55a6e69858
obsdb$

5.4.3. Script de arranque RC para Unison

Si el lector no esta al tanto del sistema de arranque RC en OpenBSD puede hacer una rápida lectura a:

https://www.fortix.com.ar/sistemadearranquercenopenbsd/

con unos conocimientos básicos ya podemos crear un script RC para que Unison arranque automáticamente en el tiempo de boot. Entonces vamos a crear el script, abrir con el editor el nuevo archivo:

/etc/rc.d/unison

y agregar el siguiente contenido:

#!/bin/ksh

daemon="/usr/local/bin/unison syncha &"
daemon_user="syncha"

. /etc/rc.d/rc.subr

rc_reload=NO

rc_cmd $1

recordar darle los permisos de ejecución con:

chmod 555 /etc/rc.d/unison

y por último en el archivo:

/etc/rc.conf.local

agregar la siguiente linea:

unison=

ahora al reiniciar el sistema, observaríamos en el log de Unison:

/var/log/unison.log

las lineas:

Unison 2.53.4 (ocaml 4.14.1) log started at 2024-07-15 at 13:50:27

Roots:
  /etc/pf/clients
  ssh://obsdb//etc/pf/clients

y el proceso cargado:

obsda ~ root # ps xau | grep unison
syncha   17180  0.0  0.5  7596  8004 p2  S       2:03PM    0:00.01 /usr/local/bin/unison syncha
syncha   27179  0.0  0.3  1684  4604 p2  Sp      2:03PM    0:00.05 ssh obsdb -e none -oIdentityFile=/home/syncha/.ssh/id_ed25519 unison -server __ne
obsda ~ root #

6. Listas de referencias

[1] https://www.openbsd.org/faq/pf/carp.html

[2] https://man.openbsd.org/carp

[3] https://man.openbsd.org/pfsync.4

[4] https://man.openbsd.org/ifconfig#CARP

[5] https://en.wikipedia.org/wiki/Common_Address_Redundancy_Protocol

[6] https://github.com/jedisct1/UCarp

[7] https://manpages.ubuntu.com/manpages/bionic/man8/ucarp.8.html

[8] https://man.openbsd.org/tcpdump.8#AUTHORS