Feeds 3

Artículos recientes

Nube de tags

Categorías

Archivo

Proyectos

Documentos

Blogs

Ideas para un servidor DNS doméstico Sat, 09 Jan 2021

No prestamos suficiente importancia a los DNS que usamos.

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.

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.

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.

Como base del post, y ya que hemos mencionado PowerDNS, vamos usar su software de DNS recursivo.

Instalación básica

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.

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 repo.powerdns.com.

En el caso de servidor recursivo, podéis descargar el software para varias distribuciones sin mayores problemas. Recordad que hablamos de PowerDNS Recursor, y no de PowerDNS Authoritative Server.

Una vez hecha la instalación básica, en distribuciones modernas seguramente tendréis un pdns-recursor ejecutando, con su correspondiente servicio de Systemd. Esto importará más adelante cuando configuremos instancias virtuales.

La configuración del servidor suele estar en /etc/powerdns/recursor.conf. Echad un ojo a los parámetros y ajustadlos a vuestro gusto.

Logueo selectivo

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 tracing. Vamos a hacer cosas un poco más interesantes que estas.

En este ejemplo loquearemos las consultas que se hagan a Sistemas Autónomos 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.

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 aquí.

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 lua-dns-script de recursor.conf, indicando la ruta completa al fichero LUA. Veamos un ejemplo antes de seguir:

badips = newNMG()
badips:addMask("192.168.10.0/24")

dropset = newDS()
dropset:add("phishing.example.net")

function ipfilter(rem, loc, dh)
        return badips:match(rem)
end

function preresolve(dq)
  if dropset:check(dq.qname) then
    dq.rcode = pdns.DROP
    return true;
  end
end

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 ipfilter, 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. preresolve 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 pdns.DROP del if.

Sigamos con el caso de uso original. Queremos loquear sistemas autónomos.

Necesitamos dos cosas antes de empezar:

  • 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 Maxmind, 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 GeoLite2-ASN.mmdb.
  • 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 lua-mmdb. Yo uso la versión más reciente, que era la 0.2 cuando escribia este post. Cuidado, con otra versión u otra librería podrían ser necesarios retoques en el código.

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.

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 postresolve, que a estas alturas ya sabéis que se ejecuta justo antes de generar la respuesta para el cliente DNS.

function postresolve(dq)
  local records = dq:getRecords()

  if records then
    local mmdb = require "mmdb"
    local geo_asn_db = mmdb.read("/usr/local/GeoLite2-ASN.mmdb") or nil

    if not geo_asn_db then
      pdnslog("Error al abrir bases de datos", pdns.loglevels.Warning)
      return false
    end

    local asn_org = nil
    for k,v in pairs(records) do
      if v.type == pdns.A then
        local asn_data = geo_asn_db:search_ipv4(v:getContent())

        if type(asn_data) == "table" then
          asn_org = asn_data["autonomous_system_organization"] or "Desconocido"
          pdnslog(dq.qname:toString() .. " devuelve " .. v:getContent() .. " de " .. asn_org, pdns.loglevels.Warning)
          return false
        end
      end
    end
  end

  return false
end

Como véis, estamos usando la función dq:getRecords(), que ofrece el API de PowerDNS, para leer todos los registros que va a devolver una consulta. Con cada registro de tipo A usamos search_ipv4, de la librería mmdb, para buscar el sistema autónomo de la IP en la base de datos GeoLite2-ASN.mmdb. Lo logueamos con 'pdnslog', pero podríamos hacer cualquier otra cosa que quisiésemos. En mi syslog de ejemplo veo cosas como estas:

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

Alta disponiblidad y balanceo

Podría pensarse que queda un poco lejos de una instalación doméstica, pero dnsdist es un software que todo el mundo que trabaje en entornos DNS debería conocer.

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:

  • 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.
  • Usar un DNS recursivo de una VPN, por ejemplo de trabajo, para resolver zonas privadas. Usar el personal para todo lo demás.

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.

La instalación de DNSDist puede hacerse desde el mismo repositorio que el resto de software de PowerDNS. Se configura en un fichero tipo /etc/dnsdist/dnsdist.conf, y suele incluir un .service de Systemd en sistemas modernos.

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 local-address) y en el puerto 54 (local-port). Aseguráos de que podéis usar el 54.

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):

setLocal('192.168.0.1')
addLocal('127.0.0.1')

En este momento sería interesante limitar (o no) el acceso a ciertas IPs o redes con setACL, aunque no lo veremos aquí.

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:

newServer({address="127.0.0.1:54", name="propio", order=1, pool="general"})
newServer({address="8.8.8.8", name="externo", order=2, pool="general"})

newServer({address="172.16.0.10", name="privado", pool="vpn"})

setPoolServerPolicy(firstAvailable, "general")

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.

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.

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:

addAction(makeRule({"example.com."}), PoolAction("vpn"))

Y mandaríamos el resto al pool "general":

addAction(AllRule(), PoolAction("general"))

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.

Habilitar DNS sobre TLS y sobre HTTPS

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.

En el mundo de PowerDNS configuramos DOT y DOH bajo el paraguas de DNSDist, que ofrece soporte para ambos sin grandes complicaciones:

addTLSLocal('192.168.0.1', 'clave_publica.pem', 'clave_privada.pem')
addDOHLocal('192.168.0.1', 'clave_publica.pem', 'clave_privada.pem')

Podéis usar los certificados que queráis, incluido Lets Encrypt.

Una vez hecho esto pasaréis a tener abiertos los puertos 853 (DOT) y 443 (DOH).

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 Wikipedia tenéis algo de información para ir tirando del hilo.

Usar DNS para dar información (Tipo RBL)

Aunque solo está documentado para el servidor autorizativo, en pdns-recursor también se pueden usar instancias virtuales. Básicamente, se trata de copiar /etc/powerdns/recursor.conf con otro nombre, por ejemplo /etc/powerdns/recursor-rbl.conf, hacer que escuche en otro puerto (local-port), hacer los ajustes que se quiera, y ejecutarlo vía systemd, con systemctl start pdns-recursor@rbl.service. 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.

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.

Para este ejemplo vamos a hacer un servicio similar a asn.rspamd.com. Si no lo conocéis, podéis probar a consultar lo que devuelve para una IP:

# dig -t txt 8.8.8.8.asn.rspamd.com +short
"15169|8.8.8.0/24|US|arin|"

Recordad que en este tipo de consulta RBL la notación de la IP es inversa.

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...

Muy sencillo. Vamos a crear el mencionado recursor-rbl.conf, 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 Github, pero va en esta línea. Usaremos preresolve porque vamos a devolver una respuesta directamente, sin consultar a la red.

function preresolve(dq)
  if dq.qtype == pdns.TXT then
    local direct_ip = nil
    local cntlabels = dq.qname:countLabels()
    if (dq.qname:isPartOf(newDN("asn.example.com")) and cntlabels == 7) then
      -- ipv4reverse saca la IP de la consulta y le da la vuelta para usarla en GeoIP.
      -- Hay un ejemplo de implementación en la referencia de Github.
      direct_ip = ipv4reverse(dq.qname:toString())
      if not direct_ip then
        dq.rcode = pdns.NXDOMAIN
        return true
      end

      local mmdb = require "mmdb"
      local geo_asn_db = mmdb.read("/usr/local/GeoLite2-ASN.mmdb") or nil
      if not geo_asn_db then
        pdnslog("Error al abrir bases de datos", pdns.loglevels.Warning)
        dq.rcode = pdns.SERVFAIL
        return true
      end

      local asn_data = geo_asn_db:search_ipv4(direct_ip)
      local asn_number = nil
      if type(asn_data) == "table" then
        asn_number = asn_data["autonomous_system_number"] or nil
        if asn_numer then
          dq.rcode = pdns.NOERROR
          dq:addAnswer(pdns.TXT, '"' .. tostring(asn_number) .. '"', 60)
          return true
        end
      end
    end
  end

  dq.rcode = pdns.NXDOMAIN
  return true
end

Lo importante es que devolvemos un registro TXT (dq.rcode y dq:addAnswer) con el id del sistema autónomo y un TTL de 60 segundos.

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:

newServer({address="127.0.0.1:55", name="rbl1", pool="rbls"})

Y después una regla de enrutamiento. Cuidado, hay que añadirla antes que la AllRule() del ejemplo de DNSDist anterior o no se ejecutará:

addAction(makeRule({"asn.example.com."}), PoolAction("rbls"))

Una vez recargado DNSDist, si todo va bien, debéis poder ejecutar algo como esto:

# dig -t txt 8.8.8.8.asn.example.com +short
"15169"

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.

Y con esto termino los ejemplos. ¿Son útiles? Seguramente no para la mayoría de gente, pero siempre está bien conocer alternativas.