Publi

Implementación de filtros de contenido “después de la cola” en Postfix [Ejemplos en Bash y Python]

Filros de contenido en Postfix
Como hemos visto en anteriores artículos, Postfix nos da una gran flexibilidad a la hora de aplicar reglar y filtros a los mensajes que van llegando. Como ejemplo tenemos la implementación de Milters. Estos filtros se ejecutan antes de introducir los mensajes en la cola de entrega por lo que, a veces, puede no ser lo que buscamos. Cuando filtramos un correo con milters, el emisor del mensaje va a saber si se entrega o no. Esto puede ser un punto positivo, pero cuando se trata de combatir el SPAM no podemos dejar que alguien se ponga a hacer pruebas contra nuestro servidor hasta que descubra nuestras reglas. Por otro lado, podemos tener necesidades especiales, como desviar el proceso de entrega de un mensaje cuando cumple una serie de reglas. Imaginémonos que cuando el mensaje tenga un determinado destinatario debemos enviarlo conectando directamente con un servidor de correo determinado. Puede que esto parezca un capricho, pero en determinados entornos empresariales, por seguridad, se suelen filtrar a conciencia los correos y tenemos que ser los demás los que nos adaptemos a ellos.

¿Dónde entran los filtros de contenido “después de la cola”?

Este tipo de filtro, como su nombre indica, after-queue, o después de la cola, se llamarán cuando el mensaje ya está en la cola de entrega. Aún tienen que pasar por un proceso para ser entregados, pero el emisor, o el servidor que los ha dejado, ya los ha enviado y ha desconectado, por lo que el control está ahora en nuestro campo. Cuando utilizábamos los Milters, teníamos bibliotecas para varios lenguajes de programación que nos permitían controlar el proceso. De hecho, libmilter nos permitía implementar el protocolo y manejo de estos filtros de forma sencilla. En cambio, con este tipo de filtros, después de la cola, vamos a recibir el mensaje completo, en texto plano, y nosotros nos tenemos que encargar de parsear el contenido del mensaje, extraer las cabeceras, analizarlas y actuar en consecuencia. Es algo más artesano, pero muy útil.

Gracias a la flexibilidad que nos da Postfix podemos implementar los filtros de varias formas. Por un lado, si el filtro es muy rápido, no va a tardar mucho en analizar los correos, incluso si son muy grandes, podemos hacer un simple script en bash, perl, python, PHP, un programa en C o lo que queramos, que simplemente recibirá el mensaje y lo enviará al siguiente paso de nuestro proceso de envío de correos. Vamos, Postfix saca de la cola el mensaje y se lo manda a nuestro filtro, nuestro filtro lo procesa y lo meterá de nuevo en la cola. Así que, un simple script puede hacer las veces de filtro de contenido, muy fácil y rápido de implementar.

Por otro lado, si queremos complicarnos un poco la vida, y no sólo complicarnos la vida, sino pensar en la escalabilidad del sistema, hacer que nuestro filtro lo procese otra máquina, un programa dentro de un contenedor de docker o lo que se nos ocurra, debemos implementar el filtro de contenido en un interfaz por red. Para ello, tenemos que crear un pequeño servidor de correo que, cuando reciba un mensaje, lo procese, y tome decisiones. Si queremos que el mensaje se envíe a través de nuestro sistema, tenemos que devolverlo a nuestro Postfix de nuevo, enviando el mensaje a través de otro puerto de envío. Si no queremos enviarlo, no tenemos que hacer nada, una vez el mensaje se ha sacado de la cola, desaparece.

Algunas utilidades, algo menos resumido

Uno de los usos más extendido es implementar filtros AntiSpam. Aunque hay muchos servidores de correo que cuando alguien envía un correo basura rechazan el mensaje nada más entrar, podemos estar dando pistas a un posible spammer. Por otro lado, muchas veces, los filtros AntiSpam suelen ser más complicados, implican consultar con varios servicios online de listas blancas y negras, aparte del análisis de la información, por lo que puede retrasar mucho el envío de los mensajes. ¿Por qué no sacar los mensajes de la cola, procesarlos, tardar lo que tengamos que tardar y volver a encolarlos?
Otro uso muy extendido es el de la búsqueda de virus o malware. Aunque, uno de mis usos favoritos es el poder cambiar la trayectoria de los correos. Me refiero a que si mandamos los correos a una dirección de desarrollo, no pase por el sistema de correo, sino que llegue a una base de datos, o a un fichero, o incluso pase directamente a un programa de análisis, o que si el mensaje es para un cierto destinatario, podamos cambiar el remitente y enviarlo desde otro servidor diferente. En este último caso, sacamos el mensaje de la cola de envío y, si el mensaje cumple unas ciertas condiciones lo volveremos a enviar, a través de otro servidor de correo, si no las cumple, el mensaje seguirá su curso. Es muy divertido.

Eso sí, os recomiendo encarecidamente que, cuando implementéis vuestros filtros de correo, escribáis en un log el mensaje que ha llegado y lo que hacéis con él. Esto nos ayudará muchísimo para el diagnóstico de problemas. Porque es posible que nuestro filtro no funcione bien, o que el mensaje que ha llegado no cumpla bien las condiciones, o incluso que haya un problema al volver a meterlo en la cola.

Configurando un filtro de contenido con un script

Como ejemplo sencillo, vamos a empezar configurando un script como filtro de correo. Es más, un script para Bash, muy sencillo, con el que recibiremos el mensaje, lo procesaremos y se lo volveremos a enviar a Postfix. Podemos crear un directorio llamado /var/spool/filters e introducirlos ahí. Por ejemplo el siguiente (lo llamamos censurado.sh):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/sh

function error() {
  local CODE="$1"
  local MSG="$2"
  echo "$MSG (Code: $CODE)"
  exit $CODE
}

INSPECT_DIR=/tmp
SENDMAIL="/usr/sbin/sendmail -G -i"
TMPFILE="in.$$"

# Códigos de salida obtenidos de <sysexits.h>
EX_TEMPFAIL=75
EX_UNAVAILABLE=69

# Limpieza de archivos temporales
trap "rm -f $TMPFILE" 0 1 2 3 15
# Start processing.
cd $INSPECT_DIR || error $EX_TEMPFAIL "$INSPECT_DIR does not exist"
cat >$TMPFILE || error $EX_TEMPFAIL "Cannot save mail to file"

if [ -n "$(grep -i 'censurado' $TMPFILE)" ]; then
  error $EX_UNAVAILABLE "censurado found!!"
fi

$SENDMAIL "$@" <$TMPFILE
exit $?

Vale, este filtro es muy sencillo, solo busca la palabra “censurado” dentro del texto del mensaje. Si lo encuentra, tira el mensaje para atrás y si no, lo devuelve a la cola. Como veis, para devolverlo a la cola, volvemos a llamar a sendmail. Y, por supuesto, podemos utilizar este script para implementar otros filtros más complicados, filtros que llamen a otros programas como sed, awk, o incluso otros programas externos que hagamos en otros lenguajes, envíen el mensaje a un servidor para su posterior procesamiento, anotar en un log el mensaje, emitir notificaciones, como por ejemplo enviar un SMS al móvil o un mensaje por Telegram a una persona cuando le manden un e-mail con una cierta palabra, vamos, lo que se nos ocurra. Eso sí, es mala idea autocontestar los correos desde aquí, sobre todo porque muchas veces, hay virus o mensajes de SPAM cuyo remitente no es el que dice ser y podemos estar autocontestando mensajes generando SPAM al mismo tiempo.

Ahora, para que Postfix ejecute este pequeño script cada vez que envíe un e-mail editamos /etc/postfix/master.cf buscamos la línea de smtp y la modificamos de la siguiente forma:

1
2
3
4
5
6
  # =============================================================
  # service type  private unpriv  chroot  wakeup  maxproc command
  #               (yes)   (yes)   (yes)   (never) (100)
  # =============================================================
smtp    inet    n       -       n       -       -       smtpd
   -o content_filter=censurado:dummy

Eso sí, el texto que en mi ejemplo son n y – lo dejamos tal y como lo tengamos en nuestro servidor, porque podemos alterar el funcionamiento del servidor de correo. Puede que smtpd tenga más argumentos en su ejecución, deberíamos añadir -o content_filter=censurado:dummy al final de los argumentos. Dummy es una configuración que indica el destino de los mensajes y puede ser utilizado en otros métodos de transporte (distinto de smtpd), pero que, por compatibilidad con versiones antiguas, es buena idea dejarlo escrito. Una vez hecho esto, más abajo en /etC/postfix/master.cf (al final, por ejemplo), debemos configurar nuestro filtro de la siguiente manera:

1
2
3
4
5
6
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
censurado unix -       n       n       -       -       pipe
   flags=Rq user=mail argv=/var/spool/filters/censurado.sh -f ${sender} -- ${recipient}

Una vez hecho esto, podemos reiniciar Postfix y ver si todo funciona bien. Algo que me gusta hacer siempre, cuando un sistema no está en producción y estamos creando filtros es meter llamadas a logger “Paso XXX” donde XXX pueden ser números o una explicación de qué estamos haciendo dentro del fichero de script de filtro. Luego, cuando haga las llamadas pertinentes al servidor de correo, donde se supone que se ha llamado al filtro, miro /var/log/syslog a ver si esos mensajes aparecen. Solo por saber que todo se ejecuta bien. Luego esas trazas las podemos eliminar o desactivar para poder activarlas en cualquier momento y diagnosticar problemas.

Configurando un filtro de contenido a través de TCP

En esta segunda opción, vamos a implementar un filtro a través de un mini-servidor SMTP que vamos a crear en Python. El objetivo es que sea otra máquina diferente la que pueda procesar el filtro. Donde digo otra máquina, puedo decir un servicio en la misma máquina (que en el futuro me podré llevar a otro lado) o un contenedor. Pero el caso es que se ejecutará por separado. Ahora Postfix, llamará a nuestro servicio a través de un puerto TCP y nuestro servicio volverá a enviar el mensaje a través de otro puerto diferente para meterlo en la cola. Y, si no queremos meterlo en la cola, no hacemos nada y listo.

He elegido hacer el script en Python porque es más sencillo de hacer y de entender, pero podríamos elegir cualquier lenguaje de programación que queramos. Ahora, se enviarán comandos SMTP y nosotros enviaremos comandos SMTP. Nuestro script /var/spool/filters/censurado.py quedaría así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import smtpd
import asyncore

import smtplib

import traceback
from Milter.utils import parse_addr

class CustomSMTPServer(smtpd.SMTPServer):

    def process_message(self, peer, mailfrom, rcpttos, data):
       
        mailfrom.replace('\'', '')
        mailfrom.replace('"', '')
       
        for recipient in rcpttos:
            recipient.replace('\'', '')
            recipient.replace('"', '')
       
        print 'Recibo un mensaje desde:', peer
        print 'Remitente:', mailfrom
        print 'Destinatario  :', rcpttos
        print 'MSG >>'
        print data
        print '>> EOT'

                if 'censurado' in data:
            print 'E-mail censurado'
            return


        try:
            server = smtplib.SMTP('localhost', 10026)
            server.sendmail(mailfrom, rcpttos, data)
            server.quit()
            print 'send successful'
        except smtplib.SMTPException:
            print 'Exception SMTPException'
        except:
            print 'Undefined exception'
            print traceback.format_exc()

        return
       
server = CustomSMTPServer(('127.0.0.1', 10025), None)

asyncore.loop()

En este caso, nuestro filtro escuchará en el puerto 10025 y cuando un mensaje pase el filtro lo enviará por SMTP al puerto 10026. Ahora, como siempre, tenemos que decirle a Postfix todo esto. Para ello, editamos /etc/postfix/master.cf (al final, por ejemplo) con lo siguiente:

1
2
3
4
5
# Recibe los mensajes
censurado unix - - n - 10 smtp -o smtp_send_xforward_command=yes -o disable_mime_output_conversion=yes

# Mete los mensajes de nuevo en la cola
localhost:10026 inet n - n - 10 smtpd -o content_filter= -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters -o smtpd_authorized_xforward_hosts=127.0.0.0/8

Ahora en /etc/postfix/main.cf añadimos estas línea:

1
content_filter = censurado:localhost:10025 receive_override_options = no_address_mappings

En este caso estamos haciendo todo en localhost, pero si tuviéramos el servicio en otra máquina, deberíamos cambiar todos los localhost y los 127.0.0.1. Yo recomiento siempre dar nombres de hosts, aunque estemos en una red pequeña con máquinas conocidas. Podemos asociar en /etc/hosts el nombre del host con la IP que corresponda dentro de la red local. En /var/spool/filters/censurado.py deberíamos cambiar, CustomSMTPServer((‘127.0.0.1’, 10025), None) por la IP o el host de la red local de la máquina donde está instalado el filtro; y en smtplib.SMTP(‘localhost’, 10026), localhost debería ser la IP o el host de la máquina donde está instalado Postfix. Luego en /etc/postfix/main.cf , deberíamos cambiar localhost por el host de la máquina que ejecuta los filtros y en /etc/postfix/master.cf, localhost:10026 será el host de la máquina que ejecuta Postfix, podremos cambiar también smtpd_authorized_xforward_hosts= por la red local donde estemos ejecutando Postfix y los filtros, por ejemplo 172.0.0.0/24.

Tras esto debemos reiniciar Postfix. Ahora ejecutamos el script de Python que ejecuta el filtro, lo dejamos ejecutando e intentamos enviar un mail con esta configuración. La ventana donde tenemos el script de Python en ejecución debería empezar a mostrar información sobre la información recibida.

Si todo funciona bien, deberíamos configurar nuestro servicio de filtro en Python como servicio de sistema, para ejecutarse en el inicio y para tener una gestión más cómoda. Podemos seguir estos pasos.

Foto principal: unsplash-logoTom Parsons

También podría interesarte....

Leave a Reply