Mercurial > hg > dhcpcd
view src/route.c @ 5567:4fe5c2a71254 draft
hooks: add NOCARRIER_ROAMING reason
This is given when the OS supports the concept of wireless roaming
or the IP setup can be persisted when the carrier drops.
When this happens, routes are moved to a higher metric (if supported)
to support non preferred but non roaming routes.
The `interface_order` hook variable will now order the interfaces
according to priority and move roaming interfaces to the back of the
list.
If resolvconf is present then it is called with the -C option
to deprecate DNS and if carrier comes back it is called again with the
-c option to activate it once more.
As part of this change, default route metrics have been changed to
support a larger number of interfaces.
base metric 1000 (was 200)
wireless offset 2000 (was 100)
IPv4LL offset 1000000 (was 10000)
roaming offset 2000000
| author | Roy Marples <roy@marples.name> |
|---|---|
| date | Sun, 27 Dec 2020 19:53:31 +0000 |
| parents | 08426e8a98a7 |
| children | 4da45107d87a |
line wrap: on
line source
/* SPDX-License-Identifier: BSD-2-Clause */ /* * dhcpcd - route management * Copyright (c) 2006-2020 Roy Marples <roy@marples.name> * All rights reserved * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include <assert.h> #include <ctype.h> #include <errno.h> #include <stdbool.h> #include <stddef.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "config.h" #include "common.h" #include "dhcpcd.h" #include "if.h" #include "if-options.h" #include "ipv4.h" #include "ipv4ll.h" #include "ipv6.h" #include "logerr.h" #include "route.h" #include "sa.h" /* Needed for NetBSD-6, 7 and 8. */ #ifndef RB_TREE_FOREACH_SAFE #ifndef RB_TREE_PREV #define RB_TREE_NEXT(T, N) rb_tree_iterate((T), (N), RB_DIR_RIGHT) #define RB_TREE_PREV(T, N) rb_tree_iterate((T), (N), RB_DIR_LEFT) #endif #define RB_TREE_FOREACH_SAFE(N, T, S) \ for ((N) = RB_TREE_MIN(T); \ (N) && ((S) = RB_TREE_NEXT((T), (N)), 1); \ (N) = (S)) #define RB_TREE_FOREACH_REVERSE_SAFE(N, T, S) \ for ((N) = RB_TREE_MAX(T); \ (N) && ((S) = RB_TREE_PREV((T), (N)), 1); \ (N) = (S)) #endif #ifdef RT_FREE_ROUTE_TABLE_STATS static size_t croutes; static size_t nroutes; static size_t froutes; static size_t mroutes; #endif static void rt_maskedaddr(struct sockaddr *dst, const struct sockaddr *addr, const struct sockaddr *netmask) { const char *addrp = addr->sa_data, *netmaskp = netmask->sa_data; char *dstp = dst->sa_data; const char *addre = (char *)dst + sa_len(addr); const char *netmaske = (char *)dst + MIN(sa_len(addr), sa_len(netmask)); dst->sa_family = addr->sa_family; #ifdef HAVE_SA_LEN dst->sa_len = addr->sa_len; #endif if (sa_is_unspecified(netmask)) { if (addre > dstp) memcpy(dstp, addrp, (size_t)(addre - dstp)); return; } while (dstp < netmaske) *dstp++ = *addrp++ & *netmaskp++; if (dstp < addre) memset(dstp, 0, (size_t)(addre - dstp)); } int rt_cmp_dest(const struct rt *rt1, const struct rt *rt2) { union sa_ss ma1 = { .sa.sa_family = AF_UNSPEC }; union sa_ss ma2 = { .sa.sa_family = AF_UNSPEC }; rt_maskedaddr(&ma1.sa, &rt1->rt_dest, &rt1->rt_netmask); rt_maskedaddr(&ma2.sa, &rt2->rt_dest, &rt2->rt_netmask); return sa_cmp(&ma1.sa, &ma2.sa); } /* * On some systems, host routes have no need for a netmask. * However DHCP specifies host routes using an all-ones netmask. * This handy function allows easy comparison when the two * differ. */ static int rt_cmp_netmask(const struct rt *rt1, const struct rt *rt2) { if (rt1->rt_flags & RTF_HOST && rt2->rt_flags & RTF_HOST) return 0; return sa_cmp(&rt1->rt_netmask, &rt2->rt_netmask); } static int rt_compare_os(__unused void *context, const void *node1, const void *node2) { const struct rt *rt1 = node1, *rt2 = node2; int c; /* Sort by masked destination. */ c = rt_cmp_dest(rt1, rt2); if (c != 0) return c; #ifdef HAVE_ROUTE_METRIC c = (int)(rt1->rt_ifp->metric - rt2->rt_ifp->metric); #endif return c; } static int rt_compare_list(__unused void *context, const void *node1, const void *node2) { const struct rt *rt1 = node1, *rt2 = node2; if (rt1->rt_order > rt2->rt_order) return 1; if (rt1->rt_order < rt2->rt_order) return -1; return 0; } static int rt_compare_proto(void *context, const void *node1, const void *node2) { const struct rt *rt1 = node1, *rt2 = node2; int c; struct interface *ifp1, *ifp2; assert(rt1->rt_ifp != NULL); assert(rt2->rt_ifp != NULL); ifp1 = rt1->rt_ifp; ifp2 = rt2->rt_ifp; /* Prefer interfaces with a carrier. */ c = ifp1->carrier - ifp2->carrier; if (c != 0) return -c; /* Prefer roaming over non roaming if both carriers are down. */ if (ifp1->carrier == LINK_DOWN && ifp2->carrier == LINK_DOWN) { bool roam1 = if_roaming(ifp1); bool roam2 = if_roaming(ifp2); if (roam1 != roam2) return roam1 ? 1 : -1; } #ifdef INET /* IPv4LL routes always come last */ if (rt1->rt_dflags & RTDF_IPV4LL && !(rt2->rt_dflags & RTDF_IPV4LL)) return -1; else if (!(rt1->rt_dflags & RTDF_IPV4LL) && rt2->rt_dflags & RTDF_IPV4LL) return 1; #endif /* Lower metric interfaces come first. */ c = (int)(ifp1->metric - ifp2->metric); if (c != 0) return c; /* Finally the order in which the route was given to us. */ return rt_compare_list(context, rt1, rt2); } static const rb_tree_ops_t rt_compare_os_ops = { .rbto_compare_nodes = rt_compare_os, .rbto_compare_key = rt_compare_os, .rbto_node_offset = offsetof(struct rt, rt_tree), .rbto_context = NULL }; const rb_tree_ops_t rt_compare_list_ops = { .rbto_compare_nodes = rt_compare_list, .rbto_compare_key = rt_compare_list, .rbto_node_offset = offsetof(struct rt, rt_tree), .rbto_context = NULL }; const rb_tree_ops_t rt_compare_proto_ops = { .rbto_compare_nodes = rt_compare_proto, .rbto_compare_key = rt_compare_proto, .rbto_node_offset = offsetof(struct rt, rt_tree), .rbto_context = NULL }; #ifdef RT_FREE_ROUTE_TABLE static int rt_compare_free(__unused void *context, const void *node1, const void *node2) { return node1 == node2 ? 0 : node1 < node2 ? -1 : 1; } static const rb_tree_ops_t rt_compare_free_ops = { .rbto_compare_nodes = rt_compare_free, .rbto_compare_key = rt_compare_free, .rbto_node_offset = offsetof(struct rt, rt_tree), .rbto_context = NULL }; #endif void rt_init(struct dhcpcd_ctx *ctx) { rb_tree_init(&ctx->routes, &rt_compare_os_ops); #ifdef RT_FREE_ROUTE_TABLE rb_tree_init(&ctx->froutes, &rt_compare_free_ops); #endif } bool rt_is_default(const struct rt *rt) { return sa_is_unspecified(&rt->rt_dest) && sa_is_unspecified(&rt->rt_netmask); } static void rt_desc(const char *cmd, const struct rt *rt) { char dest[INET_MAX_ADDRSTRLEN], gateway[INET_MAX_ADDRSTRLEN]; int prefix; const char *ifname; bool gateway_unspec; assert(cmd != NULL); assert(rt != NULL); sa_addrtop(&rt->rt_dest, dest, sizeof(dest)); prefix = sa_toprefix(&rt->rt_netmask); sa_addrtop(&rt->rt_gateway, gateway, sizeof(gateway)); gateway_unspec = sa_is_unspecified(&rt->rt_gateway); ifname = rt->rt_ifp == NULL ? "(null)" : rt->rt_ifp->name; if (rt->rt_flags & RTF_HOST) { if (gateway_unspec) loginfox("%s: %s host route to %s", ifname, cmd, dest); else loginfox("%s: %s host route to %s via %s", ifname, cmd, dest, gateway); } else if (rt_is_default(rt)) { if (gateway_unspec) loginfox("%s: %s default route", ifname, cmd); else loginfox("%s: %s default route via %s", ifname, cmd, gateway); } else if (gateway_unspec) loginfox("%s: %s%s route to %s/%d", ifname, cmd, rt->rt_flags & RTF_REJECT ? " reject" : "", dest, prefix); else loginfox("%s: %s%s route to %s/%d via %s", ifname, cmd, rt->rt_flags & RTF_REJECT ? " reject" : "", dest, prefix, gateway); } void rt_headclear0(struct dhcpcd_ctx *ctx, rb_tree_t *rts, int af) { struct rt *rt, *rtn; if (rts == NULL) return; assert(ctx != NULL); #ifdef RT_FREE_ROUTE_TABLE assert(&ctx->froutes != rts); #endif RB_TREE_FOREACH_SAFE(rt, rts, rtn) { if (af != AF_UNSPEC && rt->rt_dest.sa_family != af && rt->rt_gateway.sa_family != af) continue; rb_tree_remove_node(rts, rt); rt_free(rt); } } void rt_headclear(rb_tree_t *rts, int af) { struct rt *rt; if (rts == NULL || (rt = RB_TREE_MIN(rts)) == NULL) return; rt_headclear0(rt->rt_ifp->ctx, rts, af); } static void rt_headfree(rb_tree_t *rts) { struct rt *rt; while ((rt = RB_TREE_MIN(rts)) != NULL) { rb_tree_remove_node(rts, rt); free(rt); } } void rt_dispose(struct dhcpcd_ctx *ctx) { assert(ctx != NULL); rt_headfree(&ctx->routes); #ifdef RT_FREE_ROUTE_TABLE rt_headfree(&ctx->froutes); #ifdef RT_FREE_ROUTE_TABLE_STATS logdebugx("free route list used %zu times", froutes); logdebugx("new routes from route free list %zu", nroutes); logdebugx("maximum route free list size %zu", mroutes); #endif #endif } struct rt * rt_new0(struct dhcpcd_ctx *ctx) { struct rt *rt; assert(ctx != NULL); #ifdef RT_FREE_ROUTE_TABLE if ((rt = RB_TREE_MIN(&ctx->froutes)) != NULL) { rb_tree_remove_node(&ctx->froutes, rt); #ifdef RT_FREE_ROUTE_TABLE_STATS croutes--; nroutes++; #endif } else #endif if ((rt = malloc(sizeof(*rt))) == NULL) { logerr(__func__); return NULL; } memset(rt, 0, sizeof(*rt)); return rt; } void rt_setif(struct rt *rt, struct interface *ifp) { assert(rt != NULL); assert(ifp != NULL); rt->rt_ifp = ifp; #ifdef HAVE_ROUTE_METRIC rt->rt_metric = ifp->metric; if (if_roaming(ifp)) rt->rt_metric += RTMETRIC_ROAM; #endif } struct rt * rt_new(struct interface *ifp) { struct rt *rt; assert(ifp != NULL); if ((rt = rt_new0(ifp->ctx)) == NULL) return NULL; rt_setif(rt, ifp); return rt; } struct rt * rt_proto_add_ctx(rb_tree_t *tree, struct rt *rt, struct dhcpcd_ctx *ctx) { rt->rt_order = ctx->rt_order++; if (rb_tree_insert_node(tree, rt) == rt) return rt; rt_free(rt); return NULL; } struct rt * rt_proto_add(rb_tree_t *tree, struct rt *rt) { assert (rt->rt_ifp != NULL); return rt_proto_add_ctx(tree, rt, rt->rt_ifp->ctx); } void rt_free(struct rt *rt) { #ifdef RT_FREE_ROUTE_TABLE struct dhcpcd_ctx *ctx; assert(rt != NULL); if (rt->rt_ifp == NULL) { free(rt); return; } ctx = rt->rt_ifp->ctx; rb_tree_insert_node(&ctx->froutes, rt); #ifdef RT_FREE_ROUTE_TABLE_STATS croutes++; froutes++; if (croutes > mroutes) mroutes = croutes; #endif #else free(rt); #endif } void rt_freeif(struct interface *ifp) { struct dhcpcd_ctx *ctx; struct rt *rt, *rtn; if (ifp == NULL) return; ctx = ifp->ctx; RB_TREE_FOREACH_SAFE(rt, &ctx->routes, rtn) { if (rt->rt_ifp == ifp) { rb_tree_remove_node(&ctx->routes, rt); rt_free(rt); } } } /* If something other than dhcpcd removes a route, * we need to remove it from our internal table. */ void rt_recvrt(int cmd, const struct rt *rt, pid_t pid) { struct dhcpcd_ctx *ctx; struct rt *f; assert(rt != NULL); assert(rt->rt_ifp != NULL); assert(rt->rt_ifp->ctx != NULL); ctx = rt->rt_ifp->ctx; switch(cmd) { case RTM_DELETE: f = rb_tree_find_node(&ctx->routes, rt); if (f != NULL) { char buf[32]; rb_tree_remove_node(&ctx->routes, f); snprintf(buf, sizeof(buf), "pid %d deleted", pid); rt_desc(buf, f); rt_free(f); } break; } #if defined(IPV4LL) && defined(HAVE_ROUTE_METRIC) if (rt->rt_dest.sa_family == AF_INET) ipv4ll_recvrt(cmd, rt); #endif } static bool rt_add(rb_tree_t *kroutes, struct rt *nrt, struct rt *ort) { struct dhcpcd_ctx *ctx; bool change, kroute, result; assert(nrt != NULL); ctx = nrt->rt_ifp->ctx; /* * Don't install a gateway if not asked to. * This option is mainly for VPN users who want their VPN to be the * default route. * Because VPN's generally don't care about route management * beyond their own, a longer term solution would be to remove this * and get the VPN to inject the default route into dhcpcd somehow. */ if (((nrt->rt_ifp->active && !(nrt->rt_ifp->options->options & DHCPCD_GATEWAY)) || (!nrt->rt_ifp->active && !(ctx->options & DHCPCD_GATEWAY))) && sa_is_unspecified(&nrt->rt_dest) && sa_is_unspecified(&nrt->rt_netmask)) return false; rt_desc(ort == NULL ? "adding" : "changing", nrt); change = kroute = result = false; if (ort == NULL) { ort = rb_tree_find_node(kroutes, nrt); if (ort != NULL && ((ort->rt_flags & RTF_REJECT && nrt->rt_flags & RTF_REJECT) || (ort->rt_ifp == nrt->rt_ifp && #ifdef HAVE_ROUTE_METRIC ort->rt_metric == nrt->rt_metric && #endif sa_cmp(&ort->rt_gateway, &nrt->rt_gateway) == 0))) { if (ort->rt_mtu == nrt->rt_mtu) return true; change = true; kroute = true; } } else if (ort->rt_dflags & RTDF_FAKE && !(nrt->rt_dflags & RTDF_FAKE) && ort->rt_ifp == nrt->rt_ifp && #ifdef HAVE_ROUTE_METRIC ort->rt_metric == nrt->rt_metric && #endif sa_cmp(&ort->rt_dest, &nrt->rt_dest) == 0 && rt_cmp_netmask(ort, nrt) == 0 && sa_cmp(&ort->rt_gateway, &nrt->rt_gateway) == 0) { if (ort->rt_mtu == nrt->rt_mtu) return true; change = true; } #ifdef RTF_CLONING /* BSD can set routes to be cloning routes. * Cloned routes inherit the parent flags. * As such, we need to delete and re-add the route to flush children * to correct the flags. */ if (change && ort != NULL && ort->rt_flags & RTF_CLONING) change = false; #endif if (change) { if (if_route(RTM_CHANGE, nrt) != -1) { result = true; goto out; } if (errno != ESRCH) logerr("if_route (CHG)"); } #ifdef HAVE_ROUTE_METRIC /* With route metrics, we can safely add the new route before * deleting the old route. */ if (if_route(RTM_ADD, nrt) != -1) { if (ort != NULL) { if (if_route(RTM_DELETE, ort) == -1 && errno != ESRCH) logerr("if_route (DEL)"); } result = true; goto out; } /* If the kernel claims the route exists we need to rip out the * old one first. */ if (errno != EEXIST || ort == NULL) goto logerr; #endif /* No route metrics, we need to delete the old route before * adding the new one. */ #ifdef ROUTE_PER_GATEWAY errno = 0; #endif if (ort != NULL) { if (if_route(RTM_DELETE, ort) == -1 && errno != ESRCH) logerr("if_route (DEL)"); else kroute = false; } #ifdef ROUTE_PER_GATEWAY /* The OS allows many routes to the same dest with different gateways. * dhcpcd does not support this yet, so for the time being just keep on * deleting the route until there is an error. */ if (ort != NULL && errno == 0) { for (;;) { if (if_route(RTM_DELETE, ort) == -1) break; } } #endif /* Shouldn't need to check for EEXIST, but some kernels don't * dump the subnet route just after we added the address. */ if (if_route(RTM_ADD, nrt) != -1 || errno == EEXIST) { result = true; goto out; } #ifdef HAVE_ROUTE_METRIC logerr: #endif logerr("if_route (ADD)"); out: if (kroute) { rb_tree_remove_node(kroutes, ort); rt_free(ort); } return result; } static bool rt_delete(struct rt *rt) { int retval; rt_desc("deleting", rt); retval = if_route(RTM_DELETE, rt) == -1 ? false : true; if (!retval && errno != ENOENT && errno != ESRCH) logerr(__func__); return retval; } static bool rt_cmp(const struct rt *r1, const struct rt *r2) { return (r1->rt_ifp == r2->rt_ifp && #ifdef HAVE_ROUTE_METRIC r1->rt_metric == r2->rt_metric && #endif sa_cmp(&r1->rt_gateway, &r2->rt_gateway) == 0); } static bool rt_doroute(rb_tree_t *kroutes, struct rt *rt) { struct dhcpcd_ctx *ctx; struct rt *or; ctx = rt->rt_ifp->ctx; /* Do we already manage it? */ or = rb_tree_find_node(&ctx->routes, rt); if (or != NULL) { if (rt->rt_dflags & RTDF_FAKE) return true; if (or->rt_dflags & RTDF_FAKE || !rt_cmp(rt, or) || (rt->rt_ifa.sa_family != AF_UNSPEC && sa_cmp(&or->rt_ifa, &rt->rt_ifa) != 0) || or->rt_mtu != rt->rt_mtu) { if (!rt_add(kroutes, rt, or)) return false; } rb_tree_remove_node(&ctx->routes, or); rt_free(or); } else { if (rt->rt_dflags & RTDF_FAKE) { or = rb_tree_find_node(kroutes, rt); if (or == NULL) return false; if (!rt_cmp(rt, or)) return false; } else { if (!rt_add(kroutes, rt, NULL)) return false; } } return true; } void rt_build(struct dhcpcd_ctx *ctx, int af) { rb_tree_t routes, added, kroutes; struct rt *rt, *rtn; unsigned long long o; rb_tree_init(&routes, &rt_compare_proto_ops); rb_tree_init(&added, &rt_compare_os_ops); rb_tree_init(&kroutes, &rt_compare_os_ops); if_initrt(ctx, &kroutes, af); ctx->rt_order = 0; ctx->options |= DHCPCD_RTBUILD; #ifdef INET if (!inet_getroutes(ctx, &routes)) goto getfail; #endif #ifdef INET6 if (!inet6_getroutes(ctx, &routes)) goto getfail; #endif #ifdef BSD /* Rewind the miss filter */ ctx->rt_missfilterlen = 0; #endif RB_TREE_FOREACH_SAFE(rt, &routes, rtn) { if (rt->rt_ifp->active) { if (!(rt->rt_ifp->options->options & DHCPCD_CONFIGURE)) continue; } else if (!(ctx->options & DHCPCD_CONFIGURE)) continue; #ifdef BSD if (rt_is_default(rt) && if_missfilter(rt->rt_ifp, &rt->rt_gateway) == -1) logerr("if_missfilter"); #endif if ((rt->rt_dest.sa_family != af && rt->rt_dest.sa_family != AF_UNSPEC) || (rt->rt_gateway.sa_family != af && rt->rt_gateway.sa_family != AF_UNSPEC)) continue; /* Is this route already in our table? */ if (rb_tree_find_node(&added, rt) != NULL) continue; if (rt_doroute(&kroutes, rt)) { rb_tree_remove_node(&routes, rt); if (rb_tree_insert_node(&added, rt) != rt) { errno = EEXIST; logerr(__func__); rt_free(rt); } } } #ifdef BSD if (if_missfilter_apply(ctx) == -1 && errno != ENOTSUP) logerr("if_missfilter_apply"); #endif /* Remove old routes we used to manage. */ RB_TREE_FOREACH_REVERSE_SAFE(rt, &ctx->routes, rtn) { if ((rt->rt_dest.sa_family != af && rt->rt_dest.sa_family != AF_UNSPEC) || (rt->rt_gateway.sa_family != af && rt->rt_gateway.sa_family != AF_UNSPEC)) continue; rb_tree_remove_node(&ctx->routes, rt); if (rb_tree_find_node(&added, rt) == NULL) { o = rt->rt_ifp->options ? rt->rt_ifp->options->options : ctx->options; if ((o & (DHCPCD_EXITING | DHCPCD_PERSISTENT)) != (DHCPCD_EXITING | DHCPCD_PERSISTENT)) rt_delete(rt); } rt_free(rt); } /* XXX This needs to be optimised. */ while ((rt = RB_TREE_MIN(&added)) != NULL) { rb_tree_remove_node(&added, rt); if (rb_tree_insert_node(&ctx->routes, rt) != rt) { errno = EEXIST; logerr(__func__); rt_free(rt); } } getfail: rt_headclear(&routes, AF_UNSPEC); rt_headclear(&kroutes, AF_UNSPEC); }
