Introducción a BPF

lun, 04 sep 2017 by Foron

Aquí va otro post sin grandes cosas que aportar a los ya iniciados, pero que sí puede servir a todos aquellos que no conocen qué es el nuevo BPF; en su inicio eBPF (extended Berkeley Packet Filter), pero que cada vez más va pasando a ser, simplemente, BPF.

La inmensa mayoría de lo que voy a escribir no es contenido original. Consideradlo una introducción en la que os mostraré varios ejemplos de desarrollos ya hechos. Por supuesto, trataré de dar referencias de toda la documentación que voy a usar en este post.

¿Qué es BPF?

Hablemos del antiguo BPF, del original.

Brevemente, porque todas las presentaciones que podáis ver sobre el tema ya hacen una introducción, BPF fue un esfuerzo de allá por el 1992 para evitar la transferencia de paquetes, sobre todo de red, desde el Kernel al entorno de usuario. El objetivo era generar un bytecode seguro que pudiese ejecutarse lo más cerca posible de la red. La mayoría habéis hecho cosas tipo tcpdump -i eth0 "port 80". Internamente, y pecando de básico, ese filtro "port 80" se escribia de tal manera que pudiese engancharse al socket y así procesarse más eficientemente.

En el 2013, con Alexei Starovoitov como cabeza más visible, se dio una vuelta a este concepto y se comenzó el trabajo en el nuevo BPF. Como comentaba, todas las charlas que veáis tienen una introducción, así que mejor un par de enlaces, intro 1 y intro 2, que repetir lo que ya se ha dicho. En todo caso, resumiéndolo todo mucho, y con el riesgo de que simplificar demasiado me haga escribir algo incorrecto, lo que permite BPF es añadir código que se va a ejecutar en el Kernel ante ciertos "eventos". Dicho de otra forma, podemos ejecutar nuestro código cuando el Kernel llame o salga de las funciones tcp_v4_connect o ext4_file_open, por poner dos ejemplos. Pero no solo esto; también se pueden instrumentalizar sockets, librerías o aquellas aplicaciones que lo permitan.

Traducido a algunos ejemplos, con BPF pod(r)emos:

  • Ejecutar código para la gestión de red en las capas más bajas del Kernel. XDP (eXpress Data Path) es el nombre clave que conocer aquí (intro 3 , intro 4 e intro 5). Los usos son variados; Facebook ya están documentando que empiezan a usarlo para el balanceo de carga, otros como Cloudflare en entornos anti DDoS, tc ya tiene un clasificador basado en BPF, o el proyecto Cillium, cada vez más conocido, que está más orientado a contenedores. En realidad, no sería raro pensar que una buena parte del software que usamos en estos entornos (quizá incluyendo Open vSwitch y similares) pasen a usar BPF en mayor o menor medida en un futuro no muy lejano.
  • Pasar del entorno del Kernel al espacio de usuario para instrumentalizar librerías. Uno de los ejemplos ya disponibles del uso de uprobes muestra cómo podríamos capturar el tráfico HTTPS de un servidor web justo antes de ser cifrado. Veremos el script más adelante, pero básicamente lo que se hace es incluir nuestro código en las llamadas a SSL_read y SSL_write de openssl.
  • Usar la funcionalidad que ofrecen muchas de las aplicaciones más importantes que usamos habitualmente, com Mysql, Postgresql o lenguajes de programación como Java, Ruby, Python o Node, para conocer al detalle lo que está ocurriendo cuando se ejecutan. Las USDT (User Statically Defined Tracing) se han venido usando con Dtrace y SystemTap, y ahora también con BPF.

He dejado para el final el caso de uso seguramente más documentado. La monitorización y el análisis de rendimiento tienen ya decenas de scripts listos para su uso. Veamos algunos ejemplos.

Ejemplos

No voy a dedicar ni una línea a la instalación del software. Con la llegada de las últimas versiones estábles de las distribuciones Linux, en muchos casos con versiones de Kernel a partir de 4.9, tenemos una versión que suele recomendarse para tener ya la mayoría de funcionalidades. Esto en lo que al Kernel se refiere. El resto de utilidades alrededor de esta base se instalará en función de la distribución que uséis. Con Ubuntu puede ser tan sencillo como un apt-get install, y con otras podría obligaros a trabajar un poco más. Aquí tenéis algunas instrucciones.

Si alguno habéis seguido este último enlace, habréis visto que hemos pasado del acrónimo BPF a BCC. En el paquete BPF Compiler Collection tenemos todo lo necesario para que escribir código BPF sea más fácil. Para los que no somos capaces de escribir 20 líneas en C sin tener algún segmentation fault, lo mejor que nos ofrece este toolkit son a) aplicaciones ya hechas y b) la posibilidad de integrar el código que se ejecuta en el Kernel en scripts Python o Lua, además del propio C++. Últimamente también se está dando soporte a Go, pero no os puedo dar muchos más detalles porque no lo he probado.

Resumiendo, para los vagos, los que no tienen tiempo o los que no se ven capaces de escribir esa parte en C de la que os he hablado, BCC nos permite usar y aprender de los scripts que va publicando la comunidad en los directorios de herramientas y de ejemplos, y adaptarlos a nuestro entorno que, muchas veces, solo implicará escribir Python.

Para los que os veáis más capaces, un buen primer paso es leer este tutorial y la guía de referencia. Merece la pena tener ambos enlaces siempre a mano.

Imaginad, por ejemplo, que queréis registrar todo lo que se ejecuta en vuestras máquinas. Los ejemplos incluidos en el repositorio de BCC incluyen un script para mostrar lo que se lanza desde bash, y otro para lo que se ejecuta vía exec. Como bashreadline.py es más sencillo de leer, vamos a centrarnos en hacer un history remoto.

El esqueleto básico de todos los scripts es el siguiente:

  1. Import de la clase BPF
  2. Preparar el código C, ya sea vía fichero externo o como una variable de texto
  3. Crear una instancia de BPF haciendo referencia al código C
  4. Definir dónde (kprobe, uprobe, ...) queremos ejecutar las funciones C que hemos escrito
  5. Usar los recursos que ofrece la instancia BPF para trabajar con lo que va devolviendo el Kernel.

En el caso de bashreadline.py, y quitando lo menos relevante en este momento:

bpf_text = """
    #include <uapi/linux/ptrace.h>
    struct str_t {
        u64 pid;
        char str[80];
    };
    BPF_PERF_OUTPUT(events);

    int printret(struct pt_regs *ctx) {
        struct str_t data  = {};
        u32 pid;
        if (!PT_REGS_RC(ctx)) return 0;
        pid = bpf_get_current_pid_tgid();
        data.pid = pid;
        bpf_probe_read(&data.str, sizeof(data.str), (void *)PT_REGS_RC(ctx));
        events.perf_submit(ctx,&data,sizeof(data));
        return 0;
    };
"""

from bcc import BPF
bpf_text = """ ... """

b = BPF(text=bpf_text)
b.attach_uretprobe(name="/bin/bash", sym="readline", fn_name="printret")

def print_event(cpu, data, size):
    event = ct.cast(data, ct.POINTER(Data)).contents
    print("%-9s %-6d %s" % (strftime("%H:%M:%S"), event.pid, event.str.decode()))

b["events"].open_perf_buffer(print_event)
while 1:
    b.kprobe_poll()

El script original no llega a 60 líneas, así que no penséis que me haya comido demasiado. Si lo ejecutáis, veréis todo lo que se está ejecutando desde todos los bash de la máquina:

# ./bashreadline
TIME      PID    COMMAND
05:28:25  21176  ls -l
05:28:28  21176  date
05:28:35  21176  echo hello world
05:28:43  21176  foo this command failed

Nada os impide editar este código, así que ¿Por qué no cambiar ese print de print_event por algo que escriba en un syslog o un graphite remoto? Yo por ejemplo uso mucho ZeroMQ para estas cosas:

import zmq
contexto = zmq.Context()
eventos = contexto.socket(zmq.REQ)
eventos.connect("tcp://172.16.1.2:7658")
...
salida = print("%-9s %-6d %s" % (strftime("%H:%M:%S"), event.pid, event.str.decode()))
eventos.send_string(salida)
eventos.recv()

Vamos, que con 6 líneas (dejando a un lado el servidor escuchando en 172.16.1.2:7658), nos hemos hecho un history remoto.

Independientemente de lo útil que os haya parecido esto, ya veis que es muy fácil adaptar los scripts y hacer cosas realmente interesantes.

Vamos a ver algunos ejemplos de scripts. Todo está en github, así que podéis ir allí directamente.

Pregunta típica: ¿Cuál es el rendimiento de nuestro sistema de ficheros ext4?

El rendimiento de los dispositivos de bloque y de los diferentes sistemas de ficheros es uno de los temas más tratados entre las herramientas que ya tenemos disponibles. Hay scripts para ver la distribución de las latencias en un sistema o para hacer cosas parecidas al comando top (ejemplo top) y responder a la pregunta ¿Quién está accediendo a dispositivo en este momento? También los tenemos para mostrar distribuciones de latencias (ejemplo latencias) en un periodo de tiempo, o para analizar los sistemas de ficheros más habituales. Para ext4, por ejemplo, podemos usar ext4slower (ejemplo ext4slower) para ver qué reads, writes, opens y syncs han ido lentas, o ext4dist (ejemplo ext4dist) para ver un histograma con la distribución de las latencias de estas operaciones. En ext4slower.py, por ejemplo, se instrumentaliza la entrada y la salida de estas funciones (mirad el argumento event=)

...
# initialize BPF
b = BPF(text=bpf_text)

# Common file functions. See earlier comment about generic_file_read_iter().
b.attach_kprobe(event="generic_file_read_iter", fn_name="trace_read_entry")
b.attach_kprobe(event="ext4_file_write_iter", fn_name="trace_write_entry")
b.attach_kprobe(event="ext4_file_open", fn_name="trace_open_entry")
b.attach_kprobe(event="ext4_sync_file", fn_name="trace_fsync_entry")
b.attach_kretprobe(event="generic_file_read_iter", fn_name="trace_read_return")
b.attach_kretprobe(event="ext4_file_write_iter", fn_name="trace_write_return")
b.attach_kretprobe(event="ext4_file_open", fn_name="trace_open_return")
b.attach_kretprobe(event="ext4_sync_file", fn_name="trace_fsync_return")
...

Aunque estos scripts son un poco más difíciles de leer, si habéis echado un ojo al tutorial y a la referencia del principio del post los podréis seguir fácilmente.

El uso de la CPU es otra área muy trabajada. Aunque siempre ha habido herramientas para su análisis (no siempre se ha hecho bien), con BPF se han mejorado mucho. Un buen ejemplo son los flame graphs. Aunque la pregunta principal siempre ha sido ¿Qué está haciendo la CPU ahora?, con los últimos desarrollos que se están haciendo podéis ver tranquilamente tanto el tiempo "On-CPU" como "Off-CPU" de las tareas. Dicho de otra forma, podéis saber cuánto tiempo pasa el comando tar ejecutándose, y cuánto esperando a disco. Como vamos a ver más scripts en este post, en lugar de ver los ejemplos (que los hay, y muchos) para instrumentalizar el scheduler y todo lo relacionado con la ejecución de tareas, os voy a dar el enlace a una fantástica presentación de Brendan Gregg donde podéis ver mucho de lo que se puede hacer hoy en día con los Flame Graphs, ya sea vía BPF o vía perf. Os recomiendo que dediquéis un poco de tiempo a esto, porque es realmente interesante. Intenet está llena de referencias, así que ya tenéis un pasatiempo para un rato.

Nos saltamos la CPU y pasamos, por ejemplo, al análisis del uso de memoria.

Pongámonos en el escenario de un entorno en el que el consumo de memoria de una máquina va subiendo y subiendo. Una opción para tratar de ver si hay algo raro en la asignación de memoria es el script memleak (ejemplo memleak). Otra alternativa, como veremos más adelante, es centrar la investigación más en las propias aplicaciones.

Vamos a ver algo más de código. Digamos que queremos saber si estamos haciendo un uso eficiente de la memoria Cache. Justo para esto tenemos cachetop (ejemplo cachetop). La parte escrita en C es sencilla, una única función:

bpf_text = """
#include <uapi/linux/ptrace.h>
struct key_t {
    u64 ip;
    u32 pid;
    u32 uid;
    char comm[16];
};
BPF_HASH(counts, struct key_t);
int do_count(struct pt_regs *ctx) {
    struct key_t key = {};
    u64 zero = 0 , *val;
    u64 pid = bpf_get_current_pid_tgid();
    u32 uid = bpf_get_current_uid_gid();
    key.ip = PT_REGS_IP(ctx);
    key.pid = pid & 0xFFFFFFFF;
    key.uid = uid & 0xFFFFFFFF;
    bpf_get_current_comm(&(key.comm), 16);
    val = counts.lookup_or_init(&key, &zero);  // update counter
    (*val)++;
    return 0;
}
"""

A la que se hace referencia en la parte en Python:

...
b = BPF(text=bpf_text)
b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count")
b.attach_kprobe(event="mark_page_accessed", fn_name="do_count")
b.attach_kprobe(event="account_page_dirtied", fn_name="do_count")
b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
...

Como veis, el mismo esquema que hasta ahora. El resultado nos muestra el porcentaje de acierto en Cache. Ejecutemos un find / mientras tenemos ejecutando el script:

# ./cachetop.py
13:01:01 Buffers MB: 76 / Cached MB: 114 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
...
984 vagrant  find                 9529     2457        4      79.5%      20.5%

Si ejecutamos una segunda vez ese mismo find /, veremos que el uso del Cache es mucho mś eficiente:

# ./cachetop.py
13:01:01 Buffers MB: 76 / Cached MB: 115 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
...
1071 vagrant  find                12959        0        0     100.0%       0.0%

Con esto terminamos esta parte. Recordad, tenéis muchos más ejemplos en el repositorio de github.

Hasta ahora, con la excepción de bashreadline.py, hemos estado muy centrados en el propio Kernel. En la introducción hemos visto que BPF permite subir un poco y llegar a librerías y aplicaciones.

Cuando subimos al userspace y a las librerías, en este caso concreto, entramos en el terreno de las uprobes. En el repositorio de BCC hay un ejemplo muy gráfico de lo que se puede hacer, por ejemplo, para la instrumentalización de la librería openssl. Al principio del post ya hemos hablado, muy por encima, de sslsniff (ejemplo sslsniff). La estructura es parecida a los kprobes, aunque en este caso instrumentalizamos las llamadas a librería:

...
b = BPF(text=prog)

# It looks like SSL_read's arguments aren't available in a return probe so you
# need to stash the buffer address in a map on the function entry and read it
# on its exit (Mark Drayton)
#
if args.openssl:
    b.attach_uprobe(name="ssl", sym="SSL_write", fn_name="probe_SSL_write",
                    pid=args.pid or -1)
    b.attach_uprobe(name="ssl", sym="SSL_read", fn_name="probe_SSL_read_enter",
                    pid=args.pid or -1)
    b.attach_uretprobe(name="ssl", sym="SSL_read", fn_name="probe_SSL_read_exit", pid=args.pid or -1)
...

probe_SSL_write es la función en C del script para, en este caso, la entrada a SSL_write de libssl.

...
int probe_SSL_write(struct pt_regs *ctx, void *ssl, void *buf, int num) {
    u32 pid = bpf_get_current_pid_tgid();
    FILTER
    struct probe_SSL_data_t __data = {0};
    __data.timestamp_ns = bpf_ktime_get_ns();
    __data.pid = pid;
    __data.len = num;
    bpf_get_current_comm(&__data.comm, sizeof(__data.comm));
    if ( buf != 0) {
            bpf_probe_read(&__data.v0, sizeof(__data.v0), buf);
    }
    perf_SSL_write.perf_submit(ctx, &__data, sizeof(__data));
    return 0;
}
...

Al final, el resultado del script completo es que somos capaces de ver en texto plano lo que entra en libssl desde, por ejemplo, un servidor web.

Para terminar con esta parte vamos a ver un par de ejemplos de USDT (Userland Statically Defined Tracing). Los que habéis usado SystemTap o Dtrace ya sabréis de lo que estamos hablando. Para usar las USDT necesitamos que las aplicaciones tengan soporte para este tipo de prueba. Muchas de las más importantes lo tienen, aunque no siempre están compiladas en las versiones "normales" que se instalan en las distintas distribuciones. Para ver qué tenemos disponible en un ejecutable podemos usar el script tplist. Sacando un extracto de este buen post de Brendan Gregg, tplist muestra lo siguiente:

# tplist -l /usr/local/mysql/bin/mysqld
/usr/local/mysql/bin/mysqld mysql:filesort__start
/usr/local/mysql/bin/mysqld mysql:filesort__done
/usr/local/mysql/bin/mysqld mysql:handler__rdlock__start
/usr/local/mysql/bin/mysqld mysql:handler__rdlock__done
/usr/local/mysql/bin/mysqld mysql:handler__unlock__done
/usr/local/mysql/bin/mysqld mysql:handler__unlock__start
/usr/local/mysql/bin/mysqld mysql:handler__wrlock__start
/usr/local/mysql/bin/mysqld mysql:handler__wrlock__done
/usr/local/mysql/bin/mysqld mysql:insert__row__start
/usr/local/mysql/bin/mysqld mysql:insert__row__done
/usr/local/mysql/bin/mysqld mysql:update__row__start
/usr/local/mysql/bin/mysqld mysql:update__row__done
/usr/local/mysql/bin/mysqld mysql:delete__row__start
/usr/local/mysql/bin/mysqld mysql:delete__row__done
/usr/local/mysql/bin/mysqld mysql:net__write__start
/usr/local/mysql/bin/mysqld mysql:net__write__done
...
/usr/local/mysql/bin/mysqld mysql:command__done
/usr/local/mysql/bin/mysqld mysql:query__start
/usr/local/mysql/bin/mysqld mysql:query__done
/usr/local/mysql/bin/mysqld mysql:update__start
...

Viendo las opciones, podemos usar query_start y query_end para sacar las consultas lentas. El script se llama dbslower (ejemplo dbslower):

...
# Uprobes mode
bpf = BPF(text=program)
bpf.attach_uprobe(name=args.path, sym=mysql_func_name, fn_name="query_start")
bpf.attach_uretprobe(name=args.path, sym=mysql_func_name, fn_name="query_end")
...

Y al final tendremos algo parecido a esto:

# dbslower mysql -m 0
Tracing database queries for pids 25776 slower than 0 ms...
TIME(s)        PID          MS QUERY
6.003720       25776     2.363 /* mysql-connector-java-5.1.40 ( Revision: 402933ef52cad9aa82624e80acbea46e3a701ce6 ) */SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_conn
6.599219       25776     0.068 SET NAMES latin1
6.613944       25776     0.057 SET character_set_results = NULL
6.645228       25776     0.059 SET autocommit=1
6.653798       25776     0.059 SET sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES'
6.682184       25776     2.526 select * from users where id = 0
6.767888       25776     0.288 select id from products where userid = 0
6.790642       25776     2.255 call getproduct(0)
6.809865       25776     0.218 call getproduct(1)
6.846878       25776     0.248 select * from users where id = 1
6.847623       25776     0.166 select id from products where userid = 1
6.867363 25776 0.244 call getproduct(2)

Hay muchas aplicaciones que permiten compilarse con soporte para USDT. En realidad, no solo aplicaciones; Python, PHP, Ruby, Java o Node, por ejemplo, también nos permiten ver dónde se está atascando nuestro código.

Vamos terminando con algo un poco más didáctico que práctico.

Imaginemos que tenemos unos cuantos volúmenes NFS en una máquina y que queremos saber qué está pasando. La primera opción que usaríamos la mayoría: nfsiostat. Es una buena aplicación, basada en deltas, como tantas otras, y que nos permite saber que en x tiempo se han servido z bytes en un determinado punto de montaje. Hasta aquí todo bien.

El problema con este tipo de aplicaciones, entre otros, es que no siempre capturan bien las distribuciones bimodales; aquellas en las que el acceso es correcto la mayoría del tiempo pero, puntualmente, se dan picos de latencia más altos. Estamos hablando de un servidor web que funciona bien "casi siempre". Estas distribuciones bimodales (de acceso a disco, carga de CPU, ...) son las que hacen que la percepción de un servicio sea desastrosa, y no siempre son fáciles de tratar.

Analizar el rendimiento NFS a fondo no es para nada trivial. Incluso centrándonos solo en la parte cliente, tenemos que vigilar la red, RPC, Caches NFS, bloqueos, .... Al final, salvo que tengáis tiempo y conocimientos suficientes para hacer algo completo vía BPF, lo normal, creo, sería empezar monitorizando a nivel VFS. Para esto hay varios scripts ya hechos en el repositorio de BCC. Por ejemplo, fileslower:

root@bpf-bcc:/usr/share/bcc/tools# python fileslower 0
Tracing sync read/writes slower than 0 ms
TIME(s)  COMM           TID    D BYTES   LAT(ms) FILENAME
4.089    bash           869    W 12         0.04 prueba5.txt

Con este tipo de herramientas tampoco tendremos los detalles de los que hemos hablado (red, RPC, ...), pero sí podremos, al menos, detectar anomalías en rutas concretas y distribuciones bimodales. TIP: BPF tiene una estructura para generar histogramas, que son algo más útiles con este tipo de distribución de datos.

Vamos a terminar viendo algunos scripts más y, de paso, damos un repaso a lo que hace el Kernel cuando hacemos algunas operaciones en sistemas de ficheros NFS. No pretendo que os sean útiles, aunque lo son, por ejemplo para generar Flame Graphs.

Vamos a usar funccount un par de veces. ¿Qué se ejecuta en el Kernel que empiece por nfs_ cuando se monta un volumen de este tipo?

# python funccount -i 1  'nfs_*'
FUNC                                    COUNT
nfs_fscache_get_client_cookie               1
nfs_alloc_client                            1
nfs_show_mount_options                      1
nfs_show_path                               1
nfs_create_server                           1
nfs_mount                                   1
nfs_fs_mount                                1
nfs_show_options                            1
nfs_server_insert_lists                     1
nfs_alloc_fhandle                           1
nfs_fscache_init_inode                      1
nfs_path                                    1
nfs_try_mount                               1
nfs_setsecurity                             1
nfs_init_locked                             1
nfs_init_server                             1
nfs_set_super                               1
nfs_init_server_rpcclient                   1
nfs_parse_mount_options                     1
nfs_probe_fsinfo                            1
nfs_fhget                                   1
nfs_get_client                              1
nfs_fs_mount_common                         1
nfs_show_devname                            1
nfs_create_rpc_client                       1
nfs_alloc_inode                             1
nfs_init_timeout_values                     1
nfs_set_sb_security                         1
nfs_start_lockd                             1
nfs_verify_server_address                   1
nfs_alloc_server                            1
nfs_init_server_aclclient                   1
nfs_init_client                             1
nfs_request_mount.constprop.19              1
nfs_fill_super                              1
nfs_get_root                                1
nfs_get_option_ul                           2
nfs_alloc_fattr                             2
nfs_fattr_init                              5

¿Y cuando escribimos algo?

FUNC                                    COUNT
nfs_writeback_result                        1
nfs_put_lock_context                        1
nfs_file_clear_open_context                 1
nfs_lookup                                  1
nfs_file_set_open_context                   1
nfs_close_context                           1
nfs_unlock_and_release_request              1
nfs_instantiate                             1
nfs_start_io_write                          1
nfs_dentry_delete                           1
nfs_writehdr_free                           1
nfs_generic_pgio                            1
nfs_file_write                              1
nfs_alloc_fhandle                           1
nfs_post_op_update_inode_force_wcc_locked        1
nfs_fscache_init_inode                      1
nfs_pageio_add_request                      1
nfs_end_page_writeback                      1
nfs_writepages                              1
nfs_page_group_clear_bits                   1
nfs_initiate_pgio                           1
nfs_write_begin                             1
nfs_pgio_prepare                            1
nfs_flush_incompatible                      1
nfs_pageio_complete_mirror                  1
nfs_sb_active                               1
nfs_writehdr_alloc                          1
nfs_init_locked                             1
nfs_initiate_write                          1
nfs_generic_pg_pgios                        1
nfs_file_release                            1
nfs_refresh_inode                           1
nfs_pageio_init_write                       1
nfs_pgio_data_destroy                       1
nfs_fhget                                   1
nfs_inode_remove_request                    1
nfs_create_request                          1
nfs_free_request                            1
nfs_end_io_write                            1
nfs_open                                    1
nfs_create                                  1
nfs_pageio_init                             1
nfs_pgio_result                             1
nfs_writepages_callback                     1
nfs_writeback_update_inode                  1
nfs_do_writepage                            1
nfs_key_timeout_notify                      1
nfs_alloc_inode                             1
nfs_writeback_done                          1
nfs_write_completion                        1
nfs_pageio_complete                         1
nfs_pgio_release                            1
nfs_lock_and_join_requests                  1
nfs_page_group_destroy                      1
nfs_fscache_open_file                       1
nfs_pageio_doio                             1
nfs_pgio_header_free                        1
nfs_generic_pg_test                         1
nfs_write_end                               1
nfs_post_op_update_inode                    1
nfs_updatepage                              1
nfs_pageio_cond_complete                    1
nfs_get_lock_context                        1
nfs_inode_attach_open_context               1
nfs_file_open                               1
nfs_fattr_set_barrier                       1
nfs_init_cinfo                              1
nfs_pgheader_init                           1
nfs_create_request.part.13                  1
nfs_sb_deactive                             1
nfs_post_op_update_inode_locked             2
nfs_commit_inode                            2
nfs_refresh_inode.part.18                   2
nfs_scan_commit                             2
nfs_commit_end                              2
nfs_setsecurity                             2
nfs_release_request                         2
nfs_page_find_head_request_locked           2
nfs_reqs_to_commit                          2
nfs_unlock_request                          2
nfs_ctx_key_to_expire                       2
nfs_file_fsync                              2
nfs_file_flush                              2
nfs_page_group_sync_on_bit                  3
nfs_revalidate_inode_rcu                    3
nfs_alloc_fattr                             3
nfs_revalidate_inode                        3
nfs_do_access                               3
nfs_update_inode                            4
nfs_permission                              4
nfs_init_cinfo_from_inode                   4
nfs_refresh_inode_locked                    4
nfs_file_has_buffered_writers               4
nfs_page_group_lock                         5
nfs_fattr_init                              5
nfs_page_group_unlock                       5
nfs_pgio_current_mirror                     6
nfs_set_cache_invalid                       8
nfs_attribute_cache_expired                10

También tenemos scripts para ver la pila, por ejemplo cuando se ejecuten nfs_file_write y nfs_write_end:

# python stacksnoop nfs_file_write -p 1080
TIME(s)            FUNCTION
0.556759834        nfs_file_write
    nfs_file_write
    new_sync_write
    vfs_write
    sys_write
    system_call_fast_compare_end


#python stacksnoop nfs_write_end -p 1080
TIME(s)            FUNCTION
1.933541059        nfs_write_end
    nfs_write_end
    generic_perform_write
    nfs_file_write
    new_sync_write
    vfs_write
    sys_write
    system_call_fast_compare_end

No quiero terminar sin que veamos el script trace, una especie de navaja suiza que vale para casi todo, com podéis ver en los ejemplos trace. Sigamos viendo más información sobre las escrituras en NFS. En una terminal voy a ejecutar esto:

while true
do
  echo "hola" >> /mnt/prueba.txt
  sleep 2
done

Y en otra un par de llamadas a trace:

# python trace -p 1647 'r:c:write ((int)retval > 0) "write ok: %d", retval' -T
TIME     PID    TID    COMM         FUNC             -
21:28:15 1647   1647   bash         write            write ok: 5
21:28:17 1647   1647   bash         write            write ok: 5

# python trace -p 1647 'sys_write "written %d file", arg1'
PID    TID    COMM         FUNC             -
1647   1647   bash         sys_write        written 1 file
1647   1647   bash         sys_write        written 1 file

Y ya está. No merece la pena seguir con más ejemplos, porque todo está razonablemente bien documentado, como habéis visto. Me he dejado muchos scripts, de todo tipo, pero seguro que os habéis hecho una idea.

Ojo! Que no haya dicho ni pio sobre XDP no significa que no sea interesante. Las posibilidades y el rendimiento de los desarrollos que está haciendo la gente (firewalls para contenedores, balanceadores de carga, ...) son muy prometedores, y seguro que van a llegar muy lejos. Quién sabe si será el tema del próximo post.


Monitorización de aplicaciones con sysdig

mié, 23 nov 2016 by Foron

Historicamente, uno de los argumentos más importantes de la comunidad Solaris en relación a la, no sé si decirlo así, madurez de Linux para entornos profesionales, ha sido la falta de instrumentalización para la monitorización del sistema.

En realidad, no faltaba razón en este argumento, ya que los primeros esfuerzos, por ejemplo de la mano de Systemtap, no conseguían llegar a lo que ofrecía el fantástico Dtrace en este tipo de entornos. A pesar de haber pasado ya varios años, no creo que ninguna de estas aplicaciones haya llegado a un público masivo (en el ámbito empresarial, claro).

Sin embargo, gracias al trabajo de mucha gente, estos últimos meses están siendo espectaculares para dotar a Linux, por fin, de herramientas capaces de igualar a Solaris. De hecho, alguna de las cabezas pensantes detrás de Dtrace, como Brendan Gregg, ahora trabajando en entornos Linux, ha venido a decir que ha sido como si, después de haber estado esperando al autobús, de repente hubiesen llegado dos. Os recomiendo esta lectura, con algo de contexto sobre la evolución de estas nuevas herramientas.

Hoy en día, como decía, tenemos varias alternativas. Personalmente, estoy interesado en dos: a) eBPF, o ya últimamente BPF, a secas, y b) Sysdig.

Con BPF se está dotando al Kernel Linux de toda la instrumentalización necesaria para inspeccionar al detalle el funcionamiento del sistema. A partir de ahí, un toolkit como BCC hace uso de todas estas nuevas estructuras para facilitar la manipulación de estos datos a través de un interfaz Python o Lua. Echad un vistado a la web. Los ejemplos son particularmente interesantes.

El soporte para BPF está integrado en los Kernels estándar recientes. Es algo en plena evolución, así que, si queréis probarlo, os recomiendo que no uséis nada inferior a 4.4, aunque ya haya cosas desde antes. En realidad, la cosa va tan rápido que, ya puestos, lo mejor es que probéis con 4.9 para ver lo último de lo último que se ha incluido.

Parece claro que se tienen muchas espectativas puestas en BPF. Hay varios proyectos importantes que ya se están planteando su uso. SystemTap, por ejemplo, está empezando a implementar un backend bajo BPF. Por otro lado, y bajo el mismo paraguas de BPF, se están desarrollando nuevas tecnologías, como XDP, que prometen una serie de ventajas que ya se están empezando a contemplar en el mundo de las redes software o los contenedores. Si queréis leer algo sobre esto, aquí tenéis un enlace.

La segunda alternativa de la que os hablaba, Sysdig, aunque para el usuario final que solo quiere monitorizar sus sistemas tenga, por así decirlo y cogido con alguna pinza, el mismo objetivo que BPF, lo hace de una manera diferente. Instrumentaliza el Kernel y ofrece un backend de calidad, pero delega gran parte del trabajo a filtros y a pequeños scripts, que llaman chisels (en Lua), que se encargan del trabajo desde el punto de vista del usuario.

En este momento, si dejáis de leer este post e investigáis por vuestra cuenta durante una hora, seguramente lleguéis a la conclusión de que BPF es realmente potente, pero que cuesta empezar (hay mucho hecho ya bajo el paraguas de iovisor, y cada poco sale un nuevo script). Por otro lado, es probable que en esa misma hora lleguéis a entender de qué va Sysdig, y que aunque no sea tan "amplio" como BPF, en realidad es más que suficiente para muchos de los problemas que habitualmente tiene el usuario de a pie. Ojo, que no estoy diciendo que Sysdig sea mejor que BPF, ni remotamente. Sacad vuestras propias conclusiones, pero leed sobre ambos y probadlos antes.

Tanto BPF como Sysdig dan para muchos posts. Os recomiendo leer el blog de Brendan Gregg, la documentación y los ejemplos de github de iovisor, y el blog y la web de Sysdig para ir calentando.

Mi idea original para el artículo era usar Sysdig en algún script real, pero eso lo haría mucho más denso, y he preferido limitarlo a algunas notas de lo que se puede hacer, aunque lo disfrazaré de ejemplo real.

Imaginad que tenéis un script que procesa un fichero de logs. En función de la expresión regular va a una u otra rama de código y, al final, inserta los resultados en mysql. En un entorno real, seguramente paralelizaríamos el script para aprovechar todos los cores, quizá usando zeromq para el paso de mensajes, y quizá usando el patrón que algunos llaman pipeline. Los entornos paralelos suelen complicar la monitorización.

Vamos a suponer la locura (irónico) de que no tenemos tiempo para la optimización o el análisis de ningún script, mucho menos si es cosa de horas, y todavía menos si el mencionado script no funciona rematadamente mal.

Y aquí es cuando alguien se levanta y dice: "Tanto rollo para algo que se puede solucionar con las trazas de toda la vida, (inicio = time.time(); fin = time.time(); dif = fin - inicio) ". No seré yo el que diga que este tipo de trazas no funcionen, aunque estaremos de acuerdo en que son "limitadas". Sirven para decir que algo va lento, pero no el motivo; aparte del tiempo que lleva procesar, digamos, 80 millones de lineas de log multiplicadas por tantos "ifs" como tenga el código, que además se ejecutan en procesos independientes. Es viable, por supuesto, pero mejorable.

Afortunadamete, ya habéis dedicado una hora a mirar tanto Sysdig como BPF/BCC y, claro, habréis llegado a la conclusión de que cualquiera de las dos os pueden servir. Veamos qué podemos hacer con Sysdig.

Repasemos: Una vez instalado Sysdig (os lo dejo a vosotros), se carga un módulo de Kernel que, simplificando un poco, va a ir recogiendo, eficientemente, los datos que se vayan generando (llamadas al sistema, IO, ...), de tal manera que luego se puedan aplicar filtros y chisels que nos den la información que necesitemos.

Además, tenemos suerte, porque una de las últimas funcionalidades que se han añadido a Sysdig consiste en algo tan sencillo como marcar el inicio y el fin de un bloque de código, y usar después estas marcas para el análisis. Imaginad que tenéis la capacidad de saber fácilmente la distribución del tiempo que necesita un "if" que incluye algunas operaciones sobre una base de datos; y que además podéis saber sin nada de esfuerzo el contenido íntegro del "select" que se mandó a la base de datos, o si falló la conexión o la resolución del nombre de la máquina bbdd para ese pequeño porcentaje de bloques lentos.

Y todavía es mejor, porque para hacer esto de lo que os estoy hablando solo hay que escribir una cadena concreta en /dev/null. Es previsible que esto será muy fácil, sea cual sea el lenguaje que estéis usando. Mirad estos ejemplos, sacados directamente de la web de sysdig.

echo ">:p:mysql::" > /dev/null
... código a analizar ...
echo "<:p:mysql::" > /dev/null

Y así de fácil. Con > y < definimos el comienzo y el final del bloque, con :p: pedimos que se genere un identificador automáticamente a partir del pid del proceso (hay más opciones), y usamos mysql como tag para identificar el span (que es como se llama todo esto, tracer/span).

Pero podemos ir un poco más lejos, y usar cadenas como las siguientes:

echo ">:p:mysql.select::" > /dev/null
echo ">:p:mysql.update::" > /dev/null
echo ">:p:mysql.select:query=from tabla1:" > /dev/null
echo ">:p:mysql.update:query=tabla1:" > /dev/null

Como veis, podemos anidar tags, o incluso añadir argumentos que den más pistas sobre lo que hace cada bloque. Esto es muy útil para saber la iteración exacta dentro de un for en la que ha ocurrido un problema concreto, o el tipo de select que ha generado cierto error, por decir un par de casos.

Volviendo a lo nuestro, recordad, queremos tener controlado un script python que escribe en una base de datos, que usa zeromq, y que se basa en expresiones regulares para hacer una cosa u otra. Sin pensar mucho, tendríamos una estructura de código parecida a esta:

fsysdig = open("/dev/null/", "w")

# Abrimos fichero log, creamos procesos, ...

for linea in fsysdig:
  fsysdig.write(">:p:zmq::\n")
  fsysdig.flush()
  # Preparamos los datos, y mandamos al socket push de ZMQ
  # Este socket se bloquea al llegar al tope de memoria configurado
  # ...
  socketzmq.send_multipart(["LOG",linea])
  # ...
  # Mas actividad para este span
  fsysdig.write("<:p:zmq::\n")
  fsysdig.flush()

En otros procesos tendremos la parte de la gestión de las expresiones regulares

fsysdig = open("/dev/null/", "w")

#
# Leeriamos del socket push via un socket pull de ZMQ
# ...
#

while seguirprocesando:
  # Hacemos match de una expresion regular
  # Si se cumple, hacemos ciertas operaciones
  # ...
  if matchregexp1:
    fsysdig.write(">:p:regexp:re=regexp1:\n")
    fsysdig.flush()
    # Trabajo con la regexp1
    # ...
    # Enviamos el dato al proceso mysql via ZMQ
    # ...
    fsysdig.write("<:p:regexp::\n")
    fsysdig.flush()

  if matchregexp2:
    fsysdig.write(">:p:regexp:re=regexp2:\n")
    fsysdig.flush()
    # Trabajo con la regexp2
    # ...
    # Y enviamos el dato al proceso mysql via ZMQ
    # ...
    fsysdig.write("<:p:regexp::\n")
    fsysdig.flush()
  #
  # Hariamos algo parecido con el resto ...
  #

Ya os hacéis una idea. Como veis, estamos añadiendo argumentos al tag regexp para identificar los bloques.

Por último, otro proceso haría el trabajo contra mysql.

fsysdig = open("/dev/null/", "w")

while seguirprocesando:
  #
  # Leeriamos del socket push via ZMQ, agrupariamos, operaciones, ...
  #
    try:
      fsysdig.write(">:p:mysql:st=update:\n")
      fsysdig.flush()
      # preparariamos la operacion mysql y el resto del trabajo para este span...
      cur.execute('''insert into ...''')
      # ...
    except (MySQLdb.MySQLError, TypeError) as e:
      print "Mysql: ERROR: Al ejecutar comando mysql " + str(e)
      sys.stdout.flush()
    finally:
      # ...
      fsysdig.write("<:p:mysql::\n")
      fsysdig.flush()

Como veis, también hemos creados argumentos para identificar bloques.

Este es el tipo de código que vamos a ejecutar. Antes de eso, vamos a poner en marcha la captura de sysdig (se puede lanzar en cualquier momento).

sysdig -C 500 -s 512 -w volcado_span.scap

Básicamente estamos creando ficheros independientes de 500MB, y estamos capturando 512 bytes de bufer de IO.

Una vez tengamos Sysdig lanzado dejamos el script funcionando un rato. Tendremos varios ficheros volcado_span.scap[0-9]+. Empecemos el análisis!

Una de las utilidades principales de Sysdig es un interfaz ncurses que permite ejecutar chisels fácilmente. Se llama csysdig. En nuestro caso, vamos a leer uno de los volcados para simplificar el proceso, pero tened en cuenta que todo esto se puede hacer sobre una captura en tiempo real, sin el parámetro -r

csysdig -r volcado_span.scap8

En este listado, F2 -> Traces Summary, y nos dará el resumen de spans que se han generado.

Sumario spans

Hagamos algo más interesante. Uno de los chisels más visuales que ha escrito la gente de Sysdig se llama "spectrogram", y se utiliza para ver la distribución de las latencias de ciertos eventos. Csysdig integra una versión que muestra la distribución para los spans, como una unidad. Os dejo que la veáis vosotros (hay videos en los tutoriales de la web de Sysdig). Aquí vamos a ser un poco más brutos y vamos a mostrarla para todos los eventos que se generan dentro de los bloques con el tag "regexp":

sysdig -r volcado_span.scap8 -c spectrogram 'evtin.span.p.tag[0]=regexp'

De donde sacaríamos lo siguiente:

Spectrogram regexp

Aunque lo que os muestro no es demasiado práctico (seguramente sea más interesante empezar por spans en bloque), imaginad que tenéis accesos a red o llamadas más costosas, y que tenéis mucho rojo hacia la derecha (muchos eventos y muy lentos). Esos serían, quizá, los eventos más interesantes a analizar:

sysdig -r volcado_span.scap8 -c spectrogram 'evtin.span.p.tag[0]=regexp and evt.latency > 100000'

Como veis, estamos aplicando filtros para limitar lo que vemos:

Spectrogram regexp lentas

Este tipo de imágenes me parecen interesantes para ver si hay algún tipo de anomalía o algo que no nos parezca razonable; aunque en un caso real antes o después dejaríamos de usar los chisel visuales y pasaríamos a ver los eventos concretos que están dando guerra. De hecho, la vista spectrogram de csysdig permite elegir con el ratón partes de la imágen para pasar a modo texto.

Y esto es, para mí, de lo mejor de Sysdig: Podemos limitarnos a grandes sumarios como los que hemos visto hasta ahora, o a ver lo que está ocurriendo a nivel de llamada a sistema. Además, aunque en este artículo nos hemos centrado en un análisis "a posteriori", en la práctica podemos lanzar csysdig en tiempo real y tener integradas en un mismo interfaz todas las funcionalidades, mejoradas, que dan comandos como htop o netstat, por citar un par.

Depende del caso de uso de cada uno. Yo, por ejemplo, en el día a día uso Sysdig sobre todo para ver el tráfico entre sockets. Por ejemplo, imaginad que tenéis algún tipo de middleware que hace llamadas HTTP a un servicio externo en función de las peticiones HTTP que recibe. Suponed que no logueáis esas peticiones, y que a veces fallan. En estos casos Sysdig es realmente útil.

El chisel echo_fds, por ejemplo, es muy interesante porque muestra todo el IO de los eventos que cumplan el filtro que apliquemos. Además, lo colorea en función de si es de entrada o de salida. Por supuesto, se puede usar para HTTP, como he comentado, pero también con cualquier otro proceso que genere IO, como por ejemplo, mysqld:

sysdig  -r volcado_span.scap11 -c echo_fds 'proc.name=mysqld'

Nos hemos olvidado de los spans, como veis, y vamos directamente a los procesos mysqld. Sin acotar más, tendremos un churro similar al siguiente

.....
------ Read 4B from   127.0.0.1:52398->127.0.0.1:mysql (mysqld)
....
------ Read 87B from   127.0.0.1:52398->127.0.0.1:mysql (mysqld)
.insert into tabla(timestamp, dato1, dato2) values (1477905741, 'ejemplo1', 'ejemplo2')
------ Write 11B to   127.0.0.1:52398->127.0.0.1:mysql (mysqld)
......

Como veis, estamos logueando todo el tráfico SQL, como el insert que he dejado aquí. Creedme, este tipo de chisels, con filtros como evt.buffer contains son muy útiles para ver tráfico HTTP, cabeceras, respuestas o códigos de error, particularmente en entornos con muchos microservicios y similares.

En fin, no sé si os habéis hecho una idea de lo que se puede hacer con Sysdig. En realidad, no os he dicho nada del otro mundo; teneis mucha mas informacion en la web y el blog de Sysdig, por citar dos fuentes. En cualquier caso, la unica forma de coger soltura con esto es con el uso, asi que lo mejor que podéis hacer es probarlo.

En comparación a lo que hemos hecho, y aprovechando que estamos hablando de mysql, en este enlace tenéis el ejemplo de cómo se mostrarian las consultas lentas con BPF/BCC. Si seguís el texto del enlace, veréis que podéis usar lo que ya esta hecho (usando el script mysqld_query.py de los ejemplos de BCC, o que también podéis pedir pizza y café y llegar a muy bajo nivel gracias al uso que puede hacer BPF/BCC de la instrumentalización que ofrece mysql, antes principalmente para Dtrace, y ahora también para Linux. En todo caso, mejor si leéis el post (y el resto de la web) de Brendan Gregg para ir sacando más conclusiones.

read more

Port Knocking sin complicaciones

mié, 06 nov 2013 by Foron

(Así, pecando de básico desde el principio)

Históricamente, desde el punto de vista de la seguridad, los servidores han venido teniendo dos tipos de puertos:

  • Los que tienen que estar siempre abiertos: Los puertos HTTP o HTTPS de un servidor web, sin ir más lejos.
  • Los que sólo tienen que estar abiertos para unas IPs determinadas: SSH, por ejemplo.

La configuración, en ambos casos, siempre ha sido razonablemente sencilla; al menos si asumimos que, a menudo, el rango de IPs con acceso a esos puertos restringidos era conocido (oficinas, etc).

Con el tiempo, sobre los firewalls y todas sus variantes se han añadido otra serie de medidas "complementarias", que muchos no considerarán parte de la seguridad informática "de verdad", pero que han demostrado ser útiles si se usan adecuadamente y como parte de una solución más global. Me refiero, por ejemplo, al uso de librerías como TCPWrappers o de geolocalización, a los propios mecanismos de cada aplicación, o al uso de puertos no estándar para los servicios (los sandboxes por aplicación basados en la virtualización, Selinux y todo este tipo de medidas quedan fuera de este post).

Sin embargo, en la actualidad nos encontramos ante un problema añadido que no hemos tenido hasta la fecha: Las IPs origen que se tienen que conectar a esos servicios restringidos ya no son "tan estáticas" como antes. ¿Cómo abro el acceso SSH a un móvil? ¿Y el webmail corporativo? ¿Y el acceso IMAP?

Muchos diréis que nada como una buena VPN para solucionar este problemilla; y tendréis razón, claro. Ahora bien, el mundo de las redes privadas, por si sólo, tiene otra serie de problemas que no vamos a tratar aquí: ¿Qué tecnología VPN usamos? ¿Qué aplicación cliente? ¿A qué IPs permitimos establecer la conexión? ¿Dónde terminamos la red? ¿Qué acceso tiene un usuario de VPN una vez ha pasado ese terminador? En fin, lo dicho, todo un mundo.

En este post casi voy a limitarme a citar una herramienta más que usar a la hora de securizar un servidor: El Port Knocking. Ojo, se trata de un mecanismo adicional, y no de la solución definitiva a los problemas; pero sí es cierto que viene a ayudar con el problema del dinamismo actual de los orígenes.

El concepto general es realmente sencillo. El firewall del servidor mantiene bloqueado el puerto al que se quiere acceder, y sólo se habilita a través del envío de una secuencia determinada de paquetes. Las opciones son múltiples, y van desde simples SYN, en orden, a n puertos, hasta combinaciones más elaboradas, en las que se activan otros flags en las cabeceras.

A partir de esta idea básica, han ido apareciendo otras mejoras que vamos a ver en un minuto.

Port Knocking básico

La versión más sencilla del "protocolo" se basa, como vengo diciendo, en mandar una secuencia concreta de paquetes a varios puertos. Para su implementación en el lado del servidor, tenemos tres opciones. La primera requiere instalar software (knockd por ejemplo), y las otras dos usan únicamente iptables.

Si optáis por la vía de knockd, os tendréis que descargar el software (obviamente). Según la distribución que uséis, esto será más o menos fácil, así que no me voy a meter con la instalación.

[options]
  logfile = /var/log/knockd.log

[IMAPon]
  sequence    = 6030,6026,6031
  seq_timeout = 5
  command     = /sbin/iptables -I INPUT 2 -s %IP% -p tcp --dport 993 -j ACCEPT
  tcpflags    = syn

[IMAPoff]
  sequence    = 6040,6036,6041
  seq_timeout = 5
  command     = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 993 -j ACCEPT
  tcpflags    = syn

Esta es una configuración tipo, en mi caso de "/etc/knockd.conf". Como podéis suponer, cuando alguien envíe tres paquetes SYN a los puertos 6030,6026 y 6031, en ese orden, se ejecutará el comando definido en "command". En este ejemplo, es una simple regla iptables que permite que desde la IP origen se pueda conectar al puerto IMAP (IMAPon). Como la aplicación da la opción de lanzar más secuencias, se puede crear otra para eliminar la regla (IMAPoff).

Y poco más. En vuestro caso, tendréis que adaptar la regla iptables a vuestra configuración, o incluso podríais lanzar scripts más complejos, que por ejemplo manden un correo o alerta cada vez que se active el acceso.

Esta es la forma más simple de implementar el Port Knocking. Tiene fallos, como por ejemplo que un cambio de IP en el móvil supondría que la IP antigua tendría acceso permanente (o hasta eliminarla a mano), pero creo que son fáciles de solucionar (el match "recent" de iptables por ejemplo ofrece alternativas). Vosotros deberéis decidir si esto os sirve, o si necesitáis algo más elaborado.

La segunda forma de implementar esta versión original de Port Knocking es a través de iptables, sin software adicional. El proceso está muy bien documentado en el siempre magnífico wiki de archlinux, así que podéis seguir desde allí si optáis por esta vía.

Y para terminar, si queréis una alternativa específica de iptables, hay un módulo en xtables addons pensado para hacer Port Knocking. Se llama xt_pknock, y permite hacer cosas como esta (entre otras que veremos más adelante):

  iptables -A INPUT -p tcp -m pknock --knockports 4002,4001,4004 --strict --name IMAP --time 10 --autoclose 60 --dport 993 -j ACCEPT

El problema es que, hasta la fecha, os va a costar encontrar un kernel que traiga el módulo compilado, así que lo tendríais que hacer vosotros.

Port Knocking con autenticación

Aunque obligar a que el origen conozca la secuencia concreta que enviar al servidor sea útil, no es menos cierto que tiene margen de mejora. La más obvia va en el sentido de verificar que la conexión procede realmente desde un usuario autenticado.

Sobre esta idea, Michael Rash (autor, entre otros, de psad) implementó una mejora del Port Knocking sobre el concepto de Single Packet Authorization (SPA): fwknop. El objetivo es el mismo (abrir un puerto a través de iptables si usamos Linux), pero usando para ello un único paquete UDP con unos datos determinados. De esta manera, se evitan los problemas generados a partir del envío de múltiples paquetes (llegar desordenados, bloqueo por IDS, ...) y, además, da la opción de cifrar el payload con un algoritmo que también ofrezca autenticación. Podéis ver el listado de features y las ventajas de esta implementación en la propia web de fwknop.

La instalación de fwknopd es muy sencilla. De hecho, está disponible en muchas distribuciones. Como no podía ser de otra forma, tener muchas más funcionalidades también hace que la configuración sea algo más complicada que en el caso de knockd, aunque sigue siendo manejable.

Este es el momento en el que debería escribir algunas notas y ejemplos de configuración pero, la verdad, visto que en la web ya hay un buen tutorial, prefiero no alargar mucho más el post. Si tenéis alguna duda, escribid un comentario e intentaré resolverla. Tened en cuenta que fwknop es un proyecto "vivo", y que por lo tanto va mejorando con cada release. Las últimas versiones (a partir de la 2.5), por ejemplo, incluyen soporte para HMAC + SHA, de tal manera que se puede combinar con AES o GnuPG para mejorar la autenticación. Yo personalmente no he usado esta versión, así que no puedo comentar nada sobre esta nueva funcionalidad.

Una instalación tipo de fwknop, al menos en versiones anteriores a a la 2.5, usa dos ficheros de configuración. Cada parámetro está muy bien documentado, así que lo mejor es ir siguiendo los comentarios que veréis en fwknopd.conf y en access.conf. El primero se usa para definir si queremos poner el interfaz en modo promiscuo, el puerto en el que escucharemos los paquetes y, sobre todo, las cadenas de iptables que usaremos para incluir las reglas. Access.conf se usa para la parte más directamente relacionada con el acceso; empezando por todo lo relacionado con las claves de cifrado, y siguiendo con el contenido que puede ir en cada paquete, desde usuarios autorizados a puertos para los que se puede pedir acceso, pasando por un mecanismo de control de la IP origen desde la que se genera la solicitud.

Por último, y dejando a un lado fwknop, el módulo de Netfilter del que os he hablado antes, xt_pknock, también ofrece una versión de Port Knocking que ofrece SPA, de tal manera que se pueden escribir cosas como estas:

  ...
  iptables -A INPUT -p udp -m state --state NEW -m pknock --knockports 2000 --name IMAP --opensecret your_opensecret --closesecret your_closesecret -j DROP
  iptables -A INPUT -p tcp -m state --state NEW -m pknock --checkip --name IMAP -m tcp --dport 143 -j ACCEPT
  ...

Aún así, como os he dicho, este módulo todavía no es "demasiado fácil" de usar y, en todo caso, es más simple que lo que ofrece fwknop.

Clientes

La pregunta es: ¿Cómo se genera la secuencia que abre la puerta?

Si usamos la versión básica de Port Knocking, no hay ningún problema. Podemos usar la aplicación cliente del software (knock), o podemos usar nmap, nping, netcat, o cualquier otra aplicación que permita mandar paquetes con el flag SYN activo a un puerto concreto. Para móviles, también hay variedad; en android, por ejemplo, una búsqueda de "port knocking" da al menos dos aplicaciones gratuitas (y que funcionan, al menos en mi teléfono y tablet).

Si optamos por la versión con SPA, también tenemos aplicaciones para todo tipo de dispositivos y clientes, aunque en este caso tendremos que tirar, probablemente, por las aplicaciones creadas específicamente para fwknop. Yo personalmente no he probado las versiones para móvil, así que poco puedo aportar. En el tutorial tenéis los enlaces y sus limitaciones (sobre todo relacionadas con el uso de HMAC). Por supuesto, si estáis en Linux, no tendréis problema para usar el propio software cliente que trae fwknop.

Notas

No pretendo empezar una discusión sobre si el Port Knocking es útil o no, o de si entra dentro de lo llamado "Security through obscurity"; pero sí tengo claro que es una herramienta más, y que es perfectamente "usable" en muchos entornos. Ahora bien, aunque podamos estar de acuerdo en que el Port Knocking básico es algo limitado, la implementación de fwknop sí que es, indudablemente, mucho más completa desde el punto de vista de la seguridad informática.

read more

Instalaciones automáticas desde usb

dom, 14 jul 2013 by Foron

Vamos a suponer por un momento que estamos en un entorno en el que no podemos tener un servidor PXE en condiciones. Para ponerlo todavía peor, imaginemos que somos de los que no conseguimos encontrar un cd de Knoppix razonablemente reciente cada vez que se nos fastidia un servidor y que, a pesar de las prisas, tenemos que esperar a ver como carga todo un entorno gráfico antes de poder hacer un simple fsck.

Todo esto tendría que ser parte del pasado, al estilo de los videos Beta o los cassettes; pero no, todavía es demasiado común, así que a ver si conseguimos dar algunas ideas útiles y buscamos alternativas que, aunque no sean lo más moderno que existe, nos faciliten un poco el trabajo.

No esperéis nada original en este post. Todo lo que escribo aquí está ya más que documentado, y mucho mejor que en estas cuatro notas. Aún así, a ver si os sirve como punto de partida.

Queremos conseguir dos cosa:

  • Un método para arrancar rápidamente una distribución live, sencilla, que permita recuperar particiones, transferir ficheros, ..., este tipo de cosas.
  • Un sistema para instalar distribuciones de forma automática, usando ficheros kickstart para RedHat/CentOS/... y preseed para Debian/Ubuntu/..., pero teniendo en cuenta que no podemos usar PXE, ni Cobbler, ni nada similar.

Ya hace mucho tiempo que se pueden arrancar sistemas desde memorias USB, y además GRUB tiene funcionalidades que permiten arrancar desde una ISO. Siendo esto así, ya tenemos todo lo necesario. Si incluimos un servidor web para guardar nuestros ficheros ks y preseed y, si queremos acelerar un poco las instalaciones, los paquetes de las distribuciones CentOS y Debian (las que voy a usar en este post), conseguiremos además que las instalaciones sean automáticas y razonablemente dinámicas.

Pasos previos

Empezamos. Buscad un pendrive que no uséis para nada y podáis formatear. Desde este momento asumo que el dispositivo que estáis usando corresponde a /dev/sde, y que tiene una única partición fat normal y corriente, en /dev/sde1. Si no es así, lo de siempre: "fdisk /dev/sde" + "mkfs.vfat -n USB_INSTALACIONES /dev/sde1":

#fdisk -l /dev/sde
Disposit. Inicio    Comienzo      Fin      Bloques  Id  Sistema
/dev/sde1   *        2048    15654847     7826400    c  W95 FAT32 (LBA)

Una vez más, aseguráos de que podéis y queréis borrar el contenido del pendrive. Por supuesto, vuestro kernel debe tener soporte para este tipo de particiones, y también necesitáis las utilidades para gestionarlas. En Debian están en el paquete "dosfstools". En cualquier caso, lo normal es que ya lo tengáis todo.

Vamos a montar la partición en "mnt" (o donde queráis), con un simple:

mount /dev/sde1 /mnt

Lo siguiente es instalar grub en /dev/sde. Otra vez, cuidado con lo que hacéis, no os confudáis de dispositivo.

grub-install --no-floppy --root-directory=/mnt /dev/sde

Dejamos estos pasos básicos y vamos ya a por la configuración más específica.

Creando el menú

En realidad, no vamos a hacer nada más que configurar GRUB. Podéis ser todo lo creativos que queráis, pero para este ejemplo voy a simplificar todo lo que pueda: Ni colores, ni imágenes de fondo, ni nada de nada.

Para tener un poco de variedad, desde mi USB se va a poder arrancar lo siguiente:

  • Una Debian Wheezy sin preseed, para ir configurando a mano.
  • Un CD con utilidades de repación. El que más os guste. Para este ejemplo: Ultimate BootCD.
  • Una Slax, por si quisiera arrancar un entorno gráfico completo.
  • Una Debian Wheezy con preseed, completamente automática.
  • Una CentOS 6.4 con kickstart, completamente automática.

Prestad atención a los dos últimos elementos de la lista, porque son los que nos van a permitir ir a un servidor "vacío", arrancar desde el USB, y en 5 minutos tener una Debian o una CentOS perfectamente instalados.

Vamos a crear el menú de GRUB. Necesitamos editar el fichero "/mnt/boot/grub/grub.cfg" con lo siguiente:

menuentry "Debian Wheezy x86_64 installer" {
        set gfxpayload=800x600
        set isofile="/boot/iso/wheezy_mini_amd64.iso"
        loopback loop $isofile
        linux (loop)/linux priority=low initrd=/initrd.gz
        initrd (loop)/initrd.gz
}

menuentry "Ultimate BootCD 5.2.5" {
        loopback loop /boot/iso/ubcd525.iso
        linux (loop)/pmagic/bzImage edd=off load_ramdisk=1 prompt_ramdisk=0 rw loglevel=9 max_loop=256 vmalloc=384MiB keymap=es es_ES iso_filename=/boot/iso/ubcd525.iso --
        initrd (loop)/pmagic/initrd.img
}

menuentry "Slax Spanish 7.0.8 x86_64" {
        set isofile="/boot/iso/slax-Spanish-7.0.8-x86_64.iso"
        loopback loop $isofile
        linux (loop)/slax/boot/vmlinuz load_ramdisk=1 prompt_ramdisk=0 rw printk.time=0 slax.flags=toram from=$isofile
        initrd (loop)/slax/boot/initrfs.img
}

menuentry "Debian Wheezy x86_64 preseed" {
        set isofile="/boot/iso/wheezy_mini_amd64.iso"
        loopback loop $isofile
        linux (loop)/linux auto=true preseed/url=http://192.168.10.40/instalaciones/wheezy_preseed_131.cfg debian-installer/country=ES debian-installer/language=es debian-installer/keymap=es debian-installer/locale=es_ES.UTF-8 keyboard-configuration/xkb-keymap=es console-keymaps-at/keymap=es debconf/priority=critical netcfg/disable_dhcp=true netcfg/get_ipaddress=192.168.10.131 netcfg/get_netmask=255.255.255.0 netcfg/get_gateway=192.168.10.1 netcfg/get_nameservers=192.168.10.1 --
        initrd (loop)/initrd.gz
}

menuentry "Centos 6.4 x86_64 kickstart" {
        set isofile="/boot/iso/CentOS-6.4-x86_64-netinstall.iso"
        loopback loop $isofile
        linux (loop)/images/pxeboot/vmlinuz ip=192.168.10.131 noipv6 netmask=255.255.255.0 gateway=192.168.10.1 dns=192.168.10.1 hostname=fn131.forondarena.net ks=http://192.168.10.40/instalaciones/ks_rh6_131.ks lang=es_ES keymap=es
        initrd (loop)/images/pxeboot/initrd.img
}

menuentry "Restart" {
        reboot
}

menuentry "Shut Down" {
        halt
}

Suficiente, no necesitamos nada más. Repasemos un poco:

  • Las entradas del menú se separan en bloques "menuentry", uno para cada instalación diferente, e incluyendo las dos últimas para reiniciar y para apagar el equipo (no son muy útiles pero sirven de ejemplo).
  • Como no me gusta escribir demasiado, he definido la variable, "isofile" con la imagen que va a usar GRUB para arrancar (ahora hablamos sobre esto) en cada bloque.
  • Vamos a usar imágenes ISO "normales", y GRUB va a asumir que son la raíz de la instalación.
  • Como sabéis, cuando queremos arrancar un sistema, es habitual especificar un kernel en una línea que empieza con "linux", las opciones que queremos usar con este núcleo, y un initrd. Aquí estamos haciendo exactamente esto, pero debemos especificar la ruta dentro de la imagen ISO donde encontrar el kernel y el initrd. Lo más fácil es que montéis la imagen y veáis dónde está cada uno.
  • Las dos instalaciones automáticas usan más opciones que el resto. Lo que estamos haciendo es pasar el fichero de configuración (preseed o kickstart) que el sistema leerá vía http, y luego una serie de opciones básicas (idioma, teclado, ...). Ni todas son necesarias, ni son todas las que se pueden poner.
  • Como hemos dicho que no queremos usar PXE, asumo que tampoco queremos usar DHCP, así que las configuraciones de red son estáticas.
  • La IP que asignamos al servidor en esta fase de instalación no tiene que ser necesariamente la misma que instalaremos en el servidor, aunque en este ejemplo asumo que será así.

Fácil, ¿Verdad? El siguiente paso es descargar las imágenes que estamos usando en cada bloque (opción "isofile"). Tened en cuenta que no hay nada raro en estas ISO. Son las imágenes estándar de las distribuciones, aunque las he renombrado para que todo quede más ordenado. Para guardarlas he creado un directorio "/mnt/boot/iso/", y he copiado ahí los siguientes ficheros:

  • Para Debian, con y sin preseed: mini.iso (renombrada a wheezy_mini_amd64.iso).
  • Para CentOS: netinstall.iso.
  • Para Ultimate BootCD: ubcd.
  • Para Slax: slax.

Cuando lo tengáis todo, desmontad /mnt, y ya habremos terminado con el pendrive. Quedan los preseed/kickstart.

Ficheros kickstart y preseed

Si os fijáis en el menú de GRUB, para Debian estamos usando una referencia al fichero wheezy_preseed_131.cfg. Este fichero no es más que un preseed normal y corriente que, obviamente, está preparado para la instalación que queremos hacer. Los ficheros preseed consisten en escribir todas las respuestas a todas las opciones de menú que pueden aparecer en el instalador. Esto hace que el sistema sea muy flexible, pero también muy denso. Si queréis ver un listado con todas las opciones disponibles, id a una máquina Debian y ejecutad lo siguiente:

debconf-get-selections --installer >> wheezy_preseed_131.cfg
debconf-get-selections >> wheezy_preseed_131.cfg

Ahí tenéis, todo un "wheezy_preseed_131.cfg"; una locura. Afortunadamente, no siempre hacen falta todas las opciones. De hecho, yo en mis instalaciones para KVM uso esta versión, mucho más reducida. Claro, esto implica que si os sale un diálogo durante la instalación para el que no hemos previsto una respuesta, quizá porque vuestro hardware pida "algo" extra, la instalación automática se va a parar. En este caso tendréis que buscar la opción que os falta y añadirla.

Os recomiendo que abráis el fichero y que le déis una vuelta. Tened en cuenta que está pensado para instalaciones sobre KVM y los drivers virtio, así que el dispositivo de disco que se usa es /dev/vda. Además, uso sólo un interfaz de red, eth0. En cuanto al particionado, uso una partición para boot, primaria y de 50MB, otra para swap de unos 512MB, y por último, el resto del disco, en un grupo LVM para la raíz.

Además de esto, suelo usar un mirror local de Debian para agilizar la primera instalación. No es necesario, podéis usar un mirror público y, con ello, simplificar aún más la infraestructura. Bueno, "simplificar" por decir algo, porque no se puede decir que copiar el contenido de los DVD de instalación de Debian en un servidor web sea complicado.

Aunque el sistema sea diferente, en realidad todo esto que he dicho para Debian se aplica igual para kickstart y las instalaciones automatizadas de CentOS. Revisad si queréis este ejemplo, subidlo a un servidor web, y adaptadlo a lo que os haga falta. Tened en cuenta que también suelo usar un mirror local en estos casos (otra vez, se trata sencillamente de descomprimir los DVD).

Pruebas en KVM

Una vez instalado GRUB, con el menú y las ISO copiadas en el pendrive, podemos probar el nuevo sistema en KVM. Es muy sencillo. De hecho, si usáis virt-manager, casi no tendréis que hacer nada. Una vez creada la máquina virtual, id a los detalles y pulsad sobre "agregar nuevo hardware". Después no tenéis más que elegir "usb host device" y, de la lista, la memoria USB. Una vez agregado el dispositivo, en el arranque de la máquina virtual, justo al principio, aparecerá una opción para acceder al menú de arranque pulsando F12. Entre las opciones que os van a aparecer debería estar el pendrive.

Por último, esto es lo que veréis si todo ha ido bien:

Pantallazo GRUB

Y ya está, con esto hemos terminado. Romped vuestros CDs!!

Nota

En el post hablo sobre un servidor web, pero luego no escribo nada más sobre ello. No hay demasiado que decir; en mi caso uso un directorio "instalaciones", y en ese directorio pongo los preseed/ks. Junto a esto, debajo de "instalaciones", creo un directorio "centos/6.4-x86_64" y otro "debian/wheezy" y copio ahí el contenido de los DVD de cada distribución.

read more

Monitorización en serio. Práctica

lun, 24 jun 2013 by Foron

Después de haber hablado un poco sobre la teoría de la monitorización tal y como la veo yo, sigo con la parte práctica. Como dice Pieter Hintjens, la teoría está bien, en teoría; pero en la práctica, la práctica es mejor. Vamos a ver si doy algunas pistas de alternativas para llegar más allá de lo que permiten las herramientas de monitorización estándares.

Empecemos suponiendo que uno de nuestros servidores, un martes cualquiera, a las 10:30 a.m., aparece con el load average (uno de esos parámetros tan usados como mal interpretados) tres veces más alto que lo normal en otros martes a la misma hora. ¿Cómo de malo es esto?

  • Peligroso desde el punto de vista de la seguridad, porque puede implicar algún ataque, o que estemos mandando spam, o a saber qué.
  • Peligroso porque podría ser debido a un problema hardware, y con ello ser la antesala de una caida total del servidor.
  • Moderadamente serio en el caso en que simplemente se deba a que la conexión con nuestro servidor NFS se haya "atascado" (no es muy profesional, lo sé) de forma puntual, con lo que podría ser simplemente una pequeña congestión en la red, por decir algo.
  • ¡Estupendo! Si se debe a que la última campaña de publicidad ha tenido éxito y nuestro servidor está a pleno rendimiento. En este caso tendríamos que ver si necesitamos más hierro, pero si el rendimiento es bueno podremos estar satisfechos por estar aprovechando esas CPUs por las que pagamos un buen dinero.

En definitiva, que nos vendría bien una monitorización algo más trabajada y capaz de ir algo más allá de los simples números.

Pongámonos en el escenario de un servidor IMAP que en momentos puntuales rechaza más validaciones de lo normal. Sabemos que los usuarios se quejan porque no pueden autenticarse contra el servidor, pero no sabemos el motivo exacto. Aquí el problema es otro, porque necesitaremos saber cuándo tenemos un ratio acierto/fallo alto, y a partir de ese momento decidir qué acciones tomar, ya sea en la línea de revisar conexiones de red, carga del sistema, descriptores de ficheros, estado del backend de validación .... El gran problema en este tipo de fallos puntuales es que son "difíciles de pillar", al poder pasar en cualquier momento, y sólo durante unos pocos segundos. Además, no siempre se deben a problemas fáciles de monitorizar, como son la carga o el consumo de memoria de los servidores. El que un sistema de monitorización se conecte bien a los equipos A y B no siempre significa que A no pierda tráfico cuando habla con B.

¿Y qué aplicaciones hay que revisen todo esto? Pues no lo sé; pero aunque las hubiera, como este blog no va sobre apt-get install y siguiente-siguiente, vamos a hacer algo moderadamente artesano.

No nos volvamos locos. Por mucho que nos lo curremos, y salvo el improbable caso en el que nos den tiempo suficiente para programarlo, es muy complicado picar un sistema de monitorización, con todo lo que implica, desde cero. Hacer una aplicación capaz de leer datos del sistema, analizarlos y parsearlos, con buen rendimiento y estabilidad, no es fácil. Además, si algo hay en el "mercado", son aplicaciones para monitorización de infraestructuras, que además funcionan muy bien. Dediquemos nuestro tiempo a escribir esa capa extra propia de nuestro entorno a la que no pueden llegar las herramientas generalistas.

Si hay un sistema de monitorización que destaca sobre los demás que conozco, ese es Collectd. ¡Ojo! No digo que Collectd haga mejores gráficos que Graphite, o que sea más configurable que Cacti o Munin. Lo que quiero remarcar es que Collectd es perfecto para esto que queremos hacer. ¿Por qué?

  • Como la mayoría de aplicaciones serias, una vez configurada y puesta en marcha, podemos despreocuparnos de que se caiga o deje de funcionar.
  • Tiene buen rendimiento. Todo el núcleo y los plugins están programados en C, y aunque esto no implique automáticamente que vaya a ir rápido, en mi experiencia funciona muy bien.
  • Es modular. Tiene más de 90 plugins de todo tipo, desde los habituales para revisar la memoria o CPU, hasta otros más especializados, como Nginx o Netapp.
  • Se divide en dos grandes grupos de plugins (hay más), uno para leer datos (de CPU, memoria, ...), y otro para escribirlos (a gráficos rrd, a un Graphite externo, ...).

Pero me he dejado lo mejor para el final: El que sea modular significa que podemos quitar todo lo que no necesitemos, como por ejemplo todos los plugins que pueden afectar al rendimiento del servidor (escribir en ficheros rrd, sin ir más lejos, puede ser delicado), y con ello tener un sistema de monitorización muy poco intrusivo, pero completo. Además, y aquí está lo bueno, podemos escribir nuestros propios plugins, ya sea en perl, python o C. Usaremos esta funcionalidad para la lógica de nuestra aplicación.

El ejemplo

Volvamos al caso del servidor de correo que genera errores de validación en algunos momentos de carga alta. En este contexto, para un diagnóstico correcto, lo normal es pensar que vamos a necesitar, por lo menos, los plugins de lectura relacionados con el uso de CPU, el load-average, el consumo de memoria y el de conexiones TCP para saber la cantidad de sesiones abiertas contra el servidor de validación. Pero, además, tenemos que saber cuándo está fallando el servidor, y esto lo haremos a partir de los logs de la aplicación, y del número de "Login OK" en relación a los "Login Error". Para conseguir esta información de logs usaremos el módulo "tail". El plugin de salida que escribiremos recogerá todos estos datos, los analizará, y generará un informe que nos mandará por correo (o reiniciará el servidor, o arrancará una nueva instancia de KVM, o lo que sea que programemos).

En otros posts he escrito demasiado código, y no tengo claro que esto no sea más una forma de despistar a la gente que algo útil. Lo que sí voy a hacer es escribir una estructura de ejemplo que puede seguirse a la hora de programar plugins de Collectd.

Empecemos con la configuración más básica de Collectd, una vez instalado en el equipo a monitorizar. Tened en cuenta que siempre es interesante usar una versión razonablemente reciente (para escribir plugins en perl se necesita una versión por encima de 4.0, y para python de 4.9, que salió en el 2009).

El fichero de configuración principal de Collectd es collectd.conf, independientemente de que esté en /etc, /etc/collectd, /usr/local/etc, o en cualquier otro sitio. Es fácil de interpretar, así que me voy a limitar a lo fundamental para el post. En un entorno real deberíais leer la documentación.

Interval 10

Con esta opción especificamos cada cuánto vamos a leer datos. Si estamos leyendo el consumo de memoria del servidor, hablamos simplemente de una lectura cada 10 segundos, pero si hablamos del módulo tail, como veremos más adelante, estaremos calculando el número de veces que aparece cierto mensaje en ese intervalo determinado.

Empezamos por los plugins de entrada que no necesitan configuración, y que se instancian simplemente con un "loadplugin":

LoadPlugin cpu
LoadPlugin load
LoadPlugin memory

Otros, como no puede ser de otra forma, necesitan alguna opción:

LoadPlugin tcpconns
<Plugin "tcpconns">
  ListeningPorts false
  RemotePort "3306"
</Plugin>

Tcpconns monitoriza las conexiones TCP del servidor. En este ejemplo necesitamos saber las sesiones abiertas hacia servidores Mysql, ya que es el backend que usamos para la autenticación. En realidad, deberíamos usar el plugin de mysql, que da toda la información que se obtiene a partir de un "show status", pero para este ejemplillo nos vale con esto.

Por último, en cuanto a los plugins de lectura se refiere, necesitamos el plugin "tail", que configuraremos para que siga el log de validaciones de usuarios (maillog), y las cadenas de texto "Login OK" y Login Failed":

LoadPlugin tail
<Plugin "tail">
        <File "/var/log/maillog">
                Instance "Email_auth"
                <Match>
                        Regex "^.*Login[[:blank:]]OK.*$"
                        DSType "CounterInc"
                        Type "counter"
                        Instance "login_ok"
                </Match>
                <Match>
                        Regex "^.*Login[[:blank:]]Failed.*$"
                        DSType "CounterInc"
                        Type "counter"
                        Instance "login_failed"
                </Match>
        </File>
</Plugin>

Podéis complicar la expresión regular todo lo que queráis. Hay algunas opciones de configuración adicionales que no se muestran en este ejemplo, pero que suelen venir bien, como "ExcludeRegex", con la que se pueden quitar ciertas cadenas de la búsqueda; útil en casos como cuando necesitamos eliminar de la búsqueda los "Login OK" de usuarios de prueba que lanzan otros sistemas de monitorización. A los que conozcáis MRTG y familia, además, os sonarán los "DSType" y "Type" de la configuración. Efectivamente, podemos hacer gráficos de todo lo que encontremos usando valores medios, máximos, .... En nuestro caso viene bien un "CounterInc", que no hace más que ir sumando todos los "Login OK|Fail", y que por lo tanto va a servirnos para hacer cálculos sencillos cada 10 segundos, y también en otros periodos más largos.

Y con esto terminamos la parte de lectura de datos. La información obtenida desde estos plugins servirá para detectar anomalías en el servicio y, a partir de ahí, para hacer otra serie de tests más específicos siempre que sea necesario.

En nuestro caso de uso no queremos generar ningún gráfico, así que solo necesitamos que collectd lance una instancia del script que vamos a escribir en lo que a plugins de salida se refiere. Por ejemplo, "/usr/local/bin/monitorcorreo.py" (sí, esta vez en python):

<LoadPlugin python>
        Globals true
</LoadPlugin>
<Plugin python>
        ModulePath "/usr/local/bin"
        LogTraces true
        Import "monitorcorreo"
        <Module monitorcorreo>
                Argumento1 1
                Argumento2 "Podemos pasar argumentos al script"
        </Module>
</Plugin>

Vale, ahora solo queda escribir la lógica de lo que queremos conseguir con la monitorización. Vamos, lo importante. Recordad que tenemos que recoger los datos que nos mandan el resto de plugins, hacer las comprobaciones que tengamos que hacer y, de ser así, tomar una acción. En realidad, el que programemos el script en Python o Perl hace que no tengamos demasiados límites, más allá de los que tenga el usuario con el que ejecutemos Collectd.

El Script

Centrándonos ya en lo que sería "/usr/local/bin/monitorcorreo.py", el script debe registrarse en Collectd llamando al método collectd.register_config(funcionConfig). Por supuesto, antes debéis haber importado el módulo collectd, y deberéis haber escrito una función "funcionConfig", que básicamente debería leer las opciones de configuración que hayamos escrito en collectd.conf y hacer con ellas lo que sea que necesitemos.

El siguiente método a llamar es collectd.register_init(funcionInit). En este caso, Collectd va a llamar a la función "funcionInit" antes de empezar a leer datos. Por lo tanto, suele usarse para inicializar las estructuras, conexiones de red y demás estado del plugin. En mi caso, por ejemplo, uso este método para inicializar una instancia de la clase donde guardo el histórico de los datos que voy leyendo (hay que programarla, claro), y también creo un socket PUB/SUB basado en ZeroMQ con el que publicar toda la información relevante. Puedo usar este socket para mandar información a otros equipos (a mi PC, por ejemplo), o para conectar el proceso sin privilegios que es Collectd con otro menos expuesto que sea capaz de reiniciar servicios o de tomar otras acciones que requieran permisos de root.

Lo siguiente es registrar lo que sirve para indicar que estamos ante un plugin de escritura, con "collectd.register_write(funcionWrite)". Esta es la función que llamará Collectd cada vez que quiera escribir los datos que haya leido. "funcionWrite" es, por lo tanto, donde se ejecuta toda la lógica de nuestro script.

Como he venido diciendo, la clave de lo que haga la función funcionWrite es algo ya demasiado particular como para escribirlo aquí. Las pistas que puedo daros, sin embargo, son las siguientes:

  1. Si escribís algunas clases, con sus estructuras de datos y sus métodos, y las instanciais como "global", tendréis todo el histórico de datos (si lo queréis) durante todo el tiempo que esté collectd funcionando.

  2. Collectd va a ejecutar funcionWrite cada vez que lea desde los plugins de lectura.

  3. Si queréis hacer un ratio entre los "Login OK" y los "Login Failed" de una misma iteración de 10 segundos, con un contador incremental como CounterInc tendréis que restar los valores actuales a los de la iteración anterior, para sacar así los casos en estos 10 segundos. Dicho de otra forma, si ahora mismo hay 20 "Login OK" y 2 "Login Failed", y dentro de 10 segundos hay "27 Login OK" y 2 "Login Failed", en este intervalo de 10 segundos han habido 7 logins correctos y 0 fallidos. Este cálculo lo podéis hacer con comodidad si seguís la recomendación del punto 1. A partir de aquí podéis hacer las sumas, restas, divisiones o lo que sea que os apetezca.

  4. Todas las mediciones que manda Collectd llevan un timestamp. Cuando llegue una medición con una marca de tiempo 10 segundos mayor, será el momento de hacer todos los cálculos que queráis, porque ya tendréis la imagen completa de lo que ha pasado en ese intervalo.

  5. Para cada plugin de entrada, Collectd va a llamar a la función "funcionWrite" tantas veces como datos se generen, pasando como argumento un diccionario. En el caso del plugin de conexiones TCP, por ejemplo, se hace una llamada para cada estado posible (str(argumento)):

    collectd.Values(type='tcp_connections',type_instance='SYN_RECV',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[20.0])
    
    collectd.Values(type='tcp_connections',type_instance='FIN_WAIT1',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[2.0])
    
    collectd.Values(type='tcp_connections',type_instance='FIN_WAIT2',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[4.0])
    
    collectd.Values(type='tcp_connections',type_instance='TIME_WAIT',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[1.0])
    
    collectd.Values(type='tcp_connections',type_instance='CLOSED',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[0.0])
    
    ...
    

    Otro ejemplo, este caso para el consumo de memoria:

    collectd.Values(type='memory',type_instance='used',plugin='memory',host='192.168.10.20',time=1372071405.0433152,interval=10.0,values=[415285248.0])
    
    collectd.Values(type='memory',type_instance='buffered',plugin='memory',host='192.168.10.20',time=1372071405.0441294,interval=10.0,values=[28184576.0])
    
    collectd.Values(type='memory',type_instance='cached',plugin='memory',host='192.168.10.20',time=1372071405.0494869,interval=10.0,values=[163659776.0])
    
    collectd.Values(type='memory',type_instance='free',plugin='memory',host='192.168.10.20',time=1372071405.050016,interval=10.0,values=[2551083008.0])
    

    Por último, esto es lo que manda el plugin tail.

    collectd.Values(type='counter',type_instance='login_ok',plugin='tail',plugin_instance='Email_auth',host='192.168.10.20',time=1372071405.0442178,interval=10.0,values=[27])
    
    collectd.Values(type='counter',type_instance='login_failed',plugin='tail',plugin_instance='Email_auth',host='192.168.10.20',time=1372071405.044635,interval=10.0,values=[2])
    

    Cada plugin genera los datos propios de lo que esté monitorizando, pero la estructura es siempre la misma. Hay que tener un poco de cuidado con los valores que se devuelven en "values", porque no son siempre una medición puntual aislada. Con nuestra configuración para tail sabemos que ese "values" tiene el número de líneas con login ok o failed desde que arrancamos Collectd, pero si lo hubiésemos definido como Gauge (por ejemplo), tendríamos otro valor diferente, y entraríamos en el terreno de los valores medios, máximos y mínimos tan de MRTG.

  6. Si en una iteración se dieran las condiciones de fallo que hubiérais definido, como sería por ejemplo un 0.2% de fallos de Login en relación a los correctos, podéis usar la librería que más os guste de Python para hacer pruebas de todo tipo, desde un traceroute a una conexión a Mysql para lanzar una consulta determinada. En el caso de las validaciones, podríais completar el diagnóstico usando la librería IMAP de Python para capturar el error que devuelve el servidor. En definitiva, no hay límites.

  7. Podéis enviar el informe de diagnósito por correo, o en un fichero de texto, o en un socket ZeroMQ, o de cualquier otra forma que permita Python. Podéis reiniciar aplicaciones, lanzar instancias de KVM, ....

Para no pecar de "abstracto", este es un esqueleto de ejemplo de un monitorcorreo.py cualquiera:

import collectd
'''import time, imaplib, socket, smtplib ...'''

class ClasesDeApoyo(object):
        '''
        Estrucutras de datos para guardar los valores que recibimos desde los plugins.
        Métodos para trabajar con los datos, ya sean actuales, o históricos.
        Métodos para relacionar los datos de distintos plugins.
        Métodos para generar informes, mandar correos, ....
        Métodos para hacer traceroutes, abrir sesiones IMAP, ....
        '''

def funcionConfig(argconfig):
        '''
        En argconfig se encuentran, entre otros, los argumentos que han entrado desde collectd.conf.
        '''
        global instanciasClasesDeApoyo
        '''
        Crear una instancia de las clases de apoyo, aunque se puede dejar para Init.
        Si se van a usar los argumentos de collectd.conf, se pueden leer en un bucle.
        '''

def funcionInit():
        '''
        Esta función se usa para inicializar datos. Puede ser interesante para llamar a métodos que abran conexiones, ficheros, ....
        '''
        global instanciasClasesDeApoyo
        '''
        Inicializar estructuras.
        Si todo ha ido bien, se registra en collectd la función Write.
        '''
        collectd.register_write(funcionWrite)

def funcionWrite(argdatos):
        '''
        Este es el método al que se llama cada vez que se genere un dato.
        Este método se encarga del trabajo real del script.
        '''
        global instanciasClasesDeApoyo
        '''
        Todos los valores vienen con un timestamp. Una idea es ir guardando estos valores en una estructura.
        Idea 1: Cuando el dato que se lea tenga un timestamp 10 segundos mayor que el anterior, es el momento de aplicar los calculos que tengamos que hacer, porque en ese punto ya tendremos la información de todos los plugins.
        Idea 2: Cuando hayáis leido los n datos que sabéis que se escriben en cada iteración, es el momento de aplicar los calculos que tengamos que hacer, porque en ese punto ya tendremos la información de todos los plugins.
        No siempre hacen falta todos los datos que recibimos desde collectd. Lo siguiente es un ejemplo.
        '''
        datos = {}
        datos["host"] = str(argdatos.host)
        datos["plugininstance"] = str(argdatos.plugin_instance)
        datos["typeinstance"] = str(argdatos.type_instance)
        datos["value"] = int(argdatos.values[0])
        datos["time"] = int(argdatos.time)
        datos["localtime"] =  str(time.strftime("%F %T",time.localtime(int(argdatos.time))))

        '''
        if datos["time"] < anteriordatos["time"]:
                instanciasClasesDeApoyo.hacerCalculos(datos)
        else:
                instanciasClasesDeApoyo.guardarDatos(datos)
        '''

collectd.register_config(funcionConfig)
collectd.register_init(funcionInit)
read more