forondarenanet//www.forondarena.net/2022-06-19T19:00:00+02:00You do not really understand something unless you can explain it to your grandmotherIdeas para autenticación de doble factor doméstica2022-06-19T19:00:00+02:002022-06-19T19:00:00+02:00tag:www.forondarena.net,2022-06-19:/ideas-mfa-domestico.html<p>No hay duda de que la autenticación de doble factor va ganando importancia. Aunque pueda ser molesto en algunos casos, no parece haber debate si se dice que es una de esas tecnologías que están para quedarse, de una u otra manera.</p>
<p>Nada nuevo. Si usas un panel de control …</p><p>No hay duda de que la autenticación de doble factor va ganando importancia. Aunque pueda ser molesto en algunos casos, no parece haber debate si se dice que es una de esas tecnologías que están para quedarse, de una u otra manera.</p>
<p>Nada nuevo. Si usas un panel de control o alguna aplicación de web de alguno de los proveedores importantes, casi seguro que tendrás opciones para 2FA.</p>
<p>El objetivo de este post es dar algunas ideas para llevar el concepto detrás de la autenticación de doble factor a entornos más pequeños, o a aplicaciones que no tienen ningún tipo de integración de este tipo. ¿Cómo damos dos tipos de autenticación al puerto de correo saliente de un servidor casero? ¿De un servidor IMAP, SSH, o de alguna aplicación del siglo pasado?</p>
<p>Hablando de los ejemplos, podríamos discutir que un servidor SSH "bien trabajado", haciendo uso de todas las funcionalidades que ofrece a nivel SSL, hosts, certificados cliente, etc, da un nivel suficiente de seguridad. De la misma manera, podríamos decir que un Dovecot (hablando de servidores IMAP) integrado con <a class="reference external" href="https://github.com/PowerDNS/weakforced">Weakforced</a> y una buena política de seguridad (geolocalización, intentos de login fallidos, ...), también sube la seguridad de la aplicación a un nivel aceptable. Si, además de estas medidas, planteamos el uso de software tipo <a class="reference external" href="http://www.fail2ban.org">fail2ban</a> para crear reglas firewall en base a lo que se ve en los logs, es razonable darnos por satisfechos en, recuerdo, entornos pequeños.</p>
<p>Con esto terminaríamos el post, de no ser porque creo que estas medidas tienen dos aspectos que podrían mejorarse:</p>
<ul class="simple">
<li>El puerto del servicio está abierto, aunque fail2ban, o la gestión de accesos en general, podría cerrárselo a una IP ante el mínimo error.</li>
<li>Las medidas de seguridad adicionales se aplican sobre el propio servicio. No hay nada de malo en esto pero, tal y como lo veo yo, que un usuario IMAP use, además de su cliente de correo, un formulario web para darse acceso, es una buena mejora.</li>
</ul>
<p>En realidad, ninguna de estas ideas es nueva. El port-knocking existe hace mucho tiempo, y hay implementaciones como fwknop que han funcionado perfectamente, aunque, en este caso concreto, hace tiempo que no tiene actualizaciones. Novedad o no, ya sabéis lo que pasa en cuanto una aplicación es visible en Internet, así que creo que es importante intentar que el firewall tenga cerrado el acceso hasta que una tercera aplicación lo abra.</p>
<p>Por supuesto, ninguna medida "extra" debería deshabilitar el control de acceso propio de cualquier aplicación.</p>
<p><strong>Abrir firewall bajo demanda y por tiempo limitado</strong></p>
<p>Si me lo preguntáis a mí, a falta de cosas más elaboradas, algo tan sencillo como conseguir que un puerto esté cerrado hasta que accedamos a una URL web concreta es una medida estupenda que, en cierta medida, da algo similar a 2FA con muy poco trabajo. Hablamos de tener el puerto 993 (IMAP bajo SSL) cerrado hasta que un acceso a "<a class="reference external" href="https://www.example.com/sec_obscurity_imap.html">https://www.example.com/sec_obscurity_imap.html</a>" lo abra, idealmente durante un tiempo limitado.</p>
<p>¿Cuánta seguridad da esto? Podemos discutirlo, pero en mi experiencia, y aplico esta medida a menudo, todavía tengo que ver un intento de login causado por alguien que accede primero a una URL y luego al servicio "real". Esto sin hablar ni siquiera de securizar el acceso a la web.</p>
<p>Lo mejor de todo es que esto es trivial. Iptables ofrece un módulo razonablemente estándar que permite:</p>
<ul class="simple">
<li>Dar acceso a una IP de forma dinámica</li>
<li>Mantener la IP con acceso durante un tiempo limitado</li>
<li>Renovar el acceso cuando haya actividad desde la IP, y darla de baja cuando expire</li>
</ul>
<p>Hablamos del "match" <cite>recent</cite>. No me he encontrado ninguna instalación/distribución que no lo tenga habilitado por defecto, aunque habrá casos de todo tipo.</p>
<p>La idea general de este match suele ser registrar una IP cuando accede a una cadena iptables, y luego denegar el acceso en base a los criterios que queramos, habitualmente relacionados con evitar denegación de servicio. Vamos a darle la vuelta. En lugar de registrar una IP accediendo a una cadena, vamos a escribir en un fichero de <cite>/proc</cite> para dar de alta la dirección, y vamos a permitir el acceso iptables a las IPs dadas de alta en ese fichero durante un tiempo. En realidad, es muy sencillo porque:</p>
<p>Para dar de alta una IP:</p>
<div class="highlight"><pre><span></span><span class="nb">echo</span> +127.0.0.2 > /proc/net/xt_recent/acceso_imap
</pre></div>
<p>Ejemplo de tabla IPTables para pertir el acceso solo a las IPs del fichero. <strong>Cuidado!</strong> Mucho cuidado con tocar un firewall sin saber lo que se está haciendo:</p>
<div class="highlight"><pre><span></span>...
iptables -t filter -N IMAP
iptables -t filter -A INPUT -p tcp --dport <span class="m">993</span> -j IMAP
...
iptables -t filter -A IMAP -m recent --update --name acceso_imap --reap --seconds <span class="m">86400</span> -j ACCEPT
iptables -t filter -A IMAP -j DROP
...
</pre></div>
<p>Lo importante de esto es que</p>
<ul class="simple">
<li><strong>--update</strong>: Revisa si la IP origen está en el listado de proc y, de estarlo, actualiza el último timestamp de acceso</li>
<li><strong>--name</strong>: La referencia al listado (al fichero) de IPs autorizadas</li>
<li><strong>--reap</strong>: Para borrar IPs viejas y mantener el listado limpio</li>
<li><strong>--seconds</strong>: Solo se hace match si una IP ha sido vista durante este número de segundos</li>
</ul>
<p>Y, con esto, ya estaría, a falta de ir escribiendo en el fichero de <cite>/proc</cite> a partir de un acceso web.</p>
<p>No sé si merece la pena hablar sobre cómo escribir un fichero desde un servidor web, pero sí que creo que hace falta recordar que el usuario web no suele ser <cite>root</cite> (ni se os ocurra), y que por lo tanto no vais a poder escribir directamente en <cite>/proc/net/xt_recent/acceso_imap</cite>. Por dar una idea de implementación que puede ampliarse a otros entornos, de lo más sencilla posible:</p>
<ul class="simple">
<li>El servidor web escribe la IP desde la que se ha accedido, suponiendo que es la misma que va a usar IMAP después, en una ruta fuera del árbol web, pero con acceso para el usuario con el que se esté ejecutando.</li>
<li>Monitorizamos el fichero vía systemd, que lanza un script, como root, cuando contenga algo:</li>
</ul>
<div class="highlight"><pre><span></span><span class="c1"># cat /etc/systemd/system/imap_whitelist.path</span>
<span class="o">[</span>Path<span class="o">]</span>
<span class="nv">PathChanged</span><span class="o">=</span>/var/www/ruta_reglas_imap/accesos
<span class="o">[</span>Install<span class="o">]</span>
<span class="nv">WantedBy</span><span class="o">=</span>multi-user.target
<span class="c1"># cat /etc/systemd/system/imap_whitelist.service</span>
<span class="o">[</span>Unit<span class="o">]</span>
<span class="nv">Description</span><span class="o">=</span>Nueva whitelist IMAP
<span class="nv">After</span><span class="o">=</span>network.target
<span class="o">[</span>Service<span class="o">]</span>
<span class="nv">Type</span><span class="o">=</span>oneshot
<span class="nv">ExecStart</span><span class="o">=</span>/usr/local/bin/imap_whitelist.sh
<span class="c1"># cat /usr/local/bin/imap_whitelist.sh</span>
<span class="c1">#!/bin/bash</span>
<span class="nv">IPT_WHITELIST</span><span class="o">=</span><span class="s2">"/proc/net/xt_recent/acceso_imap"</span>
<span class="nv">WEB_WHITELIST</span><span class="o">=</span><span class="s2">"/var/www/ruta_reglas_imap/accesos"</span>
<span class="k">while</span> <span class="nb">read</span> LINE
<span class="k">do</span>
<span class="c1"># Algo de control de errores vendria bien</span>
<span class="nb">echo</span> +<span class="si">${</span><span class="nv">LINE</span><span class="si">}</span> > <span class="si">${</span><span class="nv">IPT_WHITELIST</span><span class="si">}</span>
<span class="k">done</span> <<span class="si">${</span><span class="nv">WEB_WHITELIST</span><span class="si">}</span>
> <span class="si">${</span><span class="nv">WEB_WHITELIST</span><span class="si">}</span>
<span class="nb">exit</span> <span class="m">0</span>
</pre></div>
<p>Ojo, que no es más que un ejemplo sencillo. En la práctica, se puede complicar todo lo que se quiera. Es más, probablemente se debería complicar para controlar mejor los datos (las IPs), la concurrencia, .... Tened en cuenta, además, que nos hemos basado en entornos de firewall "tradicionales". Para el que quiera hacer algo más moderno, los mapas de BPF podrían servir para hacer algo similar a lo presentado aquí.</p>
<p>Evidentemente, no estamos haciendo doble factor, como tal, desde el punto de vista de tener dos controles de acceso a la propia aplicación pero, a cambio, ocultamos completamente el acceso general al servicio a todo el que no conozca los pasos necesarios. En este momento empezaría el debate de si la obscuridad es seguridad y todas estas cosas, aunque no creo que estemos hablando de exactamente lo mismo, particularmente si tenemos en cuenta que podemos habilitar todo tipo de mecanismos de autenticación en la web que escribe la IP en el fichero.</p>
<p>El objetivo de este post es hacer algo lo más sencillo posible que se pueda poner en marcha en una tarde. Por eso, no voy a escribir nada de código para la parte web. Esto no quita que no se pueda dar alguna idea sobre soluciones más completas.</p>
<p><strong>Soluciones completas</strong></p>
<p>Imaginad que seguimos con la idea de tener una aplicación web de cuatro lineas, quizá un formulario en el que escribir una IP, o cuatro cosas más. Sobre esto queremos ofrecer autenticación por usuario/contraseña, TOTP, Oauth, o cualquier otro método. En este caso, podemos poner algo como <a class="reference external" href="http://www.gluu.org">gluu.org</a> (solo es un ejemplo) delante de la aplicación para ofrecer todos estos métodos. Podéis ver un ejemplo en este <a class="reference external" href="https://gluu.org/docs/gg/tutorials/oidc-steppedup-auth-tutorial/">tutorial</a>.</p>
<p>Hablando de soluciones completas, he preferido no citar nada sobre proveedores cloud, orquestadores o gestores de cualquier tipo. Confío en que de este post se pueden sacar ideas útiles para cualquier entorno.</p>
Ideas para un servidor DNS doméstico2021-01-09T18:00:00+01:002021-01-09T18:00:00+01:00tag:www.forondarena.net,2021-01-09:/ideas-dns-domestico.html<p>No prestamos suficiente importancia a los DNS que usamos.</p>
<p>Allá por el 2019 hubo una cierta polémica con los planes de algunos grandes proveedores de centralizar algo que, hasta ese momento, estaba muy diversificado en los ISP de cada usuario. Se empezó a hablar y a hacer campaña (no siempre …</p><p>No prestamos suficiente importancia a los DNS que usamos.</p>
<p>Allá por el 2019 hubo una cierta polémica con los planes de algunos grandes proveedores de centralizar algo que, hasta ese momento, estaba muy diversificado en los ISP de cada usuario. Se empezó a hablar y a hacer campaña (no siempre con razón) en contra de los DNS recursivos que usábamos. Además del rendimiento, también se empezó a hablar sobre cifrado, y apareceron conceptos como el DNS sobre TLS y el DNS sobre HTTPS (DOT y DOH). Todo esto generó algo de polémica, sobre todo en la industria europea. No había conferencia en la que no se tratase el asunto.</p>
<p>Pero no, no tengo intención de escribir nada al respecto. Os dejo algunas referencias de una de las voces mas criticas (la gente de PowerDNS), pero solo como punto de partida para que quien tenga interés llegue a sus propias conclusiones. Recomendaría también leer algo sobre el punto de vista contrario.</p>
<ul class="simple">
<li><a class="reference external" href="https://blog.powerdns.com/2019/12/03/doh-anti-competitive-and-network-neutrality-aspects">DOH network neutrality</a></li>
<li><a class="reference external" href="https://blog.powerdns.com/2019/09/25/centralised-doh-is-bad-for-privacy-in-2019-and-beyond/">Centralised DOH</a></li>
<li><a class="reference external" href="https://www.powerdns.com/dohdot.html">DOH and DOT</a></li>
</ul>
<p>En este post solo voy a dar algunas ideas y ejemplos para todos aquellos que quieran gestionar su propio DNS, pero que no suelan hacer mucho más que un "apt-get install", "yum install", o lo que sea, de cualquiera de los servidores recursivos disponibles.</p>
<p>Como base del post, y ya que hemos mencionado PowerDNS, vamos usar su software de <a class="reference external" href="https://doc.powerdns.com/recursor/">DNS recursivo</a>.</p>
<p><strong>Instalación básica</strong></p>
<p>Siempre he pensado que tener un DNS propio es una buena idea. Aunque cada vez sea más difícil tener el control de lo que hace el último despertador o la última televisión súper inteligente de este o aquel fabricante, personalmente siempre configuro mis cacharros para que usen mi DNS. Esto permite habilitar logueo o tomar cualquier otra medida, como bloquear consultas determinadas, más fácilmente. Ahora bien, como comentaba, siempre hay que tener claro que es muy difícil impedir a una aplicación el uso de su resolución privada, y menos todavía si es sobre DOH.</p>
<p>Teniendo esto claro, poco más que decir sobre lo que hace falta para tener una instalación básica de un servidor recursivo. Tened en cuenta que, aunque esté disponible en muchas distribuciones, a menudo es interesante instalar las versiones más recientes, disponibles en <a class="reference external" href="https://repo.powerdns.com">repo.powerdns.com</a>.</p>
<p>En el caso de servidor recursivo, podéis descargar el software para varias distribuciones sin mayores problemas. Recordad que hablamos de <em>PowerDNS Recursor</em>, y no de <em>PowerDNS Authoritative Server</em>.</p>
<p>Una vez hecha la instalación básica, en distribuciones modernas seguramente tendréis un <cite>pdns-recursor</cite> ejecutando, con su correspondiente servicio de Systemd. Esto importará más adelante cuando configuremos instancias virtuales.</p>
<p>La configuración del servidor suele estar en <cite>/etc/powerdns/recursor.conf</cite>. Echad un ojo a los parámetros y ajustadlos a vuestro gusto.</p>
<p><strong>Logueo selectivo</strong></p>
<p>Empezamos a ser un poco más creativos. De forma general, pdns-recursor, al igual que ya mayoría del software DNS disponible, permite loguear consultas. Además de las opciones de configuración habituales para loguear en Syslog y similares, también se puede activar el <a class="reference external" href="https://docs.powerdns.com/recursor/running.html#tracing-queries">tracing</a>. Vamos a hacer cosas un poco más interesantes que estas.</p>
<p>En este ejemplo loquearemos las consultas que se hagan a <a class="reference external" href="https://es.wikipedia.org/wiki/Sistema_autónomo">Sistemas Autónomos</a> concretos. Dicho de otra forma, loguearemos el nombre del Sistema Autónomo de las IPs que devuelve una consulta. Con esto será sencillo hacernos una idea de los accesos que acemos a Google, Amazon, ... (suponiendo que usen el DNS del sistema/red). Por supuesto, no es más que un ejemplo. No sería mucho más difícil loguear los accesos a IPs registradas en paises determinados, o cualquier otra cosa que se os ocurra.</p>
<p>Powerdns-recursor usa LUA como herramienta de scripting. Básicamente ofrece un API, estructuras de datos y funciones a las que llama en cada una de las fases que sigue cuando preguntamos por la resolución de un dominio. Tenéis algo de información <a class="reference external" href="https://docs.powerdns.com/recursor/lua-scripting/hooks.html">aquí</a>.</p>
<p>Simplificando un poco, tenemos que escribir algo de código LUA (muy sencillo) en la fase (función) de PowerDNS que más nos interese. Pondremos la referencia al script en la opción de configuración <cite>lua-dns-script</cite> de <cite>recursor.conf</cite>, indicando la ruta completa al fichero LUA. Veamos un ejemplo antes de seguir:</p>
<div class="highlight"><pre><span></span><span class="n">badips</span> <span class="o">=</span> <span class="n">newNMG</span><span class="p">()</span>
<span class="n">badips</span><span class="p">:</span><span class="n">addMask</span><span class="p">(</span><span class="s2">"192.168.10.0/24"</span><span class="p">)</span>
<span class="n">dropset</span> <span class="o">=</span> <span class="n">newDS</span><span class="p">()</span>
<span class="n">dropset</span><span class="p">:</span><span class="n">add</span><span class="p">(</span><span class="s2">"phishing.example.net"</span><span class="p">)</span>
<span class="kr">function</span> <span class="nf">ipfilter</span><span class="p">(</span><span class="n">rem</span><span class="p">,</span> <span class="n">loc</span><span class="p">,</span> <span class="n">dh</span><span class="p">)</span>
<span class="kr">return</span> <span class="n">badips</span><span class="p">:</span><span class="n">match</span><span class="p">(</span><span class="n">rem</span><span class="p">)</span>
<span class="kr">end</span>
<span class="kr">function</span> <span class="nf">preresolve</span><span class="p">(</span><span class="n">dq</span><span class="p">)</span>
<span class="kr">if</span> <span class="n">dropset</span><span class="p">:</span><span class="n">check</span><span class="p">(</span><span class="n">dq</span><span class="p">.</span><span class="n">qname</span><span class="p">)</span> <span class="kr">then</span>
<span class="n">dq</span><span class="p">.</span><span class="n">rcode</span> <span class="o">=</span> <span class="n">pdns</span><span class="p">.</span><span class="n">DROP</span>
<span class="kr">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="kr">end</span>
<span class="kr">end</span>
</pre></div>
<p>En estos ejemplos básicos de la propia web de PowerDNS impedimos el acceso desde equipos de nuestra red que tengan IPs de 192.168.10.0/24 con la función <cite>ipfilter</cite>, que se ejecuta en una fase muy temprana, y también bloqueamos que se consulte el dominio phishing.example.net. Imaginad que sabemos que se usa como enlace a malware en muchas campañas de spam. <cite>preresolve</cite> es adecuado para esto porque se ejecuta antes que ninguna operación de red. Podríamos añadir todos los dominios que quisiéramos en la variable "dropset", que se bloquearían con ese <cite>pdns.DROP</cite> del <cite>if</cite>.</p>
<p>Sigamos con el caso de uso original. Queremos loquear sistemas autónomos.</p>
<p>Necesitamos dos cosas antes de empezar:</p>
<ul class="simple">
<li>Una forma de mapear IPs a sistemas autónomos: Muchos habréis usado la librería GeoIP para saber el país origen de una IP. Esta no es la única base de datos que ofrece <a class="reference external" href="https://www.maxmind.com">Maxmind</a>, también podemos usar una base de datos que da información del sistema autónomo a partir de una IP. En mi suscripción gratuita a Maxmind se llama <cite>GeoLite2-ASN.mmdb</cite>.</li>
<li>Una forma de acceder a estos datos desde LUA: Hay más de una librería disponible. En Debian, por ejemplo, se puede usar el paquete <cite>lua-mmdb</cite>. Yo uso la versión más reciente, que era la <a class="reference external" href="https://github.com/daurnimator/mmdblua">0.2</a> cuando escribia este post. Cuidado, con otra versión u otra librería podrían ser necesarios retoques en el código.</li>
</ul>
<p>Antes de continuar, tened en cuenta que hablamos de instalaciones domésticas. Si queréis hacer algo más serio debéis confirmar que no estáis penalizando el rendimiento del sistema.</p>
<p>Recordad, vamos a loguear todos los sistemas autónomos de todas las IPs que se devuelven en registros A cuando consultamos un dominio. Con esta base luego sería muy sencillo buscar cadenas o números concretos. Como necesitamos la(s) IP(s) de la respuesta, vamos a usar la función <cite>postresolve</cite>, que a estas alturas ya sabéis que se ejecuta justo antes de generar la respuesta para el cliente DNS.</p>
<div class="highlight"><pre><span></span><span class="kr">function</span> <span class="nf">postresolve</span><span class="p">(</span><span class="n">dq</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">records</span> <span class="o">=</span> <span class="n">dq</span><span class="p">:</span><span class="n">getRecords</span><span class="p">()</span>
<span class="kr">if</span> <span class="n">records</span> <span class="kr">then</span>
<span class="kd">local</span> <span class="n">mmdb</span> <span class="o">=</span> <span class="nb">require</span> <span class="s2">"mmdb"</span>
<span class="kd">local</span> <span class="n">geo_asn_db</span> <span class="o">=</span> <span class="n">mmdb</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="s2">"/usr/local/GeoLite2-ASN.mmdb"</span><span class="p">)</span> <span class="ow">or</span> <span class="kc">nil</span>
<span class="kr">if</span> <span class="ow">not</span> <span class="n">geo_asn_db</span> <span class="kr">then</span>
<span class="n">pdnslog</span><span class="p">(</span><span class="s2">"Error al abrir bases de datos"</span><span class="p">,</span> <span class="n">pdns</span><span class="p">.</span><span class="n">loglevels</span><span class="p">.</span><span class="n">Warning</span><span class="p">)</span>
<span class="kr">return</span> <span class="kc">false</span>
<span class="kr">end</span>
<span class="kd">local</span> <span class="n">asn_org</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="kr">for</span> <span class="n">k</span><span class="p">,</span><span class="n">v</span> <span class="kr">in</span> <span class="nb">pairs</span><span class="p">(</span><span class="n">records</span><span class="p">)</span> <span class="kr">do</span>
<span class="kr">if</span> <span class="n">v</span><span class="p">.</span><span class="n">type</span> <span class="o">==</span> <span class="n">pdns</span><span class="p">.</span><span class="n">A</span> <span class="kr">then</span>
<span class="kd">local</span> <span class="n">asn_data</span> <span class="o">=</span> <span class="n">geo_asn_db</span><span class="p">:</span><span class="n">search_ipv4</span><span class="p">(</span><span class="n">v</span><span class="p">:</span><span class="n">getContent</span><span class="p">())</span>
<span class="kr">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">asn_data</span><span class="p">)</span> <span class="o">==</span> <span class="s2">"table"</span> <span class="kr">then</span>
<span class="n">asn_org</span> <span class="o">=</span> <span class="n">asn_data</span><span class="p">[</span><span class="s2">"autonomous_system_organization"</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">"Desconocido"</span>
<span class="n">pdnslog</span><span class="p">(</span><span class="n">dq</span><span class="p">.</span><span class="n">qname</span><span class="p">:</span><span class="n">toString</span><span class="p">()</span> <span class="o">..</span> <span class="s2">" devuelve "</span> <span class="o">..</span> <span class="n">v</span><span class="p">:</span><span class="n">getContent</span><span class="p">()</span> <span class="o">..</span> <span class="s2">" de "</span> <span class="o">..</span> <span class="n">asn_org</span><span class="p">,</span> <span class="n">pdns</span><span class="p">.</span><span class="n">loglevels</span><span class="p">.</span><span class="n">Warning</span><span class="p">)</span>
<span class="kr">return</span> <span class="kc">false</span>
<span class="kr">end</span>
<span class="kr">end</span>
<span class="kr">end</span>
<span class="kr">end</span>
<span class="kr">return</span> <span class="kc">false</span>
<span class="kr">end</span>
</pre></div>
<p>Como véis, estamos usando la función <cite>dq:getRecords()</cite>, que ofrece el API de PowerDNS, para leer todos los registros que va a devolver una consulta. Con cada registro de tipo A usamos <cite>search_ipv4</cite>, de la librería mmdb, para buscar el sistema autónomo de la IP en la base de datos <cite>GeoLite2-ASN.mmdb</cite>. Lo logueamos con 'pdnslog', pero podríamos hacer cualquier otra cosa que quisiésemos. En mi syslog de ejemplo veo cosas como estas:</p>
<div class="highlight"><pre><span></span>Jan 17 18:14:30 host1 pdns_recursor[11467]: creativecommons.org. devuelve 172.67.34.140 de CLOUDFLARENET
Jan 17 18:14:30 host1 pdns_recursor[11467]: creativecommons.org. devuelve 104.20.150.16 de CLOUDFLARENET
Jan 17 18:14:30 host1 pdns_recursor[11467]: creativecommons.org. devuelve 104.20.151.16 de CLOUDFLARENET
</pre></div>
<p><strong>Alta disponiblidad y balanceo</strong></p>
<p>Podría pensarse que queda un poco lejos de una instalación doméstica, pero <a class="reference external" href="https://dnsdist.org">dnsdist</a> es un software que todo el mundo que trabaje en entornos DNS debería conocer.</p>
<p>DNSDist es un balanceador de carga DNS que ofrece lo habitual de este tipo de software: alta disponibilidad, seguridad, distribución de tráfico entre servidores, ...). Es muy conocido en entornos grandes y, aunque su interés en entornos domésticos sea menor, hay casos de uso interesantes. Dos ejemplos:</p>
<ul class="simple">
<li>Querer usar un DNS recursivo propio, pero pasar a uno de un tercero (1.1.1.1, 8.8.8.8, 9.9.9.9, ...) si el nuestro no estuviese disponible.</li>
<li>Usar un DNS recursivo de una VPN, por ejemplo de trabajo, para resolver zonas privadas. Usar el personal para todo lo demás.</li>
</ul>
<p>Veamos cómo hacer estas cosas. Este post no es una guía de DNSDist, así que nos limitamos a mostrar la configuración mínima que sirva para hacernos una pequeña idea.</p>
<p>La instalación de DNSDist puede hacerse desde el mismo repositorio que el resto de software de PowerDNS. Se configura en un fichero tipo <cite>/etc/dnsdist/dnsdist.conf</cite>, y suele incluir un <cite>.service</cite> de Systemd en sistemas modernos.</p>
<p>Podemos instalar DNSDist y pdns-recursor en la misma máquina. Es más, no es para nada extraño. Lo normal en estos casos es configurar pdns-recursor en un puerto diferente al 53, o hacer que escuche solo en localhost, dejando el 53 público a DNSDist. A partir de ahora suponemos que hemos instalado pdns-recursor en localhost (opción de configuración <cite>local-address</cite>) y en el puerto 54 (<cite>local-port</cite>). Aseguráos de que podéis usar el 54.</p>
<p>Como ya no nos tenemos que preocupar de solaparnos con el puerto 53, podemos configurar DNSDist para que escuche en las IPs que queramos de la máquina, dejando el puerto por defecto (por supuesto, el 53):</p>
<div class="highlight"><pre><span></span><span class="n">setLocal</span><span class="p">(</span><span class="s1">'192.168.0.1'</span><span class="p">)</span>
<span class="n">addLocal</span><span class="p">(</span><span class="s1">'127.0.0.1'</span><span class="p">)</span>
</pre></div>
<p>En este momento sería interesante limitar (o no) el acceso a ciertas IPs o redes con <cite>setACL</cite>, aunque no lo veremos aquí.</p>
<p>El siguiente paso es definir servidores de backend a los que reenviar el tráfico. Los agruparemos en "pools", y siguiendo los dos casos de uso de ejemplo de los que hemos hablado, haríamos algo tipo:</p>
<div class="highlight"><pre><span></span><span class="n">newServer</span><span class="p">({</span><span class="n">address</span><span class="o">=</span><span class="s2">"127.0.0.1:54"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">"propio"</span><span class="p">,</span> <span class="n">order</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">pool</span><span class="o">=</span><span class="s2">"general"</span><span class="p">})</span>
<span class="n">newServer</span><span class="p">({</span><span class="n">address</span><span class="o">=</span><span class="s2">"8.8.8.8"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">"externo"</span><span class="p">,</span> <span class="n">order</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">pool</span><span class="o">=</span><span class="s2">"general"</span><span class="p">})</span>
<span class="n">newServer</span><span class="p">({</span><span class="n">address</span><span class="o">=</span><span class="s2">"172.16.0.10"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">"privado"</span><span class="p">,</span> <span class="n">pool</span><span class="o">=</span><span class="s2">"vpn"</span><span class="p">})</span>
<span class="n">setPoolServerPolicy</span><span class="p">(</span><span class="n">firstAvailable</span><span class="p">,</span> <span class="s2">"general"</span><span class="p">)</span>
</pre></div>
<p>En las dos primeras líneas hemos configurado dos servidores en el mismo pool, "general". Uno es nuestro pdns-recursor local, y el otro 8.8.8.8. Especificamos el orden porque es lo que usa la política de balanceo "firstAvailable". Como no limitamos el número de consultas por segundo, usará el mismo mientras no deje de responder. En cualquier caso, DNSDist permite monitorizar (incluso tiene un sencillo interfaz web) que el balanceo se esté haciendo bien.</p>
<p>Además de este pool general, también definimos el que usará el servidor de la VPN, digamos que 172.16.0.10. Cuando no tengamos acceso a esa IP, DNSDist marcará el servidor como inactivo y las consultas fallarán, aunque todo esto es muy configurable.</p>
<p>A partir de aquí se trata de mandar las consultas que hagamos para el dominio privado al pool "vpn". Suponiendo que nuestros servidores locales son subdominios de example.com sería algo tipo:</p>
<div class="highlight"><pre><span></span><span class="n">addAction</span><span class="p">(</span><span class="n">makeRule</span><span class="p">({</span><span class="s2">"example.com."</span><span class="p">}),</span> <span class="n">PoolAction</span><span class="p">(</span><span class="s2">"vpn"</span><span class="p">))</span>
</pre></div>
<p>Y mandaríamos el resto al pool "general":</p>
<div class="highlight"><pre><span></span><span class="n">addAction</span><span class="p">(</span><span class="n">AllRule</span><span class="p">(),</span> <span class="n">PoolAction</span><span class="p">(</span><span class="s2">"general"</span><span class="p">))</span>
</pre></div>
<p>Como os comento, DNSDist es muy configurable. Hay muchas reglas y acciones ya hechas para la mayoría de escenarios y, cuando no, permite programarlas vía LUA en muchas ocasiones.</p>
<p><strong>Habilitar DNS sobre TLS y sobre HTTPS</strong></p>
<p>Llegados a este punto, si queremos ganar algo de privacidad, seguramente interese sacar nuestro entorno DNS a una máquina fuera de nuestra red local.</p>
<p>En el mundo de PowerDNS configuramos DOT y DOH bajo el paraguas de DNSDist, que ofrece soporte para ambos sin grandes complicaciones:</p>
<div class="highlight"><pre><span></span><span class="n">addTLSLocal</span><span class="p">(</span><span class="s1">'192.168.0.1'</span><span class="p">,</span> <span class="s1">'clave_publica.pem'</span><span class="p">,</span> <span class="s1">'clave_privada.pem'</span><span class="p">)</span>
<span class="n">addDOHLocal</span><span class="p">(</span><span class="s1">'192.168.0.1'</span><span class="p">,</span> <span class="s1">'clave_publica.pem'</span><span class="p">,</span> <span class="s1">'clave_privada.pem'</span><span class="p">)</span>
</pre></div>
<p>Podéis usar los certificados que queráis, incluido Lets Encrypt.</p>
<p>Una vez hecho esto pasaréis a tener abiertos los puertos <cite>853 (DOT)</cite> y <cite>443 (DOH)</cite>.</p>
<p>Con esto tenemos unos DNS que permiten el uso de DOT y DOH. La pregunta natural ahora es cómo usarlos desde el portátil, el móvil o lo que sea. No es algo en lo que quiera meterme en este post. En la <a class="reference external" href="https://en.wikipedia.org/wiki/DNS_over_TLS">Wikipedia</a> tenéis algo de información para ir tirando del hilo.</p>
<p><strong>Usar DNS para dar información (Tipo RBL)</strong></p>
<p>Aunque solo está documentado para el servidor autorizativo, en pdns-recursor también se pueden usar <a class="reference external" href="https://doc.powerdns.com/authoritative/guides/virtual-instances.html">instancias virtuales</a>. Básicamente, se trata de copiar <cite>/etc/powerdns/recursor.conf</cite> con otro nombre, por ejemplo <cite>/etc/powerdns/recursor-rbl.conf</cite>, hacer que escuche en otro puerto (local-port), hacer los ajustes que se quiera, y ejecutarlo vía systemd, con <cite>systemctl start pdns-recursor@rbl.service</cite>. Con algo tan sencillo como esto se pueden tener instancias que nos interese tener separadas, y luego usar DNSDist para mandar tráfico a uno u otro proceso.</p>
<p>Indudablemente, estamos usando pdns-recursor para hacer cosas que pueden tener más sentido en el contexto de un DNS autorizativo. Aun así, estas ideas pueden llevar a casos de uso interesantes. Recordad que hablamos siempre de un entorno doméstico. En plataformas grandes hay que confirmar que el rendimiento o estabilidad del sistema son las adecuadas.</p>
<p>Para este ejemplo vamos a hacer un servicio similar a <cite>asn.rspamd.com</cite>. Si no lo conocéis, podéis probar a consultar lo que devuelve para una IP:</p>
<div class="highlight"><pre><span></span><span class="c1"># dig -t txt 8.8.8.8.asn.rspamd.com +short</span>
<span class="s2">"15169|8.8.8.0/24|US|arin|"</span>
</pre></div>
<p>Recordad que en este tipo de consulta RBL la notación de la IP es inversa.</p>
<p>Vamos a simular este fantástico servicio, aunque solo devolviendo en un TXT el número de sistema autónomo (15169 en este caso). Hacemos esto porque no es más que un ejemplo, pero podéis ser más creativos y hacer que consultas de tipo A devuelvan una IP si el origen es un sistema autónomo determinado, y con eso tendréis el interfaz para un RBL "normal" que se puede enganchar a servidores de correo, mod-security, etc...</p>
<p>Muy sencillo. Vamos a crear el mencionado <cite>recursor-rbl.conf</cite>, cambiando local-port por 55 (aseguraos que lo tenéis libre), y el script LUA (lua-dns-script=/ruta_script.lua). Aquí solo voy a pegar partes del script completo que podéis encontrar en <a class="reference external" href="https://github.com/foron/geoip-powerdns-info">Github</a>, pero va en esta línea. Usaremos <cite>preresolve</cite> porque vamos a devolver una respuesta directamente, sin consultar a la red.</p>
<div class="highlight"><pre><span></span><span class="kr">function</span> <span class="nf">preresolve</span><span class="p">(</span><span class="n">dq</span><span class="p">)</span>
<span class="kr">if</span> <span class="n">dq</span><span class="p">.</span><span class="n">qtype</span> <span class="o">==</span> <span class="n">pdns</span><span class="p">.</span><span class="n">TXT</span> <span class="kr">then</span>
<span class="kd">local</span> <span class="n">direct_ip</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="kd">local</span> <span class="n">cntlabels</span> <span class="o">=</span> <span class="n">dq</span><span class="p">.</span><span class="n">qname</span><span class="p">:</span><span class="n">countLabels</span><span class="p">()</span>
<span class="kr">if</span> <span class="p">(</span><span class="n">dq</span><span class="p">.</span><span class="n">qname</span><span class="p">:</span><span class="n">isPartOf</span><span class="p">(</span><span class="n">newDN</span><span class="p">(</span><span class="s2">"asn.example.com"</span><span class="p">))</span> <span class="ow">and</span> <span class="n">cntlabels</span> <span class="o">==</span> <span class="mi">7</span><span class="p">)</span> <span class="kr">then</span>
<span class="c1">-- ipv4reverse saca la IP de la consulta y le da la vuelta para usarla en GeoIP.</span>
<span class="c1">-- Hay un ejemplo de implementación en la referencia de Github.</span>
<span class="n">direct_ip</span> <span class="o">=</span> <span class="n">ipv4reverse</span><span class="p">(</span><span class="n">dq</span><span class="p">.</span><span class="n">qname</span><span class="p">:</span><span class="n">toString</span><span class="p">())</span>
<span class="kr">if</span> <span class="ow">not</span> <span class="n">direct_ip</span> <span class="kr">then</span>
<span class="n">dq</span><span class="p">.</span><span class="n">rcode</span> <span class="o">=</span> <span class="n">pdns</span><span class="p">.</span><span class="n">NXDOMAIN</span>
<span class="kr">return</span> <span class="kc">true</span>
<span class="kr">end</span>
<span class="kd">local</span> <span class="n">mmdb</span> <span class="o">=</span> <span class="nb">require</span> <span class="s2">"mmdb"</span>
<span class="kd">local</span> <span class="n">geo_asn_db</span> <span class="o">=</span> <span class="n">mmdb</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="s2">"/usr/local/GeoLite2-ASN.mmdb"</span><span class="p">)</span> <span class="ow">or</span> <span class="kc">nil</span>
<span class="kr">if</span> <span class="ow">not</span> <span class="n">geo_asn_db</span> <span class="kr">then</span>
<span class="n">pdnslog</span><span class="p">(</span><span class="s2">"Error al abrir bases de datos"</span><span class="p">,</span> <span class="n">pdns</span><span class="p">.</span><span class="n">loglevels</span><span class="p">.</span><span class="n">Warning</span><span class="p">)</span>
<span class="n">dq</span><span class="p">.</span><span class="n">rcode</span> <span class="o">=</span> <span class="n">pdns</span><span class="p">.</span><span class="n">SERVFAIL</span>
<span class="kr">return</span> <span class="kc">true</span>
<span class="kr">end</span>
<span class="kd">local</span> <span class="n">asn_data</span> <span class="o">=</span> <span class="n">geo_asn_db</span><span class="p">:</span><span class="n">search_ipv4</span><span class="p">(</span><span class="n">direct_ip</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">asn_number</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="kr">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">asn_data</span><span class="p">)</span> <span class="o">==</span> <span class="s2">"table"</span> <span class="kr">then</span>
<span class="n">asn_number</span> <span class="o">=</span> <span class="n">asn_data</span><span class="p">[</span><span class="s2">"autonomous_system_number"</span><span class="p">]</span> <span class="ow">or</span> <span class="kc">nil</span>
<span class="kr">if</span> <span class="n">asn_numer</span> <span class="kr">then</span>
<span class="n">dq</span><span class="p">.</span><span class="n">rcode</span> <span class="o">=</span> <span class="n">pdns</span><span class="p">.</span><span class="n">NOERROR</span>
<span class="n">dq</span><span class="p">:</span><span class="n">addAnswer</span><span class="p">(</span><span class="n">pdns</span><span class="p">.</span><span class="n">TXT</span><span class="p">,</span> <span class="s1">'"'</span> <span class="o">..</span> <span class="nb">tostring</span><span class="p">(</span><span class="n">asn_number</span><span class="p">)</span> <span class="o">..</span> <span class="s1">'"'</span><span class="p">,</span> <span class="mi">60</span><span class="p">)</span>
<span class="kr">return</span> <span class="kc">true</span>
<span class="kr">end</span>
<span class="kr">end</span>
<span class="kr">end</span>
<span class="kr">end</span>
<span class="n">dq</span><span class="p">.</span><span class="n">rcode</span> <span class="o">=</span> <span class="n">pdns</span><span class="p">.</span><span class="n">NXDOMAIN</span>
<span class="kr">return</span> <span class="kc">true</span>
<span class="kr">end</span>
</pre></div>
<p>Lo importante es que devolvemos un registro TXT (<cite>dq.rcode</cite> y <cite>dq:addAnswer</cite>) con el id del sistema autónomo y un TTL de 60 segundos.</p>
<p>Con esto ya solo queda configurar DNSDist para hacer que acceda a esta instancia de pdns-recursor cuando se consulte por el dominio "asn.example.com". Como hemos visto ya, configuramos un pool nuevo con la instancia de pdns-recursor que hemos creado antes:</p>
<div class="highlight"><pre><span></span><span class="n">newServer</span><span class="p">({</span><span class="n">address</span><span class="o">=</span><span class="s2">"127.0.0.1:55"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">"rbl1"</span><span class="p">,</span> <span class="n">pool</span><span class="o">=</span><span class="s2">"rbls"</span><span class="p">})</span>
</pre></div>
<p>Y después una regla de enrutamiento. Cuidado, hay que añadirla antes que la <cite>AllRule()</cite> del ejemplo de DNSDist anterior o no se ejecutará:</p>
<div class="highlight"><pre><span></span><span class="n">addAction</span><span class="p">(</span><span class="n">makeRule</span><span class="p">({</span><span class="s2">"asn.example.com."</span><span class="p">}),</span> <span class="n">PoolAction</span><span class="p">(</span><span class="s2">"rbls"</span><span class="p">))</span>
</pre></div>
<p>Una vez recargado DNSDist, si todo va bien, debéis poder ejecutar algo como esto:</p>
<div class="highlight"><pre><span></span><span class="o">#</span> <span class="n">dig</span> <span class="o">-</span><span class="n">t</span> <span class="n">txt</span> <span class="mf">8.8.8.8</span><span class="p">.</span><span class="n">asn</span><span class="p">.</span><span class="n">example</span><span class="p">.</span><span class="n">com</span> <span class="o">+</span><span class="n">short</span>
<span class="s2">"15169"</span>
</pre></div>
<p>Y con este esqueleto ya tenéis un ejemplo para ser más creativos con vuestros DNS. Mucha gente piensa que este tipo de cosas son explotar el interfaz DNS, pero no me voy a meter en ese debate. En cualquier caso, os recuerdo, una vez más, que generar tráfico de red dentro del código LUA, o algunas otras operaciones, pueden bloquear los hilos de ejecución de PowerDNS, así que, cuidado.</p>
<p>Y con esto termino los ejemplos. ¿Son útiles? Seguramente no para la mayoría de gente, pero siempre está bien conocer alternativas.</p>
Introducción a seguridad Zero Trust2018-03-28T21:00:00+02:002018-03-28T21:00:00+02:00tag:www.forondarena.net,2018-03-28:/introducción-a-seguridad-zero-trust.html<p>No es difícil suponer que cuando tienes la escala de Google te encuentras con problemas que el resto no tenemos, o que sí tendremos, pero en unos años. Es algo que ya ha pasado con un buen número de tecnologías, desde MapReduce hasta Kubernetes y que, quién sabe, también pasará …</p><p>No es difícil suponer que cuando tienes la escala de Google te encuentras con problemas que el resto no tenemos, o que sí tendremos, pero en unos años. Es algo que ya ha pasado con un buen número de tecnologías, desde MapReduce hasta Kubernetes y que, quién sabe, también pasará con el tema de este post. Esta vez nos centramos en el modelo de seguridad tradicional que conoce todo el mundo. Acceso limitado a la red perimetral a través de firewalls, redes DMZ, VPNs y similares son algo que todos hemos usado.</p>
<p>Una de las críticas, diría que con razón, que se hacen a este modelo es que una vez se accede a un nivel, todas las máquinas de esa capa quedan a la vista. De esta manera, es probable que un administrador de base de datos tenga acceso a servidores de almacenamiento o red, o a todas las máquinas de un mismo dominio de Windows. Algo a tener en cuenta dada la necesidad que parece que tenemos de pulsar sobre mensajes que nos mandan príncipes nigerianos.</p>
<p>Por supuesto, esta no deja de ser una simplificación. En la práctica se pueden securizar más estos niveles, se pueden usar tecnologías tipo IPSEC y similares, o se puede tener un control más específico de quién accede a dónde. Hoy en día, a menudo, aunque se conzca el problema de base, cuando la cantidad de gente que trabaja en estos entornos es limitada y controlada, puede parecer razonable asumir el riesgo en relación al coste de cualquier otra medida.</p>
<p>Claro, cuando la escala es lo suficientemente grande, el control se hace más complicado. Hay muchos servicios, de todo tipo; algunos se gestionan vía SSH, otros vía Web, o con APIs específicos. En cuanto a los administradores, además de los propios responsables de las plataformas, puede haber personal externo, estudiantes en prácticas, personal de atención al cliente, y tantos otros. Para terminar, el acceso también es más diverso. Lo que antes era un PC en una oficina, ahora puede ser un PC, o un portátil en casa, o una tablet desde la Wifi del tren.</p>
<p>Con estos antecedentes, y dados los <a class="reference external" href="https://en.wikipedia.org/wiki/Operation_Aurora">ataques</a> y problemas propios del entorno, Google comenzó un proyecto interno para cambiar su gestión de la seguridad. Evidentemente, los recursos y el nivel técnico de estas "grandes" les permiten llegar a desarrollos que para el resto son complicados de alcanzar, pero los conceptos más generales siempre pueden adaptarse y, llegado el momento, implementarse en entornos más pequeños.</p>
<p>En este post voy a hacer un resumen básico de lo que se busca con BeyondCorp, que es como ha llamado Google a su proyecto interno, pero también me gustaría ver qué software podríamos usar para hacer una implementación, aunque sea básica, de las ideas principales que se buscan. Si queréis dejar de leer ahora y pasar a documentación más completa, estos enlaces pueden serviros:</p>
<ul class="simple">
<li><a class="reference external" href="https://beyondcorp.com/">BeyondCorp</a>: Web con información básica y enlaces a algunos artículos técnicos. Está desarrollada por <a class="reference external" href="https://www.scaleft.com/">ScaleFT</a>, una de las empresas que han creado un producto en la línea de los documentos publicados por Google, y que están documentados en la propia web. Son fáciles de leer, os los recomiendo.</li>
<li><a class="reference external" href="https://cloud.google.com/beyondcorp/">Google BeyondCorp</a>: Web de referencia en Google Cloud.</li>
<li><a class="reference external" href="http://shop.oreilly.com/product/0636920052265.do">Zero Trust Networks</a>: Uno de los pocos libros que he visto sobre la materia. Son poco más de 200 páginas, y siempre podréis sacar conclusiones y ver qué os sirve y qué no.</li>
</ul>
<p><strong>¿Qué son las redes Zero Trust?</strong></p>
<p>Una red Zero Trust es una red en la que no se confía. Por lo tanto, todos los dispositivos que están el ella se gestionan como si estuviesen directamente conectados a Internet (que no deja de ser una red de este tipo), independientemente de si dan servicio externo o interno. En definitiva, se trata de complementar todo aquello para lo que el modelo tradicional es efectivo, añadiendo medidas contra lo que los estándares actuales, la segmentación de red, DMZs y similares son más limitados:</p>
<ul class="simple">
<li>Equipos comprometidos por Phising, Keyloggers, ...</li>
<li>Shells inversos en servidores con aplicaciones no actualizadas</li>
<li>Movimiento lateral entre equipos de un mismo segmento de red</li>
<li>Contraseñas vulnerables</li>
<li>Acceso a servidores internos desde servidores públicos vulnerables</li>
<li>...</li>
</ul>
<p>Las redes Zero Trust se fundamentan en lo siguiente:</p>
<ul class="simple">
<li>Asumir que el tráfico dentro de un centro de datos o de una red corporativa es seguro es erróneo. La segmentación en zonas y la seguridad perimetral que se ha venido siguiendo estos años no impiden los movimientos laterales dentro de un mismo nivel en el momento en el que un servidor o dispositivo se vea comprometido.</li>
<li>Todas las redes deben considerarse hostiles.</li>
<li>Las amenazas pueden ser tanto internas como externas.</li>
<li>Que un dispositivo pertenezca a un segmento de red no es suficiente para garantizar la confianza.</li>
<li>Todos los dispositivos, usuarios y flujo de red deben estar autorizados y autenticados.</li>
<li>Las políticas de acceso deben ser dinámicas.</li>
</ul>
<p><strong>Elementos principales</strong></p>
<p>Independientemente de cómo se implemente, una parte importante del trabajo va a ser la capa de control. Hablamos del entorno que va a identificar a usuarios y dispositivos, y que va a autorizar el acceso a los elementos de la red. Idealmente, se va a formar una entidad con el dispositivo desde el que se accede, el usuario que lo está intentando, y la aplicación. A partir de aquí, el sistema tomará una decisión basada en las variables que se consideren adecuadas, y que pueden ir en la línea de las siguientes:</p>
<ul class="simple">
<li>¿Se está accediendo desde un equipo inventariado?</li>
<li>¿El equipo tiene todas las actualizaciones de seguridad instaladas?</li>
<li>¿Desde dónde se está intentando acceder?</li>
<li>¿A qué hora?</li>
<li>¿Qué usuario está intentando acceder?</li>
<li>¿A qué aplicación o servidor se quiere acceder?</li>
<li>¿Qué método está usando para acceder? ¿Qué algoritmo de seguridad?</li>
</ul>
<p>En realidad, esta lista se podría completar con todo tipo de información adicional. El objetivo es que este sistema de control use estos datos para tomar una decisión (permitir o no), y que abra el camino para que se pueda establecer la conexión, de forma dinámica, entre ese cliente (y solo ese cliente) y el destino (y solo ese destino).</p>
<p>¿Cómo autorizamos un intento de acceso?</p>
<p>Por lo general, al hablar de autenticación hablamos de algún tipo de cifrado. En definitiva, se trata de intentar garantizar que un dispositivo origen puede acceder a un recurso, y para esto, estándares tan desarrollados como los certificados basados en X.509 son una elección lógica. Es relativamente común que se usen CAs privadas para crear los certificados que se usan en una organización.</p>
<p>Como siempre, alrededor de este concepto se pueden desarrollar todo tipo de sistemas avanzados que controlan la validez de los certificados, el inventario, y todo lo que se os ocurra. Hay gente que crea certificados válidos por poco tiempo, otros usan <a class="reference external" href="https://es.wikipedia.org/wiki/Online_Certificate_Status_Protocol">ocsp</a>, y también los hay que incluyen información que puede ser útil en la toma de decisiones en alguno de los atributos que define el protocolo.</p>
<p>¿Cómo autenticamos un intento de acceso?</p>
<p>Ojo, que autorizar y autenticar no son lo mismo. En su forma más simple, podemos estar hablando de un formulario web que se enganche a una base de datos de usuarios, sea del tipo que sea. Por lo tanto, un usuario se conectará desde un equipo autorizado, con las credenciales que tenga asignadas en ese momento.</p>
<p>Una vez más, a partir de esta idea básica se pueden hacer todo tipo de desarrollos y comprobaciones. Con los datos que tenemos ya es esta fase, podremos mostrar un listado de servidores a los que permitir la conexión, topes de tiempo para acceder, obtener datos de otros sistemas internos, como puede ser de monitorización, y tantos otros.</p>
<p>¿Cómo hacemos efectivo el acceso?</p>
<p>En estos últimos años la automatización ha sido una de las tecnologías que más han avanzado, y diría que es una de las claves que permiten implementar un sistema Zero Trust con facilidad. En su forma más sencilla, se puede usar ansible, chef, puppet, cfengine y similares para activar los permisos necesarios. Sistemas más avanzados permitirían, además, la configuración de accesos a nivel de red, vlanes o enrutamiento, en función de la infraestructura con la que se trabaje.</p>
<p><strong>Implementación de ejemplo</strong></p>
<p>Cuando se lee documentación de este tipo de tecnologías se tiende a concretar poco el software que podría usarse para implementarlas. Hagamos una prueba de concepto, aunque sin escribir código, y siempre limitándonos a lo más básico.</p>
<p>Uno de los ejes fundamentales sobre el que gira la tecnología Zero Trust es el cifrado. Para conseguirlo, nos interesaría tener una buena gestión de certificados. Hacer un inventario no es trivial, y mucho menos si queremos asociar de manera efectiva los portátiles (por ejemplo) en los que se instalan, con los usuarios a los que pertenecen. Si somos creativos, podemos hacer cosas muy sencillas pero que pueden ser interesantes, como por ejemplo definiendo atributos extra (los certificados X.509 permiten añadir cosas como teléfonos, estados, y muchos otros), o usando números de serie para los certificados. Imaginad, por ejemplo, que llevamos el control de un certificado que pertenece a un usuario (en base a los datos del <code>Subject</code>), y que incrementamos el número de serie cada vez que haya una actualización de seguridad. El usuario se instalará el nuevo certificado cuando actualice su equipo, y el sistema solo validará la sesión SSL si es así. Es algo simple, pero puede ser un buen punto de partida.</p>
<div class="highlight"><pre><span></span><span class="c1"># openssl req -new -key client_new.key -out client_new.csr</span>
-----
Country Name <span class="o">(</span><span class="m">2</span> letter code<span class="o">)</span> <span class="o">[</span>AU<span class="o">]</span>:??
State or Province Name <span class="o">(</span>full name<span class="o">)</span> <span class="o">[</span>Some-State<span class="o">]</span>:????
Locality Name <span class="o">(</span>eg, city<span class="o">)</span> <span class="o">[]</span>:????
Organization Name <span class="o">(</span>eg, company<span class="o">)</span> <span class="o">[</span>Internet Widgits Pty Ltd<span class="o">]</span>:????
Organizational Unit Name <span class="o">(</span>eg, section<span class="o">)</span> <span class="o">[]</span>:????
Common Name <span class="o">(</span>e.g. server FQDN or YOUR name<span class="o">)</span> <span class="o">[]</span>:usuario
Email Address <span class="o">[]</span>:usuario@example.com
--- No es difícil incluir más atributos aquí
Please enter the following <span class="s1">'extra'</span> attributes
to be sent with your certificate request
A challenge password <span class="o">[]</span>:
An optional company name <span class="o">[]</span>:
<span class="c1">#root@ansible:~# openssl x509 -req -days 365 -in client_new.csr -CA ca.cert -CAkey ca.key -set_serial 1 -out client_new.crt</span>
</pre></div>
<p>Enseguida veremos que los servidores web o los proxies modernos pueden leer estos valores.</p>
<p>Incluso en los casos en los que la aplicación que usemos no funcione con certificados, los navegadores, en su inmensa mayoría, sí los usan. Gracias a esto podríamos generar las reglas de acceso necesarias para un cliente desde el navegador, y permitir el acceso a esa otra aplicación con la confianza suficiente de que se origina desde un cliente válido. Las implementaciones más avanzadas usan proxies, por ejemplo de SSH, o tienen utilidades que cambian, en el caso de SSH, la configuración de los clientes y del servidor para que usen los nuevos certificados. El termino medio que planteo basa la autenticación y la autorización en un interfaz web muy compatible con los certificados, para que luego el cliente pueda conectarse al servidor de la forma más natural, que en el caso de SSH no dejan de ser las claves públicas y privadas de toda la vida. No es algo perfecto, ni mucho menos, pero es genérico y no obliga a reconfigurar los servidores constantemente.</p>
<p>Pasemos a lo que puede ser el interfaz web con la lógica necesaria para el sistema.</p>
<p>Queremos controlar el estado de los certificados en detalle, y para eso no hay nada como Nginx y su módulo para Lua Este no es un post sobre Nginx, así que solo daré algunas pistas sobre cómo se podría implementar todo esto sobre lo que estamos hablando con Nginx, pero hasta ahí.</p>
<p>Como sabéis los que conocéis Nginx, los certificados de servidor "de siempre" se leen con directivas como estas:</p>
<div class="highlight"><pre><span></span>ssl_certificate /ruta/certificado_servicio<span class="p">;</span>
ssl_certificate_key /ruta/clave_servicio<span class="p">;</span>
</pre></div>
<p>Y se puede obligar al uso de un certificado cliente, instalado en el navegador, que se haya firmado con una CA</p>
<div class="highlight"><pre><span></span>ssl_client_certificate /ruta/certificado_ca<span class="p">;</span>
ssl_verify_client on<span class="p">;</span>
</pre></div>
<p>Con esto ya sabremos que solo se accederá al sitio desde equipos en los que se haya instalado un certificado que hayamos firmado con nuestra CA. Lo que no hemos hecho es ampliar la lógica y usar el número de serie o el Subject del certificado. No hay problema, Nginx crea variables con todo lo que necesitamos, y que podemos usar en la <code>fase de acceso</code> de los distintos <code>location</code>. Si usamos el módulo Lua, no habrá ningún problema para acceder a los datos que nos manda el navegador, y para aplicar la lógica que queramos con ellos. Podemos leer datos de inventario desde una base de datos, desde redis, en un json desde un servidor independiente, .... Hay librerías para todos los gustos.</p>
<div class="highlight"><pre><span></span>location ??? <span class="o">{</span>
access_by_lua_block <span class="o">{</span>
-- En la variable <span class="nv">$ssl_client_s_dn</span> tenemos un cadena de texto con el email, el CN, OU, O, L, ST, C y otros atributos menos habituales que hayamos configurado
<span class="nb">local</span> <span class="nv">dn</span> <span class="o">=</span> ngx.var.ssl_client_s_dn
-- En la variable <span class="nv">$ssl_client_serial</span> tenemos el numero de serie del certificado del cliente.
<span class="nb">local</span> <span class="nv">serial</span> <span class="o">=</span> ngx.var.ssl_client_serial
-- A partir de aqui, accedemos a donde sea que tengamos la gestion del inventario, y permitimos el acceso, o no
-- ngx.exit<span class="o">(</span><span class="m">403</span><span class="o">)</span>
-- ngx.exit<span class="o">(</span>ngx.OK<span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
</pre></div>
<p>La autenticación es básicamente cosa de programación, del estilo de lo que hemos hecho siempre. Una ventana de acceso, login, password con credenciales del usuario, y todo lo demás. Esto se complementaría con el bloque de control de certificado que hemos descrito antes. Además, siempre se pueden incluir ventanas adicionales en las que un usuario autenticado vea los host a los que puede acceder, y quizá puedan definir una franja horaria en la que van a conectarse a ese host o grupo de hosts.</p>
<p>La idea es garantizar que el acceso se ha hecho desde un equipo autorizado, y que se ha hecho por un usuario autenticado. Al final de todo, podremos haber generado una sesión típica con el navegador cliente. Esta sesión puede servir si usamos este servidor como proxy para accesos web. Imaginad, por ejemplo, que queremos que los accesos a wp-login.php de un Wordpress solo puedan realizarse a través de este sistema. Solo necesitamos las reglas básicas para que Nginx haga de proxy a ese destino, controlando la validez de la sesión. La ventaja es que el Wordpress no necesita ningún tipo de modificación, ya que es el proxy de autenticación el encargado de controlar los accesos en este nivel.</p>
<p>Para mantener las cosas simples, imaginad que solo tenemos un número determinado de Wordpress que queremos controlar. De no ser así, esta parte también podría ser dinámica y programada sin mayor dificultad; en Nginx se puede usar Lua para elegir upstream.</p>
<div class="highlight"><pre><span></span>upstream wordpress <span class="o">{</span>
ip_hash<span class="p">;</span>
server ip_wordpress<span class="p">;</span>
</pre></div>
<p>Tendríamos una serie de <code>location</code> para gestionar el login, con sus métodos GET y POST, luego tendríamos las ventanas adicionales. Al final tendríamos otro <code>location</code> que haría el <code>proxy_pass</code>. Hay muchas formas de cifrar sesiones y cookies en Nginx. No trato de enseñarlas aquí o de hacer un bloque completo. Es solo una idea básica.</p>
<div class="highlight"><pre><span></span><span class="nv">location</span> <span class="o">=</span> /wp-login.php <span class="o">{</span>
<span class="nb">set</span> <span class="nv">$dominio</span> <span class="s1">'example.com'</span><span class="p">;</span>
access_by_lua_block <span class="o">{</span>
-- Que no se olvide la parte de autorizacion en base al certificado
--
-- Lo normal es que usasemos un POST para recibir el host al que redirigir, y que usasemos balancer_by_lua para elegir dinamicamente el upstream
--
<span class="k">if</span> ngx.var.request_method <span class="o">==</span> <span class="s2">"GET"</span> <span class="k">then</span>
<span class="k">return</span>
elseif ngx.var.request_method ~<span class="o">=</span> <span class="s2">"POST"</span> <span class="k">then</span>
ngx.exit<span class="o">(</span><span class="m">403</span><span class="o">)</span>
end
<span class="nb">local</span> <span class="nv">ck</span> <span class="o">=</span> require <span class="s2">"resty.cookie"</span>
<span class="nb">local</span> cookie, <span class="nv">err</span> <span class="o">=</span> ck:new<span class="o">()</span>
<span class="k">if</span> not cookie <span class="k">then</span>
ngx.exit<span class="o">(</span><span class="m">403</span><span class="o">)</span>
end
-- Un usuarios validado con todo OK tendria una cookie AUTHSESSION <span class="o">(</span>por ejemplo<span class="o">)</span> cifrada que usariamos para verificar que el acceso esta permitido
<span class="nb">local</span> <span class="nv">authsession</span> <span class="o">=</span> cookie:get<span class="o">(</span><span class="s2">"AUTHSESSION"</span><span class="o">)</span>
<span class="k">if</span> not authsession <span class="k">then</span>
-- No permitimos si no hay cookie
ngx.exit<span class="o">(</span><span class="m">403</span><span class="o">)</span>
end
--
-- Es un ejemplo basico de uso de cookies, pero hay otras alternativas
-- Tenemos cookie AUTHSESSION, la descifrariamos y, si fuese ok, seguimos adelante
-- Tambien hay muchas opciones para ver TTLs y confirmar que no ha expirado
--
-- Si llegase el host en un POST, podemos leerlo sin problemas
ngx.req.read_body<span class="o">()</span>
<span class="nb">local</span> args, <span class="nv">err</span> <span class="o">=</span> ngx.req.get_post_args<span class="o">()</span>
-- En args estan los parametros que han llegado en el POST
-- Si todas las comprobaciones fuesen bien, hariamos el proxy_pass
<span class="k">return</span>
<span class="o">}</span>
proxy_pass https://wordpress<span class="p">;</span>
proxy_set_header Host <span class="nv">$dominio</span><span class="p">;</span>
proxy_set_header X-Real-IP <span class="nv">$remote_addr</span><span class="p">;</span>
<span class="o">}</span>
location / <span class="o">{</span>
<span class="nb">set</span> <span class="nv">$dominio</span> <span class="s1">'example.com'</span><span class="p">;</span>
proxy_pass https://wordpress<span class="p">;</span>
proxy_set_header Host <span class="nv">$dominio</span><span class="p">;</span>
proxy_set_header X-Real-IP <span class="nv">$remote_addr</span><span class="p">;</span>
<span class="o">}</span>
</pre></div>
<p>Falta mucho código, pero no quiero convertir esto en un post sobre Nginx/Lua. La idea que quiero transmitir es que podemos meter un proxy entre nuestro destino (nos centramos en HTTP en este caso) y el cliente. La única modificación que hay que hacer al Apache o lo que sea que gestione el Wordpress es limitar los accesos a <code>wp-login.php</code>, para que solo se pueda llegar desde el proxy de acceso. De esta manera, quien quiera administrarlo tendrá que pasar por todo el proceso de validación, pero sería transparente para el resto de usuarios.</p>
<p>Para las aplicaciones basadas en HTTP es fácil gestionar un proxy. Para el resto, este tipo de proxies pueden ser más complicados, aunque en este momento se está terminando el <a class="reference external" href="https://github.com/openresty/stream-lua-nginx-module">soporte Lua para el módulo stream</a> que permitirá hacer proxies muy interesantes.</p>
<p>En cualquier caso, como he comentado al principio, es fácil complementar el control de accesos a, por ejemplo, SSH, usando la misma idea que hemos usado hasta ahora. Un usuario puede usar su navegador desde su portatil, y Nginx puede crear una petición a un servicio externo que sincronice la clave pública y cree una regla iptables que habilite el acceso. Otro proceso podría, vía cron por ejemplo, revocar ese acceso y esa regla llegado el momento.</p>
<p>Si optasemos por <code>ansible</code> sería muy sencillo hacer un pequeño servidor en <a class="reference external" href="http://flask.pocoo.org/">flask</a> o <a class="reference external" href="http://www.tornadoweb.org/en/stable/">tornado</a> que usasen el <a class="reference external" href="http://docs.ansible.com/ansible/latest/dev_guide/developing_api.html">api python</a> del propio ansible para lanzar las tareas que habilitan el acceso. En Nginx tendríamos un nuevo upstream, tipo:</p>
<div class="highlight"><pre><span></span>upstream tornado <span class="o">{</span>
ip_hash<span class="p">;</span>
server <span class="m">127</span>.0.0.1:puerto<span class="p">;</span>
</pre></div>
<p>y un location parecido al que hemos escrito para <code>wp-login.php</code> haría una llamada a ese <code>tornado</code> que lanzaría las tareas de ansible contra los módulos <a class="reference external" href="http://docs.ansible.com/ansible/latest/modules/authorized_key_module.html">authorized_key</a> e <a class="reference external" href="http://docs.ansible.com/ansible/latest/modules/iptables_module.html">iptables</a>. Con Nginx/Lua, crear una petición para un upstream es algo trivial. Aquí van unas pistas para los interesados:</p>
<div class="highlight"><pre><span></span><span class="nv">location</span> <span class="o">=</span> /acceso_ssh.html <span class="o">{</span>
<span class="nb">set</span> <span class="nv">$dominio</span> <span class="s1">'example.com'</span><span class="p">;</span>
access_by_lua_block <span class="o">{</span>
-- Que no se olvide la parte de autorizacion en base al certificado
--
-- En el POST recibiríamos el servidor destino, la IP origen, y
-- cualquier otro dato necesario para ansible
--
-- Por supuesto, toda la parte de la sesion de navegador sigue
-- siendo necesaria
--
-- Imaginemos que tenemos el host remoto en la variable hostremoto
-- del formulario que nos llega en el POST
-- El modulo Lua recibe la IP origen en la variable ngx.var.remote_addr
-- Podriamos tener en inventario las claves publicas de los usuarios
--
ngx.req.read_body<span class="o">()</span>
<span class="k">if</span> not args <span class="k">then</span>
ngx.exit<span class="o">(</span><span class="m">403</span><span class="o">)</span>
end
<span class="nb">local</span> args, <span class="nv">err</span> <span class="o">=</span> ngx.req.get_post_args<span class="o">()</span>
<span class="nb">local</span> <span class="nv">iporigen</span> <span class="o">=</span> ngx.var.remote_addr
<span class="nb">local</span> <span class="nv">host_remoto</span> <span class="o">=</span> args<span class="o">[</span><span class="s2">"hostremoto"</span><span class="o">]</span>
<span class="nb">local</span> <span class="nv">clave_ssh</span> <span class="o">=</span> args<span class="o">[</span><span class="s2">"clave_publica"</span><span class="o">]</span>
-- Simulemos que el servidor tornado recibe un json en el cuerpo, por cambiar
<span class="nb">local</span> <span class="nv">habilitar_acceso</span> <span class="o">=</span> <span class="o">{}</span>
habilitar_acceso<span class="o">[</span><span class="s2">"ip"</span><span class="o">]</span> <span class="o">=</span> iporigen
habilitar_acceso<span class="o">[</span><span class="s2">"servidor"</span><span class="o">]</span> <span class="o">=</span> host_remoto
habilitar_acceso<span class="o">[</span><span class="s2">"clave"</span><span class="o">]</span> <span class="o">=</span> clave_ssh
ngx.req.set_body_data<span class="o">(</span>cjson.encode<span class="o">(</span>habilitar_acceso<span class="o">))</span>
<span class="nv">res</span> <span class="o">=</span> ngx.location.capture<span class="o">(</span><span class="s2">"/habilitar_ssh"</span>, <span class="o">{</span><span class="nv">method</span> <span class="o">=</span> ngx.HTTP_POST<span class="o">})</span>
-- Si tornado devuelve un codigo HTTP diferente a <span class="m">200</span> es un error
<span class="k">if</span> res.status ~<span class="o">=</span> <span class="m">200</span> <span class="k">then</span>
ngx.exit<span class="o">(</span><span class="m">403</span><span class="o">)</span>
end
<span class="k">return</span>
<span class="o">}</span>
<span class="c1"># Si todo ha ido bien el acceso se habra creado (via tornado) y podremos mostrar un html</span>
<span class="c1"># o lo que sea con un mensaje de OK. Podria ser un html estatico o generado</span>
<span class="c1"># via content_by_lua</span>
<span class="o">}</span>
<span class="nv">location</span> <span class="o">=</span> /habilitar_ssh <span class="o">{</span>
<span class="c1"># A este location llega el POST con el cuerpo creado a partir</span>
<span class="c1"># de la tabla habilitar_acceso, formateado como json</span>
internal<span class="p">;</span>
<span class="c1"># Imaginemos que tenemos este endpoint en tornado donde se llama a ansible</span>
<span class="c1"># Tendria que implementar POST, y leer y parsear el json</span>
<span class="c1"># Devolveria un codigo HTTP 200 si todo hubiese ido bien</span>
proxy_pass http://tornado/habilitar_ssh<span class="p">;</span>
<span class="o">}</span>
</pre></div>
<p><strong>Conclusiones</strong></p>
<p>La verdad es que he dudado de si interesaba escribir código en este post. Evidentemente, no estamos hablando de un proyecto de fin de semana, así que, lo que he escrito, en realidad, no sirve para nada. Sin embargo, tampoco siempre se lee documentación con detalles concretos, así que esto podría dar alguna idea a alguien.</p>
<p>En cualquier caso, viniendo en cierta medida de Google (en realidad los conceptos no son nuevos, en absoluto), no sería raro que las palabras "Zero Trust" se vayan escuchando cada vez más, y que se hagan más desarrollos.</p>
<p>En definitiva, aunque la parte del código sea poco útil, espero que las ideas que hay detrás de todo esto hayan quedado claras. Se lo preguntaré a las tres personas que van a leer el post.</p>
Introducción a BPF2017-09-04T21:00:00+02:002017-09-04T21:00:00+02:00tag:www.forondarena.net,2017-09-04:/introducción-a-bpf.html<p>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.</p>
<p>La inmensa …</p><p>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.</p>
<p>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.</p>
<p><strong>¿Qué es BPF?</strong></p>
<p>Hablemos del antiguo BPF, del original.</p>
<p>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 <code>tcpdump -i eth0 "port 80"</code>. Internamente, y pecando de básico, ese filtro <code>"port 80"</code> se escribia de tal manera que pudiese engancharse al socket y así procesarse más eficientemente.</p>
<p>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, <a class="reference external" href="https://es.slideshare.net/vh21/meet-cutebetweenebpfandtracing">intro 1</a> y <a class="reference external" href="https://speakerdeck.com/tuxology/the-bsd-packet-filter">intro 2</a>, 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 <code>tcp_v4_connect</code> o <code>ext4_file_open</code>, por poner dos ejemplos. Pero no solo esto; también se pueden instrumentalizar sockets, librerías o aquellas aplicaciones que lo permitan.</p>
<p>Traducido a algunos ejemplos, con BPF pod(r)emos:</p>
<ul class="simple">
<li>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í (<a class="reference external" href="https://www.iovisor.org/technology/xdp">intro 3</a> , <a class="reference external" href="https://jvns.ca/blog/2017/04/07/xdp-bpf-tutorial/">intro 4</a> e <a class="reference external" href="https://github.com/iovisor/bpf-docs/blob/master/Express_Data_Path.pdf">intro 5</a>). Los usos son variados; Facebook ya están documentando que empiezan a usarlo para el <a class="reference external" href="http://netdevconf.org/2.1/slides/apr6/zhou-netdev-xdp-2017.pdf">balanceo de carga</a>, otros como Cloudflare en entornos anti <a class="reference external" href="https://netdevconf.org/2.1/papers/Gilberto_Bertin_XDP_in_practice.pdf">DDoS</a>, tc ya tiene un <a class="reference external" href="http://man7.org/linux/man-pages/man8/tc-bpf.8.html">clasificador</a> basado en BPF, o el proyecto <a class="reference external" href="https://github.com/cilium/cilium">Cillium</a>, 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.</li>
<li>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 <a class="reference external" href="https://wiki.openssl.org/index.php/Manual:SSL_read%283%29">SSL_read</a> y <a class="reference external" href="https://wiki.openssl.org/index.php/Manual:SSL_write%283%29">SSL_write</a> de openssl.</li>
<li>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.</li>
</ul>
<p>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.</p>
<p><strong>Ejemplos</strong></p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md">mayoría de funcionalidades</a>. 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 <code>apt-get install</code>, y con otras podría obligaros a trabajar un poco más. Aquí tenéis algunas <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/INSTALL.md">instrucciones</a>.</p>
<p>Si alguno habéis seguido este último enlace, habréis visto que hemos pasado del acrónimo BPF a <a class="reference external" href="https://github.com/iovisor/bcc">BCC</a>. 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.</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/tree/master/tools">herramientas</a> y de <a class="reference external" href="https://github.com/iovisor/bcc/tree/master/examples">ejemplos</a>, y adaptarlos a nuestro entorno que, muchas veces, solo implicará escribir Python.</p>
<p>Para los que os veáis más capaces, un buen primer paso es leer este <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/docs/tutorial.md">tutorial</a> y la <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md">guía de referencia</a>. Merece la pena tener ambos enlaces siempre a mano.</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/bashreadline.py">bash</a>, y otro para lo que se ejecuta vía <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/execsnoop.py">exec</a>. Como <code>bashreadline.py</code> es más sencillo de leer, vamos a centrarnos en hacer un <code>history</code> remoto.</p>
<p>El esqueleto básico de todos los scripts es el siguiente:</p>
<ol class="arabic simple">
<li>Import de la clase BPF</li>
<li>Preparar el código C, ya sea vía fichero externo o como una variable de texto</li>
<li>Crear una instancia de BPF haciendo referencia al código C</li>
<li>Definir dónde (kprobe, uprobe, ...) queremos ejecutar las funciones C que hemos escrito</li>
<li>Usar los recursos que ofrece la instancia BPF para trabajar con lo que va devolviendo el Kernel.</li>
</ol>
<p>En el caso de <code>bashreadline.py</code>, y quitando lo menos relevante en este momento:</p>
<div class="highlight"><pre><span></span><span class="n">bpf_text</span> <span class="o">=</span> <span class="s2">"""</span>
<span class="s2"> #include <uapi/linux/ptrace.h></span>
<span class="s2"> struct str_t {</span>
<span class="s2"> u64 pid;</span>
<span class="s2"> char str[80];</span>
<span class="s2"> };</span>
<span class="s2"> BPF_PERF_OUTPUT(events);</span>
<span class="s2"> int printret(struct pt_regs *ctx) {</span>
<span class="s2"> struct str_t data = </span><span class="si">{}</span><span class="s2">;</span>
<span class="s2"> u32 pid;</span>
<span class="s2"> if (!PT_REGS_RC(ctx)) return 0;</span>
<span class="s2"> pid = bpf_get_current_pid_tgid();</span>
<span class="s2"> data.pid = pid;</span>
<span class="s2"> bpf_probe_read(&data.str, sizeof(data.str), (void *)PT_REGS_RC(ctx));</span>
<span class="s2"> events.perf_submit(ctx,&data,sizeof(data));</span>
<span class="s2"> return 0;</span>
<span class="s2"> };</span>
<span class="s2">"""</span>
<span class="kn">from</span> <span class="nn">bcc</span> <span class="kn">import</span> <span class="n">BPF</span>
<span class="n">bpf_text</span> <span class="o">=</span> <span class="s2">""" ... """</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">bpf_text</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_uretprobe</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"/bin/bash"</span><span class="p">,</span> <span class="n">sym</span><span class="o">=</span><span class="s2">"readline"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"printret"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">print_event</span><span class="p">(</span><span class="n">cpu</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">size</span><span class="p">):</span>
<span class="n">event</span> <span class="o">=</span> <span class="n">ct</span><span class="o">.</span><span class="n">cast</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">ct</span><span class="o">.</span><span class="n">POINTER</span><span class="p">(</span><span class="n">Data</span><span class="p">))</span><span class="o">.</span><span class="n">contents</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"</span><span class="si">%-9s</span><span class="s2"> </span><span class="si">%-6d</span><span class="s2"> </span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%H:%M:%S"</span><span class="p">),</span> <span class="n">event</span><span class="o">.</span><span class="n">pid</span><span class="p">,</span> <span class="n">event</span><span class="o">.</span><span class="n">str</span><span class="o">.</span><span class="n">decode</span><span class="p">()))</span>
<span class="n">b</span><span class="p">[</span><span class="s2">"events"</span><span class="p">]</span><span class="o">.</span><span class="n">open_perf_buffer</span><span class="p">(</span><span class="n">print_event</span><span class="p">)</span>
<span class="k">while</span> <span class="mi">1</span><span class="p">:</span>
<span class="n">b</span><span class="o">.</span><span class="n">kprobe_poll</span><span class="p">()</span>
</pre></div>
<p>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:</p>
<div class="highlight"><pre><span></span><span class="c1"># ./bashreadline</span>
TIME PID COMMAND
<span class="m">05</span>:28:25 <span class="m">21176</span> ls -l
<span class="m">05</span>:28:28 <span class="m">21176</span> date
<span class="m">05</span>:28:35 <span class="m">21176</span> <span class="nb">echo</span> hello world
<span class="m">05</span>:28:43 <span class="m">21176</span> foo this <span class="nb">command</span> failed
</pre></div>
<p>Nada os impide editar este código, así que ¿Por qué no cambiar ese print de <code>print_event</code> por algo que escriba en un syslog o un graphite remoto? Yo por ejemplo uso mucho ZeroMQ para estas cosas:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">zmq</span>
<span class="n">contexto</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span>
<span class="n">eventos</span> <span class="o">=</span> <span class="n">contexto</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">REQ</span><span class="p">)</span>
<span class="n">eventos</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">"tcp://172.16.1.2:7658"</span><span class="p">)</span>
<span class="o">...</span>
<span class="n">salida</span> <span class="o">=</span> <span class="nb">print</span><span class="p">(</span><span class="s2">"</span><span class="si">%-9s</span><span class="s2"> </span><span class="si">%-6d</span><span class="s2"> </span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%H:%M:%S"</span><span class="p">),</span> <span class="n">event</span><span class="o">.</span><span class="n">pid</span><span class="p">,</span> <span class="n">event</span><span class="o">.</span><span class="n">str</span><span class="o">.</span><span class="n">decode</span><span class="p">()))</span>
<span class="n">eventos</span><span class="o">.</span><span class="n">send_string</span><span class="p">(</span><span class="n">salida</span><span class="p">)</span>
<span class="n">eventos</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span>
</pre></div>
<p>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.</p>
<p>Independientemente de lo útil que os haya parecido esto, ya veis que es muy fácil adaptar los scripts y hacer cosas realmente interesantes.</p>
<p>Vamos a ver algunos ejemplos de scripts. Todo está en github, así que podéis ir allí directamente.</p>
<p>Pregunta típica: ¿Cuál es el rendimiento de nuestro sistema de ficheros ext4?</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/biotop.py">top</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/biotop_example.txt">ejemplo top</a>) y responder a la pregunta ¿Quién está accediendo a dispositivo en este momento? También los tenemos para mostrar distribuciones de <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/biolatency.py">latencias</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/biolatency_example.txt">ejemplo latencias</a>) en un periodo de tiempo, o para analizar los sistemas de ficheros más habituales. Para ext4, por ejemplo, podemos usar <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/ext4slower.py">ext4slower</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/ext4slower_example.txt">ejemplo ext4slower</a>) para ver qué reads, writes, opens y syncs han ido lentas, o <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/ext4dist.py">ext4dist</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/ext4dist_example.txt">ejemplo ext4dist</a>) para ver un histograma con la distribución de las latencias de estas operaciones. En <code>ext4slower.py</code>, por ejemplo, se instrumentaliza la entrada y la salida de estas funciones (mirad el argumento event=)</p>
<div class="highlight"><pre><span></span><span class="o">...</span>
<span class="c1"># initialize BPF</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">bpf_text</span><span class="p">)</span>
<span class="c1"># Common file functions. See earlier comment about generic_file_read_iter().</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"generic_file_read_iter"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_read_entry"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"ext4_file_write_iter"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_write_entry"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"ext4_file_open"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_open_entry"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"ext4_sync_file"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_fsync_entry"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kretprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"generic_file_read_iter"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_read_return"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kretprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"ext4_file_write_iter"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_write_return"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kretprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"ext4_file_open"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_open_return"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kretprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"ext4_sync_file"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"trace_fsync_return"</span><span class="p">)</span>
<span class="o">...</span>
</pre></div>
<p>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.</p>
<p>El uso de la CPU es otra área muy trabajada. Aunque siempre ha habido herramientas para su análisis (no siempre se ha hecho <a class="reference external" href="http://www.brendangregg.com/blog/2017-05-09/cpu-utilization-is-wrong.html">bien</a>), con BPF se han mejorado mucho. Un buen ejemplo son los <a class="reference external" href="http://www.brendangregg.com/flamegraphs.html">flame graphs</a>. 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 <code>tar</code> 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 <a class="reference external" href="https://es.slideshare.net/brendangregg/usenix-atc-2017-visualizing-performance-with-flame-graphs">fantástica presentación</a> 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.</p>
<p>Nos saltamos la CPU y pasamos, por ejemplo, al análisis del uso de memoria.</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/memleak.py">memleak</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/memleak_example.txt">ejemplo memleak</a>). Otra alternativa, como veremos más adelante, es centrar la investigación más en las propias aplicaciones.</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/cachetop.py">cachetop</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/cachestat_example.txt">ejemplo cachetop</a>). La parte escrita en C es sencilla, una única función:</p>
<div class="highlight"><pre><span></span><span class="n">bpf_text</span> <span class="o">=</span> <span class="s">"""</span>
<span class="cp">#include</span> <span class="cpf"><uapi/linux/ptrace.h></span><span class="cp"></span>
<span class="k">struct</span> <span class="nc">key_t</span> <span class="p">{</span>
<span class="n">u64</span> <span class="n">ip</span><span class="p">;</span>
<span class="n">u32</span> <span class="n">pid</span><span class="p">;</span>
<span class="n">u32</span> <span class="n">uid</span><span class="p">;</span>
<span class="kt">char</span> <span class="n">comm</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
<span class="p">};</span>
<span class="n">BPF_HASH</span><span class="p">(</span><span class="n">counts</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">key_t</span><span class="p">);</span>
<span class="kt">int</span> <span class="nf">do_count</span><span class="p">(</span><span class="k">struct</span> <span class="nc">pt_regs</span> <span class="o">*</span><span class="n">ctx</span><span class="p">)</span> <span class="p">{</span>
<span class="k">struct</span> <span class="nc">key_t</span> <span class="n">key</span> <span class="o">=</span> <span class="p">{};</span>
<span class="n">u64</span> <span class="n">zero</span> <span class="o">=</span> <span class="mi">0</span> <span class="p">,</span> <span class="o">*</span><span class="n">val</span><span class="p">;</span>
<span class="n">u64</span> <span class="n">pid</span> <span class="o">=</span> <span class="n">bpf_get_current_pid_tgid</span><span class="p">();</span>
<span class="n">u32</span> <span class="n">uid</span> <span class="o">=</span> <span class="n">bpf_get_current_uid_gid</span><span class="p">();</span>
<span class="n">key</span><span class="p">.</span><span class="n">ip</span> <span class="o">=</span> <span class="n">PT_REGS_IP</span><span class="p">(</span><span class="n">ctx</span><span class="p">);</span>
<span class="n">key</span><span class="p">.</span><span class="n">pid</span> <span class="o">=</span> <span class="n">pid</span> <span class="o">&</span> <span class="mh">0xFFFFFFFF</span><span class="p">;</span>
<span class="n">key</span><span class="p">.</span><span class="n">uid</span> <span class="o">=</span> <span class="n">uid</span> <span class="o">&</span> <span class="mh">0xFFFFFFFF</span><span class="p">;</span>
<span class="n">bpf_get_current_comm</span><span class="p">(</span><span class="o">&</span><span class="p">(</span><span class="n">key</span><span class="p">.</span><span class="n">comm</span><span class="p">),</span> <span class="mi">16</span><span class="p">);</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">counts</span><span class="p">.</span><span class="n">lookup_or_init</span><span class="p">(</span><span class="o">&</span><span class="n">key</span><span class="p">,</span> <span class="o">&</span><span class="n">zero</span><span class="p">);</span> <span class="c1">// update counter</span>
<span class="p">(</span><span class="o">*</span><span class="n">val</span><span class="p">)</span><span class="o">++</span><span class="p">;</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="s">"""</span>
</pre></div>
<p>A la que se hace referencia en la parte en Python:</p>
<div class="highlight"><pre><span></span><span class="o">...</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">bpf_text</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"add_to_page_cache_lru"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"do_count"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"mark_page_accessed"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"do_count"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"account_page_dirtied"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"do_count"</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="s2">"mark_buffer_dirty"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"do_count"</span><span class="p">)</span>
<span class="o">...</span>
</pre></div>
<p>Como veis, el mismo esquema que hasta ahora. El resultado nos muestra el porcentaje de acierto en Cache. Ejecutemos un <code>find /</code> mientras tenemos ejecutando el script:</p>
<div class="highlight"><pre><span></span><span class="c1"># ./cachetop.py</span>
<span class="m">13</span>:01:01 Buffers MB: <span class="m">76</span> / Cached MB: <span class="m">114</span> / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
...
<span class="m">984</span> vagrant find <span class="m">9529</span> <span class="m">2457</span> <span class="m">4</span> <span class="m">79</span>.5% <span class="m">20</span>.5%
</pre></div>
<p>Si ejecutamos una segunda vez ese mismo <code>find /</code>, veremos que el uso del Cache es mucho mś eficiente:</p>
<div class="highlight"><pre><span></span><span class="c1"># ./cachetop.py</span>
<span class="m">13</span>:01:01 Buffers MB: <span class="m">76</span> / Cached MB: <span class="m">115</span> / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
...
<span class="m">1071</span> vagrant find <span class="m">12959</span> <span class="m">0</span> <span class="m">0</span> <span class="m">100</span>.0% <span class="m">0</span>.0%
</pre></div>
<p>Con esto terminamos esta parte. Recordad, tenéis muchos más ejemplos en el repositorio de <a class="reference external" href="https://github.com/iovisor/bcc/tree/master/tools">github</a>.</p>
<p>Hasta ahora, con la excepción de <code>bashreadline.py</code>, 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.</p>
<p>Cuando subimos al userspace y a las librerías, en este caso concreto, entramos en el terreno de las <code>uprobes</code>. 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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/sslsniff.py">sslsniff</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt">ejemplo sslsniff</a>). La estructura es parecida a los kprobes, aunque en este caso instrumentalizamos las llamadas a librería:</p>
<div class="highlight"><pre><span></span><span class="o">...</span>
<span class="n">b</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">prog</span><span class="p">)</span>
<span class="c1"># It looks like SSL_read's arguments aren't available in a return probe so you</span>
<span class="c1"># need to stash the buffer address in a map on the function entry and read it</span>
<span class="c1"># on its exit (Mark Drayton)</span>
<span class="c1">#</span>
<span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">openssl</span><span class="p">:</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_uprobe</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"ssl"</span><span class="p">,</span> <span class="n">sym</span><span class="o">=</span><span class="s2">"SSL_write"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"probe_SSL_write"</span><span class="p">,</span>
<span class="n">pid</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">pid</span> <span class="ow">or</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_uprobe</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"ssl"</span><span class="p">,</span> <span class="n">sym</span><span class="o">=</span><span class="s2">"SSL_read"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"probe_SSL_read_enter"</span><span class="p">,</span>
<span class="n">pid</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">pid</span> <span class="ow">or</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
<span class="n">b</span><span class="o">.</span><span class="n">attach_uretprobe</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"ssl"</span><span class="p">,</span> <span class="n">sym</span><span class="o">=</span><span class="s2">"SSL_read"</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"probe_SSL_read_exit"</span><span class="p">,</span> <span class="n">pid</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">pid</span> <span class="ow">or</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
<span class="o">...</span>
</pre></div>
<p><code>probe_SSL_write</code> es la función en C del script para, en este caso, la entrada a <code>SSL_write</code> de libssl.</p>
<div class="highlight"><pre><span></span><span class="p">...</span>
<span class="kt">int</span> <span class="n">probe_SSL_write</span><span class="p">(</span><span class="k">struct</span> <span class="nc">pt_regs</span> <span class="o">*</span><span class="n">ctx</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">ssl</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">int</span> <span class="n">num</span><span class="p">)</span> <span class="p">{</span>
<span class="n">u32</span> <span class="n">pid</span> <span class="o">=</span> <span class="n">bpf_get_current_pid_tgid</span><span class="p">();</span>
<span class="n">FILTER</span>
<span class="k">struct</span> <span class="nc">probe_SSL_data_t</span> <span class="n">__data</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
<span class="n">__data</span><span class="p">.</span><span class="n">timestamp_ns</span> <span class="o">=</span> <span class="n">bpf_ktime_get_ns</span><span class="p">();</span>
<span class="n">__data</span><span class="p">.</span><span class="n">pid</span> <span class="o">=</span> <span class="n">pid</span><span class="p">;</span>
<span class="n">__data</span><span class="p">.</span><span class="n">len</span> <span class="o">=</span> <span class="n">num</span><span class="p">;</span>
<span class="n">bpf_get_current_comm</span><span class="p">(</span><span class="o">&</span><span class="n">__data</span><span class="p">.</span><span class="n">comm</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">__data</span><span class="p">.</span><span class="n">comm</span><span class="p">));</span>
<span class="k">if</span> <span class="p">(</span> <span class="n">buf</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">bpf_probe_read</span><span class="p">(</span><span class="o">&</span><span class="n">__data</span><span class="p">.</span><span class="n">v0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">__data</span><span class="p">.</span><span class="n">v0</span><span class="p">),</span> <span class="n">buf</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">perf_SSL_write</span><span class="p">.</span><span class="n">perf_submit</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">&</span><span class="n">__data</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">__data</span><span class="p">));</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">...</span>
</pre></div>
<p>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.</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/tplist.py">tplist</a>. Sacando un extracto de este <a class="reference external" href="http://www.brendangregg.com/blog/2016-10-04/linux-bcc-mysqld-qslower.html">buen post</a> de Brendan Gregg, <code>tplist</code> muestra lo siguiente:</p>
<div class="highlight"><pre><span></span><span class="c1"># tplist -l /usr/local/mysql/bin/mysqld</span>
/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
...
</pre></div>
<p>Viendo las opciones, podemos usar <code>query_start</code> y <code>query_end</code> para sacar las consultas lentas. El script se llama <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/dbslower.py">dbslower</a> (<a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/dbslower_example.txt">ejemplo dbslower</a>):</p>
<div class="highlight"><pre><span></span><span class="o">...</span>
<span class="c1"># Uprobes mode</span>
<span class="n">bpf</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">program</span><span class="p">)</span>
<span class="n">bpf</span><span class="o">.</span><span class="n">attach_uprobe</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="n">sym</span><span class="o">=</span><span class="n">mysql_func_name</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"query_start"</span><span class="p">)</span>
<span class="n">bpf</span><span class="o">.</span><span class="n">attach_uretprobe</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="n">sym</span><span class="o">=</span><span class="n">mysql_func_name</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">"query_end"</span><span class="p">)</span>
<span class="o">...</span>
</pre></div>
<p>Y al final tendremos algo parecido a esto:</p>
<div class="highlight"><pre><span></span><span class="c1"># dbslower mysql -m 0</span>
Tracing database queries <span class="k">for</span> pids <span class="m">25776</span> slower than <span class="m">0</span> ms...
TIME<span class="o">(</span>s<span class="o">)</span> PID MS QUERY
<span class="m">6</span>.003720 <span class="m">25776</span> <span class="m">2</span>.363 /* mysql-connector-java-5.1.40 <span class="o">(</span> Revision: 402933ef52cad9aa82624e80acbea46e3a701ce6 <span class="o">)</span> */SELECT @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_conn
<span class="m">6</span>.599219 <span class="m">25776</span> <span class="m">0</span>.068 SET NAMES latin1
<span class="m">6</span>.613944 <span class="m">25776</span> <span class="m">0</span>.057 SET <span class="nv">character_set_results</span> <span class="o">=</span> NULL
<span class="m">6</span>.645228 <span class="m">25776</span> <span class="m">0</span>.059 SET <span class="nv">autocommit</span><span class="o">=</span><span class="m">1</span>
<span class="m">6</span>.653798 <span class="m">25776</span> <span class="m">0</span>.059 SET <span class="nv">sql_mode</span><span class="o">=</span><span class="s1">'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES'</span>
<span class="m">6</span>.682184 <span class="m">25776</span> <span class="m">2</span>.526 <span class="k">select</span> * from users where <span class="nv">id</span> <span class="o">=</span> <span class="m">0</span>
<span class="m">6</span>.767888 <span class="m">25776</span> <span class="m">0</span>.288 <span class="k">select</span> id from products where <span class="nv">userid</span> <span class="o">=</span> <span class="m">0</span>
<span class="m">6</span>.790642 <span class="m">25776</span> <span class="m">2</span>.255 call getproduct<span class="o">(</span><span class="m">0</span><span class="o">)</span>
<span class="m">6</span>.809865 <span class="m">25776</span> <span class="m">0</span>.218 call getproduct<span class="o">(</span><span class="m">1</span><span class="o">)</span>
<span class="m">6</span>.846878 <span class="m">25776</span> <span class="m">0</span>.248 <span class="k">select</span> * from users where <span class="nv">id</span> <span class="o">=</span> <span class="m">1</span>
<span class="m">6</span>.847623 <span class="m">25776</span> <span class="m">0</span>.166 <span class="k">select</span> id from products where <span class="nv">userid</span> <span class="o">=</span> <span class="m">1</span>
<span class="m">6</span>.867363 <span class="m">25776</span> <span class="m">0</span>.244 call getproduct<span class="o">(</span><span class="m">2</span><span class="o">)</span>
</pre></div>
<p>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.</p>
<p>Vamos terminando con algo un poco más didáctico que práctico.</p>
<p>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: <code>nfsiostat</code>. 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.</p>
<p>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.</p>
<p>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, <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/fileslower.py">fileslower</a>:</p>
<div class="highlight"><pre><span></span>root@bpf-bcc:/usr/share/bcc/tools# python fileslower <span class="m">0</span>
Tracing sync read/writes slower than <span class="m">0</span> ms
TIME<span class="o">(</span>s<span class="o">)</span> COMM TID D BYTES LAT<span class="o">(</span>ms<span class="o">)</span> FILENAME
<span class="m">4</span>.089 bash <span class="m">869</span> W <span class="m">12</span> <span class="m">0</span>.04 prueba5.txt
</pre></div>
<p>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.</p>
<p>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.</p>
<p>Vamos a usar <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/funccount.py">funccount</a> un par de veces. ¿Qué se ejecuta en el Kernel que empiece por <code>nfs_</code> cuando se monta un volumen de este tipo?</p>
<div class="highlight"><pre><span></span><span class="c1"># python funccount -i 1 'nfs_*'</span>
FUNC COUNT
nfs_fscache_get_client_cookie <span class="m">1</span>
nfs_alloc_client <span class="m">1</span>
nfs_show_mount_options <span class="m">1</span>
nfs_show_path <span class="m">1</span>
nfs_create_server <span class="m">1</span>
nfs_mount <span class="m">1</span>
nfs_fs_mount <span class="m">1</span>
nfs_show_options <span class="m">1</span>
nfs_server_insert_lists <span class="m">1</span>
nfs_alloc_fhandle <span class="m">1</span>
nfs_fscache_init_inode <span class="m">1</span>
nfs_path <span class="m">1</span>
nfs_try_mount <span class="m">1</span>
nfs_setsecurity <span class="m">1</span>
nfs_init_locked <span class="m">1</span>
nfs_init_server <span class="m">1</span>
nfs_set_super <span class="m">1</span>
nfs_init_server_rpcclient <span class="m">1</span>
nfs_parse_mount_options <span class="m">1</span>
nfs_probe_fsinfo <span class="m">1</span>
nfs_fhget <span class="m">1</span>
nfs_get_client <span class="m">1</span>
nfs_fs_mount_common <span class="m">1</span>
nfs_show_devname <span class="m">1</span>
nfs_create_rpc_client <span class="m">1</span>
nfs_alloc_inode <span class="m">1</span>
nfs_init_timeout_values <span class="m">1</span>
nfs_set_sb_security <span class="m">1</span>
nfs_start_lockd <span class="m">1</span>
nfs_verify_server_address <span class="m">1</span>
nfs_alloc_server <span class="m">1</span>
nfs_init_server_aclclient <span class="m">1</span>
nfs_init_client <span class="m">1</span>
nfs_request_mount.constprop.19 <span class="m">1</span>
nfs_fill_super <span class="m">1</span>
nfs_get_root <span class="m">1</span>
nfs_get_option_ul <span class="m">2</span>
nfs_alloc_fattr <span class="m">2</span>
nfs_fattr_init <span class="m">5</span>
</pre></div>
<p>¿Y cuando escribimos algo?</p>
<div class="highlight"><pre><span></span>FUNC COUNT
nfs_writeback_result <span class="m">1</span>
nfs_put_lock_context <span class="m">1</span>
nfs_file_clear_open_context <span class="m">1</span>
nfs_lookup <span class="m">1</span>
nfs_file_set_open_context <span class="m">1</span>
nfs_close_context <span class="m">1</span>
nfs_unlock_and_release_request <span class="m">1</span>
nfs_instantiate <span class="m">1</span>
nfs_start_io_write <span class="m">1</span>
nfs_dentry_delete <span class="m">1</span>
nfs_writehdr_free <span class="m">1</span>
nfs_generic_pgio <span class="m">1</span>
nfs_file_write <span class="m">1</span>
nfs_alloc_fhandle <span class="m">1</span>
nfs_post_op_update_inode_force_wcc_locked <span class="m">1</span>
nfs_fscache_init_inode <span class="m">1</span>
nfs_pageio_add_request <span class="m">1</span>
nfs_end_page_writeback <span class="m">1</span>
nfs_writepages <span class="m">1</span>
nfs_page_group_clear_bits <span class="m">1</span>
nfs_initiate_pgio <span class="m">1</span>
nfs_write_begin <span class="m">1</span>
nfs_pgio_prepare <span class="m">1</span>
nfs_flush_incompatible <span class="m">1</span>
nfs_pageio_complete_mirror <span class="m">1</span>
nfs_sb_active <span class="m">1</span>
nfs_writehdr_alloc <span class="m">1</span>
nfs_init_locked <span class="m">1</span>
nfs_initiate_write <span class="m">1</span>
nfs_generic_pg_pgios <span class="m">1</span>
nfs_file_release <span class="m">1</span>
nfs_refresh_inode <span class="m">1</span>
nfs_pageio_init_write <span class="m">1</span>
nfs_pgio_data_destroy <span class="m">1</span>
nfs_fhget <span class="m">1</span>
nfs_inode_remove_request <span class="m">1</span>
nfs_create_request <span class="m">1</span>
nfs_free_request <span class="m">1</span>
nfs_end_io_write <span class="m">1</span>
nfs_open <span class="m">1</span>
nfs_create <span class="m">1</span>
nfs_pageio_init <span class="m">1</span>
nfs_pgio_result <span class="m">1</span>
nfs_writepages_callback <span class="m">1</span>
nfs_writeback_update_inode <span class="m">1</span>
nfs_do_writepage <span class="m">1</span>
nfs_key_timeout_notify <span class="m">1</span>
nfs_alloc_inode <span class="m">1</span>
nfs_writeback_done <span class="m">1</span>
nfs_write_completion <span class="m">1</span>
nfs_pageio_complete <span class="m">1</span>
nfs_pgio_release <span class="m">1</span>
nfs_lock_and_join_requests <span class="m">1</span>
nfs_page_group_destroy <span class="m">1</span>
nfs_fscache_open_file <span class="m">1</span>
nfs_pageio_doio <span class="m">1</span>
nfs_pgio_header_free <span class="m">1</span>
nfs_generic_pg_test <span class="m">1</span>
nfs_write_end <span class="m">1</span>
nfs_post_op_update_inode <span class="m">1</span>
nfs_updatepage <span class="m">1</span>
nfs_pageio_cond_complete <span class="m">1</span>
nfs_get_lock_context <span class="m">1</span>
nfs_inode_attach_open_context <span class="m">1</span>
nfs_file_open <span class="m">1</span>
nfs_fattr_set_barrier <span class="m">1</span>
nfs_init_cinfo <span class="m">1</span>
nfs_pgheader_init <span class="m">1</span>
nfs_create_request.part.13 <span class="m">1</span>
nfs_sb_deactive <span class="m">1</span>
nfs_post_op_update_inode_locked <span class="m">2</span>
nfs_commit_inode <span class="m">2</span>
nfs_refresh_inode.part.18 <span class="m">2</span>
nfs_scan_commit <span class="m">2</span>
nfs_commit_end <span class="m">2</span>
nfs_setsecurity <span class="m">2</span>
nfs_release_request <span class="m">2</span>
nfs_page_find_head_request_locked <span class="m">2</span>
nfs_reqs_to_commit <span class="m">2</span>
nfs_unlock_request <span class="m">2</span>
nfs_ctx_key_to_expire <span class="m">2</span>
nfs_file_fsync <span class="m">2</span>
nfs_file_flush <span class="m">2</span>
nfs_page_group_sync_on_bit <span class="m">3</span>
nfs_revalidate_inode_rcu <span class="m">3</span>
nfs_alloc_fattr <span class="m">3</span>
nfs_revalidate_inode <span class="m">3</span>
nfs_do_access <span class="m">3</span>
nfs_update_inode <span class="m">4</span>
nfs_permission <span class="m">4</span>
nfs_init_cinfo_from_inode <span class="m">4</span>
nfs_refresh_inode_locked <span class="m">4</span>
nfs_file_has_buffered_writers <span class="m">4</span>
nfs_page_group_lock <span class="m">5</span>
nfs_fattr_init <span class="m">5</span>
nfs_page_group_unlock <span class="m">5</span>
nfs_pgio_current_mirror <span class="m">6</span>
nfs_set_cache_invalid <span class="m">8</span>
nfs_attribute_cache_expired <span class="m">10</span>
</pre></div>
<p>También tenemos scripts para ver la pila, por ejemplo cuando se ejecuten <code>nfs_file_write</code> y <code>nfs_write_end</code>:</p>
<div class="highlight"><pre><span></span><span class="c1"># python stacksnoop nfs_file_write -p 1080</span>
TIME<span class="o">(</span>s<span class="o">)</span> FUNCTION
<span class="m">0</span>.556759834 nfs_file_write
nfs_file_write
new_sync_write
vfs_write
sys_write
system_call_fast_compare_end
<span class="c1">#python stacksnoop nfs_write_end -p 1080</span>
TIME<span class="o">(</span>s<span class="o">)</span> FUNCTION
<span class="m">1</span>.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
</pre></div>
<p>No quiero terminar sin que veamos el script <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/trace.py">trace</a>, una especie de navaja suiza que vale para casi todo, com podéis ver en los <a class="reference external" href="https://github.com/iovisor/bcc/blob/master/tools/trace_example.txt">ejemplos trace</a>. Sigamos viendo más información sobre las escrituras en NFS. En una terminal voy a ejecutar esto:</p>
<div class="highlight"><pre><span></span><span class="k">while</span> <span class="n">true</span>
<span class="n">do</span>
<span class="n">echo</span> <span class="s2">"hola"</span> <span class="o">>></span> <span class="o">/</span><span class="n">mnt</span><span class="o">/</span><span class="n">prueba</span><span class="o">.</span><span class="n">txt</span>
<span class="n">sleep</span> <span class="mi">2</span>
<span class="n">done</span>
</pre></div>
<p>Y en otra un par de llamadas a <code>trace</code>:</p>
<div class="highlight"><pre><span></span><span class="c1"># python trace -p 1647 'r:c:write ((int)retval > 0) "write ok: %d", retval' -T</span>
TIME PID TID COMM FUNC -
<span class="m">21</span>:28:15 <span class="m">1647</span> <span class="m">1647</span> bash write write ok: <span class="m">5</span>
<span class="m">21</span>:28:17 <span class="m">1647</span> <span class="m">1647</span> bash write write ok: <span class="m">5</span>
<span class="c1"># python trace -p 1647 'sys_write "written %d file", arg1'</span>
PID TID COMM FUNC -
<span class="m">1647</span> <span class="m">1647</span> bash sys_write written <span class="m">1</span> file
<span class="m">1647</span> <span class="m">1647</span> bash sys_write written <span class="m">1</span> file
</pre></div>
<p>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.</p>
<p>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.</p>
Monitorización de aplicaciones con sysdig2016-11-23T21:00:00+01:002016-11-23T21:00:00+01:00tag:www.forondarena.net,2016-11-23:/monitorizacion-de-aplicaciones-con-sysdig.html<p>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.</p>
<p>En realidad, no faltaba razón en este argumento, ya que los primeros esfuerzos …</p><p>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.</p>
<p>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).</p>
<p>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 <a class="reference external" href="http://www.brendangregg.com">Brendan Gregg</a>, 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 <a class="reference external" href="http://www.brendangregg.com/blog/2016-10-27/dtrace-for-linux-2016.html">esta lectura</a>, con algo de contexto sobre la evolución de estas nuevas herramientas.</p>
<p>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) <a class="reference external" href="http://www.sysdig.org">Sysdig</a>.</p>
<p>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 <a class="reference external" href="https://github.com/iovisor/bcc">BCC</a> 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.</p>
<p>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 <a class="reference external" href="http://www.brendangregg.com/blog/2016-10-21/linux-efficient-profiler.html">último de lo último</a> que se ha incluido.</p>
<p>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 <a class="reference external" href="https://lkml.org/lkml/2016/6/14/749">backend</a> bajo BPF. Por otro lado, y bajo el mismo paraguas de BPF, se están desarrollando nuevas tecnologías, como <a class="reference external" href="https://www.iovisor.org/technology/xdp">XDP</a>, 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, <a class="reference external" href="https://github.com/cilium/cilium">aquí</a> tenéis un enlace.</p>
<p>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.</p>
<p>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 <a class="reference external" href="https://iovisor.github.io/bcc/">iovisor</a>, 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.</p>
<p>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.</p>
<p>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.</p>
<p>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 <a class="reference external" href="http://zguide.zeromq.org/page:all#Divide-and-Conquer">pipeline</a>. Los entornos paralelos suelen complicar la monitorización.</p>
<p>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.</p>
<p>Y aquí es cuando alguien se levanta y dice: "Tanto rollo para algo que se puede solucionar con las trazas de toda la vida, <code>(inicio = time.time(); fin = time.time(); dif = fin - inicio)</code> ". 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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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 <a class="reference external" href="https://github.com/draios/sysdig/wiki/Tracers">web de sysdig</a>.</p>
<div class="highlight"><pre><span></span><span class="nb">echo</span> <span class="s2">">:p:mysql::"</span> > /dev/null
... código a analizar ...
<span class="nb">echo</span> <span class="s2">"<:p:mysql::"</span> > /dev/null
</pre></div>
<p>Y así de fácil. Con <code>></code> y <code><</code> definimos el comienzo y el final del bloque, con <code>:p:</code> pedimos que se genere un identificador automáticamente a partir del pid del proceso (hay más opciones), y usamos <code>mysql</code> como tag para identificar el span (que es como se llama todo esto, tracer/span).</p>
<p>Pero podemos ir un poco más lejos, y usar cadenas como las siguientes:</p>
<div class="highlight"><pre><span></span><span class="nb">echo</span> <span class="s2">">:p:mysql.select::"</span> > /dev/null
<span class="nb">echo</span> <span class="s2">">:p:mysql.update::"</span> > /dev/null
<span class="nb">echo</span> <span class="s2">">:p:mysql.select:query=from tabla1:"</span> > /dev/null
<span class="nb">echo</span> <span class="s2">">:p:mysql.update:query=tabla1:"</span> > /dev/null
</pre></div>
<p>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 <code>for</code> en la que ha ocurrido un problema concreto, o el tipo de <code>select</code> que ha generado cierto error, por decir un par de casos.</p>
<p>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:</p>
<div class="highlight"><pre><span></span><span class="n">fsysdig</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"/dev/null/"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="c1"># Abrimos fichero log, creamos procesos, ...</span>
<span class="k">for</span> <span class="n">linea</span> <span class="ow">in</span> <span class="n">fsysdig</span><span class="p">:</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">">:p:zmq::</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="c1"># Preparamos los datos, y mandamos al socket push de ZMQ</span>
<span class="c1"># Este socket se bloquea al llegar al tope de memoria configurado</span>
<span class="c1"># ...</span>
<span class="n">socketzmq</span><span class="o">.</span><span class="n">send_multipart</span><span class="p">([</span><span class="s2">"LOG"</span><span class="p">,</span><span class="n">linea</span><span class="p">])</span>
<span class="c1"># ...</span>
<span class="c1"># Mas actividad para este span</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"<:p:zmq::</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</pre></div>
<p>En otros procesos tendremos la parte de la gestión de las expresiones regulares</p>
<div class="highlight"><pre><span></span><span class="n">fsysdig</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"/dev/null/"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="c1">#</span>
<span class="c1"># Leeriamos del socket push via un socket pull de ZMQ</span>
<span class="c1"># ...</span>
<span class="c1">#</span>
<span class="k">while</span> <span class="n">seguirprocesando</span><span class="p">:</span>
<span class="c1"># Hacemos match de una expresion regular</span>
<span class="c1"># Si se cumple, hacemos ciertas operaciones</span>
<span class="c1"># ...</span>
<span class="k">if</span> <span class="n">matchregexp1</span><span class="p">:</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">">:p:regexp:re=regexp1:</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="c1"># Trabajo con la regexp1</span>
<span class="c1"># ...</span>
<span class="c1"># Enviamos el dato al proceso mysql via ZMQ</span>
<span class="c1"># ...</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"<:p:regexp::</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="k">if</span> <span class="n">matchregexp2</span><span class="p">:</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">">:p:regexp:re=regexp2:</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="c1"># Trabajo con la regexp2</span>
<span class="c1"># ...</span>
<span class="c1"># Y enviamos el dato al proceso mysql via ZMQ</span>
<span class="c1"># ...</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"<:p:regexp::</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="c1">#</span>
<span class="c1"># Hariamos algo parecido con el resto ...</span>
<span class="c1">#</span>
</pre></div>
<p>Ya os hacéis una idea. Como veis, estamos añadiendo argumentos al tag <code>regexp</code> para identificar los bloques.</p>
<p>Por último, otro proceso haría el trabajo contra mysql.</p>
<div class="highlight"><pre><span></span><span class="n">fsysdig</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"/dev/null/"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span>
<span class="k">while</span> <span class="n">seguirprocesando</span><span class="p">:</span>
<span class="c1">#</span>
<span class="c1"># Leeriamos del socket push via ZMQ, agrupariamos, operaciones, ...</span>
<span class="c1">#</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">">:p:mysql:st=update:</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="c1"># preparariamos la operacion mysql y el resto del trabajo para este span...</span>
<span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s1">'''insert into ...'''</span><span class="p">)</span>
<span class="c1"># ...</span>
<span class="k">except</span> <span class="p">(</span><span class="n">MySQLdb</span><span class="o">.</span><span class="n">MySQLError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="nb">print</span> <span class="s2">"Mysql: ERROR: Al ejecutar comando mysql "</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="k">finally</span><span class="p">:</span>
<span class="c1"># ...</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"<:p:mysql::</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">fsysdig</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</pre></div>
<p>Como veis, también hemos creados argumentos para identificar bloques.</p>
<p>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).</p>
<div class="highlight"><pre><span></span>sysdig -C <span class="m">500</span> -s <span class="m">512</span> -w volcado_span.scap
</pre></div>
<p>Básicamente estamos creando ficheros independientes de 500MB, y estamos capturando 512 bytes de bufer de IO.</p>
<p>Una vez tengamos Sysdig lanzado dejamos el script funcionando un rato. Tendremos varios ficheros volcado_span.scap[0-9]+. Empecemos el análisis!</p>
<p>Una de las utilidades principales de Sysdig es un interfaz ncurses que permite ejecutar chisels fácilmente. Se llama <code>csysdig</code>. 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 <code>-r</code></p>
<div class="highlight"><pre><span></span>csysdig -r volcado_span.scap8
</pre></div>
<p>En este listado, F2 -> Traces Summary, y nos dará el resumen de spans que se han generado.</p>
<p><img alt="Sumario spans" class="img-fluid" src="/images/posts/sysdig/sumario_spans.png" /></p>
<p>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":</p>
<div class="highlight"><pre><span></span>sysdig -r volcado_span.scap8 -c spectrogram <span class="s1">'evtin.span.p.tag[0]=regexp'</span>
</pre></div>
<p>De donde sacaríamos lo siguiente:</p>
<p><img alt="Spectrogram regexp" class="img-fluid" src="/images/posts/sysdig/spectrogram_regexp_general.png" /></p>
<p>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:</p>
<div class="highlight"><pre><span></span>sysdig -r volcado_span.scap8 -c spectrogram <span class="s1">'evtin.span.p.tag[0]=regexp and evt.latency > 100000'</span>
</pre></div>
<p>Como veis, estamos aplicando filtros para limitar lo que vemos:</p>
<p><img alt="Spectrogram regexp lentas" class="img-fluid" src="/images/posts/sysdig/spectrogram_regexp_lentos.png" /></p>
<p>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.</p>
<p>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 <code>htop</code> o <code>netstat</code>, por citar un par.</p>
<p>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.</p>
<p>El chisel <code>echo_fds</code>, 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, <code>mysqld</code>:</p>
<div class="highlight"><pre><span></span>sysdig -r volcado_span.scap11 -c echo_fds <span class="s1">'proc.name=mysqld'</span>
</pre></div>
<p>Nos hemos olvidado de los spans, como veis, y vamos directamente a los procesos <code>mysqld</code>. Sin acotar más, tendremos un churro similar al siguiente</p>
<div class="highlight"><pre><span></span>.....
------ Read 4B from <span class="m">127</span>.0.0.1:52398->127.0.0.1:mysql <span class="o">(</span>mysqld<span class="o">)</span>
....
------ Read 87B from <span class="m">127</span>.0.0.1:52398->127.0.0.1:mysql <span class="o">(</span>mysqld<span class="o">)</span>
.insert into tabla<span class="o">(</span>timestamp, dato1, dato2<span class="o">)</span> values <span class="o">(</span><span class="m">1477905741</span>, <span class="s1">'ejemplo1'</span>, <span class="s1">'ejemplo2'</span><span class="o">)</span>
------ Write 11B to <span class="m">127</span>.0.0.1:52398->127.0.0.1:mysql <span class="o">(</span>mysqld<span class="o">)</span>
......
</pre></div>
<p>Como veis, estamos logueando todo el tráfico SQL, como el insert que he dejado aquí. Creedme, este tipo de chisels, con filtros como <code>evt.buffer contains</code> son muy útiles para ver tráfico HTTP, cabeceras, respuestas o códigos de error, particularmente en entornos con muchos microservicios y similares.</p>
<p>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.</p>
<p>En comparación a lo que hemos hecho, y aprovechando que estamos hablando de mysql, en <a class="reference external" href="http://www.brendangregg.com/blog/2016-10-04/linux-bcc-mysqld-qslower.html">este enlace</a> 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 <code>mysqld_query.py</code> de los <a class="reference external" href="https://github.com/iovisor/bcc/tree/master/examples/tracing">ejemplos</a> 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.</p>
Port Knocking sin complicaciones2013-11-06T23:00:00+01:002013-11-06T23:00:00+01:00tag:www.forondarena.net,2013-11-06:/port-knocking-sin-complicaciones.html<p>(Así, pecando de básico desde el principio)</p>
<p>Históricamente, desde el punto de vista de la seguridad, los servidores han venido teniendo dos tipos de puertos:</p>
<ul class="simple">
<li>Los que tienen que estar siempre abiertos: Los puertos HTTP o HTTPS de un servidor web, sin ir más lejos.</li>
<li>Los que sólo tienen que …</li></ul><p>(Así, pecando de básico desde el principio)</p>
<p>Históricamente, desde el punto de vista de la seguridad, los servidores han venido teniendo dos tipos de puertos:</p>
<ul class="simple">
<li>Los que tienen que estar siempre abiertos: Los puertos HTTP o HTTPS de un servidor web, sin ir más lejos.</li>
<li>Los que sólo tienen que estar abiertos para unas IPs determinadas: SSH, por ejemplo.</li>
</ul>
<p>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).</p>
<p>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).</p>
<p>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?</p>
<p>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.</p>
<p>En este post casi voy a limitarme a citar una herramienta más que usar a la hora de securizar un servidor: El <strong>Port Knocking</strong>. 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.</p>
<p>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.</p>
<p>A partir de esta idea básica, han ido apareciendo otras mejoras que vamos a ver en un minuto.</p>
<p><strong>Port Knocking básico</strong></p>
<p>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 (<a class="reference external" href="http://www.zeroflux.org/projects/knock">knockd</a> por ejemplo), y las otras dos usan únicamente iptables.</p>
<p>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.</p>
<pre>
[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
</pre><p>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).</p>
<p>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.</p>
<p>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.</p>
<p>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 <a class="reference external" href="https://wiki.archlinux.org/index.php/Port_Knocking">wiki de archlinux</a>, así que podéis seguir desde allí si optáis por esta vía.</p>
<p>Y para terminar, si queréis una alternativa específica de iptables, hay un módulo en <a class="reference external" href="http://sourceforge.net/projects/xtables-addons/">xtables addons</a> pensado para hacer Port Knocking. Se llama <a class="reference external" href="http://portknocko.berlios.de/README.html">xt_pknock</a>, y permite hacer cosas como esta (entre otras que veremos más adelante):</p>
<pre>
iptables -A INPUT -p tcp -m pknock --knockports 4002,4001,4004 --strict --name IMAP --time 10 --autoclose 60 --dport 993 -j ACCEPT
</pre><p>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.</p>
<p><strong>Port Knocking con autenticación</strong></p>
<p>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.</p>
<p>Sobre esta idea, Michael Rash (autor, entre otros, de <a class="reference external" href="http://www.cipherdyne.org/psad/">psad</a>) implementó una mejora del Port Knocking sobre el concepto de <a class="reference external" href="http://www.cipherdyne.org/fwknop/docs/SPA.html">Single Packet Authorization (SPA)</a>: <a class="reference external" href="http://www.cipherdyne.org/fwknop/">fwknop</a>. 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 <a class="reference external" href="http://www.cipherdyne.org/fwknop/docs/features.html">listado de features</a> y las <a class="reference external" href="http://www.cipherdyne.org/fwknop/docs/design-decisions.html">ventajas</a> de esta implementación en la propia web de fwknop.</p>
<p>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.</p>
<p>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 <a class="reference external" href="http://www.cipherdyne.org/fwknop/docs/fwknop-tutorial.html">tutorial</a>, 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.</p>
<p>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.</p>
<p>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:</p>
<pre>
...
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
...
</pre><p>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.</p>
<p><strong>Clientes</strong></p>
<p>La pregunta es: ¿Cómo se genera la secuencia que abre la puerta?</p>
<p>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).</p>
<p>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.</p>
<p><strong>Notas</strong></p>
<p>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.</p>
Instalaciones automáticas desde usb2013-07-14T11:37:00+02:002013-07-14T11:37:00+02:00tag:www.forondarena.net,2013-07-14:/instalaciones-automaticas-desde-usb.html<p>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 …</p><p>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.</p>
<p>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.</p>
<p>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.</p>
<p>Queremos conseguir dos cosa:</p>
<ul class="simple">
<li>Un método para arrancar rápidamente una distribución live, sencilla, que permita recuperar particiones, transferir ficheros, ..., este tipo de cosas.</li>
<li>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.</li>
</ul>
<p>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.</p>
<p><strong>Pasos previos</strong></p>
<p>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":</p>
<pre>
#fdisk -l /dev/sde
Disposit. Inicio Comienzo Fin Bloques Id Sistema
/dev/sde1 * 2048 15654847 7826400 c W95 FAT32 (LBA)
</pre><p>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.</p>
<p>Vamos a montar la partición en "mnt" (o donde queráis), con un simple:</p>
<pre>
mount /dev/sde1 /mnt
</pre><p>Lo siguiente es instalar grub en /dev/sde. Otra vez, cuidado con lo que hacéis, no os confudáis de dispositivo.</p>
<pre>
grub-install --no-floppy --root-directory=/mnt /dev/sde
</pre><p>Dejamos estos pasos básicos y vamos ya a por la configuración más específica.</p>
<p><strong>Creando el menú</strong></p>
<p>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.</p>
<p>Para tener un poco de variedad, desde mi USB se va a poder arrancar lo siguiente:</p>
<ul class="simple">
<li>Una Debian Wheezy sin preseed, para ir configurando a mano.</li>
<li>Un CD con utilidades de repación. El que más os guste. Para este ejemplo: Ultimate BootCD.</li>
<li>Una Slax, por si quisiera arrancar un entorno gráfico completo.</li>
<li>Una Debian Wheezy con preseed, completamente automática.</li>
<li>Una CentOS 6.4 con kickstart, completamente automática.</li>
</ul>
<p>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.</p>
<p>Vamos a crear el menú de GRUB. Necesitamos editar el fichero "/mnt/boot/grub/grub.cfg" con lo siguiente:</p>
<pre>
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
}
</pre><p>Suficiente, no necesitamos nada más. Repasemos un poco:</p>
<ul class="simple">
<li>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).</li>
<li>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.</li>
<li>Vamos a usar imágenes ISO "normales", y GRUB va a asumir que son la raíz de la instalación.</li>
<li>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.</li>
<li>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.</li>
<li>Como hemos dicho que no queremos usar PXE, asumo que tampoco queremos usar DHCP, así que las configuraciones de red son estáticas.</li>
<li>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í.</li>
</ul>
<p>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:</p>
<ul class="simple">
<li>Para Debian, con y sin preseed: <a class="reference external" href="http://ftp.nl.debian.org/debian/dists/wheezy/main/installer-amd64/current/images/netboot/mini.iso">mini.iso</a> (renombrada a wheezy_mini_amd64.iso).</li>
<li>Para CentOS: <a class="reference external" href="http://ftp.udl.es/pub/centos/6.4/isos/x86_64/CentOS-6.4-x86_64-netinstall.iso">netinstall.iso</a>.</li>
<li>Para Ultimate BootCD: <a class="reference external" href="http://www.ultimatebootcd.com/download.html">ubcd</a>.</li>
<li>Para Slax: <a class="reference external" href="http://www.slax.org/download/7.0.8/slax-Spanish-7.0.8-x86_64.iso">slax</a>.</li>
</ul>
<p>Cuando lo tengáis todo, desmontad /mnt, y ya habremos terminado con el pendrive. Quedan los preseed/kickstart.</p>
<p><strong>Ficheros kickstart y preseed</strong></p>
<p>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:</p>
<pre>
debconf-get-selections --installer >> wheezy_preseed_131.cfg
debconf-get-selections >> wheezy_preseed_131.cfg
</pre><p>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 <a class="reference external" href="/static/docs/posts/pxe/wheezy_preseed_131.cfg">esta</a> 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.</p>
<p>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.</p>
<p>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.</p>
<p>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 <a class="reference external" href="/static/docs/posts/pxe/ks_rh6_131.ks">este</a> 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).</p>
<p><strong>Pruebas en KVM</strong></p>
<p>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.</p>
<p>Por último, esto es lo que veréis si todo ha ido bien:</p>
<p><img alt="Pantallazo GRUB" class="img-fluid" src="/images/posts/pxe/menu_instalaciones.png" /></p>
<p>Y ya está, con esto hemos terminado. Romped vuestros CDs!!</p>
<p><strong>Nota</strong></p>
<p>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.</p>
Monitorización en serio. Práctica2013-06-24T17:35:00+02:002013-06-24T17:35:00+02:00tag:www.forondarena.net,2013-06-24:/monitorizacion-en-serio-practica.html<p>Después de haber hablado un poco sobre <a class="reference external" href="http://www.forondarena.net/monitorizacion-en-serio-teoria.html">la teoría</a> de la monitorización tal y como la veo yo, sigo con la parte práctica. Como dice <a class="reference external" href="http://zguide.zeromq.org/page:all#Asynchronous-Majordomo-Pattern">Pieter Hintjens</a>, 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 …</p><p>Después de haber hablado un poco sobre <a class="reference external" href="http://www.forondarena.net/monitorizacion-en-serio-teoria.html">la teoría</a> de la monitorización tal y como la veo yo, sigo con la parte práctica. Como dice <a class="reference external" href="http://zguide.zeromq.org/page:all#Asynchronous-Majordomo-Pattern">Pieter Hintjens</a>, 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.</p>
<p>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?</p>
<ul class="simple">
<li>Peligroso desde el punto de vista de la seguridad, porque puede implicar algún ataque, o que estemos mandando spam, o a saber qué.</li>
<li>Peligroso porque podría ser debido a un problema hardware, y con ello ser la antesala de una caida total del servidor.</li>
<li>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.</li>
<li>¡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.</li>
</ul>
<p>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.</p>
<p>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.</p>
<p>¿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.</p>
<p>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.</p>
<p>Si hay un sistema de monitorización que destaca sobre los demás que conozco, ese es <a class="reference external" href="http://www.collectd.org/">Collectd</a>. ¡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é?</p>
<ul class="simple">
<li>Como la mayoría de aplicaciones serias, una vez configurada y puesta en marcha, podemos despreocuparnos de que se caiga o deje de funcionar.</li>
<li>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.</li>
<li>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.</li>
<li>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, ...).</li>
</ul>
<p>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.</p>
<p><strong>El ejemplo</strong></p>
<p>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).</p>
<p>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.</p>
<p>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).</p>
<p>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.</p>
<pre>
Interval 10
</pre><p>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.</p>
<p>Empezamos por los plugins de entrada que no necesitan configuración, y que se instancian simplemente con un "loadplugin":</p>
<pre>
LoadPlugin cpu
LoadPlugin load
LoadPlugin memory
</pre><p>Otros, como no puede ser de otra forma, necesitan alguna opción:</p>
<pre>
LoadPlugin tcpconns
<Plugin "tcpconns">
ListeningPorts false
RemotePort "3306"
</Plugin>
</pre><p>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.</p>
<p>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":</p>
<pre>
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>
</pre><p>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.</p>
<p>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.</p>
<p>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):</p>
<pre>
<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>
</pre><p>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.</p>
<p><strong>El Script</strong></p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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:</p>
<ol class="arabic">
<li><p class="first">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.</p>
</li>
<li><p class="first">Collectd va a ejecutar funcionWrite cada vez que lea desde los plugins de lectura.</p>
</li>
<li><p class="first">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.</p>
</li>
<li><p class="first">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.</p>
</li>
<li><p class="first">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)):</p>
<blockquote>
<pre>
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])
...
</pre></blockquote>
<p>Otro ejemplo, este caso para el consumo de memoria:</p>
<blockquote>
<pre>
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])
</pre></blockquote>
<p>Por último, esto es lo que manda el plugin tail.</p>
<blockquote>
<pre>
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])
</pre></blockquote>
<p>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.</p>
</li>
<li><p class="first">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.</p>
</li>
<li><p class="first">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, ....</p>
</li>
</ol>
<p>Para no pecar de "abstracto", este es un esqueleto de ejemplo de un monitorcorreo.py cualquiera:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">collectd</span>
<span class="sd">'''import time, imaplib, socket, smtplib ...'''</span>
<span class="k">class</span> <span class="nc">ClasesDeApoyo</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="sd">'''</span>
<span class="sd"> Estrucutras de datos para guardar los valores que recibimos desde los plugins.</span>
<span class="sd"> Métodos para trabajar con los datos, ya sean actuales, o históricos.</span>
<span class="sd"> Métodos para relacionar los datos de distintos plugins.</span>
<span class="sd"> Métodos para generar informes, mandar correos, ....</span>
<span class="sd"> Métodos para hacer traceroutes, abrir sesiones IMAP, ....</span>
<span class="sd"> '''</span>
<span class="k">def</span> <span class="nf">funcionConfig</span><span class="p">(</span><span class="n">argconfig</span><span class="p">):</span>
<span class="sd">'''</span>
<span class="sd"> En argconfig se encuentran, entre otros, los argumentos que han entrado desde collectd.conf.</span>
<span class="sd"> '''</span>
<span class="k">global</span> <span class="n">instanciasClasesDeApoyo</span>
<span class="sd">'''</span>
<span class="sd"> Crear una instancia de las clases de apoyo, aunque se puede dejar para Init.</span>
<span class="sd"> Si se van a usar los argumentos de collectd.conf, se pueden leer en un bucle.</span>
<span class="sd"> '''</span>
<span class="k">def</span> <span class="nf">funcionInit</span><span class="p">():</span>
<span class="sd">'''</span>
<span class="sd"> Esta función se usa para inicializar datos. Puede ser interesante para llamar a métodos que abran conexiones, ficheros, ....</span>
<span class="sd"> '''</span>
<span class="k">global</span> <span class="n">instanciasClasesDeApoyo</span>
<span class="sd">'''</span>
<span class="sd"> Inicializar estructuras.</span>
<span class="sd"> Si todo ha ido bien, se registra en collectd la función Write.</span>
<span class="sd"> '''</span>
<span class="n">collectd</span><span class="o">.</span><span class="n">register_write</span><span class="p">(</span><span class="n">funcionWrite</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">funcionWrite</span><span class="p">(</span><span class="n">argdatos</span><span class="p">):</span>
<span class="sd">'''</span>
<span class="sd"> Este es el método al que se llama cada vez que se genere un dato.</span>
<span class="sd"> Este método se encarga del trabajo real del script.</span>
<span class="sd"> '''</span>
<span class="k">global</span> <span class="n">instanciasClasesDeApoyo</span>
<span class="sd">'''</span>
<span class="sd"> Todos los valores vienen con un timestamp. Una idea es ir guardando estos valores en una estructura.</span>
<span class="sd"> 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.</span>
<span class="sd"> 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.</span>
<span class="sd"> No siempre hacen falta todos los datos que recibimos desde collectd. Lo siguiente es un ejemplo.</span>
<span class="sd"> '''</span>
<span class="n">datos</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">datos</span><span class="p">[</span><span class="s2">"host"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">argdatos</span><span class="o">.</span><span class="n">host</span><span class="p">)</span>
<span class="n">datos</span><span class="p">[</span><span class="s2">"plugininstance"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">argdatos</span><span class="o">.</span><span class="n">plugin_instance</span><span class="p">)</span>
<span class="n">datos</span><span class="p">[</span><span class="s2">"typeinstance"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">argdatos</span><span class="o">.</span><span class="n">type_instance</span><span class="p">)</span>
<span class="n">datos</span><span class="p">[</span><span class="s2">"value"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">argdatos</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="n">datos</span><span class="p">[</span><span class="s2">"time"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">argdatos</span><span class="o">.</span><span class="n">time</span><span class="p">)</span>
<span class="n">datos</span><span class="p">[</span><span class="s2">"localtime"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"</span><span class="si">%F</span><span class="s2"> %T"</span><span class="p">,</span><span class="n">time</span><span class="o">.</span><span class="n">localtime</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">argdatos</span><span class="o">.</span><span class="n">time</span><span class="p">))))</span>
<span class="sd">'''</span>
<span class="sd"> if datos["time"] < anteriordatos["time"]:</span>
<span class="sd"> instanciasClasesDeApoyo.hacerCalculos(datos)</span>
<span class="sd"> else:</span>
<span class="sd"> instanciasClasesDeApoyo.guardarDatos(datos)</span>
<span class="sd"> '''</span>
<span class="n">collectd</span><span class="o">.</span><span class="n">register_config</span><span class="p">(</span><span class="n">funcionConfig</span><span class="p">)</span>
<span class="n">collectd</span><span class="o">.</span><span class="n">register_init</span><span class="p">(</span><span class="n">funcionInit</span><span class="p">)</span>
</pre></div>
Monitorización en serio. Teoría2012-12-07T20:46:00+01:002012-12-07T20:46:00+01:00tag:www.forondarena.net,2012-12-07:/monitorizacion-en-serio-teoria.html<p>Os ahorro tener que leer todo el post para llegar a esta conclusión: El
título es un poco sensacionalista, lo sé. Sigamos.</p>
<p>Uno de los aspectos más consolidados en lo que a la administración de
sistemas se refiere es la monitorización. No hay infraestructura
razonablemente seria que no tenga un …</p><p>Os ahorro tener que leer todo el post para llegar a esta conclusión: El
título es un poco sensacionalista, lo sé. Sigamos.</p>
<p>Uno de los aspectos más consolidados en lo que a la administración de
sistemas se refiere es la monitorización. No hay infraestructura
razonablemente seria que no tenga un ciento de monitores de carga de
CPU, memoria consumida, tráfico de red, conexiones abiertas, .... Y
junto a estos tenemos otros monitores, algo más avanzados, que por
ejemplo revisan si una sesión POP3 se establece correctamente, usando
para ello un usuario/contraseña preestablecidos; o que un una página PHP
contiene cierto texto, para lo que se hacen X consultas, también
predefinidas, en una base de datos.</p>
<p>Básicamente, esto es lo que hay; aquí nos quedamos la mayoría. Pero, ¿Es
suficiente?</p>
<p>Sigamos con un ejemplo más concreto:</p>
<p>Vamos a suponer a partir de ahora que somos los responsables de los
servidores IMAP de alguna de las empresas más grandes del sector del
correo electrónico, y que por lo tanto tenemos millones de usuarios para
los que poder acceder a su correo es, obviamente, fundamental.</p>
<p>Con el gráfico de conexiones establecidas en la mano, sabemos que el
patrón más habitual es el siguiente:</p>
<p><img alt="Grafico base" class="img-fluid" src="/images/posts/monitorizacion/diagramabase.png" /></p>
<p>Vemos que tenemos pocas conexiones abiertas durante la madrugada, y que
el número va subiendo en la medida en que empieza la jornada laboral,
con un pequeño descenso en las horas habituales de comida. Para este
ejemplo nos quedamos con esto, aunque en condiciones normales se
deberían tener en cuenta fines de semana, vacaciones, ....</p>
<p>La línea roja indica el umbral de alerta del monitor. Ya sabéis: el
móvil suena cada vez que la línea verde supere a la roja. Aunque lo
normal es que también tuviéramos un umbral de aviso amarillo y otro para
cuando las conexiones fueran demasiado bajas, por ahora nos sirve esta
versión simplificada.</p>
<p>Y llega el día en el que las conexiones establecidas contra nuestros
servidores muestran lo siguiente:</p>
<p><img alt="Grafico con alertas" class="img-fluid" src="/images/posts/monitorizacion/diagramaalter.png" /></p>
<p>Qué fácil parece todo cuando vemos el gráfico! Lamentablemente, cuando
somos responsables de unos cuantos cientos de gráficos, que por supuesto
no estamos vigilando constantemente, solo vamos a tener constancia de
ese pico de las 10 a.m., que además podría ser perfectamente un pequeño
aumento puntual de carga inocuo para el servicio.</p>
<p>Por supuesto, ni nos hemos enterado del ataque por fuerza bruta de las
04:00 a.m. que probablemente haya conseguido "adivinar" decenas de
contraseñas de usuarios, ya disponibles para el "spameo" generalizado; o
de ese problema de media tarde, que quizá sí haya supuesto una perdida
de servicio para muchos usuarios. Pero la cosa es todavía peor, porque
resulta que nuestro sistema de validación de cuentas se "volvió loco"
justo antes de esa bajada de tráfico y empezó a aceptar logins,
independientemente de la validez de la contraseña.</p>
<p>¿Cuántos sistemas de monitorización habéis visto capaces de detectar
estas situaciones? Llevadlo a otros entornos: ¿Cuantos sistemas de
monitorización concéis capaces de detectar que una tienda online en
realidad está cobrando 10 euros menos en algunos pedidos? ¿O un motor de
búsqueda que da resultados erróneos periódicamente?</p>
<p>¿Qué hacemos entonces? ¿Tiramos todos los monitores que tenemos a la
basura? Obviamenente, no. Es evidente que un servidor con una carga de
CPU alta necesita atención.</p>
<p>Sin embargo, lo que sí debemos cambiar es el punto de vista sobre el que
gira la monitorización, de tal manera que en lugar de orientarla hacia
el aspecto estrictamente operacional, lo hagamos teniendo en cuenta el
propio servicio que estamos vigilando, que no deja de ser, en
definitiva, lo único que aporta valor. Dicho de otra forma, lo que
importa no es que la máquina reviente, sino que el acceso de los
clientes falle o sea más lento. Puede parecer un cambio sutil, pero no
lo es.</p>
<p>Acercándonos otra vez a la faceta técnica, esto significa que deberíamos
prestar más atención a los siguientes aspectos:</p>
<ol class="arabic simple">
<li>La monitorización debe centrarse en las aplicaciones, y no en el
hardware, la red o las máquinas. (Lo que veníamos diciendo sobre la
percepción del servicio que tienen los usuarios).</li>
<li>La monitorización puede ser importante como mecanismo para buscar
mejoras y optimaciones para el servicio.</li>
<li>Desde el punto de vista de los sistemas, esto significa que la
monitorización debe interactuar mucho más con las aplicaciones.</li>
<li>Parámetros como la latencia o el tiempo de respuesta de una
aplicación deben cobrar más importancia.</li>
<li>Detectar las anomalías debe ser uno de los objetivos a conseguir.
Dicho de otra forma, si nuestro sistema gana una conexión nueva de
media a la semana, y si este lunes tenemos 10, el que la semana
siguiente veamos 20 debe alertarnos.</li>
<li>No se puede monitorizar lo que no se puede medir.</li>
<li>Por si no ha quedado claro, sólo se puede monitorizar lo que se puede
medir.</li>
<li>Revisar el estado de las aplicaciones hace que algunos de los
chequeos "tradicionales" dejen de ser necesarios, con lo que se
simplifica la monitorización.</li>
<li>Los monitores tradicionales siguen siendo útiles para detectar un
buen número de problemas, incluidos los relacionados con la
escalabilidad de las plataformas.</li>
</ol>
<p>Pero, ¿Cómo llevamos esto a la práctica?</p>
<p>En líneas generales, necesitamos trabajar mucho más contra los logs que
generan los servicios. Si somos los desarrolladores de nuestras
aplicaciones (una web por ejemplo), queda en nuestra mano definir y
loguear la información que consideramos importante. Si, por el
contrario, estamos usando una aplicación de un tercero (el servidor
POP/IMAP Dovecot, por citar uno), nos resultará más difícil incluir un
logueo específico, pero siempre podremos buscar la información que nos
puede aportar visibilidad extra del entorno. En este caso concreto, por
ejemplo, el número de intentos de conexión con credenciales inválidas o
el ratio logins/logoouts son métricas que nos podrían ayudar en un
momento dado, por citar dos.</p>
<p>En cuanto al software que podemos usar para la monitorización, tenemos
decenas de buenas alternativas que podemos usar. Las hay más
visuales, <a class="reference external" href="http://ganglia.sourceforge.net/">algunas</a> están pensadas para entornos muy grandes, <a class="reference external" href="https://github.com/ooyala/hastur">otras</a>
usan backends especializados (Cassandra por ejemplo), .... Queda a
nuestra elección.</p>
<p>En lo que sí están de acuerdo la mayoría de aplicaciones es en la forma
de detectar anomalías, sobre todo porque todas las que yo conozco se
basan en <a class="reference external" href="http://static.usenix.org/publications/library/proceedings/lisa2000/full_papers/brutlag/brutlag_html/index.html">este</a> estupendo documento, que a la postre sirvió para la
implementación en RRDTool. Siempre podéis diseñaros vuestro sistema,
quizá usando R y su paquete <a class="reference external" href="http://cran.r-project.org/web/packages/forecast/index.html">forecast</a>, pero esto está muy lejos del
objetivo de este post.</p>
<p>En unos días describiré con más detalle una pequeña implementación de
ejemplo sobre un servicio IMAP (Dovecot).</p>
Enrutamiento para dummies2012-09-27T22:30:00+02:002012-09-27T22:30:00+02:00tag:www.forondarena.net,2012-09-27:/enrutamiento-para-dummies.html<p>Si hay algo sobre lo que no hubiese querido escribir nunca en este blog
es sobre el enrutamiento básico en Linux. Hace 10 años quizá hubiese
sido más interesante, pero no ahora. Aún así, en este mundo del botón y
del siguiente siguiente no tengo nada claro que la gente …</p><p>Si hay algo sobre lo que no hubiese querido escribir nunca en este blog
es sobre el enrutamiento básico en Linux. Hace 10 años quizá hubiese
sido más interesante, pero no ahora. Aún así, en este mundo del botón y
del siguiente siguiente no tengo nada claro que la gente sepa
exactamente lo que hay debajo de un "route -n", así que vamos a ello.
Eso sí, para dummies. De hecho, me voy a pasar de básico, con lo que
escribiré cosas que en condiciones normales merecerían una discusión. En
fin.</p>
<p>Empezamos con el viejo comando "route -n", tan simple como siempre:</p>
<pre>
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.213.208.1 0.0.0.0 UG 0 0 0 eth0
10.213.208.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
192.168.10.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1
192.168.11.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1
</pre><p>Lo normal cuando usamos la combinación route/ifconfig en la mayoría de
las distribuciones, una puerta de enlace, una red para llegar a esa
puerta de enlace a través de un dispositivo, y luego, en este caso,
nuestras redes locales.</p>
<p>¿No hay nada más?</p>
<p>Pues sí, hay mucho más, claro. Esto no es más que una mínima porción de
la tabla de enrutamiento de vuestro kernel favorito. Vamos a verlo
pasando a la utilidad "ip", la pobre, que lleva tanto tiempo existiendo,
pero que tan poco se usa (solo comparable al uso eterno de los aliases
de IPs, pero esta es otra historia).</p>
<pre>
# ip route ls
default via 10.213.208.1 dev eth0
10.213.208.0/20 dev eth0 proto kernel scope link src 10.213.218.162
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.1
192.168.11.0/24 dev eth1 proto kernel scope link src 192.168.11.1
</pre><p>¿Todo este rollo para ver lo mismo con un formato diferente?</p>
<p>Sí, porque estamos viendo, una vez más, solo una parte de la tabla de
enrutamiento. ¡Hola "ip rule"!</p>
<pre>
# ip rule ls
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
</pre><p>Y ahora, sorpresa:</p>
<pre>
# ip route ls table default
_vacio_
# ip route ls table main
default via 10.213.208.1 dev eth0
10.213.208.0/20 dev eth0 proto kernel scope link src 10.213.218.162
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.1
192.168.11.0/24 dev eth1 proto kernel scope link src 192.168.11.1
# ip route ls table local
broadcast 10.213.208.0 dev eth0 proto kernel scope link src 10.213.218.162
local 10.213.218.162 dev eth0 proto kernel scope host src 10.213.218.162
broadcast 10.213.223.255 dev eth0 proto kernel scope link src 10.213.218.162
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
broadcast 192.168.10.0 dev eth1 proto kernel scope link src 192.168.10.1
local 192.168.10.1 dev eth1 proto kernel scope host src 192.168.10.1
broadcast 192.168.10.255 dev eth1 proto kernel scope link src 192.168.10.1
broadcast 192.168.11.0 dev eth1 proto kernel scope link src 192.168.11.1
local 192.168.11.1 dev eth1 proto kernel scope host src 192.168.11.1
broadcast 192.168.11.255 dev eth1 proto kernel scope link src 192.168.11.1
</pre><p>Vaya sorpresa... Lo que vemos con un "route -n" es en realidad la tabla
"main", que además, por ese 32766, parece tener menos prioridad que esa
tabla "local" tan curiosa. Fácil de leer, ¿Verdad? Los broadcast los
conocemos todos pero, ¿Y las rutas de tipo local? Sacado literalmente
del manual (Sí, todo esto está en el manual de ip route!!!): "the
destinations are assigned to this host. The packets are looped back and
delivered locally". Y con esto hemos terminado, solo nos queda saber que
"proto kernel" es la configuración más normal si no usamos software de
enrutamiento (quagga, por ejemplo), y que "scope link" es para rutas
broadcast y unicast mientras que "scope host" es para las locales.</p>
<p>Revisad los manuales de "ip rule" e "ip route", por favor, y entended
cada entrada de estas tablas y reglas.</p>
<p>Ya que estamos, vamos a jugar un poco con todo esto que hemos visto,
aunque seguimos en "modo sencillo" y no nos vamos a complicar demasiado.
Solo unas ideas.</p>
<div class="alert alert-info"><strong>Nota</strong>: No hagáis caso a las IPs que uso a partir de aquí. Intentaré mantenerlas coherentes, pero me las estoy inventando sobre la marcha.
</div><p>Tenemos reglas en "ip rule", tenemos tablas de enrutamiento, ...
¿Apostamos a que todo esto es modificable/configurable? ¡Por supuesto!</p>
<p>Vamos a vuestra distribución Debian favorita, y busquemos el fichero
"/etc/iproute2/rt_tables"</p>
<pre>
# cat /etc/iproute2/rt_tables
#
# reserved values
#
255 local
254 main
253 default
0 unspec
#
# local
#
#1 inr.ruhep
1001 proveedor1
1002 proveedor2
</pre><p>No, en vuestros ficheros no van a estar las líneas "proveedor1" y
"proveedor2". Las he añadido yo, alegremente. Donde estoy escribiendo
este post no hay múltiples lineas de acceso a Internet, pero me voy a
inventar, como ejemplo, que en mi equipo hay dos ADSL de proveedores
diferentes. Uno me ha asignado la IP 192.0.2.33/24, con puerta de enlace
192.0.2.1, y el segundo 198.51.100.33/24, con gateway 198.51.100.1.</p>
<p>Como ya sabemos todo sobre enrutamiento, queremos mandar a los
comerciales a través del primer proveedor, y a los técnicos a través del
segundo. Supongamos que los comerciales están todos en la subred
"172.16.0.0/24", y los técnicos en "172.16.1.0/24".</p>
<p>¡Juguemos con ip rule!</p>
<pre>
# ip rule add from 172.16.0.0/24 table proveedor1
# ip rule add from 172.16.1.0/24 table proveedor2
# ip rule ls
0: from all lookup local
32764: from 172.16.1.0/24 lookup proveedor2
32765: from 172.16.0.0/24 lookup proveedor1
32766: from all lookup main
32767: from all lookup default
</pre><p>Efectivamente, hemos separado el tráfico en dos tablas, por ahora
vacías. Es el turno de ip route:</p>
<pre>
# ip route add 172.16.0.0/16 dev eth0 src 172.16.0.1 table proveedor1
# ip route add 192.0.2.0/24 dev eth1 src 192.0.2.33 table proveedor1
# ip route add default via 192.0.2.1 table proveedor1
# ip route add 172.16.0.0/16 dev eth0 src 172.16.0.1 table proveedor2
# ip route add 198.51.100.0/24 dev eth2 src 198.51.100.33 table proveedor2
# ip route add default via 198.51.100.1 table proveedor2
</pre><p>¿Veis lo que hemos hecho? Hemos asignado dos puertas de enlace por
defecto diferentes, en base al origen, por lo que todo lo que venga
desde 172.16.0.0/24 irá por 192.0.2.1, y lo originado en 172.16.1.0/24
por 198.51.100.1. Por si fuera poco, el tráfico que no entre en ninguna
de estas dos subredes accederá a la tabla main (from all lookup main), y
con ello usará la puerta de enlace por defecto de toda la vida, esa que
pensábamos que era única.</p>
<p>Y ya está, aquí lo dejo, aunque debo recordaros que esto no es una guía
tipo copy/paste, ni remotamente.</p>
<p><strong>Notas</strong>:</p>
<ul class="simple">
<li>Como no podía ser de otra forma, el direccionamiento interno de este
post no puede salir directamente a Internet. Hay que hacer SNAT (o el
masquerade de toda la vida) en iptables. Lo normal es que conntrack
haga magia y sepa dónde mandar vuestro tráfico, pero también es
posible que se tenga que jugar más a fondo con ello.</li>
<li>Las entradas que añaden rutas a las tablas están simplificadas. Sería
deseable completarlas, por ejemplo para evitar avisos de ICMP sobre
mejores rutas si quisiésemos mandar tráfico entre redes. Lo dicho, no
es más que un ejemplo.</li>
<li>En función de las configuraciones, interfaces, firewalls, ... puede
ser posible que se tenga que cambiar algún parámetro de kernel, como
por ejemplo, lo relacionado con rp_filter (Google es vuestro amigo).
Por supuesto, ni que decir ip_forward.</li>
<li>Una vez más, esto es un mínimo ejemplo de lo mucho que se puede
hacer. Las reglas, sin ir más lejos, pueden definirse por IP origen,
destino, o incluso por marca de firewall, con lo que las
posibilidades son enormes.</li>
<li>Os recomiendo completar lo visto en este post con todo lo que hay
alrededor del enrutamiento en un kernel normal, con soporte
Netfilter. <a class="reference external" href="http://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg">Pista</a>.</li>
</ul>
Documentación útil2012-02-25T19:29:00+01:002012-02-25T19:29:00+01:00tag:www.forondarena.net,2012-02-25:/documentacion-util.html<p>La verdad, a estas alturas todavía no conozco a demasiada gente a la que
le guste documentar. Es más, cuanto más técnico es uno, menos gusta.</p>
<p>Cada uno tendrá sus motivos, pero en mi caso, saber que nadie lee los
documentos internos de instalaciones, actualizaciones, etc, que he
escrito no …</p><p>La verdad, a estas alturas todavía no conozco a demasiada gente a la que
le guste documentar. Es más, cuanto más técnico es uno, menos gusta.</p>
<p>Cada uno tendrá sus motivos, pero en mi caso, saber que nadie lee los
documentos internos de instalaciones, actualizaciones, etc, que he
escrito no me termina de hacer gracia.</p>
<p>Veamos el entorno en el que suelen moverse este tipo de documentos:</p>
<ul class="simple">
<li>Son manuales que describen la instalación de servidores y
aplicaciones, y que suelen guardarse en formato doc, pdf o similares
en sistemas de gestión documental.</li>
<li>Su lógica es que un nuevo empleado pueda revisar el documento y que
le sirva como referencia de la infraestructura.</li>
<li>Describen la instalación, paso a paso, de un servidor, por ejemplo de
correo electrónico, las aplicaciones que ejecuta, ficheros de
configuración, ....</li>
<li>Pretenden estar siempre actualizadas.</li>
</ul>
<p>¿Cuál es el problema?</p>
<p>Según lo veo yo, y al igual que pasa con tantos otros "deberes" del
trabajo (partes de actividad, gestión de procesos, ...), rompen
completamente el flujo de trabajo del empleado: Para documentar, o para
rellenar un parte de actividad, uno tiene que dejar lo que está
haciendo, abrir libreoffice, buscar la plantilla, escribir texto, ....
viene trabajo urgente, se aparca el documento, se retoma a los dos días,
no se recuerda lo que se ha escrito, se repasa, ... otra vez trabajo
prioritario, se vuelve a dejar el documento, ....</p>
<p>Y con las actualizaciones es todavía peor. No sé yo si mucha gente
actualiza sus documentos cuando cambia algo de sus servidores (algo
presumible en sistemas "vivos"), pero supone volver a dejar la rutina
del trabajo, abrir el documento, buscar lo que hay que cambiar, redactar
las modificaciones, repasar el documento, corregirlo, ....</p>
<p>Los wikis, que también se usan a menudo, tampoco están exentos de estos
problemas: No siempre es fácil ir al navegador, buscar un documento que
se escribió hace tiempo, modificarlo, ver referencias, mantenerlo al
día, ....</p>
<p>Y para colmo, ¿Necesita esta documentación un nuevo empleado? Debemos
suponer que alguien que va a trabajar con servidores de correo
electrónico sabe como se instalan y lo que significa cada opción de
configuración ¿Verdad?.</p>
<p>Con estos antecedentes, aquí van los mandamientos de forondarena.net
para el trabajo de documentación:</p>
<ol class="arabic simple">
<li>Por supuesto, hay que documentar todo aquello que no sea interno:
Manuales para clientes y proveedores, documentos específicos para
grupos de soporte y atención al cliente, ....</li>
<li>Volviendo al trabajo interno de un departamento, sólo se documenta la
definición del producto, quizá en forma de plan de proyecto, con su
alcance, su público objetivo, lo que se pretende conseguir con él,
.... En fin, estas cosas. Se pueden añadir detalles técnicos, pero
más estructurales, de infraestructura, entorno tecnológico, ....</li>
<li>Sólo hay una forma de garantizar que una instalación va a estar
siempre bien documentada: Integrando la documentación en la rutina
del trabajo. Dicho de otra forma, y concretando con un ejemplo,
usando Puppet o similares en las instalaciones (y actualizaciones), y
aprovechando su capacidad para auto-documentarse. Si alguien toca
algo a mano, Puppet lo sobrescribe. Documentación actualizada,
siempre.</li>
<li>Si por algún motivo no se puede usar un sistema de gestión
centralizada, al menos debe usarse software de control de versiones,
ya sea git, subversion, cvs, o el que sea.</li>
<li>La documentación más concreta, más detallada, de ficheros de
configuración y scripts, como por ejemplo el motivo por el que se
bloqueó una red en la configuración de apache, o la incidencia por la
que se decidió aumentar el tamaño máximo de un mensaje en Postfix,
deben estar al alcance del técnico, a ser posible sin tener que mover
la vista de la terminal.</li>
</ol>
<p>Y es este último punto el que me interesa en este post.</p>
<p>No, escribir comentarios en los mismos ficheros de configuración no es
una opción, salvo para cosas puntuales. Además, una cosa es que queramos
poder acceder rápido a la documentación, y otra que no podamos, en algún
momento, pasarla a otros formatos, como html, con enlaces entre
documentos, o pdf.</p>
<p>Entonces,.... Pues sí, sorpresa, ¡man!, el de toda la vida, y algunas
aplicaciones construidas a su alrededor permiten hacer estas cosas, y
mucho más.</p>
<p><strong>Escribiendo páginas de manual</strong></p>
<p>No, no voy a hablar sobre groff, ni mandoc, ni nada similar. Vamos a
simplificar esto, aunque no haya nada complicado en groff, que conste.
El objetivo es escribir documentación rápido, y no caer en los mismos
errores que intentamos evitar.</p>
<p>A partir de ahora, sólo voy a escribir un pequeño resumen de <a class="reference external" href="http://dailyjs.com/2012/02/16/unix-node-community/">este
post</a> del excelente blog <a class="reference external" href="http://dailyjs.com">dailyjs</a>. Muy recomendable.</p>
<p>Por un lado, necesitamos un formato sencillo en el que escribir la
documentación, y por otro, algún tipo de aplicación con la que convertir
este texto en páginas compatibles con man, html, pdf, o lo que
necesitemos.</p>
<p>Entre las opciones para el formato, la elección es <a class="reference external" href="http://daringfireball.net/projects/markdown/syntax">markdown</a> (que por
cierto es interpretado por Github), y en cuanto a la aplicación que
permite su conversión a otros formatos, tenemos varias alternativas,
como <a class="reference external" href="https://github.com/rtomayko/ronn">ronn</a>, o su versión para node.js <a class="reference external" href="https://github.com/isaacs/ronnjs">ronnjs</a>; pero en este post de
ejemplo vamos a usar <a class="reference external" href="http://mantastic.herokuapp.com/">mantastic.herokuapp.com</a>, que es una aplicación
de Heroku que hace todo el trabajo por nosotros.</p>
<p>Pasemos a un ejemplo:</p>
<p>Tenemos un servidor de correo Postfix, con su correspondiente main.cf.
Este fichero se sincroniza a través de Puppet, y se ha ido modificando
en función de bugs, mejoras, incidencias de clientes y similares a lo
largo de los meses.</p>
<p>En markdown, podríamos escribir algo como esto (que por cierto, se
explica solo):</p>
<pre>
main.cf(5) -- Configuración principal de Postfix
================================================
## SYNOPSIS
`main.cf` - Fichero de configuración principal de Postfix.
## DESCRIPTION
El fichero `main.cf` define gran parte de los cambios sobre la instalación base que se hacen a Postfix. Por lo tanto, cualquier cambio que se realice en este fichero debe ser probado en servidores de pre producción.
`main.cf` es gestionado por `puppet(1)`. Los cambios que se apliquen directamente sobre este fichero serán sobreescritos.
En la siguiente lista se encuentran las modificaciones más significativas que se han ido aplicando al fichero:
* `message_size_limit`=20000:
Se aplicó este límite de 20000 bytes a la configuración bla bla bla.
* `dovecot_destination_recipient_limit`=1:
Opción más segura para controlar las entregas locales bla bla bla. El transport dovecot se define en `master.cf(5)`
* `smtpd_client_restrictions`=check_client_access cdb:/etc/postfix/clientes.out:
Se define el fichero `clientes.out(5)`, como parte del grupo de restricciones en fase cliente, para rechazar aquellos clientes que no cumplan las normas de uso de la plataforma.
## INCIDENCIAS
[1234](http://bugtrack.example.com/?id=1234) - Cliente x con problemas de envío.
[5678](http://bugtrack.example.com/?id=5678) - Cliente z que envía miles de mensajes spam.
## HISTORY
2012-02-20, Versión inicial.
## AUTHOR
2012, Forondarena.net postmaster
## COPYRIGHT
Forondarena.net
## SEE ALSO
`puppet(1)`, `master.cf(5)`, `clientes.out(5)`
</pre><p>Si guardamos este fichero como "main.cf.5.md", y lo pasamos por la
aplicación de Heroku, obtendremos una página de manual perfectamente
formateada:</p>
<pre>
curl -F page=@main.cf.5.md http://mantastic.herokuapp.com > main.cf.5
</pre><p>Si por el contrario optáis por usar una instalación local de ronn, o de
ronnjs, se podría generar, además de lo anterior, una serie de páginas
html enlazadas entre sí, tipo wiki, aunque para eso quizá habría que
trabajar algo más el markdown.</p>
<p><strong>Conclusión</strong></p>
<p>Tal y como he venido diciendo, una buena documentación tiene tres
niveles:</p>
<ol class="arabic simple">
<li>Documentación general, de alto nivel, o externa. Puede ser un doc,
pdf, etc.</li>
<li>Documentación sobre instalaciones de máquinas, actualizaciones de
software, etc. Muy dinámica y difícil de mantener si no se integra
completamente en el día a día del trabajo. Los sistemas de
configuración centralizados son la clave.</li>
<li>Documentación detallada sobre scripts y ficheros concretos. No
conozco muchos entornos en los que se tengan realmente bien
documentados todos los scripts y ficheros de configuración. Algunos
son capaces de incluir comentarios inline, pero esto no siempre es
suficiente, y se corre el riesgo de terminar con ficheros de
configuración demasiado "densos". Sólo pueden documentarse
correctamente a través de aplicaciones muy sencillas y directas.</li>
</ol>
<p><strong>Notas</strong></p>
<p>El fichero en markdown y su correspondiente página de manual están
disponibles en <a class="reference external" href="https://github.com/foron/forondarenanet/tree/master/documentacion_util">Github</a>.</p>
<p>Una vez más, el impulso definitivo para publicar esto se debe al post de
dailyjs que he referenciado anteriormente.</p>
<p>Dejo como ejercicio el uso de ronn o ronnjs en línea de comandos, la
generación de páginas html, la gestión centralizada, o incluso montar un
servidor de páginas de manual (pista: tcpserver/tcpclient, netcat,
socat, ...).</p>
rsyslog y zeroMQ2011-11-06T23:59:00+01:002011-11-06T23:59:00+01:00tag:www.forondarena.net,2011-11-06:/rsyslog-y-zeromq.html<p>Con la evolución de los sistemas actuales, cada vez más grandes, con más
elementos interconectados, con más necesidades de comunicación; con el
cloud computing, las nubes, nieblas y demás parafernalia, llevamos ya
tiempo viendo como las diferentes implementaciones de <a class="reference external" href="http://en.wikipedia.org/wiki/AMQP">Advanced Message
Queuing Protocol</a>, como <a class="reference external" href="http://www.rabbitmq.com/">RabbitMQ</a>, van ganando más y más …</p><p>Con la evolución de los sistemas actuales, cada vez más grandes, con más
elementos interconectados, con más necesidades de comunicación; con el
cloud computing, las nubes, nieblas y demás parafernalia, llevamos ya
tiempo viendo como las diferentes implementaciones de <a class="reference external" href="http://en.wikipedia.org/wiki/AMQP">Advanced Message
Queuing Protocol</a>, como <a class="reference external" href="http://www.rabbitmq.com/">RabbitMQ</a>, van ganando más y más seguidores.</p>
<p>Desde el punto de vista de la administración de sistemas, entornos como
<a class="reference external" href="http://nova.openstack.org/">Nova</a> (parte de <a class="reference external" href="http://www.openstack.org/">Openstack</a>) hacen <a class="reference external" href="http://nova.openstack.org/devref/rabbit.html">uso</a> de este protocolo, y de
RabbitMQ más concretamente, para la comunicación entre sus múltiples
componentes.</p>
<p>Hace unos años, y en desacuerdo con la evolución de AMQP, la gente de
<a class="reference external" href="http://www.imatix.com/">Imatix</a> (ojo, es una de las empresas responsables de su implementación
original), decidió apartarse de este protocolo y desarrollar una pequeña
librería, <a class="reference external" href="http://www.zeromq.org/">zeroMQ</a>, con la que se pudieran escribir aplicaciones con
capacidades de comunicación avanzadas, pero de una manera sencilla; de
tal manera que en lugar de un elemento más bien central en la
infraestructura (AMQP), se tuviera un modelo más distribuido, en el que
la mayoría del trabajo se delegase a los extremos (las aplicaciones), y
no a ese nodo central. Usando una analogía de un documento de
<a class="reference external" href="http://www.zeromq.org/docs:welcome-from-amqp">introducción</a> a zeroMQ, estamos hablando de pasar de subversion (AMQP)
a git (zeroMQ).</p>
<p>Bien, ahora que ya tengo vuestro interés (¿Verdad?), vamos a olvidarnos
de la literatura y a buscar formas de aprovechar lo que zeroMQ puede
ofrecernos para el trabajo diario del administrador de sistemas.
Recordemos que al hablar de este tipo de tecnologías nos referimos al
paso de mensajes entre procesos, a su gestión, a su enrutamiento, .... Y
quien dice mensajes, cómo no, puede estar diciendo logs.</p>
<p>Al hablar de logs, por otro lado, hablamos de syslog, de servidores
remotos a los que enviar datos, de ficheros que abrir y procesar, de
plantillas para bases de datos en las que escribir los logs
directamente, y quizá de sistemas más avanzados, como <a class="reference external" href="https://github.com/cloudera/flume/wiki">flume</a>. ¿Sería
posible usar lo bueno que tienen estas aplicaciones, y mejorarlas con lo
que ofrece zeroMQ, sin volvernos locos en el intento?</p>
<p>Algo parecido debieron pensar en <a class="reference external" href="http://blog.aggregateknowledge.com/2011/07/07/real-time-streaming-with-rsyslog-and-zeromq/">Aggregate Knowledge</a>. Sencillamente,
han escrito un par de <a class="reference external" href="https://github.com/aggregateknowledge/rsyslog-zeromq">plugins</a> (de entrada y de salida) para rsyslog,
de tal manera que podamos usar mucho de lo que nos ofrece zeroMQ
(centrándome en el plugin de salida hablamos de sockets pub, push y rep
básicamente) desde aplicaciones externas.</p>
<p>Dicho de otra forma, en una frase, podremos conectarnos en cualquier
momento al rsyslog de un servidor, y ver lo que está logueando desde una
aplicación que nosotros mismos habremos escrito, ya sea con Python,
Perl, C, node.js, PHP, ... o con cualquiera de los muchos lenguajes para
los que hay un <a class="reference external" href="http://www.zeromq.org/bindings:_start">"binding"</a>. De esta manera, podremos procesar datos en
tiempo real o analizar lo que está pasando en una máquina, sin tener que
acceder a ella y sin tener que ejecutar ningún tipo de software
adicional en el servidor.</p>
<p>Antes de seguir, si queréis probarlo vosotros mismos, <a class="reference external" href="http://www.forondarena.net/debian/">aquí</a> os he
dejado un backport de rsyslog 5.8.5 con soporte para zeromq para Debian
Squeeze, y otro .deb de la misma versión de rsyslog para Debian Testing.
En cualquier caso, recordad que son paquetes que se han creado en el
momento en que se ha escrito este post. Por lo tanto, y más para Debian
Testing, es muy posible que las versiones hayan cambiado cuando
instaléis los paquetes, así que cuidado, bajo vuestra responsabilidad.</p>
<p>Aclarado esto, sigamos con el ejemplo.</p>
<p>Recordad que zeroMQ es una librería, y que como tal puede usarse en
cualquier aplicación, sin límites. Accederemos a los logs de rsyslog (me
estoy centrando en el plugin de salida) a través del socket adecuado, y
a partir de ahí es cosa nuestra. En todo caso, hagamos lo que hagamos,
probablemente podremos catalogar nuestra aplicación en uno de estos dos
grandes grupos (es una simplificación, obviamente):</p>
<ol class="arabic simple">
<li>Monitorización: A menudo sólo queremos ver lo que está pasando en un
servidor. No necesitamos procesar los mensajes, nos vale con hacer un
seguimiento de la actividad de una máquina. Cuando se configura
rsyslog para usar sockets tipo "pub", por ejemplo, estamos creando
una especie de feed de datos. Todos los clientes que se conecten a
ese socket (probablemente de tipo "sub") recibirán los mismos datos,
como si fuera una emisión de radio. Además, no necesitamos guardar
los mensajes si no hay nadie escuchando, podemos descartarlos.</li>
<li>Tratamiento: Los sockets tipo "push" balancean la carga
automáticamente entre todos los clientes conectados (probablemente de
tipo "pull"). Esto es perfecto cuando queremos procesar, por ejemplo,
los logs de correo de un servidor. En un primer momento podríamos
empezar con un único cliente escuchando y procesando los datos del
servidor. Si en algún momento esta aplicación no pudiera con el
volumen de logs, sería suficiente con arrancar una segunda instancia,
de tal manera que se dividiría el trabajo entre las dos,
automáticamente, y siempre que fueran lo suficientemente
inteligentes, claro.</li>
</ol>
<p>Podemos complicarnos mucho más, claro, pero lo dejaremos así por ahora.
Como he dicho anteriormente, probablemente haya simplificado demasiado
todo lo que puede hacerse, pero creo que estos dos escenarios describen
bien el potencial de la librería.</p>
<p>Vamos a ver con un par de ejemplos lo sencillo que es todo esto.
Empezamos con nuestro feed de logs.</p>
<pre>
# cat /etc/rsyslog.conf
...
*.* :omzeromq:bind=tcp://*:5557,pattern=pub;RSYSLOG_TraditionalFileFormat
...
*.* -/var/log/syslog
...
</pre><p>Los clientes tendrán que conectarse al puerto 5557. Además, como se
muestra en este ejemplo, el que usemos zeroMQ no significa que no
podamos escribirlos también en otros ficheros.</p>
<p>En Fedora 15, si quisieramos escribir una pequeña aplicación con Python
3.x, sería suficiente con lo siguiente:</p>
<pre>
# yum install python3-zmq
# cat prueba_pub_sub.py
import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
print ("Conectando a servidores…")
socket.connect ("tcp://servidor1.forondarena.net:5557")
socket.connect ("tcp://servidor2.forondarena.net:5557")
print ("Conectados a dos servidores")
socket.setsockopt(zmq.SUBSCRIBE, b"")
while 1:
string = socket.recv()
print ("Recibimos datos: ",string.decode())
</pre><p>Y, básicamente, esto es todo lo necesario. Es más, en este caso, nos
conectamos a dos servidores, "servidor1.forondarena.net" y
"servidor2.forondarena.net", con lo que recibiremos todos los mensajes
que generen ambos. Con "setsockopt" estamos poniendo un filtro para los
mensajes, que en este caso es una cadena vacía, así que no aplicaremos
ninguno. Si abrimos 10 terminales y ejecutamos este python en cada una,
veremos como se muestran los mismos mensajes en todas ellas.</p>
<p>Con los sockets push/pull la cosa es igual de sencilla:</p>
<pre>
# cat /etc/rsyslog.conf
...
*.* :omzeromq:bind=tcp://*:5557,pattern=push;RSYSLOG_TraditionalFileFormat
...
*.* -/var/log/syslog
...
</pre><p>La aplicación cliente tampoco tiene ningún misterio:</p>
<pre>
import zmq
context = zmq.Context()
socket = context.socket(zmq.PULL)
print ("Conectando a servidores…")
socket.connect ("tcp://servidor1.forondarena.net:5557")
print ("Conectados")
while 1:
string = socket.recv()
print ("Recibimos datos: ",string.decode())
</pre><p>En este caso, nos conectamos sólo a "servidor1". La gran diferencia con
el ejemplo anterior es que, si ejecutamos el script en dos terminales,
veremos un mensaje en una, y el siguiente en otra (load balance). Este
modelo es perfecto, por lo tanto, para los casos en los que las
aplicaciones generan una única línea de log independiente por evento. En
todo caso, como es nuestra aplicación la responsable del tratamiento de
los mensajes, tenemos vía libre para hacer lo que queramos con los logs,
ya sea insertándolos en MySQL una vez procesados, o en Hadoop, o en
Cassandra, o en Redis, o en MongoDB, o en ....</p>
<p>En definitiva, creo que la unión de zeroMQ con rsyslog es una estupenda
idea de la gente de Aggregate Knowledge. Además, el que zeroMQ pueda
usarse casi con cualquier lenguaje de programación abre la ventana para
que desarrollemos todo tipo de aplicaciones, ya sean en modo texto o
gráficas, para consola o para interfaces web.</p>
<p><strong>Notas</strong></p>
<p>Este post es extremadamente superficial. AMQP, zeroMQ o rsyslog dan por
sí mismos para páginas y páginas de blogs. Es más, ya las hay, y a
montones. Aquí tenéis un puñado de referencias que os pueden servir para
profundizar en el tema:</p>
<ul class="simple">
<li><a class="reference external" href="http://nichol.as/zeromq-an-introduction">Introducción a zeroMQ</a>. Estupenda introducción a zeroMQ. Empezad
por aquí.</li>
<li><a class="reference external" href="http://zguide.zeromq.org/page:all">Guía de zeroMQ</a>. Magnífica guía de zeroMQ, con ejemplos y
propuestas de patrones.</li>
<li><a class="reference external" href="http://taotetek.wordpress.com/2011/02/02/python-multiprocessing-with-zeromq/">Ejemplo1</a> de aplicación zeroMQ.</li>
<li>Otro <a class="reference external" href="http://blog.aggregateknowledge.com/2011/06/23/batch-acknowledged-pipelines-with-zeromq/">ejemplo2</a> de uno de los patrones propuestos en la guía de
zeroMQ, y unos benchmarks de Aggregate Knowledge.</li>
<li><a class="reference external" href="http://www.imatix.com/articles:whats-wrong-with-amqp">Motivos</a> por los que Imatix no está de acuerdo con la evolución de
AMQP. Cuidado, es pelín denso.</li>
</ul>
<p>No he querido incluir en el post los pasos para preparar los .deb de
rsyslog. Si os interesa, dejad un comentario o un tweet y escribo algo
al respecto.</p>
PXE para instalaciones básicas de CentOS y Debian2011-10-20T22:54:00+02:002011-10-20T22:54:00+02:00tag:www.forondarena.net,2011-10-20:/pxe-para-instalaciones-basicas-de-centos-y-debian.html<p>Otro mini-post que vuelve a salirse completamente de la idea general de
este blog. Ya hay mucha documentación sobre PXE y sobre instalaciones
automatizadas tanto de CentOS como de Debian, pero bueno, a ver si le es
útil a alguien.</p>
<p>En este caso, vamos a montar un sencillo servidor PXE …</p><p>Otro mini-post que vuelve a salirse completamente de la idea general de
este blog. Ya hay mucha documentación sobre PXE y sobre instalaciones
automatizadas tanto de CentOS como de Debian, pero bueno, a ver si le es
útil a alguien.</p>
<p>En este caso, vamos a montar un sencillo servidor PXE, que podremos usar
para hacer instalaciones de CentOS y Debian. Bueno, en realidad, podemos
instalar cualquier cosa, pero en mi caso no suelo necesitar nada más.</p>
<p>Todo esto se debe a que, para las pruebas que hago en mi laboratorio, el
mínimo de máquinas virtuales que necesito no baja de... tres, y al final
tiendo a perder mucho tiempo con las instalaciones, y poco con el
trabajo "de verdad". Por eso, nada mejor que PXE, <a class="reference external" href="http://fedoraproject.org/wiki/Anaconda/Kickstart">kickstart</a> y
<a class="reference external" href="http://wiki.debian.org/DebianInstaller/Preseed">debian-installer</a> para agilizar el proceso.</p>
<p>Ojo, que todo esto, aunque perfectamente válido, no es lo que yo usaría,
a priori, en un entorno real. Hay mucho software que automatiza lo que
vamos a hacer aquí, con un interfaz web, scripts para actualizar
repositorios, .... Por eso, creo que aplicaciones como <a class="reference external" href="https://fedorahosted.org/cobbler/">Cobbler</a> (por
citar una) deben ser la primera opción.</p>
<p>Aún así, para los que queréis aprender cómo se montan estos "inventos",
aquí van unas pinceladas.</p>
<p><strong>El problema</strong></p>
<p>Necesitamos una forma rápida de instalar servidores. En mi caso,
máquinas virtuales. Aunque hoy en día la gente de VmWare, RedHat o
Virsh+libvirt ofrecen alternativas para clonar instancias como churros,
vamos a optar por una solución genérica y mucho más divertida.</p>
<p>Las distribuciones que vamos instalar con este sistema son CentOS y
Debian. Para el caso de las basadas en kickstart, la cosa está muy
clara, pero para Debian, muchos os preguntaréis ¿Por qué no <a class="reference external" href="http://fai-project.org/">FAI</a>?
Bien, la respuesta rápida, en mi caso, es que no quiero instalar un
servidor NFS, y la última vez que miré era un requisito. Como decía,
buscad la solución que más os guste.</p>
<p>Otro problema posterior es la configuración "fina" de las instancias.
Eso no lo voy a documentar aquí, pero como pista: cfengine, puppet, ....</p>
<p><strong>La solución</strong></p>
<p>He dicho que iba a ir rápido, y ya me estoy enrollando.... Venga, lo
primero que necesitamos, como no, es una BIOS que permita instalar los
clientes vía PXE. Por supuesto, tanto Qemu/KVM como VmWare lo permiten,
así que ni una palabra más al respecto.</p>
<p>En cuanto al software necesario para el servidor de instalaciones,
necesitamos un DHCPD razonable (por ejemplo el del <a class="reference external" href="http://www.isc.org/software/dhcp">ISC</a>), un TFTPD
(por ejemplo <a class="reference external" href="http://packages.debian.org/search?keywords=atftp">atftpd</a>) y un servidor web, que en mi caso va a ser
<a class="reference external" href="http://httpd.apache.org/">apache</a>. Situemos todo en contexto:</p>
<ol class="arabic simple">
<li>La máquina virtual arranca y "habla" con DHCPD.</li>
<li>Además de la información IP, se trasmite, vía DHCP+TFTP, pxelinux.0.</li>
<li>En el caso de Debian, también se aprovecha para dar una pista sobre
dónde encontrar el fichero para debian-installer.</li>
<li>Cargamos el menú de arranque con TFTP.</li>
<li>El sistema usa apache para obtener los ficheros ks, debian-installer
y los rpm, deb y demás parafernalia.</li>
</ol>
<p>Empezamos por lo fácil: El contenido web. Tan sencillo como copiar el
contenido de los CDs de instalación de las distribuciones a una ruta del
arbol web.</p>
<pre>
# ls /var/www/instalaciones/centos/6.0-x86_64/
CentOS_BuildTag EULA images Packages repodata RPM-GPG-KEY-CentOS-Debug-6 RPM-GPG-KEY-CentOS-Testing-6
EFI GPL isolinux RELEASE-NOTES-en-US.html RPM-GPG-KEY-CentOS-6 RPM-GPG-KEY-CentOS-Security-6 TRANS.TBL
# ls /var/www/instalaciones/debian/squeeze/
autorun.inf dists firmware g2ldr.mbr install.amd md5sum.txt pool README.mirrors.html README.source setup.exe win32-loader.ini
css doc g2ldr install isolinux pics README.html README.mirrors.txt README.txt tools
</pre><p>Sigamos con la configuración para DHCP. Hemos dicho que, además de las
obvias IPs, también van a ayudar a las Debian con la definición del
fichero para debian-installer. Hay tres bloques de configuración que os
quiero enseñar:</p>
<pre>
# less /etc/dhcp/dhcpd.conf
...
subnet 192.168.10.0 netmask 255.255.255.0 {
option routers 192.168.10.1;
option domain-name-servers 192.168.10.1;
option domain-name "forondarena.net";
option tftp-server-name "inst.forondarena.net";
filename "/instalaciones/pxelinux.0";
}
host centos1 {
hardware ethernet 52:54:00:30:38:c6;
server-name "fn134.forondarena.net";
fixed-address 192.168.10.134;
}
host debian1 {
hardware ethernet 52:54:00:da:4a:92;
server-name "fn135.forondarena.net";
fixed-address 192.168.10.135;
if substring (option vendor-class-identifier, 0, 3) = "d-i" {
filename "http://inst.forondarena.net/instalaciones/squeeze_preseed_135.cfg";
}
}
...
</pre><p>Como veis, para CentOS no trasmitimos nada relacionado con el fichero
kickstart, pero para Debian sí. ¿Por qué? Pues para enriquecer un poco
el post con algo diferente, la verdad. Hay más de una forma de hacerlo.</p>
<p>Con esto ya tenemos la primera fase del arranque. Lo siguiente: El "boot
menu". Y para esto necesitamos TFTP:</p>
<pre>
/srv/tftp/instalaciones/
├── pxelinux.0
├── pxelinux.cfg
│ ├── C0A80A86
│ └── C0A80A87
├── squeeze-x86_64
│ ├── initrd.gz
│ └── vmlinuz
├── centos-6.0-x86_64
│ ├── initrd.img
│ └── vmlinuz
├── msgs
│ ├── centos
│ │ ├── boot.msg
│ │ └── general.msg
│ └── debian
│ ├── f1.txt
│ └── f2.txt
</pre><p>La mayoría de estos ficheros se pueden bajar casi desde cualquier sitio:</p>
<pre>
http://ftp.cz.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot/debian-installer/amd64/
</pre><p>Como el contenido de "msg" es obvio, vamos a los fichero importantes,
C0A80A86 y C0A80A87. ¿Qué clase de nombres son estos? Pues son las IPs,
en hexadecimal, de nuestras dos máquinas virtuales:</p>
<pre>
$ IP_ADDR="192.168.10.134"; printf '%02X' ${IP_ADDR//./ }; echo
C0A80A86
$ IP_ADDR="192.168.10.135"; printf '%02X' ${IP_ADDR//./ }; echo
C0A80A87
</pre><p>El contenido, para CentOS, en C0A80A86:</p>
<pre>
timeout 100
prompt 1
display msgs/centos/boot.msg
F1 msgs/centos/boot.msg
F2 msgs/centos/general.msg
default Centos6
label Centos6
menu label ^Centos 6 amd64
kernel centos-6.0-x86_64/vmlinuz
append initrd=centos-6.0-x86_64/initrd.img ramdisk_size=6878 ip=dhcp ks=http://192.168.10.40/instalaciones/ks_rh6_134.ks
</pre><p>Como veis, hemos especificado la ruta web para el fichero kickstart.</p>
<p>Para Debian, en C0A80A87:</p>
<pre>
# less C0A80A87
menu hshift 15
menu width 49
prompt 1
display msgs/debian/f1.txt
timeout 100
f1 msgs/debian/f1.txt
f2 msgs/debian/f2.txt
menu title Installer boot menu
menu color title * #FFFFFFFF *
...
menu tabmsgrow 18
menu tabmsg Press ENTER to boot or TAB to edit a menu entry
default install
label install
menu label ^Debian Squeeze Texto amd64
menu default
kernel squeeze-x86_64/vmlinuz
append video=vesa:ywrap,mtrr vga=788 initrd=squeeze-x86_64/initrd.gz auto=true priority=critical locale=es_ES console-keymaps-at/keymap=es --
</pre><p>En este caso, no hemos dicho dónde encontrar el fichero para
debian-installer. Ya lo sabemos.</p>
<p>Y ya casi por último, veamos los ficheros kickstart y debian-installer,
a los que vamos a acceder desde apache:</p>
<pre>
# ls -1 /var/www/instalaciones/
...
ks_rh6_134.ks
squeeze_preseed_135.cfg
</pre><p>Todos conocéis el formato de los .ks; son simples y fáciles de leer.</p>
<pre>
#version=RHEL6
install
url --url=http://192.168.10.40/instalaciones/centos/6.0-x86_64/
lang es_ES.UTF-8
keyboard es
network --device eth0 --onboot yes --bootproto static --ip 192.168.10.134 --netmask 255.255.255.0 --gateway 192.168.10.1 --nameserver 192.168.10.1 --hostname fn134.forondarena.net
rootpw --iscrypted contraseña
firewall --service=ssh
authconfig --enableshadow --passalgo=sha512 --enablefingerprint
selinux --enforcing
timezone --utc Europe/Madrid
bootloader --location=mbr --driveorder=vda --append="crashkernel=auto rhgb quiet"
clearpart --all --drives=vda
part /boot --fstype=ext4 --size=200
part pv.gjsqnW-TYE3-SDwW-646R-0SSI-hhDa-ZF6bXm --grow --size=200
volgroup vg_primario --pesize=4096 pv.gjsqnW-TYE3-SDwW-646R-0SSI-hhDa-ZF6bXm
logvol / --fstype=ext4 --name=lv_root --vgname=vg_primario --size=7400
logvol swap --name=lv_swap --vgname=vg_primario --size=588
##repo --name="CentOS" --baseurl=http://192.168.10.1/instalaciones/centos/6.0-x86_64/ --cost=100
halt
%packages
@core
@server-policy
@spanish-support
openssh*
%end
</pre><p>Es un ejemplo autogenerado para una máquina virtual tipo KVM. No le
hagáis mucho caso al contenido en sí. Lo dicho, tenéis un kilo de
documentación, e incluso aplicaciones para hacer estos ficheros.</p>
<p>Otra cosa muy diferente es debian-installer. La verdad, no conozco a
mucha gente que no se haya atascado con esto en algún momento. El
concepto es sencillo, con una opción para cada una de las opciones de
cada uno de los menús que pueden aparecer durante el proceso de
instalación. El problema es que, esto tan fácil de decir, es un listado
.... largo; y con algunos algoritmos, como el del cálculo de espacio
para particiones, nada claros. Además, como suele ser demasiado
frecuente, el que decidió los nombres de las opciones.... bueno, que no
me parece demasiado intuitivo, aunque si estás familiarizado con Debian
te haces enseguida.</p>
<p>No voy a escribir un ejemplo completo. Os vais a tener que conformar con
una pequeña muestra:</p>
<pre>
# cat /var/www/instalaciones/squeeze_preseed_135.cfg
...
# Keyboard selection.
d-i console-tools/archs select at
d-i console-keymaps-at/keymap select es
d-i keyboard-configuration/xkb-keymap select es
### Network configuration
d-i netcfg/choose_interface select eth0
d-i netcfg/disable_dhcp boolean true
# Static network configuration.
d-i netcfg/get_nameservers string 192.168.10.1
d-i netcfg/get_ipaddress string 192.168.10.135
d-i netcfg/get_netmask string 255.255.255.0
d-i netcfg/get_gateway string 192.168.10.1
d-i netcfg/confirm_static boolean true
...
# Root password, encrypted using an MD5 hash.
d-i passwd/root-password-crypted password contraseña
...
### Partitioning
d-i partman-auto/disk string /dev/sda
d-i partman-auto/method string lvm
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-lvm/device_remove_lvm_span boolean true
d-i partman-auto/purge_lvm_from_device boolean true
d-i partman-auto-lvm/new_vg_name string vg_forondarenanet
d-i partman-basicmethods/method_only boolean false
d-i partman-auto/expert_recipe string boot-root :: \
100 100 100 ext3 \
$defaultignore{ } \
$primary{ } \
device{ /dev/sda } \
$bootable{ } \
method{ format } \
format{ } \
use_filesystem{ } \
filesystem{ ext3 } \
mountpoint{ /boot } \
. \
200 1000 -1 ext4 \
$defaultignore{ } \
$primary{ } \
device{ /dev/sda } \
method{ lvm } \
vg_name{ vg_forondarenanet } \
. \
300 4000 -1 ext4 \
$lvmok{ } \
lv_name{ lvroot } \
in_vg{ vg_forondarenanet } \
method{ format } \
format{ } \
use_filesystem{ } \
filesystem{ ext4 } \
mountpoint{ / } \
. \
512 512 512 linux-swap \
$lvmok{ } \
in_vg{ vg_forondarenanet } \
lv_name{ lvswap } \
method{ swap } \
format{ } \
.
d-i partman-auto/choose_recipe select boot-root
d-i partman/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_nochanges boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-lvm/confirm_nochanges boolean true
...
# Individual additional packages to install
d-i pkgsel/include string openssh-server less
d-i pkgsel/upgrade select safe-upgrade
popularity-contest popularity-contest/participate boolean false
d-i finish-install/reboot_in_progress note
d-i debian-installer/exit/halt boolean true
# This will power off the machine instead of just halting it.
d-i debian-installer/exit/poweroff boolean true
...
</pre><p>Y así podría seguir...</p>
<p>En este caso es una máquina virtual para ESX. Me interesa que veáis la
parte de particionado. Aunque no os lo creáis, he definido una partición
primaria /boot de unos 100M, y el resto en LVM, con aproximadamente 512M
de swap, y el resto de espacio disponible para raíz.</p>
<p>Y poco más hace falta. Ahora bien, en este post, las notas que suelo
añadir al final son más importantes que nunca.</p>
<p><strong>Notas</strong></p>
<p>Nota 1: Los ficheros de instalación automática de los ejemplos hacen un
halt una vez finalizada la instalación. ¿Por qué? Porque no tengo
automatizado que la BIOS cambie por arte de magia de arranque por PXE a
arranque desde disco.</p>
<p>Nota 2: Más que una nota, un consejo. No os volváis locos con la
configuración de ficheros de sistema, usuarios y aplicaciones. Tenéis
herramientas mucho mejores para la configuración "fina", así que
plantearos añadir puppet, cfengine, chef, ... en los bloques "%packages"
y "d-i pkgsel/include", y dejarles que hagan su trabajo al arrancar la
máquina. Para CentOS (desde <a class="reference external" href="http://projects.puppetlabs.com/projects/1/wiki/Bootstrapping_With_Puppet">puppetlabs</a>):</p>
<pre>
%post
....
/sbin/chkconfig --level 345 puppet on
/bin/echo "$PUPPETIP puppet" >> /etc/hosts
/bin/echo "nameserver $NAMESERVERIP" >> /etc/resolv.conf
hostname $hostname
# Write out the hostname to a file for reboot.
/bin/echo -e "NETWORKING=yes\nHOSTNAME=$hostname" > /etc/sysconfig/network
/usr/sbin/puppetd -tv
</pre><p>y, como mini-ejemplo, un poco diferente, para Debian:</p>
<pre>
d-i preseed/late_command string in-target wget -P /tmp/ http://servidor_web/script.sh;
in-target chmod +x /tmp/script.sh; in-target /tmp/script.sh
</pre><p>dejando que ese "script.sh" haga cosas como "sed -i
's/START=no/START=yes/'", siguiendo la estructura de Debian de ficheros
en default y todas estas cosas.</p>
<p>Nota 3: Lo más importante. Este post no es suficiente para hacer que las
instalaciones automáticas os funcionen. Necesita trabajo. Si os atascáis
con algo, tweet o comentario, y lo intentaremos solucionar.</p>
Buzones compartidos con Dovecot2011-10-02T22:25:00+02:002011-10-02T22:25:00+02:00tag:www.forondarena.net,2011-10-02:/buzones-compartidos-con-dovecot.html<p>Ahora que ando reestructurando mi laboratorio, voy a aprovechar para
documentar un par de aplicaciones que estoy moviendo al nuevo <a class="reference external" href="http://www.youtube.com/watch?v=VPYegGMmuWA">hierro</a>.</p>
<p>En realidad, esto se sale un poco del objetivo de este blog, sobre todo
teniendo en cuenta que ya hay kilos de documentación sobre, en este
caso, Dovecot; pero …</p><p>Ahora que ando reestructurando mi laboratorio, voy a aprovechar para
documentar un par de aplicaciones que estoy moviendo al nuevo <a class="reference external" href="http://www.youtube.com/watch?v=VPYegGMmuWA">hierro</a>.</p>
<p>En realidad, esto se sale un poco del objetivo de este blog, sobre todo
teniendo en cuenta que ya hay kilos de documentación sobre, en este
caso, Dovecot; pero bueno, a mí me va a servir como referencia rápida, y
quizá os sea de utilidad a alguno de los cuatro que pasáis por aquí.</p>
<p>Empezamos con los buzones compartidos en Dovecot. Como siempre, pretendo
ser lo más práctico posible, así que lo mejor que se me ha ocurrido es
describir exactamente la forma en la que yo mismo tengo montado "el
invento".</p>
<p><strong>El problema</strong></p>
<p>Antes de nada, hablemos sobre las aplicaciones que debemos tener
funcionando antes de empezar:</p>
<p>Necesitamos un servidor de correo propio (probablemente Postfix) en el
que recibir los mensajes, ya sea porque el MX apunta a él o porque
usamos software tipo Fetchmail, por ejemplo. Además, tenemos varios
usuarios en nuestro sistema que acceden a su correo a través de Dovecot.</p>
<p>Entre el correo que reciben, además del privado para cada uno, es muy
probable que los usuarios estén suscritos a varias listas: de seguridad,
de usuarios, anuncios de nuevas versiones de x aplicación, .... Además,
también hay cuentas tipo helpdesk y listas para un departamento o grupo
que deben estar accesibles para varios usuarios simultaneamente.</p>
<p>Algo habitual en estos casos es, por un lado, que cada usuario gestione
sus propias subscripciones a listas de correo, y por otro, para el caso
de las cuentas tipo helpdesk de las que hemos hablado, el mandar una
copia a cada uno de los n usuarios implicados. Obviamente, esto genera
un montón de copias y mensajes repetidos que, aunque no tenga que
suponer un enorme problema, sí que es más propenso a fallos, además de
no dejar de ser "poco elegante".</p>
<p><strong>La solución</strong></p>
<p>Bien, aquí ya entramos en la forma en la que yo lo hago, que no tiene
que ser ni mucho menos la mejor o la única.</p>
<p>Vamos a empezar por el caso más fácil, los buzones públicos:</p>
<p>Por usar un ejemplo concreto, al hablar de "buzón público" me voy a
referir a listas de correo generales y accesibles para todo el mundo. El
único control de acceso que se va a hacer sobre ellas es a nivel de
sistema de ficheros, por lo que recomiendo que los usuarios del sistema
pertenezcan a un grupo concreto (o varios) siempre que quieran acceder a
una u otra lista.</p>
<p>Y digo usuarios del sistema porque cada lista va a ser un usuario de
sistema (mapeada con una cuenta de correo), de tal manera que la
suscripción será única. Si quisieramos hacer que todo el mundo tuviera
acceso a la <a class="reference external" href="http://www.postfix.org/lists.html">lista de usuarios</a> de Postfix, crearíamos el usuario
"postlista", por ejemplo, y nos suscribiríamos a la lista de Postfix con
postlista _A_T_ forondarena.net (no existe ni existirá nunca, que
conste, pero ahora ya tengo un spamtrap más :D ). Como podéis suponer,
el correo que llegue a esta dirección se guardará físicamente en (esta
parte de la configuración de Postfix/Dovecot os la dejo a vosotros):</p>
<pre>
drwxrwx--- 5 postlista postlista 4096 oct 1 17:57 /home/postlista/Maildir
</pre><p>Volviendo a los permisos a nivel de sistema operativo, hay algo
importante a tener en cuenta: Dovecot 2.x (y con ello su agente de
entrega local que usamos en Postfix) crea los ficheros de correo con los
mismos permisos que su directorio principal, y por lo tanto debemos
hacer que Maildir tenga 770, por ejemplo (siempre que optemos por los
permisos a nivel de sistema operativo para limitar el acceso).</p>
<p>Una vez hecho esto vamos a crear un directorio común que nos va a servir
como referencia para que los distintos usuarios "sepan" donde están las
listas. La estructura va a ser la siguiente:</p>
<pre>
ls -lha /home/listas/Maildir
lrwxrwxrwx 1 root root 21 sep 22 22:03 .postlista -> /home/postlista/Maildir
</pre><p>En el directorio "/home/listas" (o cualquier otro, no es un usuario del
sistema) vamos a crear un enlace simbólico para cada uno de los buzones
públicos.</p>
<p>¿Pero cómo sabe un usuario que puede acceder a esa lista pública?</p>
<p>Para esto usaremos los namespaces de IMAP (El que quiera entrar en el
detalle sobre lo que son, que busque el RFC).</p>
<p>Dovecot define los namespaces en un bloque similar al siguiente, a veces
en un único dovecot.conf, a veces en otro fichero. Debian, por ejemplo,
divide los diferentes apartados de configuración en ".conf" diferentes:</p>
<pre>
namespace {
type = public
separator = /
prefix = listas-public/
location = maildir:/home/listas/Maildir:INDEX=~/Maildir/listas-public
subscriptions = no
}
</pre><p>Adaptad la configuración a vuestro gusto, pero lo importante es que
cuando el usuario "pepe" se loguee vía IMAP, va a poder suscribirse a
todo lo listado en el directorio "/home/listas/Maildir", que además va a
aparecer en su Thunderbird en el subdirectorio listas-public. Además,
queremos que cada usuario tenga los ficheros propios de control que usa
Dovecot en su maildir privado.</p>
<p>Esta es la forma fácil de crear buzones públicos.</p>
<p>Sin embargo, IMAP define una extensión para ACLs (los interesados
también tienen RFCs al respecto), con las que podemos definir un control
de acceso mucho más detallado, y que nos permitirán hilar mucho más fino
en lo que puede hacer el usuario x en el buzón del usuario z. Para que
os hagáis una idea, y cogido directamente del <a class="reference external" href="http://wiki2.dovecot.org/ACL">wiki</a> de Dovecot, las
ACLs permiten establecer los siguientes permisos:</p>
<ul class="simple">
<li>lookup: Mailbox is visible in mailbox list. Mailbox can be subscribed
to</li>
<li>read: Mailbox can be opened for reading</li>
<li>write: Message flags and keywords can be changed, except \Seen and
\Deleted</li>
<li>write-seen: \Seen flag can be changed</li>
<li>write-deleted: \Deleted flag can be changed</li>
<li>insert: Messages can be written or copied to the mailbox</li>
<li>post: Messages can be posted to the mailbox by LDA, e.g. from Sieve
scripts</li>
<li>expunge: Messages can be expunged</li>
<li>create: Mailboxes can be created (or renamed) directly under this
mailbox (but not necessarily under its children, see ACL Inheritance
section above) (renaming also requires delete rights)</li>
<li>delete: Mailbox can be deleted</li>
<li>admin: Administration rights to the mailbox (currently: ability to
change ACLs for mailbox)</li>
</ul>
<p>En el wiki de Dovecot podéis ver que letra corresponde a cada permiso.
Por ahora lo dejamos y pasamos a la creación de un buzón compartido. Por
cierto, no sé si está claro que con "buzón" me puedo estar refiriendo
también a carpetas como "Trash", "Sent", o cualquier otra que creemos a
mano.</p>
<p>Vamos a suponer que tenemos un usuario "helpdesk" en el que recibimos
todo el correo destinado a "helpdesk _A_T_ forondarena.net" (otro
spamtrap).</p>
<pre>
/home/helpdesk/Maildir
</pre><p>Helpdesk es otro usuario del sistema, y su Maildir tiene permisos 770,
para limitar el acceso también a nivel de sistema operativo.</p>
<p>Ahora que ya tenemos el usuario del sistema y que estamos recibiendo
correo, vamos a activar el soporte para ACLs en Dovecot. Es muy
sencillo, sólo hay que añadir un par de plugins, ya sea en dovecot.conf,
o en el fichero que defina vuestra distribución.</p>
<pre>
...
mail_plugins = acl
...
protocol imap {
mail_plugins = $mail_plugins imap_acl
}
</pre><p>Y una vez tenemos los plugins, añadimos la configuración básica:</p>
<pre>
plugin {
acl = vfile
}
</pre><p>Recordemos que queremos que el usuario "pepe" pueda acceder a todo el
buzón "helpdesk". Para conseguirlo, vamos a hacer login con el usuario
helpdesk, y vamos a usar el propio protocolo IMAP para dar acceso a
pepe:</p>
<pre>
# openssl s_client -connect 192.168.10.20:993
CONNECTED(00000003)
..... (información SSL) .....
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN] Dovecot ready.
. login helpdesk password
. OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS ACL RIGHTS=texk] Logged in
. setacl INBOX pepe lrwstipekxacd
. OK Setacl complete.
</pre><p>Dicho de otra forma, hemos permitido "lrwstipekxacd" (cada permiso de la
tabla anterior es una de estas letras) en el INBOX de helpdesk al
usuario pepe.</p>
<p>¿Pero cómo sabe pepe que puede acceder a helpdesk?</p>
<p>Pues con los namespaces, claro. Vamos a crear uno de tipo "shared":</p>
<pre>
namespace {
...
type = shared
separator = /
prefix = buzones-shared/%%u/
location = maildir:%%h/Maildir:INDEX=~/Maildir/buzones-shared/%%u
...
}
</pre><p>Igual que con los buzones públicos, cuando pepe acceda a su cuenta va a
ver un directorio buzones-shared en el que aparecerá helpdesk. ¿Verdad?</p>
<p>Pues no, porque no hay forma (con un rendimiento razonable) de hacer que
Dovecot se recorra todos los buzones configurados (todos esos %%h y
%%u), y sepa a cuáles tiene acceso pepe. A fin de cuentas, pepe y
helpdesk son usuarios completamente diferentes, y no hay un enlace en un
directorio claramente identificado, como "/home/listas" en el caso de
los buzones públicos.</p>
<p>¿Entonces qué?</p>
<p>Es aquí donde entran en juego lo que en Dovecot se llaman "directorios",
y que vamos a usar para decir, en este caso a pepe, que puede acceder a
helpdesk. La configuración es la siguiente:</p>
<pre>
plugin {
acl_shared_dict = file:/etc/dovecot/acls/shared-mailboxes
}
</pre><p>Quien dice "file:" dice base de datos o fichero .db. En cualquier caso,
para este ejemplo usaremos un fichero en texto plano,
"/etc/dovecot/acls/shared-mailboxes". Tened en cuenta, una vez más, que
pepe tiene que poder leer el contenido de este fichero, y que además le
interesa poder crear un lock en el directorio mientras está trabajando
con él (vigilad los permisos unix, usad el directorio que queráis).</p>
<p>El contenido de shared-mailboxes es el siguiente:</p>
<pre>
shared/shared-boxes/user/pepe/helpdesk
1
</pre><p>Es una sintaxis que me parece particularmente "retorcida", pero es lo
que hay si no queréis usar base de datos.</p>
<p>Y con esto lo tenemos ya "casi todo" (ver notas al final del post).
Cuando pepe abra su Thunderbird y liste las carpetas a las que puede
suscribirse verá el inbox de helpdesk, en este caso a partir de la
carpeta "buzones-shared".</p>
<p><strong>Notas</strong></p>
<p>Primera nota:</p>
<p>En Dovecot, una vez que definimos un namespace, también tenemos que
definir explicitamente el namespace privado. Por lo tanto, la
configuración completa, en lo que a namespaces se refiere, tiene que
parecerse a esto:</p>
<pre>
namespace {
inbox = yes
location =
prefix =
separator = /
subscriptions = yes
type = private
}
namespace {
location = maildir:/home/listas/Maildir:INDEX=~/Maildir/listas-public
prefix = listas-public/
separator = /
subscriptions = no
type = public
}
namespace {
location = maildir:%%h/Maildir:INDEX=~/Maildir/buzones-shared/%%u
prefix = buzones-shared/%%u/
separator = /
subscriptions = no
type = shared
}
</pre><p>Una vez más, adaptad la configuración a vuestras preferencias. Por
ejeplo, quizá os venga bien añadir un "list = children" en vuestros
namespaces.</p>
<p>Segunda nota:</p>
<p>¿Si habéis hecho pruebas antes de llegar al final, os habéis dado cuenta
de que, una vez configurados los buzones shared, las listas públicas han
dejado de verse?</p>
<p>¡Claro!, hemos empezado a usar ACLs, así que hay que dar acceso a pepe a
las listas. Muy fácil, nos logueamos con el usuario de la lista que
queramos abrir, y damos acceso a pepe:</p>
<pre>
...
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN] Dovecot ready.
. login postlista password
. OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS ACL RIGHTS=texk] Logged in
. setacl INBOX pepe lrwstipekxacd
. OK Setacl complete.
. logout
</pre><p>Tercera nota:</p>
<p>Los ficheros dovecot-acl tienen el siguiente contenido de ejemplo:</p>
<pre>
user=pepe akxeilprwts
</pre><p>Pero no los editéis directamente. Usad setacl, getacl, myrights, ..., o
mejor aún, alguna extensión de Thunderbird que lo haga por vosotros.</p>
<p>Y para terminar, tened en cuenta, una vez más, que este post no es una
referencia para hacer copy/paste. Necesita trabajo (poco) si queréis
hacerlo funcionar.</p>
Gestión de logs con Solandra III2011-08-15T18:50:00+02:002011-08-15T18:50:00+02:00tag:www.forondarena.net,2011-08-15:/gestion-de-logs-con-solandra-iii.html<p>Pasamos a la tercera parte de la serie, en la que ya vamos a ver un
ejemplo concreto de lo que puede ser un sencillo interfaz para la
gestión de logs usando Solandra. Como siempre, todo el código y los
ficheros de configuración más importantes están en <a class="reference external" href="https://github.com/foron/forondarenanet/tree/master/solandra_logs">Github</a>.</p>
<p>Recordemos los …</p><p>Pasamos a la tercera parte de la serie, en la que ya vamos a ver un
ejemplo concreto de lo que puede ser un sencillo interfaz para la
gestión de logs usando Solandra. Como siempre, todo el código y los
ficheros de configuración más importantes están en <a class="reference external" href="https://github.com/foron/forondarenanet/tree/master/solandra_logs">Github</a>.</p>
<p>Recordemos los objetivos que nos hemos marcado:</p>
<ul class="simple">
<li>Ser capaces de gestionar un volumen muy importante de logs, con la
máxima escalabilidad y disponibilidad.</li>
<li>Poder añadir los logs en el sistema de una forma sencilla.</li>
<li>Tener un interfaz web desde el que poder visualizar los datos.</li>
</ul>
<p>Ya hemos hablado sobre la escalabilidad de Cassandra en los posts
anteriores, así que no vamos a volver a entrar en este punto. Veamos los
otros dos:</p>
<p><strong>Inserción de datos en el sistema</strong></p>
<p>Solandra es básicamente una adaptación de Solr, por lo que en realidad
vamos a tratar conceptos propios de esta aplicación en lo que queda de
post. Cuando veamos algo específico de Solandra, lo señalaré.</p>
<p>Uno de los elementos más importantes de la configuración del sistema es
el schema; que viene a ser el lugar en el que se definen los atributos
que forman cada "documento" (correo en este caso) que se quiere indexar.
Como veremos más adelante, una vez creada esta estructura de datos,
usaremos el comando curl para insertarla en el cluster a través de una
url determinada. Además, Solandra permite trabajar con varios schemas
diferentes de manera simultanea.</p>
<p>Simplicando un poco, el schema está compuesto por dos grandes bloques:
La definición de los tipos de datos y la lista de atributos que forman
cada documento.</p>
<p>Solr ofrece muchos tipos de datos ya creados de antemano: numéricos,
texto, fechas, .... Por si esto fuera poco, la aplicación permite
definir nuevos tipos siempre que se considere necesario. Para la gestión
de logs de correo, por ejemplo, nos podría venir bien un tipo específico
para las direcciones email:</p>
<pre>
...
<fieldType name="email" class="solr.TextField" >
<analyzer>
<tokenizer class="solr.PatternTokenizerFactory" pattern="@" />
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.TrimFilterFactory" />
</analyzer>
</fieldType>
...
</pre><p>Este tipo se basa en el TextField clásico, pero forma los tokens
alrededor de la "@". De esta manera, facilitaremos las búsquedas tanto
en base a la parte local de las direcciones como al dominio. Además,
elimina los espacios y convierte las mayúsculas en minúsculas.</p>
<p>El siguiente paso es la definición de los atributos que componen cada
documento, y que por supuesto van a ser de alguno de los tipos
disponibles en el schema.</p>
<pre>
<fields>
<field name="id" type="uuid" indexed="true" stored="true" required="true" />
<field name="in_from" type="email" indexed="true" stored="true" />
<field name="in_size" type="tint" indexed="false" stored="true" />
<field name="in_fentrada" type="tdate" indexed="true" stored="true" />
<field name="in_to" type="email" indexed="true" stored="true" />
<field name="in_to_adicional" type="email" indexed="true" stored="true" multiValued="true" />
<field name="in_fsalida" type="tdate" indexed="true" stored="true" multiValued="true" />
<field name="in_estado" type="string" indexed="false" stored="true" multiValued="true" />
<dynamicField name="*" type="ignored" multiValued="true" />
</fields>
</pre><p>Casi no hace falta explicar nada. En este sencillo ejemplo tenemos un
identificador, un origen, un tamaño, una fecha de entrada y una fecha de
entrega. Además, con cada mensaje vamos a guardar el estado de entrega
para cada destino (pueden haberse realizado varios intentos, por ejemplo
a causa del greylisting), y la lista de destinatarios adicionales para
los que iba dirigido.</p>
<p>Obviamente, es una estructura limitada. En un entorno real se debería
guardar mucha más información (antivirus, antispam, expansión de
aliases, ...).</p>
<p>Y con esto ya lo tenemos. Sólo queda volcar el schema en el cluster:</p>
<pre>
curl http://ip_cluster:8983/solandra/schema/correo --data-binary @/root/schema_correo.xml -H 'Content-type:text/xml; charset=utf-8'
</pre><p>Como hemos dicho, al igual que un "/schema/correo", se podría definir un
"/schema/web", por ejemplo, y usarlos simultáneamente.</p>
<p><strong>El volcado de datos</strong></p>
<p>El volcado de datos se puede hacer de varias formas, pero vamos a
limitarnos al uso de ficheros xml. Lo mejor es ver un ejemplo:</p>
<pre>
<add allowDups="false">
<doc>
<field name="in_from">origenspam@example.com</field>
<field name="id">b309842a-1cf7-11e0-9759-f896edbeae14</field>
<field name="in_fentrada">2011-04-30T00:17:24Z</field>
<field name="in_size">941</field>
<field name="in_to">destino1@target.example.net</field>
<field name="in_fsalida">2011-04-30T00:17:25Z</field>
<field name="in_estado">0 - 192.168.10.24_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_DF2BD74CA71/</field>
<field name="in_to_adicional">destino1@target.example.net</field>
<field name="in_to_adicional">destino2@target.example.net</field>
<field name="in_to_adicional">destino3@target.example.net</field>
<field name="in_to_adicional">destino4@target.example.net</field>
</doc>
<doc>
<field name="in_from">origenspam@example.com</field>
<field name="id">b30991b7-1cf7-11e0-9759-e819bb7f7b58</field>
<field name="in_fentrada">2011-04-30T00:17:24Z</field>
<field name="in_size">941</field>
<field name="in_to">destino2@target.example.net</field>
<field name="in_fsalida">2011-04-30T00:17:24Z</field>
<field name="in_estado">0 - 192.168.10.24_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_A945A21CD6B/</field>
<field name="in_to_adicional">destino1@target.example.net</field>
<field name="in_to_adicional">destino2@target.example.net</field>
<field name="in_to_adicional">destino3@target.example.net</field>
<field name="in_to_adicional">destino4@target.example.net</field>
</doc>
<doc>
<field name="in_from">cliente1@example.net</field>
<field name="id">b43de232-1cf7-11e0-9759-d71ac36a357f</field>
<field name="in_fentrada">2011-04-30T00:04:41Z</field>
<field name="in_size">6077</field>
<field name="in_to">greylister@example.com</field>
<field name="in_fsalida">2011-04-30T00:07:27Z</field>
<field name="in_estado">0 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_450_4.7.1_<cliente1@example.net>:
_Sender_address_rejected:_Message_delayed_now.Retry_later,_please./Giving_up_on_172.16.0.14./</field>
<field name="in_fsalida">2011-04-30T00:12:38Z</field>
<field name="in_estado">1 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_450_4.7.1_<cliente1@example.net>:
_Sender_address_rejected:_Message_delayed_now._Retry_later,_please./Giving_up_on_172.16.0.14./</field>
<field name="in_fsalida">2011-04-30T00:31:53Z</field>
<field name="in_estado">2 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_451_4.3.0_<greylister@example.com>:
_Temporary_lookup_failure/Giving_up_on_172.16.0.14./</field>
<field name="in_fsalida">2011-04-30T01:04:43Z</field>
<field name="in_estado">3 - 172.16.0.14_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_CB6A3D4A217/</field>
<field name="in_to_adicional">greylister@example.com</field>
</doc>
</add>
</pre><p>El volcado, otra vez, es muy sencillo.</p>
<pre>
curl http://ip_cluster:8983/solandra/correo/update -F stream.file=/tmp/volcado.xml
</pre><p>La conversión de logs desde el más que probable modo texto de syslog a
xml, y de ahí al cluster de Solandra, queda fuera de esta serie de
posts. De hecho, un servidor piensa que esto es lo realmente importante
y difícil para llevar este proyecto a la práctica de una manera "seria".</p>
<p><strong>El Interfaz</strong></p>
<p>¿Qué mejor que un interfaz web para mostrar los datos que hemos
almacenado en Solandra? ¡Sorpresa! la gente detŕas del proyecto
<a class="reference external" href="https://github.com/evolvingweb/ajax-solr">ajax-solr</a> ya ha hecho la mayoría del trabajo, así que sólo nos queda
modificar un puñado de ficheros, algo de código JavaScript, y ya lo
tendremos: <a class="reference external" href="http://www.forondarena.net/ajaxsolr/frontend/">Demo</a></p>
<p>Un detalle más: No queremos permitir el acceso directo a Solandra desde
la web, así que necesitamos un proxy que filtre las consultas y las
redirija al puerto de Solandra (tcp/8983 por defecto), y que en mi
laboratorio escucha en localhost. En este caso, como casi siempre que
quiero programar algún tipo de servicio para Internet sin dedicarle
mucho tiempo, he usado node.js. Para este ejemplo, y por jugar un poco
con GeoIP, he escrito un sencillo proxy que permite conexiones sólo si
tienen como referer forondarena.net y si vienen desde Europa o América
del Norte. Como no podría ser de otra forma, el código de este proxy
también está disponible en Github.</p>
<p>(Nota: Esta demo es un Solr normal, pero el funcionamiento es idéntico
al de Solandra).</p>
<p><strong>Conclusiones</strong></p>
<p>El "mercado" está lleno de soluciones de todo tipo que nos pueden ayudar
en la gestión de logs. Hay aplicaciones comerciales, como <a class="reference external" href="http://www.splunk.com/">Splunk</a>,
sistemas basados en software libre, como Solr, tecnología que nos puede
permitir crecer "ilimitadamente", como Cassandra, Hadoop o Hbase, pero
que requieren algo de trabajo; y también tenemos los mágnificos sistemas
de bases de datos, como Mysql o Postgresql. ¿Cuál elegir?</p>
<p>En una primera fase, una buena base de datos con un sencillo interfaz
web o un Solr estándar pueden servir perfectamente para gestionar todo
tipo de logs. De hecho, tanto Mysql como Solr ofrecen alternativas para
el particionado que pueden permitir este esquema en la segunda, tercera
o cuarta fase.</p>
<p>Un buen consejo que escuche hace tiempo es el de "no arreglar lo que no
está roto". Sólo deberíamos plantearnos el uso de tecnología que
probablemente no conozcamos tan bien como las anteriores cuando
realmente sea necesario. Llegado ese momento, adelante. Como siempre, la
comunidad detrás del software libre es activa y está dispuesta a ayudar.
Por si esto fuera poco, cada vez son más comunes las empresas que
ofrecen servicios alrededor de este tipo de soluciones, y que pueden
asesorarnos llegado el caso.</p>
Gestión de logs con Solandra II2011-08-05T23:31:00+02:002011-08-05T23:31:00+02:00tag:www.forondarena.net,2011-08-05:/gestion-de-logs-con-solandra-ii.html<p>Seguimos con el segundo post de la serie, en el que pasamos a dar una
descripción un poco más técnica de los componentes necesarios para poner
en marcha todo lo descrito en el primero. No vamos a entrar en demasiado
detalle. En todo caso, una vez conocidas las aplicaciones es …</p><p>Seguimos con el segundo post de la serie, en el que pasamos a dar una
descripción un poco más técnica de los componentes necesarios para poner
en marcha todo lo descrito en el primero. No vamos a entrar en demasiado
detalle. En todo caso, una vez conocidas las aplicaciones es más fácil
buscar información en la red.</p>
<p>Aunque Solandra puede encargarse de la instalación de Cassandra, aquí
vamos a usar los componentes por separado.</p>
<p><strong>Cassandra</strong></p>
<p>Cassandra es un tipo de base de datos creado siguiendo los principios
propuestos por <a class="reference external" href="http://www.allthingsdistributed.com/2007/10/amazons_dynamo.html">Dynamo</a> (Amazon) y por <a class="reference external" href="http://labs.google.com/papers/bigtable.html">BigTable</a> (Google). El que
quiera entrar en detalle tiene <a class="reference external" href="http://www.amazon.com/Cassandra-Definitive-Guide-Eben-Hewitt/dp/1449390412/">bibliografía</a> y mucha documentación
disponible.</p>
<p>Explicar el modelo de datos, la replicación o los niveles de
consistencia va más allá del objetivo de este post. Lo más interesante
en nuestro contexto es dejar claro que Cassandra es una base de datos
completamente distribuida y descentralizada, en la que todas las
máquinas del cluster cumplen el mismo y único rol, sin distinciones
entre "maestros", "esclavos", "catálogos", .... Esto significa que
añadir capacidad a un cluster de Cassandra supone básicamente añadir más
hierro. Nada más.</p>
<p>La instalación puede complicarse todo lo que queramos, pero lo básico
es:</p>
<pre>
# Cuidado con la versión
cd /tmp/ && wget http://apache.rediris.es/cassandra/0.8.2/apache-cassandra-0.8.2-bin.tar.gz
cd /usr/local/ && tar xvzf /tmp/apache-cassandra-0.8.2-bin.tar.gz
</pre><p>A partir de aquí se crean los directorios de datos y logs (por ejemplo
/var/lib/cassandra y /var/log/cassandra), y se adaptan los ficheros de
configuración (que están en /usr/local/apache-cassandra-0.8.2/conf). El
que más nos interesa ahora es cassandra.yaml. En <a class="reference external" href="https://github.com/foron/forondarenanet/tree/master/solandra_logs">github</a> hay un
ejemplo de configuración de este fichero, aunque sea casi por defecto,
de cada uno de los tres nodos que he usado en esta prueba.</p>
<p>Hay un par de opciones de configuración que pueden servir para
comprender la estructura del sistema. Un cluster de Cassandra se
entiende como un anillo en el que cada uno de los nodos gestiona un
volumen determinado de datos (replicación aparte). Básicamente estamos
hablando de una serie de claves (hashes) que se distribuyen de una forma
más o menos equilibrada entre todas las máquinas. Aunque no sea
estrictamente necesario, como para esta prueba he usado un número fijo
de nodos (3), he asignado ya desde el comienzo un 33% de datos a cada
uno. Por supuesto, en un entorno real en el que se añaden y quitan nodos
dinámicamente la gestión es diferente. La configuración en mi
laboratorio es la siguiente:</p>
<pre>
# Nodo 1
initial_token: 0
# Nodo 2
initial_token: 56713727820156410577229101238628035242
# Nodo 3
initial_token: 113427455640312821154458202477256070485
</pre><p>La otra opción que merece la pena comentar es "seed_provider".
Cassandra usa un protocolo tipo <a class="reference external" href="http://wiki.apache.org/cassandra/ArchitectureGossip">Gossip</a> para distribuir la información
entre los nodos. Esto significa que, cuando se añade un nuevo miembro al
cluster, es suficiente con indicarle un servidor activo del mismo. El
protocolo se encarga de propagar esta nueva información en todos los
nodos. Por lo tanto, la configuración se limita a especificar una (o
varias) IPs activas:</p>
<pre>
seed_provider:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: 192.168.10.145
</pre><p>Hay mucha más opciones de configuración, por supuesto, pero lo dejamos
aquí.</p>
<p>En este momento ya se podría ejecutar Cassandra, pero esperaremos a
instalar Solandra.</p>
<p><strong>Solandra</strong></p>
<p>Hablemos antes de lo que es Solr, una vez más, muy muy por encima.</p>
<p>Solr es una plataforma de búsqueda construida sobre la librería Lucene.
Simplificando mucho, y en el contexto de este post, ofrece un interfaz
XML (es el que nos interesa aquí, pero no el único) para añadir
documentos, y un API HTTP a través de cual recibir resultados en formato
JSON (entre otros).</p>
<p>Todo se verá más claro cuando creemos el schema para la gestión de logs.
Para el que quiera profundizar más en Solr, <a class="reference external" href="http://www.amazon.com/Solr-1-4-Enterprise-Search-Server/dp/1847195881/">aquí</a> tiene un libro.</p>
<p>Volvamos a Solandra. La instalación es sencilla. Una vez descargado el
tar.gz desde <a class="reference external" href="https://github.com/tjake/Solandra">githubsolandra</a>, y con ant y los binarios de java en el path,
ejecutamos lo siguiente en cada uno de los nodos:</p>
<pre>
# El nombre del fichero cambia
cd /usr/local && tar xvzf /tmp/tjake-Solandra-4f3eda9.tar.gz
cd tjake-Solandra-4f3eda9/
ant -Dcassandra=/usr/local/apache-cassandra-0.8.2 cassandra-dist
</pre><p>Si todo va bien, en unos minutos la salida estándar mostrará lo
siguiente:</p>
<pre>
....
cassandra-dist:
[copy] Copying 36 files to /usr/local/apache-cassandra/lib
[copy] Copying 8 files to /usr/local/apache-cassandra/conf
[copy] Copying 1 file to /usr/local/apache-cassandra/bin
[echo] Libraries successfully copied into cassandra distribution
[echo] Start the cassandra server with /usr/local/apache-cassandra/bin/solandra command
BUILD SUCCESSFUL
Total time: 2 minutes 43 seconds
</pre><p>Durante la instalación deberían haberse copiado las librerías y scripts
en el arbol de Cassandra. Incluyendo algunos ficheros de configuración,
que en este caso dejamos por defecto, aunque lo normal sería que los
adaptásemos.</p>
<p>Sin más, vamos a arrancar Solandra (y con ello Cassandra), en cada nodo.
Como no hemos hecho ningún cambio en el logueo, vamos a ver todos los
mensajes por la salida estándar:</p>
<pre>
cd /usr/local/apache-cassandra-0.8.2/bin/
./solandra &
</pre><p>Y con esto ya debería estar todo listo. El siguiente paso es añadir el
schema (parecido a como se haría en un Solr estándar), volcar los datos,
y preparar el interfaz web. Pero esto es cosa de un tercer post. No
pensaba escribirlo, pero bueno, este ya es demasiado largo.</p>
Gestión de logs con Solandra I2011-08-02T20:28:00+02:002011-08-02T20:28:00+02:00tag:www.forondarena.net,2011-08-02:/gestion-de-logs-con-solandra-i.html<p>Allá por el 2008, Rackspace(Mailtrust) <a class="reference external" href="http://highscalability.com/how-rackspace-now-uses-mapreduce-and-hadoop-query-terabytes-data">publicaba</a> algunos datos sobre
la forma en la que había ido evolucionado su sistema de gestión de logs
de la infraestructura de correo electrónico, y que por aquel entonces ya
superaba holgadamente los 100GB de crecimiento diario. Junto a este
documento, en una de …</p><p>Allá por el 2008, Rackspace(Mailtrust) <a class="reference external" href="http://highscalability.com/how-rackspace-now-uses-mapreduce-and-hadoop-query-terabytes-data">publicaba</a> algunos datos sobre
la forma en la que había ido evolucionado su sistema de gestión de logs
de la infraestructura de correo electrónico, y que por aquel entonces ya
superaba holgadamente los 100GB de crecimiento diario. Junto a este
documento, en una de las <a class="reference external" href="http://oreilly.com/catalog/0636920010388">referencias bibliográficas</a> sobre <a class="reference external" href="http://hadoop.apache.org">Hadoop</a>,
esta misma empresa explicaba, con algo de detalle técnico, la forma en
la que habían implementado su sistema, basado sobre todo en Hadoop y
<a class="reference external" href="http://lucene.apache.org">Lucene</a>+<a class="reference external" href="http://lucene.apache.org/solr/">Solr</a>.</p>
<p>Aunque Hadoop siga siendo una solución magnífica para la gestión de logs
en el contexto del Software Libre (siempre hablando de volúmenes de
datos realmente muy grandes), en esta serie de posts vamos a ver cómo
podemos llevar la idea de Rackspace a la práctica usando otro tipo de
tecnología, y más concretamente, <a class="reference external" href="http://cassandra.apache.org/">Cassandra</a>.</p>
<p>En realidad, como mis tres lectores no quieren posts demasiado largos,
en lugar de entrar en los detalles de lo que sería una implementación
más o menos "casera", vamos a usar una de las aplicaciones que
<a class="reference external" href="http://www.datastax.com/">Datastax</a> (una empresa que da servicios comerciales para Cassandra)
está potenciando como parte de su ecosistema alrededor de Cassandra, y
que se llama <a class="reference external" href="https://github.com/tjake/Solandra">Solandra</a>.</p>
<p><strong>El problema</strong></p>
<p>Repasemos, simplificando un poco, la evolución de Rackspace:</p>
<ol class="arabic simple">
<li>En una primera fase, los logs se almacenan en máquinas individuales.
Cuando hay alguna incidencia, algún técnico tiene que entrar a hacer
un grep. Si el negocio va bien, llegará un momento en el que el
tiempo perdido haciendo estas búsquedas será, por lo menos,
"crispante".</li>
<li>En la segunda fase, los logs pasan a gestionarse a través de un
syslog centralizado. En realidad, esta no es más que una versión algo
mejorada de la primera evolución, pero al menos facilita el trabajo.
En cualquier caso, en el fondo se sigue perdiendo mucho tiempo en la
búsqueda manual en logs.</li>
<li>La solución más natural en este punto es volcar los datos a una base
de datos, y con ello a algún tipo de interfaz web. Dejaremos a un
lado el desarrollo del frontend y de los scripts que cargan los datos
en la bbdd (que pueden no ser en absoluto triviales, en función de la
complejidad de la plataforma).</li>
</ol>
<p>Hasta aquí, vale, todo es razonablemente sencillo. Sin embargo, cuando
se gestionan digamos que 25 millones de mensajes al día, y cuando se
quieren mantener 2 años de información (es un decir), nos encontramos
con un problema.</p>
<p><strong>¿Cómo se soluciona?</strong></p>
<p>Aquí ya cada uno toma decisiones en función de su capacidad, su
presupuesto, los perfiles que tiene disponibles, .... En algunos casos,
mantener 30 días en base de datos (lo que genera la mayoría de
incidentes), puede ser suficiente. En otros casos, se trabaja con los
mecanismos que ofrecen las bases de datos para escalar (el framework
<a class="reference external" href="http://engineering.twitter.com/2010/04/introducing-gizzard-framework-for.html">Gizzard</a> de Twitter es un estupendo ejemplo, aunque no hablemos de
logs). Y por último, algunos pasan a otras soluciones, ya sean de pago o
libres. En el caso de Rackspace, por ejemplo, su opción fue Hadoop y
Lucene+Solr.</p>
<p>Una vez más, cada una de estas opciones puede ser "la mejor" en función
del entorno en el que se desarrolle. Pero claro, si quiero seguir con
este post tengo que optar por la tercera alternativa, obviamente :) .</p>
<p>Vamos por partes:</p>
<ol class="arabic simple">
<li>Queremos almacenar un volumen de datos muy significativo. Para unos
pocos GB todo esto no tiene demasiado sentido.</li>
<li>Queremos que lo único necesario para aumentar la capacidad sea añadir
nuevo hardware. Nada más. Ni cambios en la programación, ni cambios
en la arquitectura.</li>
<li>Queremos poder hacer consultas complejas sobre estos logs, en base a
origen, destino, rangos de fechas, .... Por ejemplo, sería estupendo
poder consultar todo el spam enviado a cuentas tipo info@ en toda la
plataforma en un periodo de tiempo concreto.</li>
<li>Aunque el tiempo real no es un requerimiento, es preferible poder
hacer estas consultas y recibir los resultados al momento, en una
misma sesión de navegador.</li>
</ol>
<p>Para conseguir los puntos 1 y 2 Hadoop es una solución estupenda. Para 3
y 4 es necesario más trabajo. El acceso "casi inmediato" a los datos se
conseguiría con alternativas como <a class="reference external" href="http://hbase.apache.org/">HBase</a>, tan de moda ahora que
<a class="reference external" href="http://www.facebook.com/note.php?note_id=454991608919#">Facebook</a> ha empezado a usarlo. Además, siempre disponemos de <a class="reference external" href="http://pig.apache.org">Pig</a> y
<a class="reference external" href="http://hive.apache.org">Hive</a> para simplificar las consultas. Ahora bien, de una u otra
manera, con Hadoop es bastante probable que tengamos que programar
bastante código.</p>
<p>La otra alternativa viene de la mano de Cassandra. Una vez más, los
puntos 1 y 2 son inmediatos con esta tecnología. Al igual que con
Hadoop, 3 y 4 no lo son; pero gracias a la aplicación llamada Solandra,
que no deja de ser un Solr que guarda sus índices en Cassandra, podemos
conseguir la capacidad de búsqueda de Lucene, el interfaz tipo REST que
ofrece Solr, y la escalabilidad de Cassandra. Todo en uno.</p>
<p>El post se ha alargado un poco. Dejamos la parte práctica para el
segundo (y último) mensaje de esta serie.</p>
Systemtap para detectar la actividad de ficheros de un proceso2011-07-18T18:16:00+02:002011-07-18T18:16:00+02:00tag:www.forondarena.net,2011-07-18:/systemtap-para-detectar-la-actividad-de-ficheros-de-un-proceso.html<div class="alert alert-info"><strong>Update</strong>: La versión de github del script ya es capaz de monitorizar n pids en ejecución y n programas que todavía no lo estén.
</div><p>Para retomar un poco el blog, y como hace mucho que no escribo nada
sobre SystemTap, me he planteado otro sencillo ejercicio "de repaso".</p>
<p>La verdad …</p><div class="alert alert-info"><strong>Update</strong>: La versión de github del script ya es capaz de monitorizar n pids en ejecución y n programas que todavía no lo estén.
</div><p>Para retomar un poco el blog, y como hace mucho que no escribo nada
sobre SystemTap, me he planteado otro sencillo ejercicio "de repaso".</p>
<p>La verdad es que en la actualidad ya hay un buen montón de artículos,
documentación y ejemplos sobre lo que se puede hacer con SystemTap, ya
sea para la inspección del kernel como para la de aplicaciones tipo
mysql, postgresql o python. El rpm de Scientific Linux 6, por ejemplo,
incluye más de 80 ejemplos de todo tipo, desde el análisis de procesos
hasta el del tráfico de red.</p>
<p>Aún así, como digo, aquí va un ejemplo más.</p>
<p><strong>El problema</strong></p>
<p>Vamos a suponer que tenemos un proceso ejecutando en una máquina, y que
queremos vigilar los ficheros que va abriendo.</p>
<p>Para ir un poco más lejos, queremos que se muestren también los ficheros
abierto por los hijos de ese proceso principal (sólo un nivel por
ahora).</p>
<p>Y ya puestos, queremos poder limitar la monitorización a los ficheros de
ciertas rutas. Por ejemplo, podemos querer mostrar sólo los ficheros que
se abrán en /etc, o en /lib.</p>
<p>Vamos, que el ejercicio es prácticamente una extensión de uno de los
scripts de un post anterior de este mismo blog. Pero que le voy a hacer,
sólo se me ocurren ejercicios sobre ficheros.</p>
<p>Resumiendo, y poniendo un ejemplo, tenemos un servidor postfix, con sus
habituales procesos hijo:</p>
<pre>
master─┬─pickup
│─qmgr
└─tlsmgr
</pre><p>Queremos saber que ficheros abren todos los procesos que cuelgan de
master, ya sean los tres que se están ejecutando en este momento, ya
sean los nuevos que se vayan creando. Además, queremos poder limitar el
número de ficheros vigilados a una serie de rutas determinadas.</p>
<p><strong>La solución</strong></p>
<p>El núcleo de la solución está en dos "probes":</p>
<ul class="simple">
<li>syscall.open.return</li>
<li>syscall.close.return</li>
</ul>
<p>O sea, las llamadas al sistema open y close. Más concretamente, al
momento en el que terminan de ejecutarse (return).</p>
<p>Además, vamos a usar el probe "begin" para leer argumentos, preparar
variables y demás parafernalia.</p>
<p>Comenzamos:</p>
<pre>
probe begin {
procesoArgumento = $1
printf ("El pid que vamos a tratar es %d\n", procesoArgumento);
if (argc > 1) {
for (i=2; i <= argc; i++) {
ficherosEnPath[argv[i]] = argv[i]
numeroPaths += 1
}
}
}
</pre><p>Poca explicación hace falta. En $1 tenemos el primer argumento, que va a
ser el pid a vigilar. A partir del segundo argumento, y de manera
opcional, se pueden incluir una serie de rutas a monitorizar. Todas
estas son llamadas válidas:</p>
<pre>
stap -v monitorFicheros.stp $(pidof master)
stap -v monitorFicheros.stp $(pidof master) /etc
stap -v monitorFicheros.stp $(pidof master) /etc /usr/lib
</pre><p>Una vez definido este bloque básico, pasamos a la llamada al sistema "open":</p>
<pre>
probe syscall.open.return {
proceso = pid()
padre = ppid()
hilo = tid()
ejecutable = execname()
insertarEnTabla = 0
if ( (procesoArgumento == proceso) || (procesoArgumento == padre) || (procesoArgumento == hilo) ) {
if ( (procesoArgumento == proceso) && (env_var("PWD") != "") ) {
pwd = env_var("PWD")
}
localpwd = (isinstr(env_var("PWD"), "/"))?env_var("PWD"):pwd;
filename = user_string($filename)
descriptor = $return
filename = (substr(filename, 0, 1) == "/")?filename:localpwd . "/" . filename;
if ([proceso,padre,hilo,descriptor] in tablaProcesos) {
printf ("{codigo: \"error\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor)
} else {
if (descriptor >= 0) {
if (numeroPaths > 0 ) {
foreach (ruta in ficherosEnPath) {
if (substr(filename, 0, strlen(ruta)) == ruta) {
insertarEnTabla = 1
break
}
}
}
if ( (insertarEnTabla == 1) || (numeroPaths == 0) ) {
tablaProcesos[proceso,padre,hilo,descriptor] = gettimeofday_ms()
tablaFicheros[proceso,padre,hilo,descriptor] = filename
printf ("{codigo: \"open\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, tablaProcesos[proceso,padre,hilo,descriptor])
}
}
}
}
}
</pre><p>A través de las funciones pid(), ppid() y tid() vemos si el proceso que
está ejecutando open en este momento es el proceso que nos interesa o un
hijo suyo.</p>
<p>Si cumple este requerimiento, pasamos al bloque que revisa si el fichero
que se está abriendo está en el path que nos interesa. En este caso,
para hacer el ejercicio más completo, he optado por dar unas cuantas
vueltas "de más", y he usado la función env_var("PWD") para acceder al
entorno del proceso. En la práctica esta no es la mejor forma, y por eso
hay más control en el probe, ya que no siempre existe la variable PWD en
el entorno de los procesos que llegan a este open.</p>
<p>La llamada al sistema open carga las variables $return (el valor de
retorno de la función es el id del descriptor del fichero) y $filename
(el nombre del fichero). Ojo! Cada llamada al sistema tiene unos
argumentos, y con ello "ofrece" unas variables para nuestros scripts.
Por ejemplo, mientras que en open tenemos filename, flags y mode; en
close tenemos fd, filp, files, fdt y retval. El propio SystemTap, Google
y el código fuente del kernel son ... vuestros amigos.</p>
<p>En cualquier caso, lo importante es que, cuando se dan todas las
condiciones, hacemos lo siguiente:</p>
<pre>
tablaProcesos[proceso,padre,hilo,descriptor] = gettimeofday_ms()
tablaFicheros[proceso,padre,hilo,descriptor] = filename
printf ("{codigo: \"open\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, tablaProcesos[proceso,padre,hilo,descriptor])
</pre><p>Con el printf escribimos la información del fichero abierto por salida
estándar. Las dos tablas (tablaProcesos y tablaFicheros) sirven para
guardar el momento en el que se ha abierto el fichero (con una precisión
de milisegundos), y el nombre del fichero en sí mismo, con lo que lo
tendremos más a mano cuando pasemos a la llamada al sistema close. Para
identificar un fichero usamos el índice formado con [el proceso, el
padre, el hilo y el descriptor] del fichero que se ha abierto.</p>
<p>Con toda la información en su sitio ya tenemos la primera parte del
script. Seguimos:</p>
<pre>
probe syscall.close.return {
proceso = pid()
padre = ppid()
hilo = tid()
descriptor = $fd
ejecutable = execname()
if ( ( procesoArgumento == proceso ) || ( procesoArgumento == padre ) || (procesoArgumento == hilo ) ) {
if ([proceso,padre,hilo,descriptor] in tablaProcesos) {
filename = tablaFicheros[proceso,padre,hilo,descriptor]
date = gettimeofday_ms() - tablaProcesos[proceso,padre,hilo,descriptor]
printf ("{codigo: \"close\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, date)
delete tablaProcesos[proceso,padre,hilo,descriptor]
delete tablaFicheros[proceso,padre,hilo,descriptor]
}
}
}
</pre><p>Fácil. Si se está haciendo un close de uno de los ficheros
monitorizados, se calcula el tiempo que ha estado abierto y se muestra
por la salida estándar.</p>
<p>Un ejemplo de ejecución del monitor es el siguiente:</p>
<pre>
# stap -v monitorFicheros.stp $(pidof master)
Pass 1: parsed user script and 76 library script(s) using 96688virt/22448res/2744shr kb, in 120usr/10sys/133real ms.
Pass 2: analyzed script: 8 probe(s), 22 function(s), 7 embed(s), 8 global(s) using 251840virt/48200res/3952shr kb, in 420usr/120sys/551real ms.
Pass 3: using cached /root/.systemtap/cache/02/stap_02e370ac4942b188c248e7ec11ac8e2c_19586.c
Pass 4: using cached /root/.systemtap/cache/02/stap_02e370ac4942b188c248e7ec11ac8e2c_19586.ko
Pass 5: starting run.
El pid que vamos a tratar es 1082
{codigo: "open", proceso: "smtpd", pid: 5817, ppid: 1082, tid: 5817, fichero: "/etc/hosts", descriptor: 12, date: 1310920975000}
{codigo: "close", proceso: "smtpd", pid: 5817, ppid: 1082, tid: 5817, fichero: "/etc/hosts", descriptor: 12, date: 0}
</pre><p>Sorpresa! Los printf se formatéan como datos json, por si alguna vez
quisiera hacer algo con node.js y aplicaciones como <a class="reference external" href="http://logio.org/">log.io</a>.</p>
<p>En cuanto a los "date: 0", se debe a que la precisión de milisegundos es
demasiado baja para estos ficheros y para esta prueba sin tráfico de
correo de ningún tipo.</p>
<p>El código completo (en este post sólo falta la definición de variables
globales) está accesible en <a class="reference external" href="https://github.com/foron/forondarenanet/tree/master/systemtap">github</a>, como siempre.</p>
<p><strong>Limitaciones y trabajo futuro</strong></p>
<p>Como ya he comentado, este script está lejos de ser óptimo. Además de
mi propia incapacidad, en algunos momentos he optado por dar más vueltas
de las necesarias para usar así más funciones y variables.</p>
<p class="strikeout">Mi primer objetivo era permitir la monitorización de procesos que todavía no estuvieran en ejecución. En esta versión, sin embargo, el proceso principal sí tiene que estar ejecutando antes de lanzar el stap. Esto no es algo técnicamente complicado, pero no se hace solo, y no aporta demasiado desde el punto de vista conceptual al post, así que lo he dejado.</p>
<p class="strikeout">¿Y qué pasa si el pidof da más de un pid? Me habéis pillado. Por ahora sólo trato un pid.</p>
<p>Para el que esté interesado, SystemTap permite guardar la salida estándar en un fichero usando el parámetro -o. A partir de aquí es trivial que otro script perl, python, bash, node.js o lo que sea trabaje con él.</p>
Websockets para administradores de sistemas2011-02-26T22:46:00+01:002011-02-26T22:46:00+01:00tag:www.forondarena.net,2011-02-26:/websockets-para-administradores-de-sistemas.html<p>Ya lo he escrito en el post anterior: Node.js es un proyecto muy
interesante, muy adecuado como backend en muchos entornos, y
particularmente en aquellos que dependen en gran medida del rendimiento
I/O. Como ejemplo, aquí va uno de los últimos <a class="reference external" href="http://highscalability.com/blog/2011/2/22/is-nodejs-becoming-a-part-of-the-stack-simplegeo-says-yes.html">casos de éxito</a>.</p>
<p>En este post, además …</p><p>Ya lo he escrito en el post anterior: Node.js es un proyecto muy
interesante, muy adecuado como backend en muchos entornos, y
particularmente en aquellos que dependen en gran medida del rendimiento
I/O. Como ejemplo, aquí va uno de los últimos <a class="reference external" href="http://highscalability.com/blog/2011/2/22/is-nodejs-becoming-a-part-of-the-stack-simplegeo-says-yes.html">casos de éxito</a>.</p>
<p>En este post, además de volver a usar Node.js y Redis (otro proyecto
cada vez más importante), voy a dar cuatro pinceladas sobre una de las
nuevas tendencias que van apareciendo en la web: WebSockets.</p>
<p>Para completar el ejercicio, escribiré un mini-proyecto para graficar
todo tipo de valores numéricos en un plasmoid de <a class="reference external" href="http://techbase.kde.org/Projects/Plasma/Plasmoids">KDE</a>. Bueno, en
realidad en cualquier navegador con soporte WebSockets (la lista es
reducida, como veremos más adelante). Así, pasaremos a un punto de vista
más de administrador de sistemas que de desarrollador web.</p>
<p><strong>¿Qué son los WebSockets?</strong></p>
<p>Internet es cada vez más "web". Las aplicaciones son cada vez más
dinámicas, complejas e interactivas. Esto no es malo, obviamente, pero
supone que mientras que los usuarios tienen más y más medios para
interactuar con las webs, los administradores de estos entornos han
visto como lo que antes se podía gestionar sin mayores problemas, ahora
necesita todo tipo de caches e "inventos" sólo porque el sistema de
votación de videos (por ejemplo) triplica la cantidad de accesos al
sitio y a la base de datos.</p>
<p>Estos problemas no son, ni mucho menos, nuevos. Durante los últimos años
se ha ido desarrollando mucha tecnología, para mejorar la experiencia de
usuario, por supuesto, pero también para que la comunicación
cliente/servidor sea más controlable y eficaz. Lo último: WebSockets.</p>
<p>Simplificando mucho, los WS básicamente ofrecen un canal full duplex
entre el cliente y el servidor. De esta manera, en lugar de abrir una
conexión web tradicional vía ajax (o similar) cada vez que se necesite
un dato, el navegador crea un único canal permanente con el servidor, y
lo usa tanto para enviar como para recibir datos. Esto último es
importante, ya que el servidor puede usar la vía de comunicación
activamente, por ejemplo para enviar un broadcast a todos sus clientes,
y no sólo como respuesta ante una petición.</p>
<p><strong>WS orientado a la administración de sistemas</strong></p>
<p>Entonces, tenemos una nueva tecnología, muy orientada a la web, y por lo
tanto muy fácil de usar. Además, ofrece algo que, aún sin ser tiempo
real, es lo suficientemente ágil como para usarse para cosas como la
monitorización de sistemas, que es precisamente el ejemplo con el que
voy a seguir en este post.</p>
<p>Mi planteamiento inicial era hacer una pequeña aplicación que mostrase
los datos de una base de datos <a class="reference external" href="http://oss.oetiker.ch/rrdtool/">rrd</a>. Sin embargo, esto centraba el
ejercicio demasiado en torno a la programación del módulo para Node.js,
así que he preferido reutilizar los conceptos de otros posts anteriores,
limitando el servidor WebSockets al envío de los datos que encuentre en
una base de datos Redis.</p>
<p>Para hacerlo más interesante, el cliente será un plasmoid de KDE, aunque
no deja de ser una página web normal y corriente, teniendo en cuenta que
voy a usar el motor WebKit para su implementación (se pueden programar
plasmoids en C, python, javascript, ...)</p>
<p><strong>Las limitaciones</strong></p>
<p>El protocolo WebSockets todavía es un <a class="reference external" href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-06">draft</a> del IETF. Ya sólo
teniendo esto en cuenta podemos decir que está "verde".</p>
<p>En cuanto a navegadores, se necesita lo último de lo último en versiones
(Opera, Firefox, Chrome, Safari...). Además, en el caso de Firefox (y
Opera), el soporte, aunque presente, está deshabilitado por algunas
<a class="reference external" href="http://hacks.mozilla.org/2010/12/websockets-disabled-in-firefox-4/">lagunas</a> en torno a la seguridad del protocolo cuando interactúa con
un proxy. Ahora bien, para activarlo noy hay más que cambiar una clave
(network.websocket.override-security-block).</p>
<p>Como ejemplo, para escribir este post he usado Ubuntu 10.10 y Fedora 14,
con sus versiones respectivas de KDE, y con soporte para ejecutar
plasmoids WebKit (cada distribución tiene su nombre de paquete, así que
lo buscáis vosotros :-D ).</p>
<p>En la práctica, si queréis usar WebSockets en una aplicación "de
verdad", hoy en día os recomiendo usar algo como <a class="reference external" href="http://socket.io/">socket.io</a>, que es
capaz de detectar lo que el navegador soporta, y en función de eso usar
WS o simularlo con flash.</p>
<p>Bien, empecemos con la aplicación.</p>
<p><strong>El servidor</strong></p>
<p>Para guardar los datos vamos a usar Redis, con un hash que vamos a
llamar "datos", y en el que guardaremos los valores a graficar.
Traducido a perl:</p>
<pre>
my %datos;
$datos{todos} = 40;
$datos{server1} = 10;
$datos{server2} = 20;
$datos{server3} = 10;
</pre><p>Esto en Redis:</p>
<pre>
redis-cli hset datos todos 40
redis-cli hset datos server1 10
redis-cli hset datos server1 20
redis-cli hset datos server1 10
</pre><p>Para las pruebas he usado un bucle bash:</p>
<pre>
while true; do redis-cli hset datos todos $((10 + $RANDOM % 20)); sleep 1; done;
...
</pre><p>Existen varias formas de escribir un servidor WebSockets, pero pocas tan
fácilies como Node.js y su módulo websocket-server. Tambien vamos a usar
el módulo de Redis, así que:</p>
<pre>
npm install websocket-server
npm install hiredis redis
</pre><p>Una vez instalados los componentes, es el momento de escribir las 70
líneas que hacen falta para tener un servidor 100% funcional y capaz de
gestionar tantos gráficos como queramos:</p>
<pre>
var net = require('net');
var sys = require("sys");
var ws = require("websocket-server");
var redis = require("redis");
var puertoredis = 6379;
var hostredis = 'localhost';
var server = ws.createServer({debug: true});
var client = redis.createClient(puertoredis,hostredis);
var conexion;
var mensaje;
client.on("error", function (err) {
sys.log("Redis error en la conexion a: " + client.host + " en el puerto: " + client.port + " - " + err);
});
server.addListener("connection", function(conn){
var graficomostrado = "";
var servidores = [];
sys.log("Conexion abierta: " + conn.id);
client.hkeys("datos", function (err, replies) {
replies.forEach(function (reply, i) {
sys.log("Respuesta: " + reply);
servidores.push(reply);
});
graficomostrado = servidores[0];
mensaje = {"titulo": graficomostrado, "todos": servidores};
conn.send(JSON.stringify(mensaje));
});
conn.addListener("message", function(message){
var dato = JSON.parse(message);
if (dato) {
if (dato.orden == "changeGraphic") {
graficomostrado = dato.grafico;
conn.send(JSON.stringify({changedGraphic: graficomostrado}));
client.hget("datos", graficomostrado, function (err, reply) {
mensaje = {"inicio":[(new Date()).getTime(), reply]};
conn.send(JSON.stringify(mensaje));
});
} else if (dato.orden == "getData") {
client.hget("datos", graficomostrado, function (err, reply) {
mensaje = {"actualizacion":[(new Date()).getTime(), reply]};
conn.send(JSON.stringify(mensaje));
});
}
}
});
});
server.addListener("error", function(){
sys.log("Error de algun tipo en la conexion");
});
server.addListener("disconnected", function(conn){
sys.log("Desconectada la sesion " + conn.id);
});
server.listen(80);
</pre><p>Así de fácil es. Cuando un cliente establezca una conexión se llamará a:</p>
<pre>
server.addListener("connection", function(conn) {....
</pre><p>En este ejemplo, el trabajo fundamental de este bloque es enviar al
cliente el listado de datos disponibles vía JSON:</p>
<pre>
....
mensaje = {"titulo": graficomostrado, "todos": servidores};
conn.send(JSON.stringify(mensaje));
</pre><p>A partir de aquí, la conexión está establecida. Cada vez que el cliente
la use se llamará a:</p>
<pre>
conn.addListener("message", function(message) {....
</pre><p>En este caso, el servidor atenderá las peticiones de cambio de gráfico y
de actualización de gráfico:</p>
<pre>
var dato = JSON.parse(message);
if (dato.orden == "changeGraphic") {
graficomostrado = dato.grafico;
conn.send(JSON.stringify({changedGraphic: graficomostrado}));
client.hget("datos", graficomostrado, function (err, reply) {
mensaje = {"inicio":[(new Date()).getTime(), reply]};
conn.send(JSON.stringify(mensaje));
});
} else if (dato.orden == "getData") {
client.hget("datos", graficomostrado, function (err, reply) {
mensaje = {"actualizacion":[(new Date()).getTime(), reply]};
conn.send(JSON.stringify(mensaje));
});
}
</pre><p>Y con hacer que el servidor escuche en el puerto 80... ¡A correr!</p>
<pre>
server.listen(80);
</pre><p><strong>El cliente</strong></p>
<p>El código del servidor y del cliente está disponible en <a class="reference external" href="https://github.com/foron/forondarenanet/tree/master/plasmoids_nodejs">Github</a>, así
que voy a limitar la explicación a lo fundamental, dejando a un lado el
uso de Jquery, Jquery-ui o de la excelente librería <a class="reference external" href="http://www.highcharts.com/">Highcharts</a>, que
he usado a pesar de no ser completamente libre, porque me ha permitido
terminar el ejemplo mucho más rápido.</p>
<p>Una aplicación que quiere usar WebSockets debe comprobar si el navegador
lo permite:</p>
<pre>
if (!("WebSocket" in window)) {
// No tiene soporte websockets, deshabilitamos botones.
$('#containergraficos').empty();
$('#containergraficos').html('<p>Sin soporte WebSockets.</p>');
$("#conectar").button({ disabled: true });
} else {
// Sí tiene soporte websockets
socket = new WebSocket(host);
}
</pre><p>A partir de aquí, sólo queda reaccionar ante cada evento:</p>
<pre>
socket.onopen = function(){
.......
}
socket.onmessage = function(msg){
// código íntegro en github
var resultado = JSON.parse(msg.data);
if (resultado.inicio) {
var datos = [];
datos.push({x: parseInt(resultado.inicio[0]), y: parseInt(resultado.inicio[1])});
options.series[0].data = datos;
chart = new Highcharts.Chart(options);
} else if (resultado.actualizacion) {
chart.series[0].addPoint({x: parseInt(resultado.actualizacion[0]), y: parseInt(resultado.actualizacion[1])});
} else if (resultado.changedGraphic) {
$("#grafico").button("option", "label", resultado.changedGraphic);
} else if (resultado.titulo) {
$("#grafico").button("option", "label", resultado.titulo);
var mensaje = {"orden": "changeGraphic", "grafico": resultado.titulo};
socket.send(JSON.stringify(mensaje));
}
}
socket.onclose = function(){
...
}
socket.onerror = function() {
...
}
</pre><p>En definitiva, unos pocos mensajes JSON de un lado para otro del socket.</p>
<p>Además, cada 5 segundos vamos a hacer que Highcharts pida datos nuevos:</p>
<pre>
events: {
load: function() {
var series = this.series[0];
setInterval(function() {
if (socket) {
var mensaje = {"orden": "getData", "grafico": grafico};
socket.send(JSON.stringify(mensaje));
} else {
var x = (new Date()).getTime();
series.addPoint([x, 0], true, true);
}
}, 5000); //5 segundos
}
}
</pre><p><strong>El plasmoid</strong></p>
<p>Evidentemente, hasta ahora no hemos hecho nada que no se pueda ejecutar
en un navegador. Ahora lo empaquetaremos en un plasmoid, aunque no deja
de ser más que un zip con todo el contenido y un fichero
metadata.desktop con la descripción del propio plasmoid, que entre otros
inlcuye:</p>
<pre>
[Desktop Entry]
Name=Forondarenanet
Comment=Pruebas para forondarena.net
Icon=chronometer
Type=Service
X-KDE-ServiceTypes=Plasma/Applet
X-Plasma-API=webkit
X-Plasma-MainScript=code/index.html
X-Plasma-DefaultSize=600,530
</pre><p>KDE tiene una utilidad para probar los plasmoids sin instalarlos,
llamada "plasmoidviewer". Una vez probado, se puede instalar con:</p>
<pre>
plasmapkg -i forondarenanet.plasmoid
</pre><p>Recordad que el fichero no es más que un zip. En este caso, la
estructura es la siguiente:</p>
<pre>
./metadata.desktop
./contents
./contents/code
./contents/code/css
./contents/code/css/images
./contents/code/css/images/ui-bg_gloss-wave_16_121212_500x100.png
...
./contents/code/css/images/ui-icons_cd0a0a_256x240.png
./contents/code/css/jquery-ui-1.8.9.custom.css
./contents/code/css/forondarenanet.css
./contents/code/index.html
./contents/code/js
./contents/code/js/jquery-1.4.4.min.js
./contents/code/js/highcharts.js
./contents/code/js/forondarenanet.js
./contents/code/js/jquery-ui-1.8.9.custom.min.js
./contents/code/img
</pre><p>Y poco más. Para terminar con un ejemplo, cuatro plasmoids mostrando
gráficos diferentes:</p>
<p><img alt="Pantallazo plasmoids" class="img-fluid" src="/images/posts/websockets/plasmoids.png" /></p>
<p>Que por supuesto, sólo generan 4 conexiones, a pesar de estar
actualizándose cada 5 segundos:</p>
<pre>
# netstat -nt
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 192.168.10.136:36903 192.168.10.131:80 ESTABLISHED
tcp 0 0 192.168.10.136:36904 192.168.10.131:80 ESTABLISHED
tcp 0 0 192.168.10.136:36905 192.168.10.131:80 ESTABLISHED
tcp 0 0 192.168.10.136:36906 192.168.10.131:80 ESTABLISHED
</pre>Servidor pop-before-smtp en 67 líneas con nodejs y redis2010-12-01T00:36:00+01:002010-12-01T00:36:00+01:00tag:www.forondarena.net,2010-12-01:/servidor-pop-before-smtp-en-67-lineas-con-nodejs-y-redis.html<p>Llevaba tiempo, la verdad, queriendo escribir un post sobre una de esas
nuevas herramientas que aparecen de vez en cuando, que prometen un
montón de cosas, pero que en realidad nunca sabemos hasta dónde van a
llegar. En este caso me estoy refiriendo a <a class="reference external" href="http://www.nodejs.org">node.js</a>.</p>
<p>Buscando algo que programar …</p><p>Llevaba tiempo, la verdad, queriendo escribir un post sobre una de esas
nuevas herramientas que aparecen de vez en cuando, que prometen un
montón de cosas, pero que en realidad nunca sabemos hasta dónde van a
llegar. En este caso me estoy refiriendo a <a class="reference external" href="http://www.nodejs.org">node.js</a>.</p>
<p>Buscando algo que programar se me ha ocurrido escribir un sencillo
servidor <a class="reference external" href="http://en.wikipedia.org/wiki/POP_before_SMTP">popbeforesmtp</a> con node.js. La verdad es que resulta algo
paradógico usar algo tan nuevo para esto, cuando pbs (vamos abreviando)
es uno de los sistemas de autentificación más odiados del mundo del
correo electrónico, propio de una época en la que el spam casi ni
existía (buff!!). Como decía, no es más que una excusa para hacer algo
"práctico" con node.js.</p>
<p>Los tres lectores de mi blog saben lo que es pbs, pero probablemente no
hayan oido hablar sobre node.js. A ver si sacamos algo en claro de todo
esto.</p>
<p><strong>El problema</strong></p>
<p>Digamos que tenemos un sistema con unos cuantos servidores SMTP que
usamos para enviar correo. Aunque normalmente usamos algún tipo de
validación ESMTP, tenemos que mantener la compatibilidad con algunos
clientes de correo que han venido enviando correo sin ningún problema
sólo a través de validación POP.</p>
<p>Pop before smtp permite que un cliente de correo pueda enviar un mensaje
una vez se haya validado vía POP. El servidor SMTP, por lo tanto, debe
ser capaz de guardar un histórico de conexiones y de permitir envíos,
normalmente durante unos pocos minutos, sin pedir ningún otro tipo de
credencial.</p>
<p>Ya tenemos todos los elementos que necesitamos sobre la mesa. Veamos:</p>
<ul class="simple">
<li>Un servidor SMTP sobre el que implementar "el invento".</li>
<li>Un servidor POP desde el que obtener la información de validación.</li>
<li>Una aplicación que relacione ambos mundos, y que sea capaz de
funcionar tanto en una infraestructura de una máquina como en
clusters de varios servidores POP y SMTP (con todo lo que esto
significa).</li>
</ul>
<p><strong>Un servidor SMTP</strong></p>
<p>Actualmente tenemos en el mercado open source <span class="strikeout">cuatro</span> tres servidores SMTP que podemos usar:</p>
<ul class="simple">
<li>Postfix</li>
<li>Exim</li>
<li>Sendmail</li>
<li><span class="strikeout">Qmail</span></li>
</ul>
<p>Cada uno usa sus propios mecanismos de auntentificación. Para este
ejemplo me voy a centrar en Postfix.</p>
<p>Postfix tiene muchos controles "nativos" para permitir o denegar envíos
de correo. Se pueden establecer políticas de acceso en base a IPs
origen, cuentas origen, destino, helo, contenido, .... De hecho, también
se pueden escribir reglas compuestas que combinen las anteriores.</p>
<p>Sin embargo, para pbs necesitamos mantener un listado de IPs para las
que se permiten envíos(fácil), pero que expiren cada x minutos.</p>
<p>Para estos casos más elaborados Postfix ofrece lo que se llama <a class="reference external" href="http://www.postfix.org/SMTPD_POLICY_README.html">access
policy delegation</a>, y que no es más que un sencillo API para conectarse
con una aplicación externa que se encargará de tomar las decisiones de
validación.</p>
<p>Dicho de otra forma, Postfix enviará a un socket (UNIX o TCP) la
información de la sesión SMTP que se quiere comprobar, y recibirá por
ese mismo socket el "OK" (o no) correspondiente.</p>
<p>El software que vamos a escribir, por lo tanto, va a recibir esto por un
socket:</p>
<pre>
request=smtpd_access_policy
protocol_state=RCPT
protocol_name=ESMTP
client_address=192.168.10.131
client_name=fn131.example.org
reverse_client_name=fn131.example.org
helo_name=example.org
sender=patata@example.org
recipient=nospam@example.com
......
</pre><p>A lo que tiene que responder generalmente o "OK", o "reject", o "dunno".
<a class="footnote-reference" href="#id3" id="id1">[1]</a></p>
<p><strong>Un servidor POP</strong></p>
<p>El que más nos guste (Dovecot, Courier, ...). El único requerimiento es
que escriba en un log parseable los login de usuarios.</p>
<p><strong>Una aplicación encargada de la política</strong></p>
<p>Aquí está lo interesante de este post. Veamos lo que necesitamos:</p>
<ul class="simple">
<li>Una aplicación, en nuestro caso un servidor TCP, que reciba las
conexiones desde Postfix y entregue las respuestas.</li>
<li>Una base de datos de algún tipo que almacene la información. A ser
posible, debe poder distribuirse entre muchos servidores SMTP de
forma transparente.</li>
<li>Un procesador de logs POP capaz de insertar registros en la base de
datos que usará la aplicación.</li>
</ul>
<p>Empecemos por la base de datos. Estamos hablando básicamente de "algo"
sencillo, en lo que guardar un listado de IPs, y que además auto-expire
los registros pasados unos minutos. Además, debe ser lo más rápida
posible y permitir un gran número de conexiones simultáneas.</p>
<p>Vaya, si estamos hablando de....Memcached.</p>
<p>Pros:</p>
<ul class="simple">
<li>Estable. Muchos años en entornos muy importantes.</li>
<li>Perfecta para guardar claves (IPs) y valores (cualquier cosa en este
caso. El rcpt nos podría venir bien).</li>
<li>Expira automáticamente los registros antiguos.</li>
<li>Tiene interfaces perl, python, ....</li>
<li>Rápida.</li>
</ul>
<p>Cons:</p>
<ul class="simple">
<li>¿Cómo usamos una base de datos común en un cluster?</li>
<li>¿Perdemos la información de logins ante un fallo o reinicio de la
aplicación?</li>
</ul>
<p>Por lo tanto, tenemos dos problemas. Memcache no está especialmente
pensado para funcionar en "modo cluster", sincronizando su contenido en
n servidores; y además pierde el contenido cuando se reinicia. O sea, si
tenemos que reiniciar Memcached perdemos los datos de usuarios que ya se
han validado (aunque sólo sea cosa de una media hora).</p>
<p>Obviamente, las ventajas superan a los inconvenientes. Sin embargo, en
el "mercado" tenemos una aplicación que, además de tener los pros,
supera los cons: <a class="reference external" href="http://redis.io/">Redis</a>, que permite un modelo master/slave, y además
permite volcar periódicamente la información de memoria a disco para su
recuperación en caso de caida. Es más, permite tener una especie de redo
log en disco.</p>
<p>Bien, ya tenemos el almacén de información. ¿Cómo añadimos los datos?
Usaremos cualquier aplicación de las muchas que permiten ejecutar una
acción en función de haber encontrado un patrón en un log. Un ejemplo en
perl es <a class="reference external" href="http://mailgraph.schweikert.ch/">mailwatch</a>, pero se puede usar cualquier otra. En el fondo, lo
único que hay que hacer es conectarse al nodo master de Redis, y después
ejecutar el comando "SETEX <host_validado> <segundos> <rcpt>".</p>
<p>Bien, sólo nos queda la aplicación. Necesitamos que:</p>
<ul class="simple">
<li>Escuche en un puerto TCP.</li>
<li>Esté muy orientado a eventos.</li>
<li>Tenga un muy buen rendimiento.</li>
<li>Sea sencillo de programar.</li>
</ul>
<p>Y aquí es donde aparece node.js.</p>
<p><strong>node.js</strong></p>
<p>Hay algo indudable en la informática de hoy en día: Cada vez está más
orientada a la web. En este entorno se usan muchos lenguajes, desde php
a .net; pero si hay uno que ha usado "casi" todo el mundo, ese es
javascript.</p>
<p>Node.js es una forma de llevar javascript fuera del contexto de los
navegadores web. Básicamente es una especie de wrapper para <a class="reference external" href="http://code.google.com/p/v8/">V8</a>, el
compilador de javascript de Google. Y esto es algo muy importante,
porque estamos hablando de uno de los compiladores más avanzados que hay
hoy en día. Evidentemente, node.js no consigue el rendimiento de C, pero
supera con cierta facilidad a perl o python, todo además sin perder la
familiaridad y versatilidad de javascript.</p>
<p>Node.js, además de usar V8, implementa algunas de las funcionalidades
que hacen falta para ejecutarse fuera de un navegador, como la
manipulación de datos binarios.</p>
<p>La otra característica de node es que está completamente orientado a
eventos, y por lo tanto necesita un cierto cambio de chip por parte del
programador. En node.js casi nada es síncrono: Cuando queremos acceder a
un fichero, por ejemplo, en lugar de esperar a que el disco se sitúe en
la posición adecuada y lea los datos, seguimos con la ejecución del
programa y con el resto de eventos, hasta que los datos estén
disponibles. Según <a class="reference external" href="http://developer.yahoo.com/blogs/ydn/posts/2010/07/multicore_http_server_with_nodejs/">Yahoo</a>, gracias a este modelo un único procesador
Xeon a 2.5GHz ejecutando un proxy inverso puede servir cerca de 2100
peticiones por segundo <a class="footnote-reference" href="#id4" id="id2">[2]</a>.</p>
<p>Dejo la instalación de node.js (y de Redis, y de mailgraph, ...) para el
lector. Veamos el código de nuestro servidor:</p>
<p>Empezamos por la configuración de Postfix, en /etc/postfix/main.cf:</p>
<pre>
smtpd_recipient_restrictions = .... check_policy_service inet:127.0.0.1:10000 ....
</pre><p>Nuestro servidor va a escuchar en localhost y en el puerto 10000.</p>
<p>Y ahora vamos con el código, en sus 67 líneas, aunque para ser
correctos, falta la mayoría de la gestión de errores:</p>
<pre>
var net = require('net');
var sys = require("sys");
var redis = require("redis");
var puertoplcy = 10000;
var hostplcy = 'localhost';
var puertoredis = 6379;
var hostredis = 'localhost';
</pre><p>Hemos definido las variables y las librerías que vamos a usar,
incluyendo una "third party" para acceso a Redis que hemos instalado con
(npm install redis).</p>
<pre>
var server = net.createServer(function (stream) {
var client = redis.createClient(puertoredis,hostredis);
client.on("error", function (err) {
sys.log("Redis error en la conexion a: " + client.host + " en el puerto: " + client.port + " - " + err);
});
stream.setEncoding('utf8');
stream.setNoDelay(true);
stream.on('connect', function () {
sys.log("Conexion nueva.");
});
stream.on('data', function (data) {
sys.log("Correo entrante:\n" + data);
var clientaddr, tmpclientaddr;
var rcpt, tmprcpt;
var datos = data.split('\n');
for (var i = 0; i < datos.length; i++ ) {
if (datos[i] && datos[i].match(/client_address=/) ) {
tmpclientaddr = datos[i].split("=");
clientaddr = tmpclientaddr[1].trim();
sys.log("Extraido el client address: " + clientaddr);
} else if (datos[i] && datos[i].match(/recipient=/) ) {
tmprcpt = datos[i].split("=");
rcpt = tmprcpt[1].trim();
sys.log("Extraido el rcpt to: " + rcpt);
}
}
client.get(clientaddr, function (err, reply) {
if (err) {
sys.log("Get error: " + err);
}
if (reply) {
sys.log("Se ha encontrado el host: " + clientaddr + " que ha usado: " + reply);
stream.write('action=ok\n\n');
} else {
sys.log("El host: " + clientaddr + " con el usuario: " + rcpt + " no se ha validado");
stream.write('action=reject 550 Relay denegado.\n\n');
}
});
});
stream.on('end', function () {
sys.log("Fin de la conexion");
stream.end();
});
});
</pre><p>Este es el servidor, con un poco de logueo por consola que en
condiciones normales se debería quitar. Hemos definido eventos para
conexiones nuevas, para cuando entran datos nuevos, ...; tenemos un
callback para cuando leemos datos de Redis, .....</p>
<p>Al final, si tenemos el dato devolvemos un "action=ok\n\n", y si no lo
tenemos pasamos un "action=reject 550 Relay denegado.\n\n", aunque
esto puede variar en función de la configuración de Postfix.</p>
<p>Y por último, hacemos que el servidor escuche en el puerto 10000 de
localhost:</p>
<pre>
server.listen(puertoplcy, hostplcy);
</pre><p>Y ya está. Todo esto lo guardamos en un fichero (pbs_filter.js por
ejemplo), y lo ejecutamos desde una shell:</p>
<pre>
/usr/local/bin/node pbs_filter.js
</pre><p>Bueno, para terminar, una imagen del planteamiento:</p>
<p><img alt="Esquema ejemplo" class="img-fluid" src="/images/posts/nodejs/redis_nodejs.png" /></p>
<p>En definitiva, node.js es otro más de esos proyectos que aparecen
periódicamente y que cogen cierta fuerza. Esto no es nada nuevo; hay
muchos proyectos abandonados que tuvieron "su momento", pero creo que
tiene ciertas características que lo hacen interesante. El tiempo dirá
si tiene futuro.</p>
<p><strong>Notas</strong></p>
<table class="docutils footnote" frame="void" id="id3" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id1">[1]</a></td><td><a class="reference external" href="http://www.postfix.org/">postfix</a> tiene muy buena documentación. RTFineM.</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id4" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id2">[2]</a></td><td>Node.js, igual que Memcached y otros, no es multi-hilo. En el mismo enlace de Yahoo hay una interesante discusión sobre el uso de varios cores.</td></tr>
</tbody>
</table>
Computación distribuida con Hadoop IV2010-05-30T01:35:00+02:002010-05-30T01:35:00+02:00tag:www.forondarena.net,2010-05-30:/computacion-distribuida-con-hadoop-iv.html<p>Antes de que me <a class="reference external" href="http://www.ibm.com/developerworks/linux/library/l-hadoop-1/index.html">adelanten</a>, y sobre todo por las durísimas presiones
por parte de las tres personas que leen este blog :-), voy a terminar
esta serie de posts, hablando sobre una de las herramientas que se han
implementado para dar un mayor nivel de abstracción (o facilidad de uso …</p><p>Antes de que me <a class="reference external" href="http://www.ibm.com/developerworks/linux/library/l-hadoop-1/index.html">adelanten</a>, y sobre todo por las durísimas presiones
por parte de las tres personas que leen este blog :-), voy a terminar
esta serie de posts, hablando sobre una de las herramientas que se han
implementado para dar un mayor nivel de abstracción (o facilidad de uso,
como se prefiera decir) a Hadoop.</p>
<p>Hadoop tiene un problema, o al menos un argumento que se usa en su
contra: Desarrollar una aplicación MapReduce lleva demasiado tiempo. A
pesar de que el Streaming del que hablamos en posts anteriores lo
solucione en parte, es cierto que si queremos trabajar de forma "nativa"
con el framework tenemos que estar preparados para escribir un buen
puñado de líneas de código.</p>
<p>Para dar salida este problema han ido apareciendo distintas
alternativas, entre las que destacan <a class="reference external" href="http://hadoop.apache.org/hive/">Hive</a> (by Facebook) y <a class="reference external" href="http://hadoop.apache.org/pig/">Pig</a> (by
Yahoo). En este post me voy a centrar en esta última, aunque esto no
significa, ni mucho menos, que sea mejor que Hive.</p>
<p>Gracias a Pig se pueden escribir "en 4 lineas" aplicaciones MapReduce.
Dicho de otra forma, permite que nos centremos en el qué queremos hacer
con nuestros datos, y no en el cómo lo hacemos. Se basa en dos
componentes:</p>
<ul class="simple">
<li>Un sencillo lenguaje para escribir los scripts, llamado "pig latin".</li>
<li>El ""compilador"", que trasforma los scripts en aplicaciones
MapReduce para su ejecución en el cluster Hadoop.</li>
</ul>
<p>Junto a esto, Pig tambén ofrece un shell interactivo (se llama grunt),
para poder ejecutar comandos.</p>
<p>Bien, terminada la teoría (fácil, ¿no?), vamos a centrarnos en los
datos, tal y como hemos dicho antes. (Para el que no haya leido los
posts anteriores, este sería un buen momento).</p>
<p>Hasta ahora hemos estado preparando nuestra plataforma de logueo para
poder disponer de años, y quizá terabytes, de logs en un mismo lugar,
barato y redundado, del que poder sacar información útil en unos pocos
minutos. Recordemos:</p>
<ol class="arabic simple">
<li>Los logs de todos nuestros servidores se consolidan en una/varias
máquinas syslog (o similar).</li>
<li>Los logs se copian tal cual en Hadoop. A pesar de que vayan a estar
replicados, no es mala idea tener una copia extra en cinta.</li>
<li>Ejecutamos un primer script MapReduce que lee los scripts y los
adapta al formato que queremos y sobre el que trabajaremos.</li>
<li>Ya no necesitamos los ficheros originales, los podemos borrar de
Hadoop. En caso de desastre, siempre podríamos recurrir a las cintas.</li>
</ol>
<p><strong>¿Cómo guardábamos los logs?</strong></p>
<p>El resultado del reducer.pl son líneas como esta:</p>
<pre>
010031011.AB263566.pc1@mailer.example.org 1268223531 usuario@example.org destinovarios@example.com destinovarios@example.com mail.example.org[10.0.2.34],15853,1268223532,5A4FE29B3B6,43A9B28B357,Mar 10 12:18:52 destinovarios@example.com destinovarios@example.com Antispam sent (250 OK sent queued as 43A9B28B357
</pre><p>Recordad que el script guarda mucha información formateada en base64,
por lo que en realidad tenemos algo similar a esto:</p>
<pre>
2010031011.AB263566.pc1@mailer.example.org 1268223531 usuario@example.org destinovarios@example.com destinovarios@example.com texto_en_base64
</pre><p><strong>¿Por qué?</strong></p>
<p>Porque todas las consultas que se suelen hacer son del tipo:</p>
<ul class="simple">
<li>No he recibido un mensaje enviado entre las x y las z del martes,
enviado desde esta cuenta, y con destino a esta otra.</li>
<li>Quiero todos los mensajes que se recibieron en tal buzón.</li>
</ul>
<p>La estructura de logs que hemos preparado va a permitir que los scripts
sean muy sencillos, sin ningún tipo de join o similar (ahora lo vemos).
Dejaremos al interfaz web desde el que se van a ver los resultados el
peso de formatear las entradas y hacerlas "visuales".</p>
<p><strong>¿Interfaz web?</strong></p>
<p>Nada mejor que un sencillo interfaz web en el que escribir los
parámetros de búsqueda, preparar el script .pig, ejecutarlo, y mostrar
los resultados una vez termine. Evidentemente, no voy a escribir aquí el
formulario, pero por dar un ejemplo, imaginemos que queremos saber los
correos que han llegado desde infojobs entre las 8 a.m. del 11 de marzo,
y las 12 a.m. del mismo día.</p>
<p>Empezamos pasando las fechas a timestamp. Muy fácil:</p>
<pre>
$ date -u -d 'Thu Mar 11 08:00:00 2010' '+%s'
1268294400
$ date -u -d 'Thu Mar 11 12:00:00 2010' '+%s'
1268308800
</pre><p>Con esto y con la dirección origen ya tenemos todo lo necesario. Ahora
lo normal sería escribir un fichero .pig, pero aquí vamos a usar el
intérprete de comandos grunt para ver los resultados paso a paso:</p>
<pre>
$ pig
10/05/25 21:57:29 INFO pig.Main: Logging error messages to: /usr/local/pig-0.7.0/pig_1274817449369.log
2010-05-25 21:57:29,688 [main] INFO org.apache.pig.backend.hadoop.executionengine.HExecutionEngine - Connecting to hadoop file system at: hdfs://fn140
2010-05-25 21:57:30,149 [main] INFO org.apache.pig.backend.hadoop.executionengine.HExecutionEngine - Connecting to map-reduce job tracker at: fn140:8021
grunt>
</pre><p>Empezamos cargando los datos:</p>
<pre>
grunt> REGISTROS = LOAD '/user/hadoop/logs_formateados' USING PigStorage('\t') AS (msgid:chararray, fecha:long, origen:chararray, destinoreal:chararray, destinoentrada:chararray, resto:chararray);
</pre><p>Con este comando hemos cargado los datos desde el directorio
/user/hadoop/logs_formateados en la variable REGISTROS. Los ficheros de
entrada se separan con un tabulador <a class="footnote-reference" href="#id2" id="id1">[1]</a>, con el schema que hemos venido
usando. Veamos, antes de seguir, si Pig ha entendido lo que realmente
queremos:</p>
<pre>
grunt> ILLUSTRATE REGISTROS
2010-05-25 23:58:36,627 [main] INFO org.apache.pig.backend.hadoop.executionengine.HExecutionEngine - Connecting to hadoop file system at: hdfs://fn140
2010-05-25 23:58:36,681 [main] INFO org.apache.pig.backend.hadoop.executionengine.HExecutionEngine - Connecting to map-reduce job tracker at: fn140:8021
2010-05-25 23:58:36,791 [main] INFO org.apache.hadoop.mapreduce.lib.input.FileInputFormat - Total input paths to process : 1
2010-05-25 23:58:36,792 [main] INFO org.apache.pig.backend.hadoop.executionengine.util.MapRedUtil - Total input paths to process : 1
--------------------------------------------------------------------------------------------------------------------
| REGISTROS | msgid: chararray | fecha: long | origen: chararray | destinoreal: chararray | destinoentrada: chararray | resto: chararray |
--------------------------------------------------------------------------------------------------------------------
| | 6B160A8CF29E064XA067E116FA2A458CAD8A66@GLOBL0310V01.exampleserv.com | 1268304751 | pr1user@example.com | mga@example.net | mga@example.net | texto_base64 |
--------------------------------------------------------------------------------------------------------------------
</pre><p>Ahora que tenemos todos los registros, vamos a filtrarlos en base a los
criterios que hemos definido:</p>
<pre>
grunt> FILTRADOS = FILTER REGISTROS BY (origen matches '.+infojobs.+') AND (fecha > 1268294400) AND (fecha < 1268301600);
</pre><p>Hecho esto sólo nos queda agruparlos por msgid. Recordad, tenemos dos
entradas por mensaje: una para el nivel antispam, y otra para la
entrega. En realidad, en base al msgid podríamos seguir un mensaje por
tantos saltos como tuviéramos (algo parecido a lo que Mailtrust ha
documentado que hace):</p>
<pre>
grunt> AGRUPADOS = GROUP FILTRADOS BY msgid;
</pre><p>Y con esto ya tenemos el resultado. Lo podríamos confirmar con el
comando DUMP, que muestra los datos por salida estándar, pero lo vamos a
guardar directamente en un fichero:</p>
<pre>
grunt> STORE AGRUPADOS INTO '/tmp/fichero_resultados.txt' USING PigStorage();
2010-05-26 22:17:03,942 [main] WARN org.apache.pig.PigServer - Encountered Warning IMPLICIT_CAST_TO_LONG 2 time(s).
2010-05-26 22:17:03,946 [main] INFO org.apache.pig.impl.logicalLayer.optimizer.PruneColumns - No column pruned for REGISTROS
2010-05-26 22:17:03,946 [main] INFO org.apache.pig.impl.logicalLayer.optimizer.PruneColumns - No map keys pruned for REGISTROS
2010-05-26 22:17:03,946 [main] INFO org.apache.pig.ResourceSchema - Insert two-level access to Resource Schema
2010-05-26 22:17:03,983 [main] WARN org.apache.pig.PigServer - Encountered Warning IMPLICIT_CAST_TO_LONG 2 time(s).
2010-05-26 22:17:03,985 [main] INFO org.apache.pig.backend.hadoop.executionengine.HExecutionEngine - (Name: Store(hdfs://fn140/tmp/fichero_resultados.txt:PigStorage) - 1-1007 Operator Key: 1-1007)
2010-05-26 22:17:03,986 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MultiQueryOptimizer - MR plan size before optimization: 1
2010-05-26 22:17:03,986 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MultiQueryOptimizer - MR plan size after optimization: 1
2010-05-26 22:17:04,019 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.JobControlCompiler - mapred.job.reduce.markreset.buffer.percent is not set, set to default 0.3
2010-05-26 22:17:05,180 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.JobControlCompiler - Setting up single store job
2010-05-26 22:17:05,188 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - 1 map-reduce job(s) waiting for submission.
2010-05-26 22:17:05,191 [Thread-120] WARN org.apache.hadoop.mapred.JobClient - Use GenericOptionsParser for parsing the arguments. Applications should implement Tool for the same.
2010-05-26 22:17:05,545 [Thread-120] INFO org.apache.hadoop.mapreduce.lib.input.FileInputFormat - Total input paths to process : 1
2010-05-26 22:17:05,546 [Thread-120] INFO org.apache.pig.backend.hadoop.executionengine.util.MapRedUtil - Total input paths to process : 1
2010-05-26 22:17:06,382 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - HadoopJobId: job_201005262134_0008
2010-05-26 22:17:06,382 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - More information at: http://fn140:50030/jobdetails.jsp?jobid=job_201005262134_0008
2010-05-26 22:17:06,384 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - 0% complete
2010-05-26 22:17:19,214 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - 10% complete
2010-05-26 22:17:20,719 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - 20% complete
...............
2010-05-26 22:17:28,241 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - 60% complete
2010-05-26 22:17:41,788 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - 100% complete
2010-05-26 22:17:41,788 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - Successfully stored result in: "hdfs://fn140/tmp/fichero_resultados.txt"
2010-05-26 22:17:41,794 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - Records written : 72
2010-05-26 22:17:41,794 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - Bytes written : 44727
2010-05-26 22:17:41,794 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - Spillable Memory Manager spill count : 0
2010-05-26 22:17:41,794 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - Proactive spill count : 0
2010-05-26 22:17:41,794 [main] INFO org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MapReduceLauncher - Success!
</pre><p>Vaya, parece que tenemos 72 usuarios suscritos a infojobs. Veamos un
ejemplo de lo que se ha escrito en "/tmp/fichero_resultados.txt" (es un
path de HDFS, no el /tmp local). Los saltos de línea los he puesto yo
para que sea más visual, y he escrito la versión sin codificar en
base64:</p>
<pre>
1268304853778.112.POCFQELSVS@[hellboy07]
{
(1268304853778.112.POCFQELSVS@[hellboy07],1268308417,aviso_incripciones@push.infojobs.net,usuario@example.com,
usuario@example.com,localhost[127.0.0.1],11854,1268308418,9ABC12FACB4,-1,
Mar 11 11:53:38 usuario@example.com usuario@example.com Maildrop sent (delivered via maildrop service))
,
(1268304853778.112.POCFQELSVS@[hellboy07],1268308417,aviso_incripciones@push.infojobs.net,usuario@example.com,
usuario@example.com,push3.infojobs.net[79.171.25.71],11107,1268308418,3B0213FAECD,9ABC12FACB4,
Mar 11 11:53:38 usuario@example.com usuario@example.com Antispam sent (250 OK queued as 9ABC12FACB4)
}
</pre><p>Ahora sólo necesitamos que nuestro interfaz web formatee el resultado y
lo saque por pantalla. Fácil y sencillo.</p>
<p>Resumiendo, con estas cuatro líneas podemos hacer cualquier búsqueda en
los logs, tengan el tamaño que tengan:</p>
<pre>
REGISTROS = LOAD '/user/hadoop/logs_formateados' USING PigStorage('\t') AS (msgid:chararray, fecha:long, origen:chararray, destinoreal:chararray, destinoentrada:chararray, resto:chararray);
FILTRADOS = FILTER REGISTROS BY (origen matches '.+infojobs.+') AND (fecha > 1268294400) AND (fecha < 1268301600);
AGRUPADOS = GROUP FILTRADOS BY msgid;
STORE AGRUPADOS INTO '/tmp/fichero_resultados.txt' USING PigStorage();
</pre><p><strong>Referencias</strong></p>
<p>Hay muchos artículos y documentación disponible en la red sobre Hadoop.
En cuanto a bibliografía, que yo sepa hay al menos tres libros
disponibles:</p>
<ul class="simple">
<li>Pro Hadoop, de la editorial Apress.</li>
<li>Hadoop: The Definitive Guide, de la editorial O'Reilly.</li>
<li>Hadoop in Action, de la editorial Manning.</li>
</ul>
<p><strong>Notas</strong></p>
<table class="docutils footnote" frame="void" id="id2" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id1">[1]</a></td><td>La verdad es que he hecho trampa. El reducer original separaba el id de mensaje del resto de datos con un tabulador, pero luego usaba un espacio simple. Esto es porque al principio tenía pensado trabajar otros aspectos de Hadoop y Pig, como el Chaining y las "user defined functions". Al final, para no tener que escribir más posts, he cambiado el reducer para que separe todos los campos con tabuladores.</td></tr>
</tbody>
</table>
Computación distribuida con Hadoop III2010-03-21T23:23:00+01:002010-03-21T23:23:00+01:00tag:www.forondarena.net,2010-03-21:/computacion-distribuida-con-hadoop-iii.html<p>En este tercer post de la serie dejamos la teoría para ver un caso
práctico de aplicación de Hadoop, que completaré en un cuarto post.</p>
<p>Como sabemos, gracias a Hadoop conseguimos un sistema con mucha
capacidad de almacenamiento, a un coste por mega reducido, redundado, de
rendimiento prácticamente lineal (más …</p><p>En este tercer post de la serie dejamos la teoría para ver un caso
práctico de aplicación de Hadoop, que completaré en un cuarto post.</p>
<p>Como sabemos, gracias a Hadoop conseguimos un sistema con mucha
capacidad de almacenamiento, a un coste por mega reducido, redundado, de
rendimiento prácticamente lineal (más máquinas = más rendimiento) y
sobre el que ejecutar de forma pararela todo tipo de aplicaciones que se
puedan implementar como MapReduce.</p>
<p>Con estos datos, hay una utilidad práctica que viene inmediatamente a la
mente: tratamiento de logs. Veamos:</p>
<ul class="simple">
<li>Me gustaría decir lo contrario, pero personalmente no conozco a nadie
que maneje el dinero destinado a proyectos que no ponga pegas a una
inversión fuerte para el tratamiento de logs. El coste por mega de
Hadoop es difícil de superar.</li>
<li>Si combinamos Hadoop, por ejemplo para datos con hasta un año de
antiguedad, con un soporte en cinta para el resto, lo tendremos fácil
para cumplir cualquier legislación en lo relacionado a retención de
datos.</li>
<li>Sin llegar a los niveles de Facebook o Rackspace, es probable que el
volumen de logs que guardemos durante un año sea lo suficientemente
grande como para hacer difícil su tratamiento si un juez (por
ejemplo) pide todos los correos desde la cuenta x a la cuenta z de
los últimos siete meses, o todos los acceso al foro web de fulanito.
MapReduce y Hadoop permiten ejecutar este tipo de tareas sin mayores
problemas, y con un rendimiento prácticamente lineal.</li>
</ul>
<p>En este entorno, planteo un ejemplo de tratamiento de logs de Postfix de
lo más simple, con un filtro de contenido en modo post-queue (Amavis,
por ejemplo, en su uso más habitual). Vamos, nada nuevo.</p>
<p>El objetivo sería, además de los puntos mencionados anteriormente, poder
realizar cualquier tipo de consulta sobre los logs de meses (o años), en
un tiempo razonable. Y digo tiempo razonable porque en este tipo de
consultas no suele hacer falta tener el resultado al momento. Se acepta
esperar durante 1,5 o 10 minutos hasta que se genere el resultado.</p>
<p>Resumiendo, tenemos una infraestructura parecida a esta:</p>
<p><img alt="Infraestructura Hadoop y Amavis" class="img-fluid" src="/images/posts/hadoop/infra_postfix_amavis.gif" /></p>
<p>Todos los servidores envían los logs de manera centralizada a un syslog
remoto, que además de hacer una copia diaria a cinta, almacena la
información en Hadoop, usando alguno de los muchos interfaces que se
ofrecen, como pueden ser FUSE o FTP.</p>
<p>Junto a esto tendremos una máquina, preferiblemente un interfaz web,
para generar trabajos MapReduce. Básicamente necesitaremos un formulario
(cuenta origen, destino, fechas, ...), y algo de control sobre los
trabajos que ya se han realizado para mostrar los resultados.</p>
<p><strong>Streaming</strong></p>
<p>Se pueden crear trabajos MapReduce para Hadoop de varias formas, pero en
este ejemplo me voy a limitar a lo que se llama "Streaming". Dicho de
otra forma (aunque no sea del todo cierto), a programar aplicaciones
MapReduce en perl o python (o en cualquier lenguaje que pueda leer y
escribir en la entrada y salida estándar).</p>
<p>La gran ventaja que tiene el Streaming es que permite desarrollar
prototipos de una forma rápida, aunque no sea lo más eficiente. En
cualquier caso, sobra para este ejemplo.</p>
<p>La mejor forma de explicar lo que es el Streaming en Hadoop es ver el
equivalente, en versión linux:</p>
<pre>
cat datos_entrada.txt | mapper.pl | sort | reducer.pl
</pre><p>El mapper.pl recibirá los datos desde los ficheros de log, línea por
línea, y los tratará, escribiendo por salida estándar con el formato
clave "tabulador" valor.</p>
<p>Esta salida se ordena, y pasa a un reducer. Hadoop garantiza que todas
las claves serán tratadas por el mismo reducer, aunque éste puede, y
normalmente así lo hará, procesar varias claves (este es un buen momento
para recordar que los ficheros de entrada pueden ser, por decir algo,
200GB de datos, sin que suponga ningún problema para Hadoop).</p>
<p>Volviendo al ejemplo, veamos unas cuantas líneas de log de postfix:</p>
<pre>
Feb 20 14:27:03 server postfix/smtpd[17875]: E78D13073B2: client=mail.example.com[10.0.0.6]
Feb 20 14:27:03 server postfix/cleanup[17822]: E78D13073B2: message-id=<F0E59D47X9B343A7DFRAB1F0E45@EUW0600375>
Feb 20 14:27:04 server postfix/qmgr[5370]: E78D13073B2: from=<prueba@example.com>, size=20827, nrcpt=1 (queue active)
Feb 20 14:27:04 server postfix/smtp[17980]: E78D13073B2: to=<destino@example.org>, relay=127.0.0.1[127.0.0.1]:10025, delay=0.6, delays=0.08/0/0/0.51, dsn=2.0.0, status=sent (250 Ok: queued as 6A737307316)
Feb 20 14:27:04 server postfix/qmgr[5370]: E78D13073B2: removed
Feb 20 14:27:04 server postfix/smtpd[17882]: 6A737307316: client=localhost[127.0.0.1]
Feb 20 14:27:04 server postfix/cleanup[17817]: 6A737307316: message-id=<F0E59D47X9B343A7DFRAB1F0E45@EUW0600375>
Feb 20 14:27:04 server postfix/qmgr[5370]: 6A737307316: from=<prueba@example.com>, size=21719, nrcpt=1 (queue active)
Feb 20 14:27:04 server postfix/pipe[16491]: 6A737307316: to=<destino@example.org>, relay=maildrop, delay=0.16, delays=0.11/0.01/0/0.04, dsn=2.0.0, status=sent (delivered via maildrop service)
Feb 20 14:27:04 server postfix/qmgr[5370]: 6A737307316: removed
</pre><p>Esta es la información que postfix loguea al recibir y entregar un
mensaje usando un filtro after-queue. He ordenado los logs para que sea
más claro, pero en la realidad no es así.</p>
<p>En este ejemplo se recibe un correo desde mail.example.com, y se le
asigna el queue-id E78D13073B2. También se loguea el message-id, el
origen y el tamaño.</p>
<p>Una vez se ha encolado el mensaje, se envía a Amavis, a través de
127.0.0.1:10025. Como se puede ver en el ejemplo, en este punto sabemos
el destino del mensaje, y también si fue aceptado o no. Lo sabemos
porque el log hace referencia al queue id que amavis reinjecta en
postfix. De no haberlo aceptado no tendríamos un queue id en el log.</p>
<p>Este nuevo mensaje mantiene el message-id, el tamaño es algo mayor, y
termina entregandose, en este caso en la máquina local, usando la
aplicación courier-maildrop.</p>
<p>Bien, esto no es más que un ejemplo. En la realidad hay que tratar
muchos casos: aliases, reenvíos a máquinas remotas, bounces, mensajes
que pasan a deferred por no haber podido entregarse, ....</p>
<p>Además, e insistiendo en que esto no es más que un ejemplo, voy a asumir
dos cosas, que aunque se cumplen a menudo, no son ciertas:</p>
<ul class="simple">
<li>Que el queue-id es único.</li>
<li>Que el message-id es único.</li>
</ul>
<p><strong>Mapper.pl</strong></p>
<p>El proceso comienza con Hadoop enviando los ficheros de entrada (logs) a
la entrada estándar de los <a class="reference external" href="http://github.com/foron/forondarenanet/blob/master/mapper.pl">mapper.pl</a> que estemos ejecutando en el
cluster, que tratarán cada línea y la enviarán por salida estándar con
el formato:</p>
<pre>
queueid<tabulador>informacion_separada_por_comas
</pre><p>Para separar la clave del valor se usa un tabulador. Normalmente, los
campos del valor se separan con espacios, pero en este ejemplo he
preferido las "comas".</p>
<p>Veamos un trozo del código:</p>
<pre>
..............
while (<>)
..............
if ($linea =~ /^(\w{3} \d{2} \d{2}:\d{2}:\d{2}) .+ postfix\/smtpd\[\d+\]: (\w+): client=(\S+)$/) {
$estructura{fecha} = $1; $estructura{qid} = $2; $estructura{host} = $3;
print $estructura{qid} . "\t" . "Conexion," . join (",",$estructura{fecha},$estructura{host}) . "\n";
}
elsif ($linea =~ /^(\w{3} \d{2} \d{2}:\d{2}:\d{2}) .+ postfix\/smtp\[\d+\]: (\w+): to\=\<([^ ]+)\>, relay=127.0.0.1.*status=(.*) (\w+)\)$/) {
$estructura{fecha} = $1; $estructura{qid} = $2; $estructura{to} = $3; $estructura{origto} = $3; $estructura{status} = $4; $estructura{relay} = "Antispam"; $estructura{rqid} = $5; $estructura{status} =~ s/,//g;
print $estructura{qid} . "\t" . "Antispam," . join (",",$estructura{fecha},$estructura{to},$estructura{to},$estructura{relay},$estructura{status},$estructura{rqid}) . "\n";
}
...........
}
</pre><p>Tenemos varios "if/elsif", que adaptan la línea del log de entrada al
formato que quiero. Además, he añadido un texto con el tipo de entrada
de log que estamos tratando, y que sabemos por el "if" en el que estamos
(Antispam en este ejemplo), para luego usarlo en el reducer.</p>
<p>El código completo incluye también la detección de los mensajes que
Amavis considera que son spam, en los que pone el rqid como 0. Además,
en aquellos casos donde "no proceda" aplicar el concepto de rqid pongo
-1 como valor. Todo esto servirá en el reducer.</p>
<p>Volviendo a los logs de ejemplo, el resultado de ejecutar el mapper
sobre ellos es:</p>
<pre>
E78D13073B2 Conexion,Feb 20 14:27:03,mail.example.com[10.0.0.6]
E78D13073B2 Msgid,Jan 20 06:27:03,F0E59D47X9B343A7DFRAB1F0E45@EUW0600375
E78D13073B2 Origen,Feb 20 14:27:04,prueba@example.com,20827
E78D13073B2 Antispam,Feb 20 14:27:04,destino@example.org,destino@example.org,Antispam,sent (250 Ok: queued as,6A737307316)
E78D13073B2 Cierre,Feb 20 14:27:04
6A737307316 Conexion,Feb 20 14:27:04,localhost[127.0.0.1]
6A737307316 Msgid,Feb 20 14:27:04,F0E59D47X9B343A7DFRAB1F0E45@EUW0600375
6A737307316 Origen,Feb 20 14:27:04,prueba@example.com,21719
6A737307316 Maildrop,Feb 20 14:27:04,destino@example.org,destino@example.org,Maildrop,sent (delivered via maildrop service),-1
6A737307316 Cierre,Feb 20 14:27:04
</pre><p>Después de esto Hadoop ejecutaría un sort (en el ejemplo ya están
ordenados por queue-id), y volvería a enviar el resultado a los
reducer.pl del cluster. Por lo tanto, sabemos que un reducer va a
recibir todas las líneas de log correspondientes a un mismo queue-id.</p>
<p><strong>Reducer.pl</strong></p>
<p>Pasamos directamente a ver algo del código del <a class="reference external" href="http://github.com/foron/forondarenanet/blob/master/reducer.pl">reducer.pl</a>:</p>
<pre>
my %mensaje = (qid => "",
fecha => "",
servidor => "",
msgid => "",
origen => "",
size => "",
to => "",
origto => "",
rqid => "",
relay => "",
status => "",
cierre => ""
);
..............
while () {
chomp;
($qid, $valor) = split /\t/;
if ($qid ne $qidanterior) {
# tenemos un queueid nuevo
if ($qidanterior ne "") {
&mostrar_datos();
%mensaje = (qid => "",
fecha => "",
desdeip => "",
msgid => "",
origen => "",
size => "",
to => "",
origto => "",
rqid => "",
status => "",
cierre => ""
);
}
$qidanterior = $qid;
}
# seguimos procesando los datos del mismo qid
$mensaje{qid} = $qid;
@datos = split(",",$valor);
switch ($datos[0]) {
case "Conexion" {
$mensaje{fecha} = $datos[1];
$mensaje{desdeip} = $datos[2];
}
case "Msgid" {
$mensaje{msgid} = $datos[2];
}
case "Origen" {
$mensaje{origen} = $datos[2];
$mensaje{size} = $datos[3];
}
case "Antispam" {
$mensaje{to} .= $datos[2] . "-separador-";
$mensaje{origto} .= $datos[3] . "-separador-";
$mensaje{status} .= $datos[1] . " " .$datos[2] . " " . $datos[3] . " " . $datos[4] . " " . $datos[5] . "-separador-";
$mensaje{rqid} = $datos[6];
}
................
sub mostrar_datos {
................
if ( ($mensaje{fecha} ne "") && ($mensaje{cierre} ne "") ) {
# mensaje completo: Salida estandar
# hay que revisar que se haya pasado por todas las fases
# y realizar comprobaciones, que aqui no se hacen
$fechaentrada = `date -u -d '$mensaje{fecha}' '+%s'`; chomp $fechaentrada;
$fechasalida = `date -u -d '$mensaje{cierre}' '+%s'`; chomp $fechasalida;
$datoscodificables = $mensaje{desdeip} .",".$mensaje{size}.",".$fechasalida.",".$mensaje{qid}.",".$mensaje{rqid}.",".$mensaje{status};
$resto = encode_base64($datoscodificables,"");
print $mensaje{msgid} . "\t" . $fechaentrada . " " . $mensaje{origen} . " " . $mensaje{to} . " " . $mensaje{origto} . $resto . "\n";
} else {
# mensaje incompleto. Guardar en otro fichero para poder seguir el tracking
# Falta por hacer
}
}
</pre><p>Por lo tanto, cogemos cada línea de entrada, y ayudándonos de la
descripción del tipo de línea que hemos añadido en el mapper, vamos
completando la estructura de datos que después será el resultado (por
fin) que queremos.</p>
<p>Un ejemplo de la ejecución de este trabajo en mi cluster de prueba:</p>
<pre>
$ hadoop jar /usr/local/hadoop/contrib/streaming/hadoop-0.20.2-streaming.jar -input /user/hadoop/mail_logs -output /user/hadoop/logs_formateados/domingo_14_03 -file /home/hadoop/mapper.pl -file /home/hadoop/reducer.pl -mapper "/usr/bin/perl -w mapper.pl" -reducer "/usr/bin/perl -w reducer.pl"
packageJobJar: [/home/hadoop/mapper.pl, /home/hadoop/reducer.pl, /home/hadoop/hadoop-unjar4902963819550178151/] [] /tmp/streamjob534817239215339193.jar tmpDir=null
10/03/18 22:20:02 INFO mapred.FileInputFormat: Total input paths to process : 4
10/03/18 22:20:02 INFO streaming.StreamJob: getLocalDirs(): [/home/hadoop/mapred/local]
10/03/18 22:20:02 INFO streaming.StreamJob: Running job: job_201003182146_0001
10/03/18 22:20:02 INFO streaming.StreamJob: To kill this job, run:
10/03/18 22:20:02 INFO streaming.StreamJob: /usr/local/hadoop/bin/hadoop job -Dmapred.job.tracker=fn140:8021 -kill job_201003182146_0001
10/03/18 22:20:02 INFO streaming.StreamJob: Tracking URL: http://fn140.forondarena.net:50030/jobdetails.jsp?jobid=job_201003182146_0001
10/03/18 22:20:03 INFO streaming.StreamJob: map 0% reduce 0%
10/03/18 22:20:17 INFO streaming.StreamJob: map 8% reduce 0%
10/03/18 22:20:18 INFO streaming.StreamJob: map 11% reduce 0%
10/03/18 22:20:20 INFO streaming.StreamJob: map 15% reduce 0%
.....
10/03/18 22:21:56 INFO streaming.StreamJob: map 92% reduce 25%
10/03/18 22:21:59 INFO streaming.StreamJob: map 100% reduce 25%
....
10/03/18 23:14:13 INFO streaming.StreamJob: map 100% reduce 98%
10/03/18 23:15:55 INFO streaming.StreamJob: map 100% reduce 99%
10/03/18 23:17:31 INFO streaming.StreamJob: map 100% reduce 100%
10/03/18 23:18:25 INFO streaming.StreamJob: Job complete: job_201003182146_0001
10/03/18 23:18:25 INFO streaming.StreamJob: Output: /user/hadoop/logs_formateados/domingo_14_03
</pre><p>El resultado es un fichero en la ruta
"/user/hadoop/logs_formateados/domingo_14_03", con líneas formadas
por un message-id, seguido por un tabulador, seguido del origen, el
destino, el destino antes de la expansión de aliases, y del resto de la
información, codificada en base64, todo separado por espacios.</p>
<p>No hace falta decir que el formato de salida en un entorno real se
debería adaptar a lo que se necesitase. De hecho, Rackspace usa una
estructura binaria, donde entre otras cosas se guarda la información de
los diferentes saltos que hace un correo dentro de su sistema. Este
ejemplo, por supuesto, es mucho más sencillo.</p>
<p>Al final del proceso hemos obtenido un fichero, que es el que de verdad
queremos en nuestro cluster, con toda la información de cada mensaje que
ha entrado en nuestra red en un periodo de tiempo concreto (digamos que
cada 4 horas volcamos los logs de syslog a Hadoop y ejecutamos la
tarea), como se ve en los siguientes ejemplos (sin la codificación en
base64, para que sean más fáciles de seguir):</p>
<p><em>Mensaje que se entrega en un buzón local</em>:</p>
<pre>
ED56A03DA2274E90CC2DX6DC158CA01365E@mailer.example.org 1268225082 origen@example.org destino@example.com destino@example.com mail.example.org[10.0.2.34],2430,1268225085,B6A6A296E55,158AB296F24,Mar 10 12:44:45 destino@example.com destino@example.com Antispam sent (250 OK sent queued as 158AB296F24
ED56A03DA2274E90CC2DX6DC158CA01365E@mailer.example.org 1268225085 origen@example.org destino@example.com destino@example.com localhost[127.0.0.1],3081,1268225085,158AB296F24,-1,Mar 10 12:44:45 destino@example.com destino@example.com Maildrop sent (delivered via maildrop service)
</pre><p>Por lo tanto:</p>
<ul class="simple">
<li>El mensaje con id
ED56A03DA2274E90CC2DX6DC158CA01365E@mailer.example.org</li>
<li>Entró en el sistema a las 1268225082</li>
<li>Desde el servidor mail.example.org[10.0.2.34]</li>
<li>El origen era origen@example.org</li>
<li>Y el destino destino@example.com (no es un alias)</li>
<li>Una vez pasado por el sistema antispam se reencola con queueid
158AB296F24</li>
<li>El mensaje de la entrega al servidor antispam es: sent (250 OK sent
queued as 158AB296F24)</li>
<li>Una vez ha pasado el filtro antispam el mensaje se reencola a las
1268225085</li>
<li>Por supuesto desde el servidor localhost[127.0.0.1]</li>
<li>Y el mensaje de la entrega local es Mar 10 12:44:45 Maildrop sent
(delivered via maildrop service), sólo al destino destino@example.com</li>
</ul>
<p><em>Mensaje que se entrega a un servidor remoto</em>:</p>
<pre>
1982C18.15A.1B26824474D6.JavaMail.root@10.1.2.3 1268219047 origen@example.net destino@example.com destino@example.com mail.example.net[10.5.3.2],1901,1268219051,CA6782FABFB,B6C7A2FAACA,Mar 10 11:04:11 destino@example.com destino@example.com Antispam sent (250 OK sent queued as B6C7A2FAACA
1982C18.15A.1B26824474D6.JavaMail.root@10.1.2.3 1268219049 origen@example.net destinoreal@remoto.example.org destino@example.com localhost[127.0.0.1],2686,1268219051,B6C7A2FAACA,-1,Mar 10 11:04:11 destinoreal@remoto.example.org destino@example.com mail.example.org[10.0.2.34]:25 sent (250 2.0.0 Ok: queued as 423872AB862)
</pre><ul class="simple">
<li>Su messageid es 1982C18.15A.1B26824474D6.JavaMail.root@10.1.2.3</li>
<li>Entro en el sistema a las 1268219047</li>
<li>Desde el servidor mail.example.net[10.5.3.2]</li>
<li>Desde el origen origen@example.net</li>
<li>Y al destino destino@example.com, que en la fase antispam todavía no
se expande a ningún alias</li>
<li>El mensaje se relaciona con B6C7A2FAACA</li>
<li>Y el mensaje de la entrega es Mar 10 11:04:11 destino@example.com
destino @example.com Antispam sent (250 OK sent queued as B6C7A2FAACA)</li>
<li>Se reencola, a través de localhost, a las 1268219049</li>
<li>El mensaje pasa a ser la dirección real
destinoreal@remoto.example.org,
proveniente del alias destino@example.com</li>
<li>El mensaje se entrega en mail.example.org[10.0.2.34]:25</li>
<li>Con el mensaje sent (250 2.0.0 Ok: queued as 423A72AB262)</li>
</ul>
<p><em>Mensaje que es spam</em>:</p>
<pre>
9545850218.UWD8KHA190@spammer.example.info 1268238332 spammer@example.net destino@example.com destino@example.com mail.example.info[10.8.2.12],2141,1268238333,F355429B3A5,0,Mar 10 16:25:33 destino@example.com destino@example.com Antispam sent (250 OK sent mensaje_rechazado por spam
</pre><p>En este caso el mensaje es spam, y no se relaciona con ningún otro queueid.</p>
<p><em>Mensaje que se entrega a varias cuentas</em>:</p>
<pre>
2010031011.AB263566.pc1@mailer.example.org 1268223531 usuario@example.org destinovarios@example.com destinovarios@example.com mail.example.org[10.0.2.34],15853,1268223532,5A4FE29B3B6,43A9B28B357,Mar 10 12:18:52 destinovarios@example.com destinovarios@example.com Antispam sent (250 OK sent queued as 43A9B28B357
2010031011.AB263566.pc1@mailer.example.org 1268223532 usuario@example.org destinolocal@example.com-separador-destinoremoto@example.info destinovarios@example.com-separador-destinovarios@example.com localhost[127.0.0.1],16525,1268223532,43A9B28B357,-1,Mar 10 12:18:52 destinolocal@example.com destinovarios@example.com Maildrop sent (delivered via maildrop service)-separador-Mar 10 12:18:52 destinoremoto@example.info destinovarios@example.com mail.example.info[10.8.2.12]:25 sent (250 2.0.0 Ok: queued as 452362CA51A)
</pre><ul class="simple">
<li>Ahora tenemos un mensaje con id
2010031011.AB263566.pc1@mailer.example.org</li>
<li>Que se envía desde usuario@example.org</li>
<li>Al destino destinovarios@example.com</li>
<li>Que a su vez es un alias a dos cuentas destinolocal@example.com y
destinoremoto@example.info</li>
<li>Los mensajes de entrega a cada una de ellas son:<ul>
<li>Mar 10 12:18:52 destinolocal@example.com Maildrop sent (delivered
via maildrop service)</li>
<li>Mar 10 12:18:52 destinoremoto@example.info
mail.example.info[10.8.2.12]:25 sent (250 2.0.0 Ok: queued as
452362CA51A)</li>
</ul>
</li>
</ul>
<p>Como se puede ver, ya tenemos toda la información necesaria, debidamente
preformateada, por lo que ya no necesitamos los ficheros mail.log, que
podríamos borrar de Hadoop (que no de las cintas).</p>
<p>La información contenida en estos ficheros nos servirá para preparar
consultas, bien directamente con otros trabajos MapReduce, bien con
otras herramientas, tal y como mostraré en el siguiente post de la
serie, en el que nos aprovecharemos del formato de los ficheros para
hacer consultas por message-id, cuenta origen, fecha o cuenta destino
(según entró en el sistema o una vez expandidos los aliases).</p>
<p>En este punto hay que volver a recordar que Hadoop está pensado para
trabajar con cantidades muy grandes de información, en los que hacer un
simple "grep" o usar una base de datos relacional estándar resulta
impracticable.</p>
<p>Por último, tened en cuenta que el código de este post, además de ser
sólo un ejemplo, es completamente Alpha. Aparte de no funcionar del todo
bien (tiene varios fallos que he detectado pero que no he corregido
todavía), necesitaría tener en cuenta muchos más casos, como mensajes
cuya entrega se retrasa durante días (que no deja de ser algo normal en
los sistemas de correo reales).</p>
<p>En el cuarto y último post de la serie: acceso a Hadoop a través de una
herramienta de apoyo, como <a class="reference external" href="http://hadoop.apache.org/pig/">pig</a>.</p>
Computación distribuida con Hadoop II2010-02-05T23:15:00+01:002010-02-05T23:15:00+01:00tag:www.forondarena.net,2010-02-05:/computacion-distribuida-con-hadoop-ii.html<p>Continúo la serie de posts sobre Hadoop describiendo, muy por encima,
los componentes y la estructura que suelen tener este tipo de sistemas.
Como ya he dicho en otros posts, este blog pretende ser sobre todo
práctico, así que no me voy a extender demasiado.</p>
<p>Gráficamente un cluster Hadoop se …</p><p>Continúo la serie de posts sobre Hadoop describiendo, muy por encima,
los componentes y la estructura que suelen tener este tipo de sistemas.
Como ya he dicho en otros posts, este blog pretende ser sobre todo
práctico, así que no me voy a extender demasiado.</p>
<p>Gráficamente un cluster Hadoop se puede parecer mucho a esto:</p>
<p><img alt="Infraestructura Hadoop" class="img-fluid" src="/images/posts/hadoop/infra_hadoop.png" /></p>
<p><strong>JobTracker:</strong></p>
<p>En un cluster hay un único JobTracker. Su labor principal es la gestión
de los TaskTrackers, entre los que distribuye los trabajos MapReduce que
recibe. Es, por lo tanto, el interfaz principal que tienen los usuarios
para acceder al cluster.</p>
<p><strong>TaskTracker:</strong></p>
<p>Un cluster tiene n TaskTrackers que se encargan de la ejecución de las
tareas map y reduce en los nodos. Cada uno de los TaskTrackers gestiona
la ejecución de estas tareas en una máquina del cluster. El JobTracker
se encarga, a su vez, de controlarlos.</p>
<p><strong>NameNode:</strong></p>
<p>Las funciones principales del NameNode son el almacenamiento y la
gestión de los metadatos del sistema de ficheros distribuido y ofrecer
el interfaz principal que tiene el usuario para acceder al contenido
HDFS. En un cluster hay un único proceso NameNode.</p>
<p>Sin los metadatos que mantiene el NameNode no se sabría en qué nodo
está cada bloque, además de perderse la información sobre la estructura
de directorios. Es, por lo tanto, el componente más importante del
cluster, y debe estar redundado. Hadoop no ofrece de manera nativa
ningún mecanismo de alta disponibilidad, pero sí dispone de herramientas
que permiten replicar la información. Entre ellas está el NameNode
secundario, que permite guardar una copia de los metadatos (en realidad
hace más cosas) en una máquina diferente al NameNode. Eso sí, no es una
copia en tiempo real, y no ofrece failover automático en caso de fallo
del primario. Para esto necesitamos usar otro tipo de herramientas,
entre las que destacan <a class="reference external" href="http://www.drbd.org/">DRBD</a> y <a class="reference external" href="http://www.linux-ha.org/Heartbeat">Heartbeat</a>.</p>
<p><strong>DataNode:</strong></p>
<p>Estos procesos ofrecen el servicio de almacenamiento de bloques para el
sistema de ficheros distribuido. Son coordinados por el NameNode.</p>
<p>Estos son los procesos principales del sistema, pero hay otros, como el
Balancer, que se encarga de equilibrar la distribución de los bloques
entre los DataNodes, por ejemplo cuando se añade un nuevo servidor.</p>
<p>Mi laboratorio se limita a cuatro servidores: un NameNode+JobTracker y
tres DataNode+TaskTracker, en cuatro máquinas virtuales de un core y 1G
de memoria, para un total de 5G de almacenamiento.</p>
<pre>
hadoop@fn140:/usr/local/hadoop/conf$ hadoop dfsadmin -report
Configured Capacity: 8695013376 (8.1 GB)
Present Capacity: 5455364096 (5.08 GB)
DFS Remaining: 5455290368 (5.08 GB)
DFS Used: 73728 (72 KB)
DFS Used%: 0%
Under replicated blocks: 0
Blocks with corrupt replicas: 0
Missing blocks: 0
-------------------------------------------------
Datanodes available: 3 (3 total, 0 dead)
Name: 192.168.10.142:50010
Decommission Status : Normal
Configured Capacity: 2898337792 (2.7 GB)
DFS Used: 24576 (24 KB)
Non DFS Used: 1079918592 (1.01 GB)
DFS Remaining: 1818394624(1.69 GB)
DFS Used%: 0%
DFS Remaining%: 62.74%
Last contact: Sun Jan 31 16:10:58 CET 2010
Name: 192.168.10.143:50010
Decommission Status : Normal
Configured Capacity: 2898337792 (2.7 GB)
DFS Used: 24576 (24 KB)
Non DFS Used: 1080020992 (1.01 GB)
DFS Remaining: 1818292224(1.69 GB)
DFS Used%: 0%
DFS Remaining%: 62.74%
Last contact: Sun Jan 31 16:10:58 CET 2010
Name: 192.168.10.141:50010
Decommission Status : Normal
Configured Capacity: 2898337792 (2.7 GB)
DFS Used: 24576 (24 KB)
Non DFS Used: 1079709696 (1.01 GB)
DFS Remaining: 1818603520(1.69 GB)
DFS Used%: 0%
DFS Remaining%: 62.75%
Last contact: Sun Jan 31 16:10:56 CET 2010
</pre><p>En los próximos dos posts voy a dejar la "teoría" para pasar a la
práctica, empezando por una de las aplicaciones más obvias de Hadoop,
como es el tratamiento de logs; para seguir por una de las utilidades
que existen para facilitar el desarrollo de aplicaciones MapReduce:
<a class="reference external" href="http://hadoop.apache.org/pig/">pig</a>.</p>
Computación distribuida con Hadoop I2010-01-17T20:39:00+01:002010-01-17T20:39:00+01:00tag:www.forondarena.net,2010-01-17:/computacion-distribuida-con-hadoop-i.html<p>Allá por el año 2003, cuando Google ya dominaba el mundo de los
buscadores, muchos administradores de sistemas nos preguntábamos por la
tecnología que usarían para indexar páginas, calcular Pageranks,
gestionar el almacenamiento ....</p>
<p>En ese momento Google publicó varios documentos al respecto, como
<a class="reference external" href="http://research.google.com/archive/mapreduce-osdi04.pdf">este</a> sobre MapReduce y este <a class="reference external" href="http://research.google.com/archive/gfs-sosp2003.pdf">otro</a> sobre …</p><p>Allá por el año 2003, cuando Google ya dominaba el mundo de los
buscadores, muchos administradores de sistemas nos preguntábamos por la
tecnología que usarían para indexar páginas, calcular Pageranks,
gestionar el almacenamiento ....</p>
<p>En ese momento Google publicó varios documentos al respecto, como
<a class="reference external" href="http://research.google.com/archive/mapreduce-osdi04.pdf">este</a> sobre MapReduce y este <a class="reference external" href="http://research.google.com/archive/gfs-sosp2003.pdf">otro</a> sobre su sistema de ficheros GFS
(Google File System), que daban algunas pistas, y que a la larga han
sido la base de varios proyectos, como <a class="reference external" href="http://hadoop.apache.org">Hadoop</a>.</p>
<p>Pretendo que este blog sea sobre todo práctico, así que no voy a
escribir demasiado ni sobre la teoría que hay detrás de MapReduce
(programación funcional, map/reduce, bla bla bla) ni sobre Google y GFS.
Para el que quiera leer algo más sobre la tecnología de Google
recomiendo este excelente <a class="reference external" href="http://www.storagemojo.com">blog</a> en el que se pueden encontrar varios
posts sobre el buscador.</p>
<p><strong>El problema</strong></p>
<p>El problema es sencillo y se resume en el volumen de datos, cada vez
mayor, que generan las aplicaciones; y que por supuesto hay que tratar.
Por dar un ejemplo, en el 2008 Yahoo! gestionaba unos 5PB de
almacenamiento para generar su "webmap" <a class="footnote-reference" href="#id4" id="id1">[1]</a>, que es uno de los
componentes que usa su buscador.
Otro ejemplo es Rackspace, y su división de correo, Mailtrust, que
genera más de 150GB de logs de correo al día. Por supuesto, además de
ser mucho volumen acomulado, es fundamental que se pueda tratar de una
forma ágil para dar una respuesta rápida a clientes que piden cosas como
"el correo que me mandó fulanito hace 3 meses no me ha llegado" o
"quiero todos los correos recibidos en mi dominio en los últimos 5
meses". Esto es razonablemente sencillo cuando se controlan unos pocos
cientos de GB, pero se hace mucho más difícil cuando la escala pasa al
Tera.</p>
<p>El que haya nombrado estos dos ejemplos no es casual, ya que Yahoo! y
Rackspace son dos de los usuarios más notables (junto a Facebook,
LastFM, ...) del software que voy a describir en los siguientes posts:
Hadoop.</p>
<p><strong>¿Qué es Hadoop?</strong></p>
<p>Resumiendo mucho, Hadoop es un framework software que intenta
implementar los conceptos de MapReduce y GFS que presentó Google. Está
pensado, por lo tanto, para el tratamiento distribuido de grandes
cantidades de datos en máquinas de bajo coste y con poca fiabilidad.</p>
<p>Partiendo de esta base, parece claro que hace falta un sistema de
ficheros pensado para almacenar mucha información y con tolerancia ante
fallos. Aquí es donde aparece <a class="reference external" href="http://hadoop.apache.org/hdfs/">HDFS</a> (Hadoop Distributed File System).
Algunas de sus características:</p>
<ul class="simple">
<li>El tamaño de bloque por defecto en este sistema de ficheros es de
64MB, pero muchas veces se aumenta a 128MB.</li>
<li>Ofrece la mencionada disponibilidad al dejar varias copias de cada
bloque en máquinas diferentes.</li>
<li>Usa el concepto de "rack awareness", para saber dónde está cada
bloque y qué proceso de cálculo puede acceder más rápido a él.</li>
<li>Su diseño facilita la lectura secuencial de datos (razonable en
discos estándar de bajo coste), pero lo hace sacrificando otras
características propias de los sistemas de ficheros POSIX. Por
ejemplo, que nadie piense editar un fichero en HDFS y borrar la línea
número 2000.</li>
</ul>
<p>Por otro lado, el framework implementa el concepto MapReduce, que como
ya he escrito consiste en dividir el trabajo entre n servidores para
presentar el resultado después de forma coherente. Veamos un ejemplo:</p>
<p>Digamos <a class="footnote-reference" href="#id5" id="id2">[2]</a> que tenemos un cluster de 4 máquinas Hadoop que guardan logs
de apache de 40 servidores balanceados, para un total de 100GB al día.
Nuestro objetivo es diseñar un trabajo MapReduce para sumar los accesos
que ha hecho cada IP en todos los frontales.</p>
<p>Los logs van a ser sencillos y se van a limitar a "FECHA IP URL".</p>
<p>Servidor 1:</p>
<pre>
FECHA 172.17.2.34 /index.html
FECHA 172.17.7.13 /contenido/futbol.html
FECHA 172.17.2.34 /img/banner.jpg
FECHA 172.17.2.42 /index.html
</pre><p>Servidor 2:</p>
<pre>
FECHA 172.17.2.34 /contenido/peliculas.html
FECHA 172.17.7.13 /img/futbol.jpg
</pre><p>Servidor 3:</p>
<pre>
FECHA 172.17.7.13 /index.html
FECHA 172.17.2.34 /peliculas/bladerunner.html
FECHA 172.17.2.42 /img/banner.jpg
</pre><p>Servidor 4:</p>
<pre>
FECHA 172.17.2.42 /contenido/series.html
FECHA 172.17.2.42 /img/series.jpg
</pre><p>A partir de estos datos Hadoop empezará la fase Map, que se ejecuta
paralelamente en las máquinas del cluster, y que consiste en adaptar las
líneas de log al formato clave< tabulador >valor.</p>
<p>Volviendo al ejemplo, la clave será la IP, y el valor la URL (no
necesitamos la fecha).
El resultado de la fase Map es el siguiente <a class="footnote-reference" href="#id6" id="id3">[3]</a>:</p>
<pre>
(172.17.2.34 [/index.html /img/banner.jpg /contenido/peliculas.html /peliculas/bladerunner.html])
(172.17.2.42 [/index.html /img/banner.jpg /contenido/series.html /img/series.jpg])
(172.17.7.13 [/contenido/futbol.html /img/futbol.jpg /index.html])
</pre><p>Hemos generado un listado ordenado por IP, en el que se identifican
todos los accesos que ha habido.</p>
<p>Una vez hecho esto Hadoop entrega trozos de esta salida a n procesos
Reduce, que en este sencillo ejemplo sólo tienen que contar las URL que
aparecen en el valor, para generar algo como lo siguiente:</p>
<pre>
(172.17.2.34 4)
(172.17.2.42 4)
(172.17.7.13 3)
</pre><p>Esto no es más que un ejemplo. En la realidad, Hadoop se ha usado para
cosas tan diversas como convertir 4TB de artículos de periódico
escaneados en formato tiff en 11 millones de pdfs; todo usando unas 100
instancias de amazon y en menos de 24 horas, por un total de 240$ sin
contar ancho de banda, como se puede ver <a class="reference external" href="http://open.blogs.nytimes.com/2007/11/01/self-service-prorated-super-computing-fun/?scp=1&sq=self%20service%20prorated&st=cse">aquí</a>.</p>
<p>El próximo post describirá una instalación básica del sistema, y el
siguiente será una prueba algo más interesante de uso con logs de
postfix+amavis, muy al estilo Mailtrust, y usando perl y quizá alguno de
los proyectos de apoyo que han surgido alrededor de Hadoop; como Hive,
que fue desarrollado por Facebook para crear trabajos MapReduce desde
una consola con sintaxis muy similar a SQL.</p>
<p><strong>Notas</strong></p>
<table class="docutils footnote" frame="void" id="id4" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id1">[1]</a></td><td>En el 2008 el webmap de Yahoo! era un grafo de un trillón de vértices(enlaces web) y 100 billones de nodos (URL únicas).</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id5" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id2">[2]</a></td><td>Los tres o cuatro lectores de este blog se enfadan si no uso al menos un "digamos" o "supongamos" en cada post.</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="id6" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id3">[3]</a></td><td>En realidad hay más fases, como sort-suffle.</td></tr>
</tbody>
</table>
Monitorización del kernel con SystemTap II2009-04-11T18:56:00+02:002009-04-11T18:56:00+02:00tag:www.forondarena.net,2009-04-11:/monitorizacion-del-kernel-con-systemtap-ii.html<p>A veces (seguro que pocas), nos encontramos ante una máquina que por
algún motivo ha dejado de trabajar como se esperaba.
Digamos que el rendimiento o el IO del equipo baja, sin motivo visible.
Digamos que no hay logs que indiquen problemas, ni errores del sistema.
Nada.
Este es el …</p><p>A veces (seguro que pocas), nos encontramos ante una máquina que por
algún motivo ha dejado de trabajar como se esperaba.
Digamos que el rendimiento o el IO del equipo baja, sin motivo visible.
Digamos que no hay logs que indiquen problemas, ni errores del sistema.
Nada.
Este es el momento para el kung fu, el voodoo o todo tipo de técnicas
adivinatorias que tanto usamos los informáticos. Muchas veces es
suficiente, pero no siempre, y no hay nada peor que no ser capaz de dar
una explicación a un problema.</p>
<p><img alt="Systemtap" class="img-fluid" src="/images/posts/systemtap/systemtap.png" /></p>
<p>Supongamos que tenemos la arquitectura de la imagen, con un par de
balanceadores de carga, unas cuantas máquinas iguales que pueden ser
servidores web, ftp, pop, smtp o cualquier otra cosa, y un par de
servidores de almacenamiento nfs.
Teniendo en cuenta que los balanceadores de carga suelen dar la
posibilidad de asignar diferentes pesos a cada máquina balanceada, ¿Por
qué no usar uno de estos servidores para la monitorización del sistema?
Es perfectamente posible enviar un poco menos de tráfico a la máquina
destinada a SystemTap, de tal manera que no afecte al rendimiento
global, para que podamos darnos un mecanismo para tener nuestra
infraestructura controlada.</p>
<p>En este post sólo voy a referenciar algunos scripts que están
disponibles en la <a class="reference external" href="http://sourceware.org/systemtap/">web</a> y en el <a class="reference external" href="http://www.redbooks.ibm.com/abstracts/redp4469.html">redpaper</a>. Además, el propio software
viene con muchos ejemplos. En CentOS se puede instalar el rpm
"systemtap-testsuite" para probar varios scripts.</p>
<p>Vamos a empezar con un par de ejemplos para usar más en un entorno
académico que en la práctica.</p>
<p>Digamos que queremos aprender "lo que pasa" cuando hacemos un "ls" en un
directorio de una partición ext3. Con este sencillo script:</p>
<pre>
#! /usr/bin/env stap
probe module("ext3").function("*").call
{
printf("%s -> %s\n", thread_indent(1), probefunc())
}
probe module("ext3").function("*").return
{
printf("%s <- %s\n", thread_indent(-1), probefunc())
}
</pre><p>Vamos a hacer un printf cada vez que comience y termine la ejecución de
cualquier función del módulo ext3. En el printf vamos a tabular el
nombre de la función que se ha ejecutado. El resultado es algo así:</p>
<pre>
...
2 ls(31789): <- ext3_permission
0 ls(31789): -> ext3_dirty_inode
8 ls(31789): -> ext3_journal_start_sb
21 ls(31789): <- ext3_journal_start_sb
26 ls(31789): -> ext3_mark_inode_dirty
31 ls(31789): -> ext3_reserve_inode_write
35 ls(31789): -> ext3_get_inode_loc
39 ls(31789): -> __ext3_get_inode_loc
52 ls(31789): <- __ext3_get_inode_loc
56 ls(31789): <- ext3_get_inode_loc
61 ls(31789): <- ext3_reserve_inode_write
67 ls(31789): -> ext3_mark_iloc_dirty
74 ls(31789): <- ext3_mark_iloc_dirty
77 ls(31789): <- ext3_mark_inode_dirty
83 ls(31789): -> __ext3_journal_stop
87 ls(31789): <- __ext3_journal_stop
90 ls(31789): <- ext3_dirty_inode
0 ls(31789): -> ext3_permission
...
</pre><p>Como he dicho, no es algo demasiado práctico, pero puede servir a algún
profesor para suspender a un buen porcentaje de pobres alumnos :-)
Sigamos con algún otro ejemplo. Digamos que queremos programar un
"nettop" que nos diga qué conexiones se están abriendo en una máquina en
cada momento. Con un script similar al siguiente lo tendremos en unos
minutos:</p>
<pre>
#! /usr/bin/env stap
global ifxmit, ifrecv
probe netdev.transmit
{
ifxmit[pid(), dev_name, execname(), uid()] <<< length
}
probe netdev.receive
{
ifrecv[pid(), dev_name, execname(), uid()] <<< length
}
function print_activity()
{
printf("%5s %5s %-7s %7s %7s %7s %7s %-15s\n",
"PID", "UID", "DEV", "XMIT_PK", "RECV_PK",
"XMIT_KB", "RECV_KB", "COMMAND")
foreach ([pid, dev, exec, uid] in ifrecv-) {
n_xmit = @count(ifxmit[pid, dev, exec, uid])
n_recv = @count(ifrecv[pid, dev, exec, uid])
printf("%5d %5d %-7s %7d %7d %7d %7d %-15s\n",
pid, uid, dev, n_xmit, n_recv,
n_xmit ? @sum(ifxmit[pid, dev, exec, uid])/1024 : 0,
n_recv ? @sum(ifrecv[pid, dev, exec, uid])/1024 : 0,
exec)
}
print("\n")
delete ifxmit
delete ifrecv
}
probe timer.ms(5000), end, error
{
print_activity()
}
</pre><p>¿Qué es lo que hemos hecho? Hemos generado tres "probes". Por un lado,
cuando se recibe o trasmite a través de la red añadimos a los arrays
"ifxmit, ifrecv" información sobre qué proceso y en qué interfaz ha
enviado o recibido. Por otro lado, cada 5000 ms o cuando haya un error o
termine el script mostramos la información por pantalla. El resultado
puede ser algo similar a lo siguiente:</p>
<pre>
PID UID DEV XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
31485 500 eth0 1 1 0 0 sshd
y pasados unos miles de milisegundos ...
PID UID DEV XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
15337 48 eth0 4 5 5 0 httpd
31485 500 eth0 1 1 0 0 sshd
</pre><p>Como se ve, tengo una sesión ssh abierta permanentemente, y he
consultado una página web en el servidor monitorizado.
Por supuesto, esto puede no ser muy práctico en servidores muy activos,
pero puede dar alguna idea a algún administrador.
Igual que hemos hecho un "nettop", también podemos hacer un "disktop"
que muestre un resultado como el siguiente:</p>
<pre>
Sat Apr 11 17:22:03 2009 , Average: 0Kb/sec, Read: 0Kb, Write: 0Kb
UID PID PPID CMD DEVICE T BYTES
48 15342 15239 httpd dm-0 W 210
48 15343 15239 httpd dm-0 W 210
48 15337 15239 httpd dm-0 W 210
48 15336 15239 httpd dm-0 W 210
</pre><p>Lo que hecho es hacer el mismo wget varias veces. Para el que tenga
curiosidad, los procesos httpd (que por cierto usa el usuario id=48)
ejecutando en el servidor son:</p>
<pre>
# pstree -p | grep http
|-httpd(15239)-+-httpd(15336)
| |-httpd(15337)
| |-httpd(15338)
| |-httpd(15339)
| |-httpd(15340)
| |-httpd(15341)
| |-httpd(15342)
| `-httpd(15343)
</pre><p>El script disktop está disponible en
"/usr/share/systemtap/testsuite/systemtap.examples/io" del rpm
"systemtap-testsuite".</p>
<p>Volvamos al primer ejemplo del post. Teníamos un grupo de servidores que
han dejado de funcionar como deben. Digamos que sospechamos de la
velocidad con la que nuestros servidores nfs están sirviendo el
contenido de los servidores web.
Podemos probar aquellos ficheros que necesiten más de 1 segundo para
abrirse:</p>
<pre>
#!/usr/bin/stap
global open , names
probe begin {
printf("%10s %12s %30s\n","Process" , "Open time(s)" , "File Name")
}
probe kernel.function("sys_open").return{
open[execname(),task_tid(task_current()),$return] = gettimeofday_us()
names[execname(),task_tid(task_current()),$return] = user_string($filename)
}
probe kernel.function("sys_close"){
open_time_ms = (gettimeofday_us() - open[execname(),task_tid(task_current()), $fd])
open_time_s = open_time_ms / 1000000
if ((open_time_s >= 1) && (names[execname(),task_tid(task_current()), $fd] != "")) {
printf("%10s %6d.%.5d %30s\n", execname(),
open_time_s,open_time_ms%1000000,names[execname(),task_tid(task_current()), $fd])
}
}
</pre><p>Con este sencillo script estaría hecho:</p>
<pre>
Process Open time(s) File Name
httpd 8.471285 /var/www/html/lectura_lenta.html
</pre><p>En este caso al servidor web le ha costado 8.5 segundos servir el
fichero lectura_lenta.html. Después será responsabilidad nuestra buscar, en su caso, la solución.</p>
<p>En definitiva, systemtap es una herramienta muy completa, pero que como
tal requiere algo de práctica para ser útil. No sé si alguna vez va a
tener mucho éxito en entornos de producción, pero no está de más saber
que existe.</p>
Monitorización del kernel con SystemTap I2009-02-28T17:29:00+01:002009-02-28T17:29:00+01:00tag:www.forondarena.net,2009-02-28:/monitorizacion-del-kernel-con-systemtap-i.html<p>Empiezo otra serie de dos posts. En este caso sobre algo que tenía
"pendiente" desde hace ya tiempo. De hecho, normalmente escribiría mis
propios ejemplos para publicarlos aquí, pero para ir más rápido me voy a
limitar a referenciar los scripts que usaré para mostrar las
funcionalidades de ..... <a class="reference external" href="http://sourceware.org/systemtap/">SystemTap</a>.
SystemTap …</p><p>Empiezo otra serie de dos posts. En este caso sobre algo que tenía
"pendiente" desde hace ya tiempo. De hecho, normalmente escribiría mis
propios ejemplos para publicarlos aquí, pero para ir más rápido me voy a
limitar a referenciar los scripts que usaré para mostrar las
funcionalidades de ..... <a class="reference external" href="http://sourceware.org/systemtap/">SystemTap</a>.
SystemTap es una herramienta que sirve para monitorizar en tiempo real
lo que está pasando con un kernel linux. Quitando el detalle de que el
kernel tiene que tener ciertas opciones compiladas (luego las comento),
SystemTap tiene la gran ventaja de no requerir ningún tipo de reinicio
para empezar a trabajar.</p>
<p><strong>¿Qué podemos monitorizar con SystemTap?</strong></p>
<p>Pues ..... prácticamente todo lo que queramos. La segunda parte de este
post tratará algunos ejemplos, pero por dar alguna pista, con este
software podemos desde vigilar los procesos que más I/O están generando
a programarnos un "nettop" que diga los procesos que más están usando la
red.</p>
<p><strong>¿Cómo funciona SystemTap?</strong></p>
<p>Con SystemTap se le dice al kernel que ejecute una rutina cuando ocurre
un evento, que puede basarse, por citar dos ejemplos, en el tiempo (cada
n segundos) o en una llamada del sistema (al ejecutarse un vfs_read).
Para esto se usa un lenguaje de programación propio con el que se
definen los "probe points" y las diferentes funciones. A pesar de
ofrecer muchas posibilidades, el propio lenguaje tiene un control
especial sobre los bucles infinitos, el acceso a memoria o la
recursividad, por poner tres ejemplos. ¿Por qué tanto control? Pues
porque a partir de este código se genera un módulo de kernel que se
carga en el sistema. Claro, no hace falta decir las consecuencias de un
bucle "mal hecho" a tan bajo nivel.</p>
<p><strong>¿Qué necesita el kernel de linux para poder usar SystemTap?</strong></p>
<p>Para empezar, cómo no, necesitamos un núcleo capaz de cargar módulos.
Después, deberemos activar el soporte para los distintos tipos de debug
que tiene el kernel, como el del sistema de ficheros, el del propio
kernel o los kprobes. Traducido a formato .config, necesitamos las
opciones CONFIG_DEBUG_INFO, CONFIG_KPROBES, CONFIG_RELAY,
CONFIG_DEBUG_FS, CONFIG_MODULES y CONFIG_MODULES_UNLOAD.
Algunas distribuciones incluyen kernels específicos con estas opciones
ya activadas.</p>
<p><strong>¿Por qué SystemTap y no strace o gdb?</strong></p>
<p>Bueno, esta seguramente sea una buena pregunta, al menos hasta ver los
ejemplos de la segunda parte de este post; pero resumiendo, con
SystemTap podemos:</p>
<ul class="simple">
<li>Ver de forma integrada y unificada lo que pasa en el kernel y en las
aplicaciones que ejecuta.</li>
<li>Probar aplicaciones multihilo.</li>
<li>Monitorizar aplicaciones multiproceso, como las cliente-servidor, en
las que ambos componentes son procesos independientes.</li>
<li>Monitorizar en tiempo real y a prácticamente la velocidad de
ejecución original.</li>
<li>Escribir nuestros propios monitores que den detalles que aplicaciones
"generalistas" no son capaces de dar.</li>
</ul>
<p><strong>¿Cuáles son los aspectos negativos del invento?</strong></p>
<p>Evidentemente, no todos son ventajas con SystemTap. Podríamos hablar de
los inconvenientes técnicos, porque las opciones de debug del kernel
ralentizan un poco la velocidad del sistema, pero yo creo que los
mayores problemas vienen por el aprendizaje necesario para usar el
software. Para empezar, hay que tener un cierto conocimiento del kernel
de linux; después, hay que saber las posibilidades del lenguaje de
programación, y por último hay que ser capaz de interpretar los
resultados. Todo esto desmoralizará a más de uno, seguro, que preferirá
seguir con top, htop, vmstat y demás familia antes de meterse en este
embolado.
Afortunadamente, ya hay multitud de scripts disponibles en Internet
para todo tipo de situaciones.</p>
<p>Suelo terminar los posts con una referencia bibliográfica. En este caso
no creo que haya ningún libro sobre SystemTap, pero sí que hay un
redpaper de IBM (que debería pasar a ser un redBOOK pronto :-) ):
<a class="reference external" href="http://www.redbooks.ibm.com/abstracts/redp4469.html">SystemTap: Instrumenting the Linux Kernel for Analyzing Performance and
Functional Problems</a></p>
Monitorización orientada a host con OSSEC III2008-11-16T18:10:00+01:002008-11-16T18:10:00+01:00tag:www.forondarena.net,2008-11-16:/monitorizacion-orientada-a-host-con-ossec-iii.html<p>En este último post de la serie vamos a ver, como siempre muy por
encima, la funcionalidad "active response" que ofrece ossec. En
definitiva, se trata de ser capaces de ejecutar un script cuando se
activa un evento. Esto normalmente se usa para añadir reglas en
firewalls, para añadir reglas …</p><p>En este último post de la serie vamos a ver, como siempre muy por
encima, la funcionalidad "active response" que ofrece ossec. En
definitiva, se trata de ser capaces de ejecutar un script cuando se
activa un evento. Esto normalmente se usa para añadir reglas en
firewalls, para añadir reglas tcp wrapper o para bloquear usuarios, pero
en el fondo se puede hacer cualquier cosa que se pueda escribir en un
programa.
Por ejemplo, si una de las máquinas monitorizadas es un servidor de
correo se podría añadir una IP que generase una alerta a una lista de
acceso, o a una base de datos <a class="reference external" href="http://en.wikipedia.org/wiki/DNSBL">RBL</a>. O también se podrían añadir reglas
de <a class="reference external" href="http://www.modsecurity.org/">modsecurity</a> en un servidor web.</p>
<p>Para el ejemplo de este post, digamos que como administradores ya
estamos usando psad para crear reglas en nuestro firewall. Digamos,
además, que sólo queremos que sea psad el responsable de las reglas
dinámicas en el cortafuegos.</p>
<p><img alt="Infraestructura ossec" class="img-fluid" src="/images/posts/ossec/infra_ossec.png" /></p>
<p>Rootkits, accesos no permitidos, .... No parece necesario justificar la
necesidad de tener el firewall monitorizado, ¿Verdad?</p>
<p>Nuestro objetivo es crear reglas para psad desde ossec.
Primero definimos el comando en ossec.conf para poder usarlo más
adelante en la configuración de la respuesta activa.</p>
<pre>
<command>
<name>psad</name>
<executable>psad.sh</executable>
<expect>user,srcip</expect>
</command>
</pre><p>El script "psad.sh" (que guardaremos en
$ossec_instalacion/active-response/bin con permisos de ejecución) tiene
el siguiente contenido:</p>
<pre>
#!/bin/bash
# Anyade una regla de psad usando la IP origen. Tambien pasamos el usuario, pero no lo vamos a usar.
# Entrada: user, srcip
# Salida: Nada. Ejecuta una regla psad
ACCION=$1
USUARIO=$2
IP=$3
psad -fw-block-ip $IP
</pre><p>Ahora sólo queda configurar la respuesta activa en ossec.conf.</p>
<pre>
<active-response>
<disabled>no</disabled>
<command>psad</command>
<location>defined-agent</location>
<agent_id>001</agent_id>
<level>10</level>
<rules_group>authentication_failures</rules_group>
</active-response>
</pre><p>Estamos diciendo que vamos a ejecutar en el agente 001 (el firewall) el
comando psad cuando ocurra un evento de nivel 10 y del grupo
authentication_failures (por ejemplo la regla 5720).</p>
<p>Veamos si funciona bien intentando loguearnos varias veces desde una
máquina (después de haber recargado la configuración de ossec). Este es
el resultado</p>
<pre>
Nov 9 00:50:03 firewall psad: added iptables auto-block against 192.168.10.133 for 3600 seconds
Chain PSAD_BLOCK (1 references)
target prot opt source destination
DROP all -- 192.168.10.133 0.0.0.0/0
</pre><p>Como siempre, y para aquellos que quieran profundizar más, recomiendo el
libro <a class="reference external" href="http://www.elsevierdirect.com/product.jsp?isbn=9781597492409">http://www.elsevierdirect.com/product.jsp?isbn=9781597492409</a>.</p>
Monitorización orientada a host con OSSEC II2008-11-13T23:26:00+01:002008-11-13T23:26:00+01:00tag:www.forondarena.net,2008-11-13:/monitorizacion-orientada-a-host-con-ossec-ii.html<p>En mi primer post sobre ossec he mostrado los pasos para hacer una
instalación básica y completamente estándar de ossec. En este post vamos
a ver lo que hay por debajo del software.</p>
<p>Ossec usa tres componentes: un detector de rootkits, una herramienta
para revisar la integridad del sistema, y …</p><p>En mi primer post sobre ossec he mostrado los pasos para hacer una
instalación básica y completamente estándar de ossec. En este post vamos
a ver lo que hay por debajo del software.</p>
<p>Ossec usa tres componentes: un detector de rootkits, una herramienta
para revisar la integridad del sistema, y el analizador de logs. Todas
ellas se configuran en el fichero ossec.conf.</p>
<p>Este fichero de configuración tiene una estructura muy clara, en formato
xml, en el que se definen varios bloques.</p>
<p>En este post sólo voy a dar algunas ideas básicas sobre el formato con
el que se definen las reglas que describen los eventos detectables en
los logs.</p>
<p>Tanto el chequeo de integridad como el detector de rootkits se
configuran de una forma muy similar, así que lo dejaré para que quien
quiera se pelee con ello.</p>
<p>En ossec.conf se definen los logs que se quieren monitorizar dentro de
secciones "localfile". Un ejemplo:</p>
<pre>
<localfile>
<log_format>syslog</log_format>
<location>/var/log/auth.log</location>
</localfile>
</pre><p>En este caso se dice que queremos vigilar auth.log, del tipo syslog.
Existen varios formatos definidos, como apache o snort-full, por citar
dos ejemplos. Syslog se usa en los casos en los que se loguea un único
evento por linea. Sort-full, por poner otro ejemplo, está adaptado al
tipo de log que deja snort cuando usa el formato de salida full.</p>
<p>Una vez definidos los formatos, empezamos a hablar de reglas. El primer
paso es que ossec detecte qué tipo de entrada de log es. Para esto se
usan los decoders (decoder.xml) Veamos un ejemplo.</p>
<pre>
<decoder name="sshd">
<program_name>^sshd</program_name>
</decoder>
</pre><p>Este decoder "se activa" cuando la entrada de log es generada por el
programa sshd (sshd[5493] en el siguiente ejemplo):</p>
<pre>
Feb 3 19:22:33 server sshd[5493]: Accepted publickey for prueba from 192.168.10.2 port 50560 ssh2
</pre><p>Pero, a pesar de la importancia de esta sencilla regla (después veremos
por qué), necesitamos extender el decoder para que sea útil.</p>
<pre>
<decoder name="ssh-failed">
<parent>sshd</parent>
<prematch>^Failed \S+ </prematch>
<regex offset="after_prematch">^for (\S+) from (\S+) port \d+ \w+$</regex>
<order>user, srcip</order>
</decoder>
</pre><p>Gracias a la primera y simple regla "sshd" nos aseguramos que ossec sólo
va a analizar la entrada de log contra esta segunda regla, mucho más
compleja, si dicho log está relacionado con ssh. Esto es una gran mejora
en el rendimiento del sistema.</p>
<p>Sin entrar en explicaciones detalladas, esta regla se cumple con este
tipo de log:</p>
<pre>
Jul 26 22:06:10 server sshd[3727]: Failed password for prueba from 192.168.10.2 port 50519 ssh2
</pre><p>Y además crea dos "variables"; una con el usuario que ha intentado
conectarse y otra con la IP origen.</p>
<p>Teniendo estos datos ya podemos empezar con las reglas "de verdad".
Veamos algo del fichero sshd_rules.xml</p>
<pre>
<rule id="5700" level="0" noalert="1">
<decoded_as>sshd</decoded_as>
<description>SSHD messages grouped.</description>
</rule>
<rule id="5716" level="5">
<if_sid>5700</if_sid>
<match>^Failed|^error: PAM: Authentication</match>
<description>SSHD authentication failed.</description>
<group>authentication_failed,</group>
</rule>
</pre><p>De manera similar a los decoders, se define una primera regla básica
para ssh, que ayudará a ossec en no tener que analizar reglas de apache
(por ejmplo) en logs de ssh.</p>
<p>La segunda regla (5716) generará una alerta de nivel 5 cuando haya un
intento de login erroneo.
Pero también se pueden crear reglas compuestas que activarán alertas de
mayor nivel si hay x intentos fallidos desde una misma IP</p>
<pre>
<rule id="5720" level="10" frequency="6">
<if_matched_sid>5716</if_matched_sid>
<same_source_ip />
<description>Multiple SSHD authentication failures.</description>
<group>authentication_failures,</group>
</rule>
</pre><p>Gracias al formato xml es muy sencillo añadir nuevas reglas. También se
pueden enviar correos con las alertas a cuentas de correo diferentes en
base, por ejemplo, al agente que ha generado la alerta o al nivel de la
misma.</p>
<p>En el tercer y último post de la serie vamos a integrar ossec con psad.</p>
Inspección de tráfico con tcpdump y tcpflow2008-11-08T00:05:00+01:002008-11-08T00:05:00+01:00tag:www.forondarena.net,2008-11-08:/inspeccion-de-trafico-con-tcpdump-y-tcpflow.html<p>Antes de seguir con el segundo post sobre ossec, voy a dar un par de
pistas sobre cómo ver el tráfico que pasa por una sesión tcp. Esto sí
que es todo un mundo, así que me limito, como casi siempre, a dar cuatro
detalles para que quien quiera se …</p><p>Antes de seguir con el segundo post sobre ossec, voy a dar un par de
pistas sobre cómo ver el tráfico que pasa por una sesión tcp. Esto sí
que es todo un mundo, así que me limito, como casi siempre, a dar cuatro
detalles para que quien quiera se haga una idea de lo que se puede hacer
y siga investigando.</p>
<p>Por supuesto, el que se pueda "espiar" lo que se está trasmitiendo no
significa que debamos (ni podamos) hacerlo, al menos si no queremos
tener problemas legales.</p>
<p>Vamos a empezar por lo básico. Necesitamos capturar tráfico para poderlo
analizar después con cierta tranquilidad. Para esto usamos el conocido
<a class="reference external" href="http://www.tcpdump.org/">tcpdump</a>, aunque podemos usar otros, como por ejemplo, <a class="reference external" href="http://www.snort.org/">snort</a>.</p>
<pre>
tcpdump -n -i eth1 -s 1515 -U -w /tmp/captura.pcap '(tcp port 20) or (tcp port 21) or (tcp port 25)'
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 1515 bytes
</pre><p>Para saber lo que hacen los parámetros nada mejor que el manual :-)</p>
<p>Dejamos esta terminal abierta y con el comando en ejecución. Vamos a ir
analizando lo que se vuelca en captura.pcap en otra terminal.</p>
<p><strong>Primera prueba</strong>.</p>
<p>Desde otra máquina vamos a hacer un telnet al puerto 25 y a mandar un correo.</p>
<pre>
telnet 192.168.10.1 25
Trying 192.168.10.1...
Connected to 192.168.10.1.
Escape character is '^]'.
220 smtp.example.com ESMTP Postfix
helo prueba.example.com
250 smtp.example.com
rset
250 2.0.0 Ok
helo prueba.example.com
250 smtp.example.com
mail from: prueba@example.com
250 2.1.0 Ok
rcpt to: prueba1@example.com
250 2.1.5 Ok
data
354 End data with .
Subject: Titulo del correo
Este texto se ve en la captura
.
250 2.0.0 Ok: queued as 1F7426C420
quit
221 2.0.0 Bye
Connection closed by foreign host.
</pre><p>Nuestro volcado tiene datos.... vamos a ver que tiene usando <a class="reference external" href="http://www.circlemud.org/~jelson/software/tcpflow/">tcpflow</a>.</p>
<pre>
# tcpflow -r captura.pcap -c port 25
192.168.010.001.00025-192.168.010.002.41403: 220 smtp.example.com ESMTP Postfix
192.168.010.002.41403-192.168.010.001.00025: helo prueba.example.com
192.168.010.001.00025-192.168.010.002.41403: 250 smtp.example.com
192.168.010.002.41403-192.168.010.001.00025: rset
192.168.010.001.00025-192.168.010.002.41403: 250 2.0.0 Ok
192.168.010.002.41403-192.168.010.001.00025: helo prueba.example.com
192.168.010.001.00025-192.168.010.002.41403: 250 smtp.example.com
192.168.010.002.41403-192.168.010.001.00025: mail from: prueba@example.com
192.168.010.001.00025-192.168.010.002.41403: 250 2.1.0 Ok
192.168.010.002.41403-192.168.010.001.00025: rcpt to: prueba1@example.com
192.168.010.001.00025-192.168.010.002.41403: 250 2.1.5 Ok
192.168.010.002.41403-192.168.010.001.00025: data
192.168.010.001.00025-192.168.010.002.41403: 354 End data with .
192.168.010.002.41403-192.168.010.001.00025: Subject: Titulo del correo
192.168.010.002.41403-192.168.010.001.00025: Este texto se ve en la captura
192.168.010.002.41403-192.168.010.001.00025: .
192.168.010.001.00025-192.168.010.002.41403: 250 2.0.0 Ok: queued as 1F7426C420
192.168.010.002.41403-192.168.010.001.00025: quit
192.168.010.001.00025-192.168.010.002.41403: 221 2.0.0 Bye
</pre><p>Sorpresa, tcpflow ha generado, en esta caso por salida estándar (-c),
todo lo que ha sido capturado en el puerto 25.</p>
<p><strong>Segunda prueba</strong>.</p>
<p>Bien, vamos con algo un poco diferente, pero igual de fácil (recordad
que esto no son más que ideas)</p>
<p>Ahora vamos a suponer que he programado un rootkit que se llama exploit,
y que lo voy a subir por ftp a la máquina que estamos monitorizando.</p>
<pre>
ftp 192.168.10.1
Connected to 192.168.10.1.
220 Este es un servidor privado. Por favor cierre la sesion inmediatamente.
Name (192.168.10.1:prueba):
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> put exploit
local: exploit remote: exploit
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 File receive OK.
101992 bytes sent in 0.00 secs (29433.1 kB/s)
ftp> quit
221 Goodbye.
</pre><p>Muy bien, ahora veamos lo que tenemos, empezando por el puerto 21, y
después por el 20.</p>
<pre>
# tcpflow -r captura.pcap -c port 21
192.168.010.001.00021-192.168.010.002.50427: 220 Este es un servidor privado. Por favor cierre la sesion inmediatamente.
192.168.010.002.50427-192.168.010.001.00021: USER prueba
192.168.010.001.00021-192.168.010.002.50427: 331 Please specify the password.
192.168.010.002.50427-192.168.010.001.00021: PASS secreto
192.168.010.001.00021-192.168.010.002.50427: 230 Login successful.
192.168.010.002.50427-192.168.010.001.00021: SYST
192.168.010.001.00021-192.168.010.002.50427: 215 UNIX Type: L8
192.168.010.002.50427-192.168.010.001.00021: TYPE I
192.168.010.001.00021-192.168.010.002.50427: 200 Switching to Binary mode.
192.168.010.002.50427-192.168.010.001.00021: PORT 192,168,10,2,205,32
192.168.010.001.00021-192.168.010.002.50427: 200 PORT command successful. Consider using PASV.
192.168.010.002.50427-192.168.010.001.00021: STOR exploit
192.168.010.001.00021-192.168.010.002.50427: 150 Ok to send data.
192.168.010.001.00021-192.168.010.002.50427: 226 File receive OK.
192.168.010.002.50427-192.168.010.001.00021: QUIT
192.168.010.001.00021-192.168.010.002.50427: 221 Goodbye.
</pre><p>Vamos a hacer ahora que tcpflow genere un fichero con el tráfico del
puerto 20. Para esto quitamos el parámetro -c.</p>
<pre>
# tcpflow -r captura.pcap port 20
# file 192.168.010.002.52512-192.168.010.001.00020
192.168.010.002.52512-192.168.010.001.00020: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.8, stripped
</pre><p>Sorpresa, tenemos un fichero binario ..... con el exploit. Bueno, en
realidad en una copia de /bin/ls, pero vale para el ejemplo</p>
<pre>
# strings 192.168.010.002.52512-192.168.010.001.00020
.......
Usage: %s [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort.
Mandatory arguments to long options are mandatory for short options too.
-a, --all do not ignore entries starting with .
-A, --almost-all do not list implied . and ..
--author with -l, print the author of each file
-b, --escape print octal escapes for nongraphic characters
--block-size=SIZE use SIZE-byte blocks
.......
</pre><p>¿Qué pasa en la realidad, cuando tenemos cientos o miles de sesiones de
correo o ftp y queremos ver una concreta? Por un lado debemos guardar el
tráfico que queremos investigar, claro, pero luego debemos saber qué
sesiones se han establecido, a qué hora, cuánto tráfico ha pasado por
ellas, entre que puertos, .... Para esto hay mucho software, pero un
buen ejemplo es <a class="reference external" href="http://qosient.com/argus/">argus</a>. A partir de aquí, podemos generar expresiones
más elaboradas para usar en tcpflow (algo más que "port 20")</p>
Monitorización orientada a host con OSSEC I2008-11-01T23:10:00+01:002008-11-01T23:10:00+01:00tag:www.forondarena.net,2008-11-01:/monitorizacion-orientada-a-host-con-ossec-i.html<p>En el mundo del software libre hay multitud de proyectos que, debido a
su calidad, han terminado siendo comprados por empresas para ser usados
como parte de sus soluciones comerciales. Dos ejemplos son <a class="reference external" href="http://www.sguil.net/">sguil</a> y
<a class="reference external" href="http://www.ossec.net/">ossec</a>.</p>
<p>En la próxima serie de dos posts voy a comentar muy por encima lo …</p><p>En el mundo del software libre hay multitud de proyectos que, debido a
su calidad, han terminado siendo comprados por empresas para ser usados
como parte de sus soluciones comerciales. Dos ejemplos son <a class="reference external" href="http://www.sguil.net/">sguil</a> y
<a class="reference external" href="http://www.ossec.net/">ossec</a>.</p>
<p>En la próxima serie de dos posts voy a comentar muy por encima lo
principal de ossec.</p>
<p>Ossec es un proyecto relacionado con la seguridad informática, y
particularmente con lo que se suele llamar <a class="reference external" href="http://en.wikipedia.org/wiki/Host-based_intrusion_detection_system">HIDS</a>. Dicho de otra forma,
y simplificando mucho, se trata de una aplicación que monitoriza una
máquina, y que detecta las anomalías que puedan producirse en el
sistema. Para hacer esto usa sobre todo una herramienta para la
detección de rootkits, otra para revisar la integridad de ficheros y
ejecutables del sistema, y por último un potente sistema para analizar
logs.</p>
<p>En cuanto a la arquitectura del sistema, usa básicamente un modelo de
servidor/agentes, con lo que tendremos una máquina (servidor) encargada
de recibir y actuar en base a la información que reciba de los agentes
que, en definitiva, son las máquinas que están siendo monitorizadas. La
forma de actuar del servidor es, por una parte, enviando noficaciones, y
por otro lado, si así se configura, generando reglas firewall que se
ejecutarán en los propios agentes.</p>
<p>Este primer post sólo va a describir la instalación de ossec. Dejamos
para posts posteriores la configuración.</p>
<p>La instalación de ossec es muy sencilla. Es suficiente con ejecutar el
típico install.sh del fichero de instalación que se puede descargar
desde la web.</p>
<pre>
sh install.sh
</pre><p>Una vez seleccionamos el idioma de instalación empieza el tema.</p>
<pre>
1- What kind of installation do you want (server, agent, local or help)? server
- Server installation chosen.
2- Setting up the installation environment.
- Choose where to install the OSSEC HIDS [/var/ossec]: /usr/local/ossec-1.6
- Installation will be made at /usr/local/ossec-1.6 .
3- Configuring the OSSEC HIDS.
3.1- Do you want e-mail notification? (y/n) [y]:
- What's your e-mail address? zzz@yyyyyyy.net
- We found your SMTP server as: mail.yyyyyyy.net.
- Do you want to use it? (y/n) [y]:
--- Using SMTP server: mail.yyyyyyy.net.
3.2- Do you want to run the integrity check daemon? (y/n) [y]:
- Running syscheck (integrity check daemon).
3.3- Do you want to run the rootkit detection engine? (y/n) [y]:
- Running rootcheck (rootkit detection).
3.4- Active response allows you to execute a specific
command based on the events received. For example,
you can block an IP address or disable access for
a specific user.
More information at:
http://www.ossec.net/en/manual.html#active-response
- Do you want to enable active response? (y/n) [y]: n
- Active response disabled.
3.5- Do you want to enable remote syslog (port 514 udp)? (y/n) [y]:
- Remote syslog enabled.
3.6- Setting the configuration to analyze the following logs:
-- /var/log/messages
-- /var/log/auth.log
-- /var/log/syslog
-- /var/log/vsftpd.log
-- /var/log/mail.info
-- /var/log/dpkg.log
-- /var/log/snort/alert (snort-full file)
-- /var/log/apache2/error.log (apache log)
-- /var/log/apache2/access.log (apache log)
- If you want to monitor any other file, just change
the ossec.conf and add a new localfile entry.
Any questions about the configuration can be answered
by visiting us online at http://www.ossec.net .
--- Press ENTER to continue ---
</pre><p>A partir de aquí empieza a compilar los distintos componenetes. He
elegido una instalación para el servidor que recibirá los eventos, y he
activado tanto la detección de rootkits como la revisión de integridad,
pero he dejado sin activar la respuesta activa, que básicamente se trata
de añadir entradas a hosts.deny y reglas de firewall.</p>
<p>El syslog remoto lo vamos a usar para recibir mensajes desde los
agentes.</p>
<p>Por cierto, mientras escribía estas líneas el sistema se ha terminado de
compilar:</p>
<pre>
- System is Debian (Ubuntu or derivative).
- Init script modified to start OSSEC HIDS during boot.
- Configuration finished properly.
- To start OSSEC HIDS:
/usr/local/ossec-1.6/bin/ossec-control start
- To stop OSSEC HIDS:
/usr/local/ossec-1.6/bin/ossec-control stop
- The configuration can be viewed or modified at /usr/local/ossec-1.6/etc/ossec.conf
Thanks for using the OSSEC HIDS.
If you have any question, suggestion or if you find any bug,
contact us at contact@ossec.net or using our public maillist at
ossec-list@ossec.net
( http://www.ossec.net/main/support/ ).
More information can be found at http://www.ossec.net
--- Press ENTER to finish (maybe more information below). ---
</pre><p>A partir de aquí, por un lado tendremos que configurar ossec
(ossec.conf), y después tendremos que ir añadiendo los agentes con la
utilidad /usr/local/ossec-1.6/bin/manage_agents.</p>
<p>Vamos a arrancar el servidor y a añadir un par de agentes, uno en linux
y otro en windows. Bueno, el agente windows no lo voy a documentar aquí.
Tiene un instalador gráfico que se descarga desde la web, y los pasos
son muy similares.</p>
<pre>
/usr/local/ossec-1.6/bin/ossec-control start
Starting OSSEC HIDS v1.6 (by Third Brigade, Inc.)...
Started ossec-maild...
Started ossec-execd...
Started ossec-analysisd...
Started ossec-logcollector...
Started ossec-remoted...
Started ossec-syscheckd...
Started ossec-monitord...
Completed.
# ps aux | grep osse
ossecm 5579 0.0 0.0 2076 528 ? S 17:45 0:00 /usr/local/ossec-1.6/bin/ossec-maild
ossec 5587 0.0 0.1 2664 1312 ? S 17:45 0:00 /usr/local/ossec-1.6/bin/ossec-analysisd
root 5591 0.0 0.0 1796 468 ? S 17:45 0:00 /usr/local/ossec-1.6/bin/ossec-logcollector
root 5603 2.5 0.0 1944 512 ? D 17:45 0:04 /usr/local/ossec-1.6/bin/ossec-syscheckd
ossec 5607 0.0 0.0 1980 480 ? S 17:45 0:00 /usr/local/ossec-1.6/bin/ossec-monitord
ossecm 5610 0.0 0.0 2076 300 ? S 17:45 0:00 /usr/local/ossec-1.6/bin/ossec-maild
# ./manage_agents
***********************************************
* OSSEC HIDS v1.6 Agent manager. *
* The following options are available: *
***********************************************
(A)dd an agent (A).
(E)xtract key for an agent (E).
(L)ist already added agents (L).
(R)emove an agent (R).
(Q)uit.
Choose your action: A,E,L,R or Q: A
- Adding a new agent (use '\q' to return to the main menu).
Please provide the following:
* A name for the new agent: agente1
* The IP Address of the new agent: 172.17.0.131
* An ID for the new agent[001]:
Agent information:
ID:001
Name:agente1
IP Address:172.17.0.131
Confirm adding it?(y/n): y
Agent added.
# ./manage_agents
***********************************************
* OSSEC HIDS v1.6 Agent manager. *
* The following options are available: *
***********************************************
(A)dd an agent (A).
(E)xtract key for an agent (E).
(L)ist already added agents (L).
(R)emove an agent (R).
(Q)uit.
Choose your action: A,E,L,R or Q: E
Available agents:
ID: 001, Name: agente1, IP: 172.17.0.131
Provide the ID of the agent to extract the key (or '\q' to quit): 001
Agent key information for '001' is:
XDAxIGZuMTMxIDE5Mi4xNjguMTXuMTMxIDZzY2IzZjg1Y2JmYjVmOaBhMWM0MWRkMTNjMWQ4OWY4MZMyMjkyYTRiOTk5YjJlZ4U5MjRm5zU0ZGE1N2I3NTk=
** Press ENTER to return to the main menu.
</pre><p>Y ya está. Ahora sólo queda instalar el agente en el servidor a
monitorizar. La clave se usa para la comunicación agente/servidor.</p>
<pre>
# ./install.sh
...
1- What kind of installation do you want (server, agent, local or help)? agent
- Agent(client) installation chosen.
2- Setting up the installation environment.
- Choose where to install the OSSEC HIDS [/var/ossec]: /usr/local/ossec-1.6
- Installation will be made at /usr/local/ossec-1.6 .
3- Configuring the OSSEC HIDS.
3.1- What's the IP Address of the OSSEC HIDS server?: 172.17.0.1
- Adding Server IP 172.17.0.1
3.2- Do you want to run the integrity check daemon? (y/n) [y]:
- Running syscheck (integrity check daemon).
3.3- Do you want to run the rootkit detection engine? (y/n) [y]:
- Running rootcheck (rootkit detection).
3.4 - Do you want to enable active response? (y/n) [y]: n
- Active response disabled.
3.5- Setting the configuration to analyze the following logs:
-- /var/log/messages
-- /var/log/auth.log
-- /var/log/syslog
-- /var/log/mail.info
-- /var/log/dpkg.log
- If you want to monitor any other file, just change
the ossec.conf and add a new localfile entry.
Any questions about the configuration can be answered
by visiting us online at http://www.ossec.net .
--- Press ENTER to continue ---
</pre><p>Y ahora copiamos la clave pública que antes hemos sacado del servidor:</p>
<pre>
./bin/manage_agents
***********************************************
* OSSEC HIDS v1.6 Agent manager. *
* The following options are available: *
***********************************************
(I)mport key from the server (I).
(Q)uit.
Choose your action: I or Q: I
* Provide the Key generated by the server.
* The best approach is to cut and paste it.
*** OBS: Do not include spaces or new lines.
Paste it here (or '\q' to quit): XDAxIGZuMTMxIDE5Mi4xNjguMTXuMTMxIDZzY2IzZjg1Y2JmYjVmOaBhMWM0MWRkMTNjMWQ4OWY4MZMyMjkyYTRiOTk5YjJlZ4U5MjRm5zU0ZGE1N2I3NTk=
Agent information:
ID:001
Name:agente1
IP Address:172.17.0.131
Confirm adding it?(y/n): y
Added.
** Press ENTER to return to the main menu.
</pre><p>Arrancamos el agente... y ya está.</p>
<pre>
/usr/local/ossec-1.6/bin/ossec-control start
</pre><p>Y no hay nada más que hacer para tener lo básico de ossec. Por supuesto,
todo es muy configurable, y ampliable en base a lo que cada uno
necesite.</p>
<p>Desde otra máquina, me intento conectar digamos que 10 veces usando ssh
con usuarios invalidos..... sorpresa, recibo este correo.</p>
<pre>
OSSEC HIDS Notification.
2008 Sep 21 22:30:40
Received From: (agente1) 172.17.0.131->/var/log/auth.log
Rule: 5712 fired (level 10) -> "SSHD brute force trying to get access to the system."
Portion of the log(s):
Sep 21 19:06:32 fn131 sshd[5990]: Invalid user aaaaab from 172.17.0.2
Sep 21 19:06:26 fn131 sshd[5986]: Failed password for invalid user aaaaa from 172.17.0.2 port 39010 ssh2
Sep 21 19:06:23 fn131 sshd[5986]: Failed password for invalid user aaaaa from 172.17.0.2 port 39010 ssh2
Sep 21 19:06:21 fn131 sshd[5986]: Failed none for invalid user aaaaa from 172.17.0.2 port 39010 ssh2
Sep 21 19:06:21 fn131 sshd[5986]: Invalid user aaaaa from 172.17.0.2
Sep 21 19:06:09 fn131 sshd[5984]: Failed password for invalid user aaaah from 172.17.0.2 port 39009 ssh2
Sep 21 19:06:06 fn131 sshd[5984]: Failed none for invalid user aaaah from 172.17.0.2 port 39009 ssh2
</pre><p>Algunos ejemplos con distintos tipos de eventos:</p>
<pre>
OSSEC HIDS Notification.
2008 Oct 09 21:29:03
Received From: servidor->syscheck
Rule: 550 fired (level 7) -> "Integrity checksum changed."
Portion of the log(s):
Integrity checksum changed for: '/etc/dovecot/dovecot.conf'
Size changed from '45438' to '45460'
Old md5sum was: 'bd5c81584dad9725045ec0e52eb0c15c'
New md5sum is : '439061951cdeb975c21d37b4cc5f8649'
Old sha1sum was: '8d7ade9a74b9e8ff4d6758c14bdb070ccb15b198'
New sha1sum is : '8e7d34ba6328a1e5d38645972b369e700f7d05b2'
--END OF NOTIFICATION
OSSEC HIDS Notification.
2008 Oct 07 18:29:00
Received From: servidor->/var/log/dpkg.log
Rule: 2902 fired (level 7) -> "New dpkg (Debian Package) installed."
Portion of the log(s):
2008-10-07 18:29:00 status installed unzip 5.52-12
--END OF NOTIFICATION
OSSEC HIDS Notification.
2008 Oct 05 17:42:23
Received From: agente1->/var/log/messages
Rule: 5104 fired (level 8 ) -> "Interface entered in promiscuous(sniffing) mode."
Portion of the log(s):
Oct 5 17:42:21 nurn kernel: device lo entered promiscuous mode
--END OF NOTIFICATION
</pre><p>En el próximo post veremos un poco sobre la configuración de todas estas
reglas.</p>
Firewalls proactivos con psad II2008-09-07T22:42:00+02:002008-09-07T22:42:00+02:00tag:www.forondarena.net,2008-09-07:/firewalls-proactivos-con-psad-ii.html<p>En el anterior post hemos visto lo más básico de psad. De hecho, he
resumido alguna cosa tanto que los que ya conozcan el software pueden
decir que no he sido todo lo riguroso que debiera. Un ejemplo, la parte
relacionada con las variables IPT_AUTO_CHAINn. Mi objetivo, más que
ser …</p><p>En el anterior post hemos visto lo más básico de psad. De hecho, he
resumido alguna cosa tanto que los que ya conozcan el software pueden
decir que no he sido todo lo riguroso que debiera. Un ejemplo, la parte
relacionada con las variables IPT_AUTO_CHAINn. Mi objetivo, más que
ser completamente riguroso, era dar una visión global del software.</p>
<p>En fin, dicho esto, vamos a ver alguna otra cosilla que se puede hacer
con psad.</p>
<p>Recordemos el problema. Queremos poder añadir a nuestro firewall
perimetral reglas que con el tiempo vayan expirando sin tener que estar
encima. Queremos, además, que estas reglas se añadan en base a
decisiones que tomen los servidores web o smtp en base a sus propios
mecanismos, quitando al firewall perimetral la responsabilidad del
análisis del tráfico del nivel de aplicación.</p>
<p>Hay varias formas de hacerlo. Lo más fácil es usar el propio comando
psad. Por ejemplo:</p>
<pre>
# psad -fw-block-ip 10.0.5.98
[+] Writing 10.0.5.98 to socket; psad will add the IP
within 5 seconds.
</pre><p>Es tan sencillo como esto. Veamos el resultado:</p>
<pre>
# psad --fw-list
[+] Listing chains from IPT_AUTO_CHAIN keywords...
Chain PSAD_BLOCK (1 references)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * * 10.0.5.98 0.0.0.0/0
</pre><p>Para quitar esta IP es suficiente con esperar los 600 segundos
configurados, o bien ejecutar:</p>
<pre>
# psad --fw-rm-block-ip 10.0.5.98
[+] Writing 10.0.5.98 to socket; psad will remove the IP
within 5 seconds.
# psad --fw-list
[+] Listing chains from IPT_AUTO_CHAIN keywords...
Chain PSAD_BLOCK (1 references)
pkts bytes target prot opt in out source destination
</pre><p>A partir de aquí, seguro que a cada uno se le ocurren cinco formas de
hacer que un servidor genere los comandos para que el firewall añada las
reglas.</p>
<p>La forma en la que cada administrador gestiona lo que puede hacer con
psad es particular de cada infraestructura, pero sin duda, una
combinación de psad, <a class="reference external" href="http://cipherdyne.org/fwsnort/">fwsnort</a> y otras técnicas como el <a class="reference external" href="http://cipherdyne.org/fwknop/">port
knocking</a> añaden funcionalidades muy interesantes que en muchos casos
ni siquera el software/hardware de pago ofrecen.</p>
Firewalls proactivos con psad2008-09-06T22:30:00+02:002008-09-06T22:30:00+02:00tag:www.forondarena.net,2008-09-06:/firewalls-proactivos-con-psad.html<p>En este post voy a documentar un uso alternativo que se puede dar a
<a class="reference external" href="http://cipherdyne.org/psad/">psad</a>, un interesante software pensado para la detección de escaneos
de puertos, que fue creado como parte de <a class="reference external" href="http://bastille-linux.sourceforge.net/">bastille linux</a> en el 1999.</p>
<p><strong>El problema</strong>:</p>
<p>Tenemos un firewall que gestiona una red digamos que de servidores …</p><p>En este post voy a documentar un uso alternativo que se puede dar a
<a class="reference external" href="http://cipherdyne.org/psad/">psad</a>, un interesante software pensado para la detección de escaneos
de puertos, que fue creado como parte de <a class="reference external" href="http://bastille-linux.sourceforge.net/">bastille linux</a> en el 1999.</p>
<p><strong>El problema</strong>:</p>
<p>Tenemos un firewall que gestiona una red digamos que de servidores web
y smtp, para los que evidentemente tenemos acceso libre por los puertos
25 y 80, pero que reciben multitud de ataques de todo tipo. Las propias
aplicaciones tienen sus mecanismos de seguridad, pero al final siempre
añadimos muchas de las IPs que generan los ataques en el firewall
perimetral. Claro, al final tenemos cientos de IPs en los firewalls que
somos incapaces de mantener, y que no hacen más que complicar su
gestión. Para colmo, la mayoría de esas IP sólo lo intentan durante unos
pocos minutos.</p>
<p><strong>Una solución</strong>:</p>
<p>Queremos un sistema con el que podamos añadir IPs al firewall, y que
expiren, si así lo decidimos, en un cierto plazo de tiempo. Vamos a
utilizar psad para esto, que además, al mismo precio, nos sirve para
detectar escaneos de puertos. En este primer post sobre todo escribiré
sobre lo básico de psad, que en el próximo iremos adaptando al problema.</p>
<p>Vamos a ir instalando. Una opción es descargar el sofware y usar su
propio instalador desde <a class="reference external" href="http://cipherdyne.org/psad/">cipherdyne.org/psad/</a>, pero en este caso voy a
usar el software ya precompilado para <a class="reference external" href="http://packages.debian.org/search?keywords=psad&searchon=names&suite=all&section=all">Debian</a>. Por cierto, más que
recomendable dar una vuelta por cipherdyne.org y ver el software que hay
disponible.</p>
<pre>
aptitude install -R psad
</pre><p>No quiero que installe bastille, de ahí que use -R. Psad es una
aplicación que en su mayoría está programada en perl, así que
dependiendo de lo que cada uno tenga en su sistema instalará más o menos
librerías.</p>
<p>Una instalación por defecto de psad lee los datos que a través de syslog
se pasan a la tubería /var/lib/psad/psadfifo (un pipe de los de toda la
vida), con lo que lo primero es hacer que kern.info se mande a dicho
pipe, con algo tan sencillo como añadir a syslog.conf:</p>
<pre>
kern.info |/var/lib/psad/psadfifo
</pre><p>Después de reiniciar syslog, ya está todo practicamente listo ....</p>
<p><strong>Configuración</strong>:</p>
<p>Dependiendo de la forma de instalación y de la distribución que usemos,
seguramente las rutas, nombres de pipes y alguna otra opción podrían ser
diferentes, con lo que lo descrito aquí es más conceptual que algo con
lo que se pueda hacer copy/paste.</p>
<p>Como psad es un software que sobre todo lee logs, es fundamental que
nuestro firewall loguee lo que queremos tratar, que puede ser sólo lo
del propio cortafuegos, o también lo que otras máquinas puedan enviarle.
En definitiva, que aquí está la clave del invento.</p>
<p>Como referencia, hay que tener en cuenta que psad mira que en las líneas
de logs que recibe existan las cadenas IN= y OUT=, con lo que asume que
son de iptables. Esto nos será útil posteriormente.</p>
<p>Toda la configuración se hace en /etc/psad/psad.conf. En el directorio
/etc/psad hay muchos otros ficheros, algunos los trataré en este post,
pero otro no. Hay que tener en cuenta que psad es un software que hace
más cosas que lo que voy a describir aquí.</p>
<p>La configuración es muy sencilla de leer y muy documentada, "se explica
sola", así que sólo voy a comentar lo más útil para hacerse una idea.</p>
<p>Las dos siguientes variables definen las redes locales y las que no lo
son, muy al estilo <a class="reference external" href="http://www.snort.org">snort</a>.</p>
<pre>
HOME_NET 192.168.10.0/24, 172.16.0.0/16; # un ejemplo
EXTERNAL_NET any;
</pre><p>Estas redes se definen porque psad usa las reglas de snort para detectar
tráfico sospechoso, aunque las usa en cierta medida de forma diferente.
Por ejemplo, para psad todo el tráfico que loguea está destinado a lo
que para snort sería HOME_NET.</p>
<p>Las siguientes variables definen los niveles de peligro. Desde el punto
de vista de escaneo de puertos, estos niveles definen el número de
paquetes recibidos. Desde el punto de vista de otro tipo de actividad
maliciosa, estos niveles se definen según el tipo de ataque o si la IP
origen es conocida por anteriores bloqueos.</p>
<pre>
DANGER_LEVEL1 5; ### Number of packets.
DANGER_LEVEL2 15;
DANGER_LEVEL3 150;
DANGER_LEVEL4 1500;
DANGER_LEVEL5 10000;
</pre><p>Con la siguiente línea hacemos que Psad no sea sólo algo pasivo, sino
que añada reglas a nuestro firewall.</p>
<pre>
ENABLE_AUTO_IDS Y;
</pre><p>Digamos que queremos bloquear IPs a partir del nivel 3:</p>
<pre>
AUTO_IDS_DANGER_LEVEL 3;
</pre><p>Y que queremos que los bloqueos duren 600 segundos</p>
<pre>
AUTO_BLOCK_TIMEOUT 600;
</pre><p>Por supuesto, usamos iptables:</p>
<pre>
IPTABLES_BLOCK_METHOD Y;
</pre><p>La siguiente variable define la/s cadenas en las que queremos que se
añadan reglas de bloqueo. Su sintaxis es:
Target,Direction,Table,From_chain,Jump_rule_position,To_chain,Rule_position.</p>
<p>Por ejemplo:</p>
<pre>
IPT_AUTO_CHAIN1 DROP, src, nat, PREROUTING, 1, PSAD_BLOCK, 1;
</pre><p>Lo más interesante aqui es que DROPeamos con reglas añadidas a la tabla
nat y la cadena PSAD_BLOCK. Se pueden añadir IPT_AUTO_CHAINn reglas.
No hace falta decir que debemos haber creado anteriormente la cadena
PSAD_BLOCK</p>
<p>Psad puede incluso ejecutar scripts externos. La IP origen se pasa a
estos scripts en la variable SRCIP.</p>
<pre>
ENABLE_EXT_SCRIPT_EXEC N;
</pre><p>Hay muchas opciones a configurar. Como he dicho antes aquí no hay más
que unas cuantas para dar una idea de lo que se puede hacer.</p>
<p>Otro fichero interesante es el /etc/psad/auto_dl, en el que podemos
configurar listas blancas y negras de IPs. Por ejemplo:</p>
<pre>
127.0.0.1 0;
10.111.21.23 5 tcp/22;
</pre><p>Con estas dos reglas permitimos todo lo que venga desde 127.0.0.1 y
damos un danger_level de 5 a todo lo que venga de 10.111.21.23 y sea
tráfico ssh.</p>
<p>Otro fichero interesante es /etc/psad/signatures, que incluye unas 200
firmas de snort, pero ligeramente modificadas para poder pasar
información a psad. Por ejemplo:</p>
<pre>
alert icmp $EXTERNAL_NET any -> $HOME_NET any (msg:"ICMP PING undefined code"; icode:>0; itype:8; classtype:misc-activity; sid:365; psad_id:100195; psad_dl:2;)
</pre><p>Es una definición de regla relacionada con icmp, y que tiene un
identificador en psad de 100195 y que asigna un danger_level de 2.</p>
<p>Ahora sólo quedaría arrancar psad, con:</p>
<pre>
/etc/init.d/psad start
</pre><p><strong>Pruebas de funcionamiento</strong>:</p>
<p>Veamos lo que pasa cuando alguien nos hace un escaneo de puertos. Como
nota, tengo un danger_level (DL) de 3 a partir del cual se añaden
reglas de bloqueo.</p>
<pre>
Sep 6 21:48:05 psad: src: 87.218.x.y signature match: "MISC MS Terminal Server communication attempt" (sid: 100077) tcp port: 3389
Sep 6 21:48:05 psad: src: 87.218.x.y signature match: "MISC Microsoft PPTP communication attempt" (sid: 100082) tcp port: 1723
Sep 6 21:48:05 psad: scan detected: 87.218.x.y -> 83.213.x.y tcp: [21-6347] flags: SYN tcp pkts: 130 DL: 2
Sep 6 21:48:11 psad: src: 87.218.x.y signature match: "BACKDOOR DoomJuice file upload attempt" (sid: 2375) tcp port: 3141
Sep 6 21:48:11 psad: src: 87.218.x.y signature match: "DOS Real Audio Server communication attempt" (sid: 100112) tcp port: 7070
Sep 6 21:48:11 psad: src: 87.218.x.y signature match: "BACKDOOR Subseven DEFCON8 2.1 connection Attempt" (sid: 107) tcp port: 16959
Sep 6 21:48:11 psad: scan detected: 87.218.x.y -> 83.213.x.y tcp: [2-32770] flags: SYN tcp pkts: 214 DL: 3
Sep 6 21:48:11 psad: added iptables auto-block against 87.218.x.y for 600 seconds
Sep 6 21:48:16 psad: scan detected: 87.218.x.y -> 83.213.x.y tcp: [104-13722] flags: SYN tcp pkts: 30 DL: 3
</pre><p>Donde 87.218.x.y es la IP origen del escaneo y 83.213.x.y el destino.</p>
<p>En el firewall:</p>
<pre>
# iptables -L PSAD_BLOCK -n -t nat
Chain PSAD_BLOCK (1 references)
target prot opt source destination
DROP all -- 87.218.x.y 0.0.0.0/0
</pre><p>Y a los 600 segundos, se vacia.</p>
<pre>
Sep 6 21:58:13 psad: removed iptables auto-block against 87.218.x.y
# iptables -L PSAD_BLOCK -n -t nat
Chain PSAD_BLOCK (1 references)
target prot opt source destination
</pre><p>Lo que he hecho hasta ahora es lo básico de psad. En el próximo post
seguiré con alguna otra cosilla interesante.</p>
<p>Para el que quiera buscar más información, nada como el libro <a class="reference external" href="http://nostarch.com/firewalls.htm">linux
firewalls</a>. Un libro realmente excelente de una editorial que merece la
pena.</p>