diff options
Diffstat (limited to 'src/ndisc/nm-lndp-ndisc.c')
| -rw-r--r-- | src/ndisc/nm-lndp-ndisc.c | 629 |
1 files changed, 629 insertions, 0 deletions
diff --git a/src/ndisc/nm-lndp-ndisc.c b/src/ndisc/nm-lndp-ndisc.c new file mode 100644 index 00000000..3bc1590e --- /dev/null +++ b/src/ndisc/nm-lndp-ndisc.c @@ -0,0 +1,629 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* nm-lndp-ndisc.c - Router discovery implementation using libndp + * + * This program 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, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2013 Red Hat, Inc. + */ + +#include "nm-default.h" + +#include "nm-lndp-ndisc.h" + +#include <string.h> +#include <arpa/inet.h> +#include <netinet/icmp6.h> +/* stdarg.h included because of a bug in ndp.h */ +#include <stdarg.h> +#include <ndp.h> + +#include "nm-ndisc-private.h" +#include "NetworkManagerUtils.h" +#include "platform/nm-platform.h" +#include "platform/nmp-netns.h" + +#define _NMLOG_PREFIX_NAME "ndisc-lndp" + +/*****************************************************************************/ + +typedef struct { + struct ndp *ndp; + + GIOChannel *event_channel; + guint event_id; +} NMLndpNDiscPrivate; + +/*****************************************************************************/ + +struct _NMLndpNDisc { + NMNDisc parent; + NMLndpNDiscPrivate _priv; +}; + +struct _NMLndpNDiscClass { + NMNDiscClass parent; +}; + +/*****************************************************************************/ + +G_DEFINE_TYPE (NMLndpNDisc, nm_lndp_ndisc, NM_TYPE_NDISC) + +#define NM_LNDP_NDISC_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMLndpNDisc, NM_IS_LNDP_NDISC) + +/*****************************************************************************/ + +static gboolean +send_rs (NMNDisc *ndisc, GError **error) +{ + NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc); + struct ndp_msg *msg; + int errsv; + + errsv = ndp_msg_new (&msg, NDP_MSG_RS); + if (errsv) { + errsv = errsv > 0 ? errsv : -errsv; + g_set_error_literal (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "cannot create router solicitation"); + return FALSE; + } + ndp_msg_ifindex_set (msg, nm_ndisc_get_ifindex (ndisc)); + + errsv = ndp_msg_send (priv->ndp, msg); + ndp_msg_destroy (msg); + if (errsv) { + errsv = errsv > 0 ? errsv : -errsv; + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "%s (%d)", + g_strerror (errsv), errsv); + return FALSE; + } + + return TRUE; +} + +_NM_UTILS_LOOKUP_DEFINE (static, translate_preference, enum ndp_route_preference, NMNDiscPreference, + NM_UTILS_LOOKUP_DEFAULT (NM_NDISC_PREFERENCE_INVALID), + NM_UTILS_LOOKUP_ITEM (NDP_ROUTE_PREF_LOW, NM_NDISC_PREFERENCE_LOW), + NM_UTILS_LOOKUP_ITEM (NDP_ROUTE_PREF_MEDIUM, NM_NDISC_PREFERENCE_MEDIUM), + NM_UTILS_LOOKUP_ITEM (NDP_ROUTE_PREF_HIGH, NM_NDISC_PREFERENCE_HIGH), +); + +static int +receive_ra (struct ndp *ndp, struct ndp_msg *msg, gpointer user_data) +{ + NMNDisc *ndisc = (NMNDisc *) user_data; + NMNDiscDataInternal *rdata = ndisc->rdata; + NMNDiscConfigMap changed = 0; + struct ndp_msgra *msgra = ndp_msgra (msg); + struct in6_addr gateway_addr; + guint32 now = nm_utils_get_monotonic_timestamp_s (); + int offset; + int hop_limit; + + /* Router discovery is subject to the following RFC documents: + * + * http://tools.ietf.org/html/rfc4861 + * http://tools.ietf.org/html/rfc4862 + * + * The biggest difference from good old DHCP is that all configuration + * items have their own lifetimes and they are merged from various + * sources. Router discovery is *not* contract-based, so there is *no* + * single time when the configuration is finished and updates can + * come at any time. + */ + _LOGD ("received router advertisement at %u", now); + + /* DHCP level: + * + * The problem with DHCP level is what to do if subsequent + * router advertisements carry different flags. Currently we just + * rewrite the flag with every inbound RA. + */ + { + NMNDiscDHCPLevel dhcp_level; + + if (ndp_msgra_flag_managed (msgra)) + dhcp_level = NM_NDISC_DHCP_LEVEL_MANAGED; + else if (ndp_msgra_flag_other (msgra)) + dhcp_level = NM_NDISC_DHCP_LEVEL_OTHERCONF; + else + dhcp_level = NM_NDISC_DHCP_LEVEL_NONE; + + /* when receiving multiple RA (possibly from different routers), + * let's keep the "most managed" level. */ + G_STATIC_ASSERT_EXPR (NM_NDISC_DHCP_LEVEL_MANAGED > NM_NDISC_DHCP_LEVEL_OTHERCONF); + G_STATIC_ASSERT_EXPR (NM_NDISC_DHCP_LEVEL_OTHERCONF > NM_NDISC_DHCP_LEVEL_NONE); + dhcp_level = MAX (dhcp_level, rdata->public.dhcp_level); + + if (dhcp_level != rdata->public.dhcp_level) { + rdata->public.dhcp_level = dhcp_level; + changed |= NM_NDISC_CONFIG_DHCP_LEVEL; + } + } + + /* Default gateway: + * + * Subsequent router advertisements can represent new default gateways + * on the network. We should present all of them in router preference + * order. + */ + gateway_addr = *ndp_msg_addrto (msg); + { + NMNDiscGateway gateway = { + .address = gateway_addr, + .timestamp = now, + .lifetime = ndp_msgra_router_lifetime (msgra), + .preference = translate_preference (ndp_msgra_route_preference (msgra)), + }; + + if (nm_ndisc_add_gateway (ndisc, &gateway)) + changed |= NM_NDISC_CONFIG_GATEWAYS; + } + + /* Addresses & Routes */ + ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREFIX) { + guint8 r_plen; + struct in6_addr r_network; + + /* Device route */ + + r_plen = ndp_msg_opt_prefix_len (msg, offset); + if (r_plen == 0 || r_plen > 128) + continue; + nm_utils_ip6_address_clear_host_address (&r_network, ndp_msg_opt_prefix (msg, offset), r_plen); + + if (ndp_msg_opt_prefix_flag_on_link (msg, offset)) { + NMNDiscRoute route = { + .network = r_network, + .plen = r_plen, + .timestamp = now, + .lifetime = ndp_msg_opt_prefix_valid_time (msg, offset), + }; + + if (nm_ndisc_add_route (ndisc, &route)) + changed |= NM_NDISC_CONFIG_ROUTES; + } + + /* Address */ + if ( r_plen == 64 + && ndp_msg_opt_prefix_flag_auto_addr_conf (msg, offset)) { + NMNDiscAddress address = { + .address = r_network, + .timestamp = now, + .lifetime = ndp_msg_opt_prefix_valid_time (msg, offset), + .preferred = ndp_msg_opt_prefix_preferred_time (msg, offset), + }; + + if (address.preferred > address.lifetime) + address.preferred = address.lifetime; + if (nm_ndisc_complete_and_add_address (ndisc, &address)) + changed |= NM_NDISC_CONFIG_ADDRESSES; + } + } + ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_ROUTE) { + NMNDiscRoute route = { + .gateway = gateway_addr, + .plen = ndp_msg_opt_route_prefix_len (msg, offset), + .timestamp = now, + .lifetime = ndp_msg_opt_route_lifetime (msg, offset), + .preference = translate_preference (ndp_msg_opt_route_preference (msg, offset)), + }; + + if (route.plen == 0 || route.plen > 128) + continue; + + /* Routers through this particular gateway */ + nm_utils_ip6_address_clear_host_address (&route.network, ndp_msg_opt_route_prefix (msg, offset), route.plen); + if (nm_ndisc_add_route (ndisc, &route)) + changed |= NM_NDISC_CONFIG_ROUTES; + } + + /* DNS information */ + ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_RDNSS) { + static struct in6_addr *addr; + int addr_index; + + ndp_msg_opt_rdnss_for_each_addr (addr, addr_index, msg, offset) { + NMNDiscDNSServer dns_server = { + .address = *addr, + .timestamp = now, + .lifetime = ndp_msg_opt_rdnss_lifetime (msg, offset), + }; + + /* Pad the lifetime somewhat to give a bit of slack in cases + * where one RA gets lost or something (which can happen on unreliable + * links like WiFi where certain types of frames are not retransmitted). + * Note that 0 has special meaning and is therefore not adjusted. + */ + if (dns_server.lifetime && dns_server.lifetime < 7200) + dns_server.lifetime = 7200; + if (nm_ndisc_add_dns_server (ndisc, &dns_server)) + changed |= NM_NDISC_CONFIG_DNS_SERVERS; + } + } + ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_DNSSL) { + char *domain; + int domain_index; + + ndp_msg_opt_dnssl_for_each_domain (domain, domain_index, msg, offset) { + NMNDiscDNSDomain dns_domain = { + .domain = domain, + .timestamp = now, + .lifetime = ndp_msg_opt_rdnss_lifetime (msg, offset), + }; + + /* Pad the lifetime somewhat to give a bit of slack in cases + * where one RA gets lost or something (which can happen on unreliable + * links like WiFi where certain types of frames are not retransmitted). + * Note that 0 has special meaning and is therefore not adjusted. + */ + if (dns_domain.lifetime && dns_domain.lifetime < 7200) + dns_domain.lifetime = 7200; + if (nm_ndisc_add_dns_domain (ndisc, &dns_domain)) + changed |= NM_NDISC_CONFIG_DNS_DOMAINS; + } + } + + hop_limit = ndp_msgra_curhoplimit (msgra); + if (rdata->public.hop_limit != hop_limit) { + rdata->public.hop_limit = hop_limit; + changed |= NM_NDISC_CONFIG_HOP_LIMIT; + } + + /* MTU */ + ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_MTU) { + guint32 mtu = ndp_msg_opt_mtu(msg, offset); + if (mtu >= 1280) { + if (rdata->public.mtu != mtu) { + rdata->public.mtu = mtu; + changed |= NM_NDISC_CONFIG_MTU; + } + } else { + /* All sorts of bad things would happen if we accepted this. + * Kernel would set it, but would flush out all IPv6 addresses away + * from the link, even the link-local, and we wouldn't be able to + * listen for further RAs that could fix the MTU. */ + _LOGW ("MTU too small for IPv6 ignored: %d", mtu); + } + } + + nm_ndisc_ra_received (ndisc, now, changed); + return 0; +} + +static void * +_ndp_msg_add_option (struct ndp_msg *msg, int len) +{ + void *ret = (uint8_t *)msg + ndp_msg_payload_len (msg); + + len += ndp_msg_payload_len (msg); + if (len > ndp_msg_payload_maxlen (msg)) + return NULL; + + ndp_msg_payload_len_set (msg, len); + return ret; +} + +#define NM_ND_OPT_RDNSS 25 +typedef struct { + struct nd_opt_hdr header; + uint16_t reserved; + uint32_t lifetime;; + struct in6_addr addrs[0]; +} NMLndpRdnssOption; + +#define NM_ND_OPT_DNSSL 31 +typedef struct { + struct nd_opt_hdr header; + uint16_t reserved; + uint32_t lifetime; + char search_list[0]; +} NMLndpDnsslOption; + +static gboolean +send_ra (NMNDisc *ndisc, GError **error) +{ + NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc); + NMNDiscDataInternal *rdata = ndisc->rdata; + guint32 now = nm_utils_get_monotonic_timestamp_s (); + int errsv; + struct in6_addr *addr; + struct ndp_msg *msg; + struct nd_opt_prefix_info *prefix; + int i; + + errsv = ndp_msg_new (&msg, NDP_MSG_RA); + if (errsv) { + errsv = errsv > 0 ? errsv : -errsv; + g_set_error_literal (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "cannot create a router advertisement"); + return FALSE; + } + + ndp_msg_ifindex_set (msg, nm_ndisc_get_ifindex (ndisc)); + + /* Multicast to all nodes. */ + addr = ndp_msg_addrto (msg); + addr->s6_addr32[0] = htonl(0xff020000); + addr->s6_addr32[1] = 0; + addr->s6_addr32[2] = 0; + addr->s6_addr32[3] = htonl(0x1); + + ndp_msgra_router_lifetime_set (ndp_msgra (msg), NM_NDISC_ROUTER_LIFETIME); + + /* The device let us know about all addresses that the device got + * whose prefixes are suitable for delegating. Let's announce them. */ + for (i = 0; i < rdata->addresses->len; i++) { + NMNDiscAddress *address = &g_array_index (rdata->addresses, NMNDiscAddress, i); + guint32 age = now - address->timestamp; + guint32 lifetime = address->lifetime; + guint32 preferred = address->preferred; + + /* Clamp the life times if they're not forever. */ + if (lifetime != 0xffffffff) + lifetime = lifetime > age ? lifetime - age : 0; + if (preferred != 0xffffffff) + preferred = preferred > age ? preferred - age : 0; + + prefix = _ndp_msg_add_option (msg, sizeof(*prefix)); + if (!prefix) { + /* Maybe we could sent separate RAs, but why bother... */ + _LOGW ("The RA is too big, had to omit some some prefixes."); + break; + } + + prefix->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; + prefix->nd_opt_pi_len = 4; + prefix->nd_opt_pi_prefix_len = 64; + prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK; + prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO; + prefix->nd_opt_pi_valid_time = htonl(lifetime); + prefix->nd_opt_pi_preferred_time = htonl(preferred); + prefix->nd_opt_pi_prefix.s6_addr32[0] = address->address.s6_addr32[0]; + prefix->nd_opt_pi_prefix.s6_addr32[1] = address->address.s6_addr32[1]; + prefix->nd_opt_pi_prefix.s6_addr32[2] = 0; + prefix->nd_opt_pi_prefix.s6_addr32[3] = 0; + } + + if (rdata->dns_servers->len) { + NMLndpRdnssOption *option; + int len = sizeof(*option) + sizeof(option->addrs[0]) * rdata->dns_servers->len; + + option = _ndp_msg_add_option (msg, len); + if (option) { + option->header.nd_opt_type = NM_ND_OPT_RDNSS; + option->header.nd_opt_len = len / 8; + option->lifetime = htonl (900); + + for (i = 0; i < rdata->dns_servers->len; i++) { + NMNDiscDNSServer *dns_server = &g_array_index (rdata->dns_servers, NMNDiscDNSServer, i); + option->addrs[i] = dns_server->address; + } + } else { + _LOGW ("The RA is too big, had to omit DNS information."); + } + + } + + if (rdata->dns_domains->len) { + NMLndpDnsslOption *option; + NMNDiscDNSDomain *dns_server; + int len = sizeof(*option); + char *search_list; + + for (i = 0; i < rdata->dns_domains->len; i++) { + dns_server = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i); + len += strlen (dns_server->domain) + 2; + } + len = (len + 8) & ~0x7; + + option = _ndp_msg_add_option (msg, len); + if (option) { + option->header.nd_opt_type = NM_ND_OPT_DNSSL; + option->header.nd_opt_len = len / 8; + option->lifetime = htonl (900); + + search_list = option->search_list; + for (i = 0; i < rdata->dns_domains->len; i++) { + NMNDiscDNSDomain *dns_domain = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i); + uint8_t domain_len = strlen (dns_domain->domain); + + *search_list++ = domain_len; + memcpy (search_list, dns_domain->domain, domain_len); + search_list += domain_len; + *search_list++ = '\0'; + } + } else { + _LOGW ("The RA is too big, had to omit DNS search list."); + } + } + + errsv = ndp_msg_send (priv->ndp, msg); + + ndp_msg_destroy (msg); + if (errsv) { + errsv = errsv > 0 ? errsv : -errsv; + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "%s (%d)", + g_strerror (errsv), errsv); + return FALSE; + } + + return TRUE; +} + +static int +receive_rs (struct ndp *ndp, struct ndp_msg *msg, gpointer user_data) +{ + NMNDisc *ndisc = user_data; + + nm_ndisc_rs_received (ndisc); + return 0; +} + +static gboolean +event_ready (GIOChannel *source, GIOCondition condition, NMNDisc *ndisc) +{ + nm_auto_pop_netns NMPNetns *netns = NULL; + NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc); + + _LOGD ("processing libndp events"); + + if (!nm_ndisc_netns_push (ndisc, &netns)) + return G_SOURCE_CONTINUE; + + ndp_callall_eventfd_handler (priv->ndp); + return G_SOURCE_CONTINUE; +} + +static void +start (NMNDisc *ndisc) +{ + NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc); + int fd = ndp_get_eventfd (priv->ndp); + + g_return_if_fail (!priv->event_channel); + g_return_if_fail (!priv->event_id); + + priv->event_channel = g_io_channel_unix_new (fd); + priv->event_id = g_io_add_watch (priv->event_channel, G_IO_IN, (GIOFunc) event_ready, ndisc); + + /* Flush any pending messages to avoid using obsolete information */ + event_ready (priv->event_channel, 0, ndisc); + + switch (nm_ndisc_get_node_type (ndisc)) { + case NM_NDISC_NODE_TYPE_HOST: + ndp_msgrcv_handler_register (priv->ndp, receive_ra, NDP_MSG_RA, nm_ndisc_get_ifindex (ndisc), ndisc); + break; + case NM_NDISC_NODE_TYPE_ROUTER: + ndp_msgrcv_handler_register (priv->ndp, receive_rs, NDP_MSG_RS, nm_ndisc_get_ifindex (ndisc), ndisc); + break; + default: + g_assert_not_reached (); + } +} + +/*****************************************************************************/ + +static inline int +ipv6_sysctl_get (NMPlatform *platform, const char *ifname, const char *property, int min, int max, int defval) +{ + return (int) nm_platform_sysctl_get_int_checked (platform, + NMP_SYSCTL_PATHID_ABSOLUTE (nm_utils_ip6_property_path (ifname, property)), + 10, + min, + max, + defval); +} + +static void +nm_lndp_ndisc_init (NMLndpNDisc *lndp_ndisc) +{ +} + +NMNDisc * +nm_lndp_ndisc_new (NMPlatform *platform, + int ifindex, + const char *ifname, + NMUtilsStableType stable_type, + const char *network_id, + NMSettingIP6ConfigAddrGenMode addr_gen_mode, + NMNDiscNodeType node_type, + GError **error) +{ + nm_auto_pop_netns NMPNetns *netns = NULL; + NMNDisc *ndisc; + NMLndpNDiscPrivate *priv; + int errsv; + + g_return_val_if_fail (NM_IS_PLATFORM (platform), NULL); + g_return_val_if_fail (!error || !*error, NULL); + g_return_val_if_fail (network_id, NULL); + + if (!nm_platform_netns_push (platform, &netns)) + return NULL; + + ndisc = g_object_new (NM_TYPE_LNDP_NDISC, + NM_NDISC_PLATFORM, platform, + NM_NDISC_STABLE_TYPE, (int) stable_type, + NM_NDISC_IFINDEX, ifindex, + NM_NDISC_IFNAME, ifname, + NM_NDISC_NETWORK_ID, network_id, + NM_NDISC_ADDR_GEN_MODE, (int) addr_gen_mode, + NM_NDISC_NODE_TYPE, (int) node_type, + NM_NDISC_MAX_ADDRESSES, ipv6_sysctl_get (platform, ifname, + "max_addresses", + 0, G_MAXINT32, NM_NDISC_MAX_ADDRESSES_DEFAULT), + NM_NDISC_ROUTER_SOLICITATIONS, ipv6_sysctl_get (platform, ifname, + "router_solicitations", + 1, G_MAXINT32, NM_NDISC_ROUTER_SOLICITATIONS_DEFAULT), + NM_NDISC_ROUTER_SOLICITATION_INTERVAL, ipv6_sysctl_get (platform, ifname, + "router_solicitation_interval", + 1, G_MAXINT32, NM_NDISC_ROUTER_SOLICITATION_INTERVAL_DEFAULT), + NULL); + + priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc); + + errsv = ndp_open (&priv->ndp); + + if (errsv != 0) { + errsv = errsv > 0 ? errsv : -errsv; + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "failure creating libndp socket: %s (%d)", + g_strerror (errsv), errsv); + g_object_unref (ndisc); + return NULL; + } + return ndisc; +} + +static void +dispose (GObject *object) +{ + NMNDisc *ndisc = (NMNDisc *) object; + NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc); + + nm_clear_g_source (&priv->event_id); + g_clear_pointer (&priv->event_channel, g_io_channel_unref); + + if (priv->ndp) { + switch (nm_ndisc_get_node_type (ndisc)) { + case NM_NDISC_NODE_TYPE_HOST: + ndp_msgrcv_handler_unregister (priv->ndp, receive_ra, NDP_MSG_RA, nm_ndisc_get_ifindex (ndisc), ndisc); + break; + case NM_NDISC_NODE_TYPE_ROUTER: + ndp_msgrcv_handler_unregister (priv->ndp, receive_rs, NDP_MSG_RS, nm_ndisc_get_ifindex (ndisc), ndisc); + break; + default: + g_assert_not_reached (); + } + ndp_close (priv->ndp); + priv->ndp = NULL; + } + + G_OBJECT_CLASS (nm_lndp_ndisc_parent_class)->dispose (object); +} + +static void +nm_lndp_ndisc_class_init (NMLndpNDiscClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + NMNDiscClass *ndisc_class = NM_NDISC_CLASS (klass); + + object_class->dispose = dispose; + ndisc_class->start = start; + ndisc_class->send_rs = send_rs; + ndisc_class->send_ra = send_ra; +} |