/* * routes.c * This file is part of Network-inador * * Copyright (C) 2022 - Félix Arreola Rodríguez * * Network-inador is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Network-inador is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Network-inador; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include #include #include #include #include "flist.h" #include "network-inador-private.h" #include "routes.h" //#include "manager.h" int _route_compare_nexthop_v4 (const void * a, const void * b); int _route_compare_nexthop_v6 (const void * a, const void * b); void route_ask_delayed_delroute (NetworkInadorHandle *handle) { if (handle->pipe_routes[1] > 0) { write (handle->pipe_routes[1], "", 1); } } int _route_same_list_nexthops (int family, FList *nexthops_a, FList *nexthops_b) { int count_a, count_b; RouteNH *nha, *nhb; FList *p_a, *p_b; count_a = f_list_length (nexthops_a); count_b = f_list_length (nexthops_b); if (count_a != count_b) return 1; p_a = nexthops_a; p_b = nexthops_b; while (p_a != NULL) { nha = (RouteNH *) p_a->data; nhb = (RouteNH *) p_b->data; if (family == AF_INET) { if (_route_compare_nexthop_v4 (nha, nhb) != 0) return 1; } else if (family == AF_INET6) { if (_route_compare_nexthop_v6 (nha, nhb) != 0) return 1; } p_a = p_a->next; p_b = p_b->next; } return 0; } Route *_route_search_route (FList *list_routes, sa_family_t family, uint8_t tos, uint32_t table, void *dest, uint32_t prefix, uint32_t priority) { FList *g; Route *route; int family_size = 0; if (family == AF_INET) { family_size = sizeof (struct in_addr); } else if (family == AF_INET6) { family_size = sizeof (struct in6_addr); } for (g = list_routes; g != NULL; g = g->next) { route = (Route *) g->data; if (route->family != family) continue; /* Por tos si genera diferencia */ if (route->tos != tos) continue; if (route->table != table) continue; /* Por métrica */ if (route->priority != priority) continue; if (memcmp (&route->dest, dest, family_size) == 0 && route->prefix == prefix) { return route; } } return NULL; } int _route_compare_nexthop_v4 (const void * a, const void * b) { int ret; RouteNH *nha = (RouteNH *) a, *nhb = (RouteNH *) b; ret = memcmp (&nha->gw, &nhb->gw, 4); if (ret != 0) return ret; ret = nha->out_index - nhb->out_index; if (ret != 0) return ret; ret = nha->nh_weight - nhb->nh_weight; return ret; } int _route_compare_nexthop_v6 (const void * a, const void * b) { int ret; RouteNH *nha = (RouteNH *) a, *nhb = (RouteNH *) b; ret = memcmp (&nha->gw, &nhb->gw, 16); if (ret != 0) return ret; ret = nha->out_index - nhb->out_index; if (ret != 0) return ret; ret = nha->nh_weight - nhb->nh_weight; return ret; } FList * _route_sort_nexthops (int family, FList *nexthops) { if (family == AF_INET) { return f_list_sort (nexthops, _route_compare_nexthop_v4); } else if (family == AF_INET6) { return f_list_sort (nexthops, _route_compare_nexthop_v6); } return nexthops; } int routes_receive_message_newroute (struct nl_msg *msg, void *arg) { NetworkInadorHandle *handle = (NetworkInadorHandle *) arg; struct nlmsghdr *reply; Route *route = NULL; uint32_t table, prefix; int remaining, remaining2; int family, family_size = 0; struct rtmsg *rtm_hdr; struct nlattr *attr, *nest_attr_gw; RouteNH *next_hop; struct rtnexthop *nhptr; int rtnhp_len; int multipath_len; unsigned char *p; uint8_t route_type, tos; struct_addr dest; uint32_t priority = 0; int was_new = 0, was_update = 0; FList *route_list = NULL; FList *old_next_hops = NULL; reply = nlmsg_hdr (msg); if (reply->nlmsg_type != RTM_NEWROUTE) return NL_SKIP; /* Recuperar la tabla */ rtm_hdr = nlmsg_data (reply); table = rtm_hdr->rtm_table; /* Recuperar la familia */ family = rtm_hdr->rtm_family; if (family == AF_INET) { family_size = sizeof (struct in_addr); route_list = handle->route_v4_tables; } else if (family == AF_INET6) { family_size = sizeof (struct in6_addr); route_list = handle->route_v6_tables; } /* Recuperar el tos, porque marca diferencia entre rutas */ tos = rtm_hdr->rtm_tos; route_type = rtm_hdr->rtm_type; /* Recuperar el prefijo */ prefix = rtm_hdr->rtm_dst_len; /* Recuperar solo el destino */ memset (&dest, 0, sizeof (dest)); nlmsg_for_each_attr(attr, reply, sizeof (struct rtmsg), remaining) { switch (nla_type (attr)) { case RTA_DST: if (nla_len (attr) != family_size) { /* Tamaño incorrecto para la IP */ continue; } memcpy (&dest, nla_data (attr), family_size); break; case RTA_PRIORITY: if (nla_len (attr) != 4) { /* Tamaño incorrecto para la prioridad */ continue; } priority = nla_get_u32 (attr); break; case RTA_TABLE: if (nla_len (attr) != 4) { /* Tamaño incorrecto para el id de la tabla */ continue; } table = nla_get_u32 (attr); break; } } /* TODO: Revisar si la ruta que procesamos ya existe */ route = _route_search_route (route_list, family, tos, table, &dest, prefix, priority); if (route == NULL) { /* Nueva ruta */ route = (Route *) malloc (sizeof (Route)); memset (route, 0, sizeof (Route)); route->family = family; route->type = route_type; route->table = table; route->prefix = prefix; route->tos = tos; route->priority = priority; route->for_delete = 0; memcpy (&route->dest, &dest, family_size); if (family == AF_INET) { handle->route_v4_tables = f_list_append (handle->route_v4_tables, route); } else if (family == AF_INET6) { handle->route_v6_tables = f_list_append (handle->route_v6_tables, route); } was_new = 1; } else { /* Liberar los next-hops, puesto que volverán a ser creados */ //f_list_free_full (route->nexthops, free); old_next_hops = route->nexthops; route->nexthops = NULL; route->for_delete = 0; } if (route->protocol != rtm_hdr->rtm_protocol) { route->protocol = rtm_hdr->rtm_protocol; was_update = 1; } if (route->scope != rtm_hdr->rtm_scope) { route->scope = rtm_hdr->rtm_scope; was_update = 1; } /* Pre-reservar el primer siguiente brinco y ligar */ next_hop = (RouteNH *) malloc (sizeof (RouteNH)); memset (next_hop, 0, sizeof (RouteNH)); nlmsg_for_each_attr(attr, reply, sizeof (struct rtmsg), remaining) { switch (nla_type (attr)) { case RTA_PREFSRC: if (nla_len (attr) != family_size) { continue; } memcpy (&route->prefsrc, nla_data (attr), family_size); break; case RTA_GATEWAY: if (nla_len (attr) != family_size) { continue; } memcpy (&next_hop->gw, nla_data (attr), family_size); break; case RTA_OIF: if (nla_len (attr) != 4) { /* Tamaño incorrecto para el índice de la interfaz */ continue; } next_hop->out_index = nla_get_u32 (attr); break; /* TODO: Revisar si RTA_PREF, RTA_CACHEINFO es útil */ case RTA_MULTIPATH: nhptr = (struct rtnexthop*) nla_data (attr); multipath_len = nla_len (attr); while (multipath_len > 0) { next_hop->nh_flags = nhptr->rtnh_flags; next_hop->nh_weight = nhptr->rtnh_hops; next_hop->out_index = nhptr->rtnh_ifindex; /* Revisar en los atributos si tiene gw */ p = ((unsigned char *) nhptr) + sizeof (struct rtnexthop); nest_attr_gw = nla_find ((const struct nlattr *) p, nhptr->rtnh_len - sizeof (struct rtnexthop), RTA_GATEWAY); if (nest_attr_gw != NULL && nla_len (nest_attr_gw) == family_size) { memcpy (&next_hop->gw, nla_data (nest_attr_gw), family_size); } multipath_len -= nhptr->rtnh_len; nhptr = (struct rtnexthop *) (((unsigned char *) nhptr) + nhptr->rtnh_len); /* Reservar el siguiente next-hop */ if (multipath_len > 0) { route->nexthops = f_list_append (route->nexthops, next_hop); next_hop = (RouteNH *) malloc (sizeof (RouteNH)); memset (next_hop, 0, sizeof (RouteNH)); } } break; } } route->nexthops = f_list_append (route->nexthops, next_hop); route->nexthops = _route_sort_nexthops (family, route->nexthops); if (_route_same_list_nexthops (family, route->nexthops, old_next_hops) == 1) { /* Son diferentes */ was_update = 1; } /* Liberar la lista vieja de next_hops */ f_list_free_full (old_next_hops, free); if (was_new) { /* Enviar aquí evento de ruta agregada */ //manager_send_event_route_add (handle, route); } else if (was_update) { /* Enviar actualización */ //manager_send_event_route_update (handle, route); } return NL_SKIP; } int routes_receive_message_delroute (struct nl_msg *msg, void *arg) { NetworkInadorHandle *handle = (NetworkInadorHandle *) arg; struct nlmsghdr *reply; Route *route = NULL; uint32_t table, prefix; int remaining; int family, family_size = 0; struct rtmsg *rtm_hdr; struct nlattr *attr; uint8_t route_tos; struct_addr dest; uint32_t priority = 0; FList *route_list = NULL; reply = nlmsg_hdr (msg); if (reply->nlmsg_type != RTM_DELROUTE) return NL_SKIP; /* Recuperar la tabla */ rtm_hdr = nlmsg_data (reply); table = rtm_hdr->rtm_table; /* Recuperar la familia */ family = rtm_hdr->rtm_family; if (family == AF_INET) { family_size = sizeof (struct in_addr); route_list = handle->route_v4_tables; } else if (family == AF_INET6) { family_size = sizeof (struct in6_addr); route_list = handle->route_v6_tables; } /* Recuperar el tos de la ruta*/ route_tos = rtm_hdr->rtm_tos; /* Recuperar el prefijo */ prefix = rtm_hdr->rtm_dst_len; /* Recuperar solo el destino */ memset (&dest, 0, sizeof (dest)); nlmsg_for_each_attr(attr, reply, sizeof (struct rtmsg), remaining) { switch (nla_type (attr)) { case RTA_DST: if (nla_len (attr) != family_size) { /* Tamaño incorrecto para la IP */ continue; } memcpy (&dest, nla_data (attr), family_size); break; case RTA_PRIORITY: if (nla_len (attr) != 4) { /* Tamaño incorrecto para la prioridad */ continue; } priority = nla_get_u32 (attr); break; case RTA_TABLE: if (nla_len (attr) != 4) { /* Tamaño incorrecto para el id de la tabla */ continue; } table = nla_get_u32 (attr); break; } } route = _route_search_route (route_list, family, route_tos, table, &dest, prefix, priority); if (route == NULL) { /* ¿Notificación de eliminar una ruta que no existe? Super raro */ return NL_SKIP; } if (family == AF_INET) { /* Eliminar de la lista ligada */ handle->route_v4_tables = f_list_remove (handle->route_v4_tables, route); } else if (family == AF_INET6) { handle->route_v6_tables = f_list_remove (handle->route_v6_tables, route); } /* Notificar del evento */ //manager_send_event_route_del (handle, route); /* Eliminar todos los next-hops primero */ f_list_free_full (route->nexthops, free); free (route); return NL_SKIP; } static int _route_wait_ack_or_error (struct nl_msg *msg, void *arg) { int *ret = (int *) arg; struct nlmsgerr *l_err; struct nlmsghdr *reply; reply = nlmsg_hdr (msg); if (reply->nlmsg_type == NLMSG_ERROR) { l_err = nlmsg_data (reply); *ret = l_err->error; } return NL_SKIP; } static int _route_wait_error (struct sockaddr_nl *nla, struct nlmsgerr *l_err, void *arg) { int *ret = (int *) arg; *ret = l_err->error; return NL_SKIP; } int routes_add (NetworkInadorHandle *handle, Route *route) { struct nl_msg * msg, *msg_nh; struct rtmsg route_hdr; int ret, error; int family_size = 0; struct_addr empty; int hop_count; RouteNH *nh; FList *g; struct rtnexthop nexthop_hdr, *nhptr; char buffer_nexthops[8192]; int size_nexthops; route_hdr.rtm_family = route->family; route_hdr.rtm_dst_len = route->prefix; route_hdr.rtm_src_len = 0; route_hdr.rtm_tos = route->tos; route_hdr.rtm_table = route->table; route_hdr.rtm_protocol = route->protocol; route_hdr.rtm_scope = route->scope; route_hdr.rtm_type = route->type; route_hdr.rtm_flags = 0; msg = nlmsg_alloc_simple (RTM_NEWROUTE, NLM_F_REQUEST | NLM_F_CREATE); ret = nlmsg_append (msg, &route_hdr, sizeof (route_hdr), NLMSG_ALIGNTO); if (ret != 0) { nlmsg_free (msg); return -1; } if (route->family == AF_INET) { family_size = sizeof (struct in_addr); } else if (route->family == AF_INET6) { family_size = sizeof (struct in6_addr); } ret = nla_put (msg, RTA_DST, family_size, &route->dest); if (route->priority != 0) { ret |= nla_put (msg, RTA_PRIORITY, 4, &route->priority); } memset (&empty, 0, sizeof (empty)); if (memcmp (&empty, &route->prefsrc, family_size) != 0) { ret |= nla_put (msg, RTA_PREFSRC, family_size, &route->prefsrc); } hop_count = f_list_length (route->nexthops); if (hop_count <= 1) { /* Agregar por el método 1 */ nh = (RouteNH *) route->nexthops->data; ret |= nla_put (msg, RTA_OIF, 4, &nh->out_index); if (memcmp (&empty, &nh->gw, family_size) != 0) { ret |= nla_put (msg, RTA_GATEWAY, family_size, &nh->gw); } } else { /* Tenemos múltiples next-hops */ //nest = nla_nest_start (msg, RTA_MULTIPATH); size_nexthops = 0; g = route->nexthops; while (g != NULL) { nh = (RouteNH *) g->data; msg_nh = nlmsg_alloc (); if (msg_nh == NULL) break; nexthop_hdr.rtnh_len = 0; nexthop_hdr.rtnh_flags = nh->nh_flags; nexthop_hdr.rtnh_hops = nh->nh_weight; nexthop_hdr.rtnh_ifindex = nh->out_index; nlmsg_append (msg_nh, &nexthop_hdr, sizeof (nexthop_hdr), 0); if (memcmp (&empty, &nh->gw, family_size) != 0) { /* Tiene un gw el next-hop */ nla_put (msg_nh, RTA_GATEWAY, family_size, &nh->gw); } /* Corregir la longitud */ nhptr = nlmsg_data (nlmsg_hdr (msg_nh)); nhptr->rtnh_len = (nlmsg_hdr (msg_nh))->nlmsg_len - NLMSG_HDRLEN; /* Copiar al buffer de los next-hops */ memcpy (&buffer_nexthops[size_nexthops], nhptr, nhptr->rtnh_len); size_nexthops += nhptr->rtnh_len; nlmsg_free (msg_nh); g = g->next; } ret |= nla_put (msg, RTA_MULTIPATH, size_nexthops, buffer_nexthops); } if (ret != 0) { nlmsg_free (msg); return -1; } nl_complete_msg (handle->nl_sock_route, msg); ret = nl_send (handle->nl_sock_route, msg); nlmsg_free (msg); if (ret <= 0) { return -1; } error = 0; nl_socket_modify_cb (handle->nl_sock_route, NL_CB_VALID, NL_CB_CUSTOM, _route_wait_ack_or_error, &error); nl_socket_modify_cb (handle->nl_sock_route, NL_CB_INVALID, NL_CB_CUSTOM, _route_wait_ack_or_error, &error); nl_socket_modify_cb (handle->nl_sock_route, NL_CB_ACK, NL_CB_CUSTOM, _route_wait_ack_or_error, &error); nl_socket_modify_err_cb (handle->nl_sock_route, NL_CB_CUSTOM, _route_wait_error, &error); ret = nl_recvmsgs_default (handle->nl_sock_route); if (ret < 0 || error < 0) { return -1; } return 0; } int routes_del (NetworkInadorHandle *handle, Route *route) { struct nl_msg * msg; struct rtmsg route_hdr; int ret, error; int family_size = 0; struct_addr empty; int hop_count; RouteNH *nh; FList *g; memset (&route_hdr, 0, sizeof (route_hdr)); route_hdr.rtm_family = route->family; route_hdr.rtm_dst_len = route->prefix; route_hdr.rtm_tos = route->tos; route_hdr.rtm_table = route->table; msg = nlmsg_alloc_simple (RTM_DELROUTE, NLM_F_REQUEST); ret = nlmsg_append (msg, &route_hdr, sizeof (route_hdr), NLMSG_ALIGNTO); if (ret != 0) { nlmsg_free (msg); return -1; } if (route->family == AF_INET) { family_size = sizeof (struct in_addr); } else if (route->family == AF_INET6) { family_size = sizeof (struct in6_addr); } ret = nla_put (msg, RTA_DST, family_size, &route->dest); if (route->priority != 0) { ret |= nla_put (msg, RTA_PRIORITY, 4, &route->priority); } if (ret != 0) { nlmsg_free (msg); return -1; } nl_complete_msg (handle->nl_sock_route, msg); ret = nl_send (handle->nl_sock_route, msg); nlmsg_free (msg); if (ret <= 0) { return -1; } error = 0; nl_socket_modify_cb (handle->nl_sock_route, NL_CB_VALID, NL_CB_CUSTOM, _route_wait_ack_or_error, &error); nl_socket_modify_cb (handle->nl_sock_route, NL_CB_INVALID, NL_CB_CUSTOM, _route_wait_ack_or_error, &error); nl_socket_modify_cb (handle->nl_sock_route, NL_CB_ACK, NL_CB_CUSTOM, _route_wait_ack_or_error, &error); nl_socket_modify_err_cb (handle->nl_sock_route, NL_CB_CUSTOM, _route_wait_error, &error); ret = nl_recvmsgs_default (handle->nl_sock_route); if (ret < 0 || error < 0) { return -1; } return 0; } void routes_ask (NetworkInadorHandle *handle) { struct nl_msg * msg; struct rtmsg rtm_hdr = { .rtm_family = AF_UNSPEC, }; int ret; FList *g, *n; Route *r; /* Recorrer todas las rutas en la tabla de ruteo y marcarlas para eliminación */ for (g = handle->route_v4_tables; g != NULL; g = g->next) { r = (Route *) g->data; r->for_delete = 1; } for (g = handle->route_v6_tables; g != NULL; g = g->next) { r = (Route *) g->data; r->for_delete = 1; } /* Ahora sí, mandar la petición */ msg = nlmsg_alloc_simple (RTM_GETROUTE, NLM_F_REQUEST | NLM_F_DUMP); if (msg == NULL) { return; } ret = nlmsg_append (msg, &rtm_hdr, sizeof (rtm_hdr), NLMSG_ALIGNTO); if (ret != 0) { nlmsg_free (msg); return; } nl_complete_msg (handle->nl_sock_route, msg); ret = nl_send (handle->nl_sock_route, msg); nlmsg_free (msg); nl_socket_modify_cb (handle->nl_sock_route, NL_CB_VALID, NL_CB_CUSTOM, routes_receive_message_newroute, handle); nl_recvmsgs_default (handle->nl_sock_route); /* Ejecutar las eliminaciones */ for (g = handle->route_v4_tables; g != NULL; g = n) { n = g->next; r = (Route *) g->data; if (r->for_delete == 0) continue; handle->route_v4_tables = f_list_remove (handle->route_v4_tables, r); /* Notificar del evento */ //manager_send_event_route_del (handle, r); /* Eliminar todos los next-hops primero */ f_list_free_full (r->nexthops, free); free (r); } for (g = handle->route_v6_tables; g != NULL; g = n) { n = g->next; r = (Route *) g->data; if (r->for_delete == 0) continue; handle->route_v6_tables = f_list_remove (handle->route_v6_tables, r); /* Notificar del evento */ //manager_send_event_route_del (handle, r); /* Eliminar todos los next-hops primero */ f_list_free_full (r->nexthops, free); free (r); } } int _routes_table_find_by_number (const void * left, const void * right) { RouteTable *a, *b; a = (RouteTable *) left; b = (RouteTable *) right; return a->table != b->table; } void _routes_table_parse_file (NetworkInadorHandle *handle, FILE *fd) { FList *g; int ret; RouteTable *rtable, temp_table; char buffer[2048]; while (fgets (buffer, sizeof (buffer), fd), feof (fd) == 0) { if (buffer[0] == '#') continue; /* Ignorar las lineas con comentarios */ ret = sscanf (buffer, "%d %s", &(temp_table.table), temp_table.name); if (ret < 2) { continue; } g = f_list_find_custom (handle->route_tables_names, &temp_table, _routes_table_find_by_number); if (g != NULL) { /* El número de tabla ya existe, marcar y actualizar el nombre */ rtable = (RouteTable *) g->data; rtable->for_delete = 0; rtable->was_new = 0; } else { /* No existe, crear nueva */ rtable = (RouteTable *) malloc (sizeof (RouteTable)); rtable->table = temp_table.table; rtable->for_delete = 0; rtable->was_new = 1; } /* En cualquier caso actualizar el nombre */ strncpy (rtable->name, temp_table.name, sizeof (rtable->name)); handle->route_tables_names = f_list_append (handle->route_tables_names, rtable); } } void routes_tables_read (NetworkInadorHandle *handle, int do_notify) { FILE *fd; FList *g, *h; RouteTable *rtable; DIR *dir; struct dirent *direntry; int len; char buffer[4096]; g = handle->route_tables_names; while (g != NULL) { rtable = (RouteTable *) g->data; rtable->for_delete = 1; rtable->was_new = 0; g = g->next; } //f_list_free_full (handle->route_tables_names, g_free); //handle->route_tables_names = NULL; /* Intentar abrir /etc/iproute2/rt_tables */ fd = fopen ("/etc/iproute2/rt_tables", "r"); if (fd != NULL) { _routes_table_parse_file (handle, fd); fclose (fd); } /* Ahora leer todo el directorio /etc/iproute2/rt_tables.d/ y buscar archivos *.conf */ dir = opendir ("/etc/iproute2/rt_tables.d"); if (dir != NULL) { while (direntry = readdir (dir), direntry != NULL) { len = strlen (direntry->d_name); /* Buscar por archivos que terminen en .conf */ if (len > 5 && strcmp (&(direntry->d_name[len - 5]), ".conf") == 0) { /* Intentar abrir este archivo y parsearlo */ snprintf (buffer, sizeof (buffer), "/etc/iproute2/rt_tables.d/%s", direntry->d_name); fd = fopen (buffer, "r"); if (fd != NULL) { _routes_table_parse_file (handle, fd); fclose (fd); } } } closedir (dir); } /* Ahora, todas las tablas que están marcadas para eliminar, eliminarlas */ g = handle->route_tables_names; while (g != NULL) { h = g->next; rtable = (RouteTable *) g->data; if (rtable->for_delete) { handle->route_tables_names = f_list_delete_link (handle->route_tables_names, g); if (do_notify) { //manager_send_event_route_table_del (handle, rtable); } free (rtable); } g = h; } g = handle->route_tables_names; while (g != NULL) { rtable = (RouteTable *) g->data; if (rtable->was_new) { if (do_notify) { //manager_send_event_route_table_add (handle, rtable); } rtable->was_new = 0; } g = g->next; } } void routes_init (NetworkInadorHandle *handle) { /* Si es la primera vez que nos llaman, descargar una primera lista de interfaces */ routes_ask (handle); /* Inicializar los nombres de las tablas de ruteo */ routes_tables_read (handle, FALSE); } void routes_clean_up (NetworkInadorHandle *handle) { FList *g; Route *route; for (g = handle->route_v4_tables; g != NULL; g = g->next) { route = (Route *) g->data; f_list_free_full (route->nexthops, free); route->nexthops = NULL; } f_list_free_full (handle->route_v4_tables, free); handle->route_v4_tables = NULL; for (g = handle->route_v6_tables; g != NULL; g = g->next) { route = (Route *) g->data; f_list_free_full (route->nexthops, free); route->nexthops = NULL; } f_list_free_full (handle->route_v6_tables, free); handle->route_v6_tables = NULL; /* FIXME: Falta liberar el nombre de las tablas */ }