Página siguiente Página anterior Índice general

4. Información para programadores

Le voy a contar un secreto: mi hámster hizo todo el código. Yo sólo era una vía, una `fachada' si quiere, en el gran plan de mi mascota. Por tanto, no me culpe a mí si existen fallos. Culpe al lindo peludo.

4.1 Comprendiendo ip_tables

iptables proporciona simplemente un vector de reglas en memoria (de ahí el nombre `iptables'), e información tal como por dónde deberían comenzar el recorrido los paquetes de cada gancho. Después de que una tabla es registrada, el espacio de usuario puede leer y reemplazar sus contenidos utilizando getsockopt() y setsockopt().

iptables no se registra en ningún gancho de netfilter: cuenta con que otros módulos lo hagan y le administren los paquetes apropiados.

Estructuras de datos de ip_tables

Por conveniencia, se utiliza la misma estructura de datos para representar un regla en el espacio de usuario y dentro del kernel, aunque algunos campos sólo se utilizan dentro del kernel.

Cada regla consiste en las partes siguientes:

  1. Una estructura `struct ipt_entry'.
  2. Cero o más estructuras `struct ipt_entry_match', cada una con una cantidad variable de datos (0 o más bytes) dentro de ella.
  3. Una estructura `struct ipt_entry_target', con una cantidad variable de datos (0 o más bytes) dentro de ella.

La naturaleza variable de las reglas proporciona una enorme flexibilidad a las extensiones, como veremos, especialmente porque cada concordancia (match) u objetivo (target) puede llevar una cantidad de datos arbitraria. Eso, sin embargo, acarrea unas cuantas trampas: tenemos que tener cuidado con la alineación. Esto lo hacemos asegurándonos de que las estructuras `ipt_entry', `ipt_entry_match' e `ipt_entry_target' tienen el tamaño conveniente, y de que todos los datos son redondeados a la máxima alineación de la máquina, utilizando la macro IPT_ALIGN().

La estructura `struct ipt_entry' tiene los siguientes campos:

  1. Una parte `struct ipt_ip', que contiene las especificaciones para la cabecera IP que tiene que concordar.
  2. Un campo de bits `nf_cache' que muestra qué partes del paquete ha examinado esta regla.
  3. Un campo `target_offset' que indica el offset del principio de esta regla donde comienza la estructura ipt_entry_target. Esto siempre debe alinearse correctamente (con la macro IPT_ALIGN).
  4. Un campo `next_offset' que indica el tamaño total de esta regla, incluyendo las concordancias y el objetivo. Esto siempre debe alinearse correctamente con la macro IPT_ALIGN.
  5. Un campo `comefrom', utilizado por el kernel para seguir el recorrido del paquete.
  6. Un campo `struct ipt_counters' que contiene los contadores de paquetes y de bytes que han concordado con esta regla.

Las estructuras `struct ipt_entry_match' y `struct ipt_entry_target' son muy similares, en el sentido de que contienen un campo de longitud total, alineado con IPT_ALIGN, (`match_size' y `target_size' respectivamente) y una unión que contiene el nombre de la concordancia u objetivo (para el espacio de usuario), y un puntero (para el kernel).

Debido a la naturaleza engañosa de la estructura de datos de las reglas, se proporcionan algunas rutinas de ayuda:

ipt_get_target()

Esta función (inline) devuelve un puntero al objetivo de una regla.

IPT_MATCH_ITERATE()

Esta macro llama a la función especificada cada vez que se produce una concordancia en la regla en cuestión. El primer argumento de la función es la estructura `struct ipt_match_entry', y el resto de argumentos (si los hay) son los proporcionados por la macro IPT_MATCH_ITERATE().

IPT_ENTRY_ITERATE()

Esta función recibe un puntero a una entrada, el tamaño total de la tabla de entradas, y una función a la que llamar. El primer argumento de la función es la estructura `struct ipt_entry', y el resto de argumentos (si los hay) son los proporcionados por la macro IPT_ENTRY_ITERATE().

ip_tables desde el espacio de usuario

El espacio de usuario dispone de cuatro operaciones: puede leer la tabla actual, leer la información (posiciones de los ganchos y tamaño de la tabla), reemplazar la tabla (y obtener los contadores antiguos), y añadir nuevos contadores.

Esto permite simular cualquier operación atómica desde el espacio de usuario: se hace mediante la biblioteca libiptc, que proporciona una cómoda semántica "añadir/borrar/reemplazar" para los programas.

Ya que estas tablas son trasladadas al espacio del kernel, la alineación se convierte en un asunto importante en máquinas que tienen reglas de tipo distintas para el espacio de usuario y el espacio del kernel (p.ej. Sparc64 con un (userland) de 32 bits). Estos casos se resuelven cancelando la definición de IPT_ALIGN para estas plataformas en `libiptc.h'.

Recorrido y uso de ip_tables

El kernel comienza el recorrido en la posición indicada por el gancho en particular. Esa regla se examina, y si los elementos de `struct ipt_ip' concuerdan, se comprueba cada `struct ipt_entry_match' en orden (se llama a la función de concordancia asociada con esa concordancia). Si la función de concordancia devuelve 0, la iteración se deteiene en esa regla. Si establece el valor de `hotdrop' a 1, el paquete será rechazado inmediatamente (esto se usa para algunos paquetes sospechosos, como en la función de concordancia tcp).

Si la iteración continúa hasta el final, los contadores se incrementan y se examina la estructura `struct ipt_entry_target': si es un objetivo estándar, se lee el campo `veredict' (negativo significa el veredicto de un paquete, positivo significa un offset al que saltar). Si la respuesta es positiva y el offset no es el de la regla siguiente, se establece la variable `back', y el valor anterior de `back' se coloca en el campo `comefrom' de esa regla.

Para objetivos no estándar, se llama a la función de objetivo: ésta devuelve un veredicto (los objetivos no estándar no pueden saltar, ya que esto interrumpiría el código estático de loop-detection). El veredicto puede ser IPT_CONTINUE, para continuar en la siguiente regla.

4.2 Extendiendo iptables

Como soy vago, iptables es muy extensible. Esto es básicamente una excusa para encajarle el trabajo a otras personas, que es de lo que se trata el open source (cf. el Software Libre, como diría RMS, va sobre la libertad, y yo me he basado en sus palabras para escribir esto).

Extender iptables implica potencialmente dos partes: extender el kernel escribiendo un nuevo módulo, y posiblemente extender el programa de espacio de usuario iptables, escribiendo una nueva biblioteca compartida.

El kernel

Escribir un módulo para el kernel es bastante sencillo, como puede ver a partir de los ejemplos. Una cosa de la que hay que estar avisado es que su código debe ser reentrante: puede haber un paquete entrando desde el espacio de usuario, mientras que otro llega a través de una interrupción. De hecho, con SMP puede haber un paquete por interrupción y por CPU en las versiones 2.3.4 y superiores.

Las funciones que necesita conocer son las siguientes:

init_module()

Ésta es el punto de entrada del módulo. Devuelve un número de error negativo, o 0 si se registra con éxito en netfilter.

cleanup_module()

Ésta es el punto de salida del módulo; lo desregistra de netfilter.

ipt_register_match()

Ésta se utiliza para registrar un nuevo tipo de concordancia. Se le pasa una estructura `struct ipt_match', que normalmente se declara como una variable estática (file-scope).

ipt_register_target()

Ésta se utiliza para registrar un nuevo tipo de objetivo. Se le pasa una estructura `struct ipt_target', que normalmente se declara como una variable estática (file-scope).

ipt_unregister_target()

Utilizada para desregistrar su objetivo.

ipt_unregister_match()

Utilizada para desregistrar su concordancia.

Una advertencia sobre hacer cosas delicadas (como proporcionar contadores) en el espacio extra de su nueva concordancia u objetivo. En las máquinas SMP, toda la tabla se duplica utilizando memcpy en cada CPU: si realmente quiere preservar información central, debería echarle un vistazo al método utilizado en la concordancia `limit'.

Nuevas funciones de concordancia

Las nuevas funciones de concordancia se escriben normalmente como un módulo independiente. Es posible tener extensibilidad en estos módulos, aunque normalmente no es necesario. Una manera sería utilizar la función `nf_register_sockopt' del sistema netfilter para permitir a los usuarios hablar directamente al módulo. Otra manera sería exportar los símbolos para que otros módulos se registren, de la misma manera que lo hacen netfilter e iptables.

El corazón de su nueva función de concordancia es la estructura `struct ipr_match' que le pasa a `ipt_register_match()'. Esta estructura tiene los siguientes campos:

list

En este campo se puede poner lo que sea, digamos `{ NULL, NULL }'.

name

Este campo es el nombre de la función de concordancia, referido por el espacio de usuario. El nombre deber ser igual al nombre del módulo (es decir, si el nombre es "mac", el módulo debe ser "ipt_mac.o") para que funcione la auto-carga.

match

Este campo es un puntero a una función de concordancia, que recibe el skb, los punteros a los dispositivos in y out (uno de los cuales podría ser NULL, dependiendo del gancho), un puntero a los datos de concordancia de la regla que concordó, el tamaño de esa regla, el offset IP (si es distinto de cero se refiere a un fragmento no inicial), un puntero a la cabecera del protocolo (es decir, justo después de la cabecera IP), la longitud de los datos (es decir, la longitud del paquete menos la longitud de la cabecera IP), y finalmente un puntero a una variable `hotdrop'. Devuelve algo distinto de cero si el paquete concuerda, y puede poner `hotdrop' a 1 si devuelve 0, para indicar que el paquete debe rechazarse inmediantemente.

checkentry

Este campo es un puntero a una función que comprueba las especificaciones de una regla; si devuelve 0, no se aceptará la regla del usuario. Por ejemplo, el tipo de concordancia "tcp" sólo aceptará paquetes tcp, y por tanto, si la estructura `struct ipt_ip' de la regla no especifica que el protocolo debe ser tcp, se devuelve cero. El argumento tablename permite a su concordancia controlar en qué tablas puede utilzarse, y `hook_mask' es una máscara de bits de ganchos desde los que se puede llamar a esta regla: si su concordancia no tiene sentido desde algunos ganchos netfilter, puede evitarlo aquí.

destroy

Este campo es un puntero a una función que es llamada cuando se borra una entrada que utiliza esta concordancia. Esto le permite reservar recursos dinámicamente en el checkentry y limpiarlos aquí.

me

A este campo se le asigna `&__this_module', que da un puntero a su módulo. Hace que el contador de uso suba y baje al crearse y destruirse reglas de ese tipo. Esto impide que un usuario pueda eliminar el módulo (y por tanto llamar a cleanup_module()) si una regla se refiere a él.

Nuevos objetivos

Los objetivos nuevos se escriben normalmente como un módulo independiente. Las discusiones de la sección de arriba sobre las `Nuevas funciones de concordancia' se aplican igualmente aquí.

El núcleo de su nuevo objetivo es la estructura `struct ipt_target' que le pasa a `ipt_register_target()'. Esta estructura consta de los siguientes campos:

list

En este campo se puede poner lo que sea, digamos `{ NULL, NULL }'.

name

Este campo es el nombre de la función de objetivo, referido por el espacio de usuario. El nombre debe concordar con el nombre del módulo (es decir, si el nombre es "REJECT", el módulo debe ser "ipt_REJECT.o") para que funcione la auto-carga.

target

Esto es un puntero a la función de objetivo, que recibe el skbuff, los punteros a los dispositivos in y out (de los que cualquiera puede ser NULL), un puntero a los datos del objetivo, el tamaño de los datos del objetivo, y la posición de la regla en la tabla. La función de objetivo devuelve una posición absoluta no negativa hacia la que saltar, o un veredicto negativo (que es el veredicto negado menos uno).

checkentry

Este campo es un puntero a una función que comprueba las especificaciones de una regla; si devuelve 0, entonces no se aceptará la regla del usuario.

destroy

Este campo es un puntero a una función que es llamada cuando se borra una entrada que utiliza este objetivo. Esto le permite reservar recursos dinámicamente en el checkentry y limpiarlos aquí.

me

A este campo se le asigna `&__this_module', que da un puntero a su módulo. Hace que el contador de uso suba y baje al crearse reglas con este objetivo. Esto impide que un usuario pueda eliminar el módulo (y por tanto llamar a cleanup_module()) si una regla se refiere a él.

Nuevas tablas

Puede crear una tabla nueva para sus propósitos específicos si lo desea. Para hacerlo, llame a `ipt_register_table()' con una estructura `struct ipt_table', que tiene los siguientes campos:

list

En este campo se puede poner lo que sea, digamos `{ NULL, NULL }'.

name

Este campo es el nombre de la función de tabla, referido por el espacio de usuario. El nombre debe concordar con el nombre del módulo (es decir, si el nombre es "nat", el módulo debe ser "iptable_nat.o") para que funcione la auto-carga.

table

Esto es una estructura `struct ipt_replace' completamente rellenada, utilizada por el espacio de usuario para reemplazar una tabla. Al puntero `counters' debe asignársele NULL. Esta estructura de datos puede declararse como `__initdata' para que sea descartada después del arranque.

valid_hooks

Esto es una máscara de bits de los ganchos IPv4 de netfilter que introducirá en la tabla: se utiliza para comprobar que esos puntos de entrada son válidos, y para calcular los posibles ganchos para las funciones `checkentry()' de ipt_match e ipt_target.

lock

Esto es el read-write spinlock para toda la tabla; inicialícela a RW_LOCK_UNLOCKED. This is the read-write spinlock for the entire table; initialize it to RW_LOCK_UNLOCKED.

private

Este campo es utilizado internamente por el código de ip_tables.

Herramienta del espacio de usuario

Ahora que ya ha escrito un bonito y reluciente módulo del kernel, quizá quiera controlar sus opciones desde el espacio de usuario. En vez de tener una versión independiente de iptables para cada extensión, he utilizado la última tecnología de los años 90: los furbies. Perdón, quería decir bibliotecas compartidas.

Generalmente, las nuevas tablas no requieren ninguna extensión de iptables: el usuario sólo tiene que utilizar la opción `-t' para hacer que use la nueva tabla.

La biblioteca compartida debe tener una función `_init()', a la que se llamará automáticamente durante la carga: el equivalente moral de la función del módulo del kernel `init_module()'. Ésta llamará a `register_match()' o a `register_target()', dependiendo de si su biblioteca compartida proporciona una nueva concordancia o un nuevo objetivo.

Sólo necesita proporcionar una biblioteca compartida si quiere inicializar parte de la estructura o proporcionar opciones adicionales. Por ejemplo, el objetivo `REJECT' no requiere nada de esto, por lo que no hay ninguna biblioteca compartida.

Hay funciones útiles definidas en la cabecera `iptables.h', especialmente:

check_inverse()

comprueba si un argumento es realmente un `!', y si es así, activa el flag `invert' si no estaba ya activado. Si devuelve verdadero, hay que incrementar optind, como en los ejemplos.

string_to_number()

convierte una cadena en un número dentro del rango dado, devolviendo -1 si está malformada o fuera de rango.

exit_error()

debe llamarse si se encuentra un error. Normalmente, el primer argumento es `PARAMETER_PROBLEM', que significa que el usuario no usó correctamente la línea de comandos.

Nuevas funciones de concordancia

Su función de biblioteca compartida _init() le pasa a `register_match()' un puntero a una estructura estática `struct iptables_match' que tiene los siguientes campos:

next

Este puntero se utiliza para construir una lista enlazada de concordancias (como la utiliza para listar las reglas). Inicialmente debe asignársele el valor NULL.

name

El nombre de la función de concordancia. Debe concordar con el nombre de la librería (por ejemplo "tcp" para `libipt_tcp.so').

version

Normalmente se le asigna la macro NETFILTER_VERSION: esto se hace para asegurar que el binario iptables no utiliza por error una biblioteca compartida equivocada.

size

El tamaño de los datos de concordancia para esta concordancia; debe utilizar la macro IPT_ALIGN() para asegurarse de que está alineado correctamente.

userspacesize

En algunas concordancias, el kernel cambia internamente algunos campos (el objetivo `limit' es un caso). Esto significa que un simple `memcmp()' es insuficiente para comparar dos reglas (algo requerido para la funcionalidad de borrar reglas concordantes). Si éste es el caso, coloque todos los campos que no cambian al principio de la estructura, y ponga aquí el tamaño de estos campos. De todas formas, esto será casi siempre idéntico al campo `size'.

help

Una función que imprime la sintaxis de la opción.

init

Esta función se puede utilizar para inicializar el espacio extra (si lo hay) de la estructura ip_entry_match, y establecer algún bit de nfcache; si está examinando algo no expresable mediante los contenidos de `linux/include/netfilter_ipv4.h', entonces haga simplemente un OR con el bit NFC_UNKNOWN. Se llamará a la función después de `parse()'.

parse

Esta función es llamada cuando se observa una opción desconocida en la línea de comandos: devuelve distinto de cero si la opción era realmente para su bilbioteca. `invert' es verdadero si ya se ha observado un `!'. El puntero `flags' es de uso exclusivo para su biblioteca de concordancia, y normalmente se utiliza para guardar una máscara de bits de opciones que se han especificado. Asegúrese de ajustar el campo nfcache. Si es necesario, puede extender el tamaño de la estructura `ipt_entry_match' haciendo una nueva reserva de memoria, pero entonces debe asegurarse de que el tamaño se pasa a través de la macro IPT_ALIGN()

final_check

Esta función es llamada después de que se haya analizado sintácticamente la línea de comandos, y se le pasa el entero `flags' reservado para su biblioteca. Esto le permite comprobar si no se ha especificado alguna de las opciones obligatorias, por ejemplo: llamar a `exit_error()' si éste es el caso.

print

Esta función es utilizada por el código de listado de cadenas, para imprimir (a la salida estándar) la información de concordancia extra (si la hay) de una regla. El flag numérico se activa si el usuario especificó la opción `-n'.

save

Esta función es el inverso de parse: es utilizada por `iptables-save' para reproducir las opciones que crearon la regla.

extra_opts

Esto es una lista (terminada en NULL) de las opciones extra que ofrece su biblioteca. Se unen a las opciones actuales y son pasadas a getopt_long; para más detalles, lea la página man. El código devuelto por getopt_long se convierte en el primer argumento (`c') de su función `parse()'.

Existen elementos adicionales al final de esta estructura para el uso interno de iptables: no necesita asignarles nada.

Nuevos objetivos

La función _init() de su biblioteca compartida le pasa a `register_target()' un puntero a una estructura estática `struct iptables_target', que tiene campos similares a la estructura iptables_match detallada más arriba.

A veces, un objetivo no necesita de una biblioteca de espacio de usuario; de todas formas, debe crear una trivial: existían demasiados problemas con bibliotecas mal colocadas.

Utilizando `libiptc'

libiptc es la biblioteca de control de iptables, diseñada para listar y manipular las reglas del módulo del kernel iptables. Aunque su aplicación actual es para el programa iptables, hace muy sencillo escribir otras herramientas. Necesita ser root para utilizar estas funciones.

Las propias tablas del kernel son simplemente una tabla de reglas, y una serie de números que representan los puntos de entrada. Mediante la biblioteca, se proporcionan los nombres de las cadenas ("INPUT", etc.) como una abstracción. Las cadenas definidas por el usuario se etiquetan insertando un nodo de error antes de la cabecera de la cadena, que contiene el nombre de la cadena de la sección de datos extra del objetivo (las posiciones de la cadena montada están definidas por los tres puntos de entrada de la tabla).

Cuando se llama a `iptc_init()', se lee la tabla, incluyendo los contadores. La tabla se manipula mediante las funciones `iptc_insert_entry()', `iptc_replace_entry()', `iptc_append_entry()', `iptc_delete_entry()', `iptc_delete_num_entry()', `iptc_flush_entries()', `iptc_zero_entries()', `iptc_create_chain()' `iptc_delete_chain()', y `iptc_set_policy()'.

Los cambios en la tabla no se efectúan hasta que se llama a la función `iptc_commit()'. Esto significa que es posible que dos usuarios de la biblioteca operando en la misma cadena compitan; para prevenir esto habría que hacer un bloqueo, y actualmente no se hace.

Sin embargo, no existe carrera entre los contadores; los contadores se añaden al kernel de tal manera que los incrementos de contador que hay entre la lectura y escritura de la tabla todavía siguen presentándose en la nueva tabla.

Hay varias funciones de ayuda:

iptc_first_chain()

Esta función devuelve el primer nombre de cadena de la tabla.

iptc_next_chain()

Esta función devuelve el siguiente nombre de cadena de la tabla: NULL significa que no hay más cadenas.

iptc_builtin()

Devuelve verdadero si el nombre de cadena dado es el nombre de una cadena montada.

iptc_first_rule()

Esta función devuelve un puntero a la primera regla del nombre de cadena dado: NULL si es una cadena vacía.

iptc_next_rule()

Esta función devuelve un puntero a la siguiente regla de la cadena: NULL significa el final de la cadena.

iptc_get_target()

Esta función obtiene el objetivo de una regla dada. Si es un objetivo extendido, se devuelve el nombre del objetivo. Si es un salto a otra cadena, se devuelve el nombre de esa cadena. Si es un veredicto (p.ej. DROP), se devuelve su nombre. Si no tiene objetivo (una regla tipo accounting), entonces se devuelve la cadena vacía.

Tenga en cuenta que debe utilizarse esta función en vez de utilizar directamente el valor del campo `veredicto' de la estructura ipt_entry, ya que ofrece todas las interpretaciones del veredicto estándar especificadas arriba.

iptc_get_policy()

Esta función obtiene la política de una cadena montada, y rellena el argumento `counters' con las estadísticas de esa política.

iptc_strerror()

Esta función devuelve una explicación más detallada de un fallo de código en la biblioteca iptc. Si una función falla, siempre establece la variable errno: este valor puede pasarse a iptc_strerror() para producir un mensaje de error.

4.3 Comprendiendo NAT

Bienvenido a la Traducción de Direcciones de Red del kernel. Tenga en cuenta que la infraestructura ofrecida está diseñada más para ser completa que para ser eficiente, y puede que algunos ajustes futuros aumenten notablemente la eficiencia. Por el momento estoy contento de que al menos funcione.

El NAT está separado en el seguimiento de conexiones (que no manipula paquetes) y el propio código NAT. El seguimiento de conexiones también está diseñado para que pueda utilizarlo un módulo de iptables, por lo que hace distinciones sutiles en los estados, que a NAT no le interesan en absoluto.

Seguimiento de conexiones

El seguimiento de conexiones se acopla en los ganchos de alta prioridad NF_IP_LOCAL_OUT y NF_IP_PRE_ROUTING para poder interceptar los paquetes antes de que entren en el sistema.

El campo nfct del skb es un puntero al interior de la estructura ip_conntrack, a un elemento del vector infos[]. Así podemos saber el estado del skb mediante el elemento de este vector al que está apuntando: este puntero codifica la estructura de estado y la relación de este skb con ese estado.

La mejor manera de extraer el campo `nfct' es llamando a `ip_conntrack_get()', que devuelve NULL si no está inicializado, o el puntero de conexión, y rellena ctinfo, que describe la relación del paquete con esa conexión. Este tipo enumerado tiene varios valores:

IP_CT_ESTABLISHED

El paquete es parte de una conexión establecida, y va en la dirección original.

IP_CT_RELATED

El paquete está relacionado con la conexión, y está pasando en la dirección original.

IP_CT_NEW

El paquete intenta crear una nueva conexión (obviamente, va en la dirección original).

IP_CT_ESTABLISHED + IP_CT_IS_REPLY

El paquete es parte de una conexión extablecida, en la dirección de respuesta.

IP_CT_RELATED + IP_CT_IS_REPLY

El paquete está relacionado con la conexión, y está pasando en la dirección de respuesta.

Por tanto, se puede identificar un paquete de respuesta comprobando si es >= IP_CT_IS_REPLY.

4.4 Extendiendo el seguimiento de conexiones/NAT

Estos sistemas están diseñados para alojar cualquier número de protocolos y diferentes tipos de correspondencia (mapping). Algunos de estos tipos de correspondencia pueden ser bastante específicos, como el tipo de correspondencia load-balancing/fail-over.

Internamente, el seguimiento de conexiones convierte un paquete en una "n-upla", que representa las partes interesantes del paquete, antes de buscar ligaduras o reglas que concuerden con él. Esta n-upla tiene una parte manipulable y una parte no manipulable, llamadas "src" y "dst", ya que éste es el aspecto del primer paquete en el mundo del Source NAT [SNAT, NAT de origen] (sería un paquete de respuesta en el mundo del Destination NAT [DNAT, NAT de destino]). En todos los paquetes del mismo flujo y en la misma dirección, esta n-upla es igual.

Por ejemplo, la parte manipulable de la n-upla de un paquete TCP son la IP de origen y el puerto de origen, y la parte no manipulable son la IP de destino y el puerto de destino. Sin embargo, las partes manipulable y no manipulable no necesitan ser del mismo tipo; por ejemplo, la parte manipulable de la n-upla de un paquete ICMP es la IP de origen y el id ICMP, y la parte no manipulable es la IP de destino y el tipo y código ICMP.

Toda n-upla tiene una inversa, que es la n-upla de los paquetes de respuesta del flujo. Por ejemplo, la inversa de un paquete ICMP ping con id 12345, desde 192.168.1.1 y hacia 1.2.3.4, es un paquete ping-reply con id 12345, desde 1.2.3.4 hacia 192.168.1.1.

Estas n-uplas, representadas por la estructura `struct ip_conntrack_tuple', se utilizan ampliamente. De hecho, junto con el gancho desde el que vino el paquete (que tiene influye en el tipo de manipulación esperada) y el dispositivo implicado, suponen toda la información del paquete.

La mayoría de las n-uplas están contenidas dentro de una estructura `struct ip_conntrack_tuple_hash', que añade una entrada que es una lista doblemente enlazada, y un puntero a la conexión a la que pertenece la n-upla.

Una conexión está representada por la estructura `struct ip_conntrack'; tiene dos campos `struct ip_conntrack_tuple_hash': uno referido a la dirección del paquete original (tuplehash[IP_CT_DIR_ORIGINAL]), y otro referido a los paquetes de la dirección de respuesta (tuplehash[IP_CT_DIR_REPLY]).

De todas maneras, la primera cosa que hace el código NAT es ver si el código de seguimiento de conexiones consiguió extraer una n-upla y encontrar una conexión existente, mirando el campo nfct del skbuff; esto nos dice si es un intento de conexión nueva, y si no lo es, qué dirección tiene; en el último caso, se realizan con anterioridad las manipulaciones determinadas para esa conexión.

Si era el comienzo de una conexión nueva, buscamos una regla para esa n-upla, utilizando el mecanismo de recorrido estándar de iptables. Si una regla concuerda, se utiliza para inicializar las manipulaciones para esa dirección y para la respuesta; se le dice al código de seguimiento de conexiones que la respuesta que espera ha cambiado. Luego, es manipulado como se explica arriba.

Si no hay regla, se crea una ligadura (binding) `null': normalmente, esto no hace corresponder al paquete, pero existe para asegurarnos de que no hacemos corresponder otro flujo sobre uno ya existente. A veces no puede crearse la ligadura null, porque ya hemos hecho corresponder un flujo existente sobre ella, en cuyo caso la manipulación por-protocolo (per-protocol) puede intentar rehacer la correspondencia (remap), aunque sea nominalmente una ligadura `null'.

Objetivos NAT estándar

Los objetivos NAT son como cualquier otro objetivo de iptables, excepto en que insisten en ser utilizados sólo en la tabla `nat'. Los objetivos SNAT y DNAT reciben una estructura `struct ip_nat_multi_range' como datos extra; esto se utiliza para especificar el rango de direcciones a los que se puede enlazar una correspondencia. Un elemento de rango, la estructura `struct ip_nat_range', consiste en una dirección IP inclusiva mínima y máxima, y un valor específico de protocolo (p.ej. puertos TCP) inclusivo máximo y mínimo. También hay sitio para flags, que dicen si la dirección IP puede corresponderse (a veces sólo queremos corresponder la parte específica de protocolo de una n-upla, no la IP), y otra para decir que la parte específica de protocolo del rango es válida.

Un multi-rango es un vector de estos elementos `struct ip_nat_range'; esto significa que un rango podría ser "1.1.1.1-1.1.1.2 ports 50-55 AND 1.1.1.3 port 80". Cada elemento se añade al rango (una unión, para los que les guste la teoría).

Nuevos protocolos

Dentro del kernel

Implementar un protocolo nuevo significa primero decidir cuáles deben ser las partes manipulables y no manipulables de la n-upla. Todo en la n-upla tiene la propiedad de que identifica al flujo unívocamente. La parte manipulable de la n-upla es la parte con la que usted puede hacer NAT: para el TCP esto es el puerto de origen, para el ICMP es el id; algo que se utiliza para que sea un "identificador de flujo". La parte no manipulable es el resto del paquete que identifica unívocamente al flujo, pero con lo que no podemos trastear (p.ej. el puerto TCP de destino, o el tipo ICMP).

Una vez que ha decidido esto, puede escribir una extensión al código de seguimiento de conexiones en el directorio, y meterse a rellenar la estructura `ip_conntrack_protocol' que necesita pasarle a `ip_conntrack_register_protocol()'.

Los campos de `struct ip_conntrack_protocol' son:

list

Asígnele '{ NULL, NULL }'; utilizado para coserle a la lista.

proto

Su número de protocolo; vea `/etc/protocols'.

name

El nombre de su protocolo. Éste es el nombre que verá el usuario; normalmente, es mejor si es el nombre canónico que aparece en `/etc/protocols'.

pkt_to_tuple

La función que rellena las partes específicas de protocolo de la n-upla, dado un paquete. El puntero `datah' apunta al principio de su cabecera (justo después de la cabecera IP), y datalen es la longitud del paquete. Si el paquete no es lo suficientemente largo para contener la información de la cabecera, devuelve 0; sin embargo, datalen siempre tendrá al menos 8 bytes (forzado por el sistema).

invert_tuple

Esta función se usa simplemente para transformar la parte específica de protocolo de la n-upla en el aspecto que tendría una respuesta a ese paquete.

print_tuple

Esta función se utiliza para imprimir la parte específica de protocolo de una n-upla; normalmente se almacena mediante sprintf() en el búfer especificado. Se devuelve el número de caracteres utilizados del búfer. Esto se utiliza para imprimir los estados para la entrada en /proc.

print_conntrack

Esta función se utiliza para imprimir la parte privada de la estructura conntrack, si hay alguna. También se utiliza para imprimir los estados en /proc.

packet

Se llama a esta función cuando se observa un paquete que es parte de una conexión establecida. Se obtiene un puntero a la estructura conntrack, la cabecera IP, la longitud y el ctinfo. Hay que devolver un veredicto para el paquete (normalmente NF_ACCEPT), o -1 si el paquete no es una parte válida de la conexión. Puede borrar la conexión de esta función si lo desea, pero debe utilizar el siguiente idioma para evitar carreras (vea ip_conntrack_proto_icmp.c):

if (del_timer(&ct->timeout))
        ct->timeout.function((unsigned long)ct);

new

Se llama a esta función cuando un paquete crea una conexión por primera vez; no hay argumento ctinfo, ya que el primer paquete tiene ctinfo IP_CT_NEW por definición. Devuelve 0 para no aprobar la creación de la conexión, o el timeout de la conexión en jiffies.

Una vez que ha escrito su nuevo protocolo y comprobado que puede hacer seguimiento con él, es hora de enseñarle a NAT cómo traducirlo. Esto significa escribir un nuevo módulo, una extensión al código NAT, y meterse a rellenar la estructura `ip_nat_protocol' que necesita pasarle a `ip_nat_protocol_register()'.

list

Asígnele '{ NULL, NULL }'; utilizado para coserle a la lista.

name

El nombre de su protocolo. Éste es el nombre que verá el usuario; es mejor si es el nombre canónico que aparece en `/etc/protocols' para que funcione la auto-carga, como veremos después.

protonum

Su número de protocolo; vea `/etc/protocols'.

manip_pkt

Ésta es la otra mitad de la función de seguimiento de conexiones pkt_to_tuple: puede pensar en ella como en "tuple_to_pkt". Sin embargo, hay algunas diferencias: se obtiene un puntero al comienzo de la cabecera IP y la longitud total del paquete. Esto es así porque algunos protocolos (UDP, TCP) necesitan conocer la cabecera IP. Se obtiene el campo ip_nat_tuple_manip de la n-upla (es decir, el campo "src"), en vez de toda la n-upla, y el tipo de manipulación que se va a realizar.

in_range

Esta función se usa para saber si la parte manipulable de una n-upla dada está dentro del rango dado. Esta función tiene un poco de trampa: obtenemos el tipo de manipulación que se ha aplicado a la n-upla, que nos dice cómo interpretar el rango (¿es un rango de origen o un rango de destino lo que tratamos de obtener?).

Esta función se utiliza para comprobar si una correspondencia (mapping) existente nos coloca dentro del rango adecuado, y también comprueba si no se necesita ninguna manipulación.

unique_tuple

Esta función es el corazón de NAT: dada una n-upla y un rango, vamos a alterar la parte del protocolo de la n-upla para colocarla dentro del rango, y hacerla única. Si se puede encontrar una n-upla sin utilizar dentro del rango, devuelve 0. También obtenemos un puntero a la estructura conntrack, que se requiere para ip_nat_used_tuple().

El método usual es simplemente iterar la parte del protocolo de la n-upla a través del rango, aplicando `ip_nat_used_tuple()' sobre ella, hasta que una devuelva falso.

Tenga en cuenta que ya se ha comprobado el caso de correspondencia nula (null-mapping): o está fuera del rango dado, o ya está cogido.

Si IP_NAT_RANGE_PROTO_SPECIFIED no está activado, significa que el usuario está haciendo NAT, no NAPT: hace algo razonable con el rango. Si no es deseable ninguna correspondencia (por ejemplo, en TCP, una correspondencia de destino no debe cambiar el puerto TCP a menos que se le ordene), devuelve 0.

print

Dado un búfer de caracteres, una n-upla de concordancia y una máscara, escribe la parte específica de protocolo y devuelve la longitud del búfer utilizado.

print_range

Dado un búfer de caracteres y un rango, escribe la parte de protocolo del rango y devuelve la longitud del búfer utilizado. Si IP_NAT_RANGE_PROTO_SPECIFIED no está activado para este rango, no se llamará a esta función.

Nuevos objetivos NAT

Ésta es la parte realmente interesante. Se pueden escribir nuevos objetivos NAT que proporcionen un nuevo tipo de correspondencia; el paquete por defecto trae dos nuevos objetivos adicionales: MASQUERADE y REDIRECT. Son bastante sencillos e ilustran el potencial que tiene escribir un objetivo NAT nuevo.

Están escritos igual que cualquier otro objetivo de iptables, pero internamente extraen la conexión y llaman a `ip_nat_setup_info()'.

Ayudantes de protocolo para UDP y TCP

Esto todavía está en desarrollo.

4.5 Comprendiendo netfilter

Netfilter es muy sencillo, y está descrito con bastante profundidad en las secciones anteriores. Sin embargo, a veces es necesaro ir más allá de lo que ofrecen las infraestructuras de NAT o ip_tables, o usted puede querer reemplazarlas completamente.

Una cuestión importante de netfilter (bueno, en el futuro) es el cacheado. Todo skb tiene un campo `nfcache': una máscara de bits que indica qué campos de la cabecera se examinaron, y si el paquete fue alterado o no. La idea es que cada gancho desactivado de netfilter haga OR en su bit relevante, *The idea is that each hook off netfilter OR's in the bit relevant to it,* para que luego podamos escribir un sistema de caché que sea lo suficientemente listo para darse cuenta de cuándo no es necesario que los paquetes pasen a través de netfilter.

Los bits más importantes son NFC_ALTERED, que significa que el paquete fue alterado (esto ya se utiliza en el gancho IPv4 NF_IP_LOCAL_OUT para re-enrutar los paquetes alterados), y NFC_UNKNOWN, que significa que no debe hacerse cacheado porque se ha examinado una propiedad que no puede ser expresada. En caso de duda, simplemente active el flag NFC_UNKNOWN del campo nfcache del skb de su gancho.

4.6 Escribiendo nuevos módulos netfilter

Conectándose a los ganchos netfilter

Para recibir/filtrar paquetes dentro del kernel, simplemente hay que escribir un módulo que registre un "gancho netfilter". Esto es básicamente una expresión de interés en algún punto dado; los puntos actuales son específicos para un protocolo, y están definidos en las cabeceras de netfilter específicas para un protocolo, como "netfilter_ipv4.h".

Para registrar y desregistrar ganchos netfilter, se utilizan las funciones `nf_register_hook' y `nf_unregister_hook'. Ambas reciben un puntero a una estructura `struct nf_hook_ops', que se rellenan de la manera siguiente:

list

Utilizado para coserle a la lista enlazada: asígnele '{ NULL, NULL }'

hook

La función a la que se llama cuando un paquete llega a este punto de gancho. Su función debe devolver NF_ACCEPT, NF_DROP o NF_QUEUE. Si es NF_ACCEPT, se llamará al próximo gancho enlazado a ese punto. Si es NF_DROP, el paquete es rechazado. Si es NF_QUEUE, se coloca el paquete en la cola. Se recibe un puntero a un puntero skb, por lo que puede reemplazar completamente el skb si lo desea.

flush

Actualmente no se usa: está diseñada para transmitir la cuenta de paquetes cuando se limpia la caché. Puede que nunca se implemente: asígnele NULL.

pf

La familia de protocolos, por ejemplo, `PF_INET' para IPv4.

hooknum

El número del gancho en el que está interesado, por ejemplo, `NF_IP_LOCAL_OUT'.

Procesando paquetes en la cola

Actualmente, esta interfaz la utiliza ip_queue; puede registrarse para manejar los paquetes de la cola de un protocolo dado. Esto tiene una semántica parecida a registrarse para un gancho, excepto en que puede bloquearse procesando un paquete, y sólo puede ver los paquetes a los que un gancho haya respondido `NF_QUEUE'.

Las dos funciones utilizadas para registrar interés en los paquetes de la cola son `nf_register_queue_handler()' y `nf_unregister_queue_handler()'. La función que usted registra será llamada con el puntero `void *' que le pasó a `nf_register_queue_handler()'.

Si nadie está registrado para manejar el protocolo, entonces devolver NF_QUEUE es lo mismo que devolver NF_DROP.

Una vez que ha registrado interés en los paquetes de cola, empiezan a entrar en la cola. Puede hacer lo que quiera con ellos, pero debe llamar a `nf_reinject()' cuando haya acabado (no sirve hacer simplemente un kfree_skb()). Se le pasa el dkb, la estuctura `struct nf_info' que recibió el manejador de la cola, y un veredicto: NF_DROP hace que sean rechazados, NF_ACCEPT hace que continúen iterando a través de los ganchos, NF_QUEUE hace que entren de nuevo en la cola, y NF_REPEAT hace que se consulte de nuevo el gancho que puso al paquete en la cola (cuidado con los bucles infinitos).

Puede mirar dentro de la estructura `struct nf_info' si quiere información auxiliar sobre el paquete, como las interfaces y el gancho en el que estaba.

Recibiendo comandos desde el espacio de usuario

Es corriente que los componentes de netfilter quieran interactuar con el espacio de usuario. El método para hacer esto es utilizar el mecanismo setsockopt. Tenga en cuenta que cada protocolo tiene que modificarse para llamar a nf_setsockopt() para los números setsockopt que no entiende (y nf_getsockopt() para los números getsockopt), y hasta ahora sólo se han modificado IPv4, IPv6 y DECnet.

Utilizando una técnica ya familiar, registramos una estructura `struct nf_sockopt_ops' utilizando la llamada nf_register_sockopt(). Los campos de esta estructura son como sigue:

list

Utilizado para coserlo a la lista enlazada: asígnele '{ NULL, NULL }'.

pf

La familia de protocolos que está manejando, p.ej. PF_INET.

set_optmin

y

set_optmax

Éstos especifican el rango (exclusivo) de números setsockopt manejados. Por tanto, poner 0 y 0 significa que no tiene números setsockopt.

set

Ésta es la función a la que se llama cuando el usuario llama a uno de sus setsockopts. Debe comprobar que tienen capacidad NET_ADMIN dentro de esta función.

get_optmin

y

get_optmax

Éstos especifican el rango (exclusivo) de números getsockopt manejados. Por tanto, poner 0 y 0 significa que no tiene números getsockopt.

get

Ésta es la función que se llama cuando el usuario llama a uno de sus números getsockopt. Debe comprobar que tienen capacidad NET_ADMIN dentro de esta función.

Los dos campos finales son de uso interno.

4.7 Manejo de paquetes en el espacio de usuario

Utilizando la bilbioteca libipq y el módulo `ip_queue', ahora casi todo lo que se puede hacer dentro del kernel se puede hacer desde el espacio de usuario. Esto significa que, con una pequeña pérdida de velocidad, puede desarrollar completamente su código en el espacio de usuario. A menos que trate de filtrar anchos de banda muy grandes, este método es superior a la manipulación de paquetes desde el kernel.

En los primeros días de netfilter, probé esto portando al espacio de usuario una versión embrionaria de iptables. Netfilter abre las puertas para que la gente escriba sus propios y eficientes módulos de manipulación de paquetes en el lenguaje que quieran.


Página siguiente Página anterior Índice general