// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (C) 2013 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-lndp-ndisc.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;
GSource *event_source;
} 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, NMNDisc)
/*****************************************************************************/
static gboolean
send_rs (NMNDisc *ndisc, GError **error)
{
NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE (ndisc);
struct ndp_msg *msg;
int errsv;
errsv = ndp_msg_new (&msg, NDP_MSG_RS);
if (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 = nm_errno_native (errsv);
g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
"%s (%d)",
nm_strerror_native (errsv), errsv);
return FALSE;
}
return TRUE;
}
static NMIcmpv6RouterPref
_route_preference_coerce (enum ndp_route_preference pref)
{
switch (pref) {
case NDP_ROUTE_PREF_LOW:
return NM_ICMPV6_ROUTER_PREF_LOW;
case NDP_ROUTE_PREF_MEDIUM:
return NM_ICMPV6_ROUTER_PREF_MEDIUM;
case NDP_ROUTE_PREF_HIGH:
return NM_ICMPV6_ROUTER_PREF_HIGH;
}
/* unexpected value must be treated as MEDIUM (RFC 4191). */
return NM_ICMPV6_ROUTER_PREF_MEDIUM;
}
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;
gint32 now = nm_utils_get_monotonic_timestamp_sec ();
int offset;
int hop_limit;
guint32 val;
/* 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 %d", (int) now);
gateway_addr = *ndp_msg_addrto (msg);
if (IN6_IS_ADDR_UNSPECIFIED (&gateway_addr))
g_return_val_if_reached (0);
/* 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.
*/
{
const NMNDiscGateway gateway = {
.address = gateway_addr,
.timestamp = now,
.lifetime = ndp_msgra_router_lifetime (msgra),
.preference = _route_preference_coerce (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 ( IN6_IS_ADDR_UNSPECIFIED (&r_network)
|| IN6_IS_ADDR_LINKLOCAL (&r_network))
continue;
if (ndp_msg_opt_prefix_flag_on_link (msg, offset)) {
const 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) {
if (nm_ndisc_complete_and_add_address (ndisc, &address, now))
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 = _route_preference_coerce (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) {
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 Wi-Fi 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_dnssl_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 Wi-Fi 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;
}
val = ndp_msgra_reachable_time (msgra);
if (val && rdata->public.reachable_time_ms != val) {
rdata->public.reachable_time_ms = val;
changed |= NM_NDISC_CONFIG_REACHABLE_TIME;
}
val = ndp_msgra_retransmit_time (msgra);
if (val && rdata->public.retrans_timer_ms != val) {
rdata->public.retrans_timer_ms = val;
changed |= NM_NDISC_CONFIG_RETRANS_TIMER;
}
/* 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 (ndisc);
NMNDiscDataInternal *rdata = ndisc->rdata;
gint32 now = nm_utils_get_monotonic_timestamp_sec ();
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) {
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 = NM_CLAMP ((gint64) now - (gint64) address->timestamp, 0, G_MAXUINT32 - 1);
guint32 lifetime = address->lifetime;
guint32 preferred = address->preferred;
/* Clamp the life times if they're not forever. */
if (lifetime != NM_NDISC_INFINITY)
lifetime = lifetime > age ? lifetime - age : 0;
if (preferred != NM_NDISC_INFINITY)
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 = nm_errno_native (errsv);
g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
"%s (%d)",
nm_strerror_native (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 (int fd,
GIOCondition condition,
gpointer user_data)
{
gs_unref_object NMNDisc *ndisc = g_object_ref (NM_NDISC (user_data));
nm_auto_pop_netns NMPNetns *netns = NULL;
NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE (ndisc);
_LOGD ("processing libndp events");
if (!nm_ndisc_netns_push (ndisc, &netns)) {
/* something is very wrong. Stop handling events. */
nm_clear_g_source_inst (&priv->event_source);
return G_SOURCE_REMOVE;
}
ndp_callall_eventfd_handler (priv->ndp);
return G_SOURCE_CONTINUE;
}
static void
start (NMNDisc *ndisc)
{
NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE (ndisc);
int fd;
g_return_if_fail (!priv->event_source);
fd = ndp_get_eventfd (priv->ndp);
priv->event_source = nm_g_unix_fd_source_new (fd,
G_IO_IN,
G_PRIORITY_DEFAULT,
event_ready,
ndisc,
NULL);
g_source_attach (priv->event_source, NULL);
/* Flush any pending messages to avoid using obsolete information */
event_ready (fd, 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 int
ipv6_sysctl_get (NMPlatform *platform, const char *ifname, const char *property, int min, int max, int defval)
{
return nm_platform_sysctl_ip_conf_get_int_checked (platform,
AF_INET6,
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,
gint32 ra_timeout,
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_RA_TIMEOUT, (int) ra_timeout,
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 (ndisc);
errsv = ndp_open (&priv->ndp);
if (errsv != 0) {
errsv = nm_errno_native (errsv);
g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
"failure creating libndp socket: %s (%d)",
nm_strerror_native (errsv), errsv);
g_object_unref (ndisc);
return NULL;
}
return ndisc;
}
static void
dispose (GObject *object)
{
NMNDisc *ndisc = NM_NDISC (object);
NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE (ndisc);
nm_clear_g_source_inst (&priv->event_source);
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;
}