NetworkManager/src/rdisc/nm-rdisc.c
Lubomir Rintel f85728ecff core: support IPv6 duplicate address detection
NMDevice detects the DAD failures by watching the removal of tentative
addresses (happens for DAD of addresses with valid lifetime, typically
discovered addresses) or changes to addresses with dadfailed flag (permanent
addresses, typically link-local and manually configured addresses).
It retries creation of link-local addresses itself and lets RDisc know about
the rest so that it can decide if it's rdisc-managed address and retry
with a new address.

Currently NMDevice doesn't do anything useful about link-local address DAD
failures -- it just fails the link-local address addition instead of just
timing out, which happened before. RDisc just logs a warning and removes
the address from the list.

However, with RFC7217 stable privacy addresses the use of a different address
and thus a recovery from DAD failures would be possible.
2015-11-02 20:27:35 +01:00

731 lines
20 KiB
C

/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* nm-rdisc.c - Perform IPv6 router discovery
*
* 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 "config.h"
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include "nm-rdisc.h"
#include "nm-rdisc-private.h"
#include "nm-default.h"
#include "nm-utils.h"
#define _NMLOG_PREFIX_NAME "rdisc"
typedef struct {
int solicitations_left;
guint send_rs_id;
gint64 last_rs;
guint ra_timeout_id; /* first RA timeout */
guint timeout_id; /* prefix/dns/etc lifetime timeout */
} NMRDiscPrivate;
#define NM_RDISC_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_RDISC, NMRDiscPrivate))
G_DEFINE_TYPE (NMRDisc, nm_rdisc, G_TYPE_OBJECT)
enum {
CONFIG_CHANGED,
RA_TIMEOUT,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
/******************************************************************/
gboolean
nm_rdisc_add_gateway (NMRDisc *rdisc, const NMRDiscGateway *new)
{
int i, insert_idx = -1;
for (i = 0; i < rdisc->gateways->len; i++) {
NMRDiscGateway *item = &g_array_index (rdisc->gateways, NMRDiscGateway, i);
if (IN6_ARE_ADDR_EQUAL (&item->address, &new->address)) {
if (new->lifetime == 0) {
g_array_remove_index (rdisc->gateways, i--);
return TRUE;
}
if (item->preference != new->preference) {
g_array_remove_index (rdisc->gateways, i--);
continue;
}
memcpy (item, new, sizeof (*new));
return FALSE;
}
/* Put before less preferable gateways. */
if (item->preference < new->preference && insert_idx < 0)
insert_idx = i;
}
if (new->lifetime)
g_array_insert_val (rdisc->gateways, MAX (insert_idx, 0), *new);
return !!new->lifetime;
}
/**
* complete_address:
* @rdisc: the #NMRDisc
* @addr: the #NMRDiscAddress
*
* Adds the host part to the address that has network part set.
* If the address already has a host part, add a different host part
* if possible (this is useful in case DAD failed).
*
* Can fail if a different address can not be generated (DAD failure
* for an EUI-64 address or DAD counter overflow).
*
* Returns: %TRUE if the address could be completed, %FALSE otherwise.
**/
static gboolean
complete_address (NMRDisc *rdisc, NMRDiscAddress *addr)
{
if (!rdisc->iid.id) {
_LOGW ("complete-address: can't generate an EUI-64 address: no interface identifier");
return FALSE;
}
if (addr->address.s6_addr32[2] == 0x0 && addr->address.s6_addr32[3] == 0x0) {
_LOGD ("complete-address: adding an EUI-64 address");
nm_utils_ipv6_addr_set_interface_identfier (&addr->address, rdisc->iid);
return TRUE;
}
_LOGW ("complete-address: can't generate a new EUI-64 address");
return FALSE;
}
gboolean
nm_rdisc_complete_and_add_address (NMRDisc *rdisc, NMRDiscAddress *new)
{
int i;
if (!complete_address (rdisc, new))
return FALSE;
for (i = 0; i < rdisc->addresses->len; i++) {
NMRDiscAddress *item = &g_array_index (rdisc->addresses, NMRDiscAddress, i);
if (IN6_ARE_ADDR_EQUAL (&item->address, &new->address)) {
gboolean changed;
if (new->lifetime == 0) {
g_array_remove_index (rdisc->addresses, i--);
return TRUE;
}
changed = item->timestamp + item->lifetime != new->timestamp + new->lifetime ||
item->timestamp + item->preferred != new->timestamp + new->preferred;
*item = *new;
return changed;
}
}
/* we create at most max_addresses autoconf addresses. This is different from
* what the kernel does, because it considers *all* addresses (including
* static and other temporary addresses).
**/
if (rdisc->max_addresses && rdisc->addresses->len >= rdisc->max_addresses)
return FALSE;
if (new->lifetime)
g_array_insert_val (rdisc->addresses, i, *new);
return !!new->lifetime;
}
gboolean
nm_rdisc_add_route (NMRDisc *rdisc, const NMRDiscRoute *new)
{
int i, insert_idx = -1;
for (i = 0; i < rdisc->routes->len; i++) {
NMRDiscRoute *item = &g_array_index (rdisc->routes, NMRDiscRoute, i);
if (IN6_ARE_ADDR_EQUAL (&item->network, &new->network) && item->plen == new->plen) {
if (new->lifetime == 0) {
g_array_remove_index (rdisc->routes, i--);
return TRUE;
}
if (item->preference != new->preference) {
g_array_remove_index (rdisc->routes, i--);
continue;
}
memcpy (item, new, sizeof (*new));
return FALSE;
}
/* Put before less preferable routes. */
if (item->preference < new->preference && insert_idx < 0)
insert_idx = i;
}
if (new->lifetime)
g_array_insert_val (rdisc->routes, CLAMP (insert_idx, 0, G_MAXINT), *new);
return !!new->lifetime;
}
gboolean
nm_rdisc_add_dns_server (NMRDisc *rdisc, const NMRDiscDNSServer *new)
{
int i;
for (i = 0; i < rdisc->dns_servers->len; i++) {
NMRDiscDNSServer *item = &g_array_index (rdisc->dns_servers, NMRDiscDNSServer, i);
if (IN6_ARE_ADDR_EQUAL (&item->address, &new->address)) {
if (new->lifetime == 0) {
g_array_remove_index (rdisc->dns_servers, i);
return TRUE;
}
if (item->timestamp != new->timestamp || item->lifetime != new->lifetime) {
*item = *new;
return TRUE;
}
return FALSE;
}
}
if (new->lifetime)
g_array_insert_val (rdisc->dns_servers, i, *new);
return !!new->lifetime;
}
/* Copies new->domain if 'new' is added to the dns_domains list */
gboolean
nm_rdisc_add_dns_domain (NMRDisc *rdisc, const NMRDiscDNSDomain *new)
{
NMRDiscDNSDomain *item;
int i;
for (i = 0; i < rdisc->dns_domains->len; i++) {
item = &g_array_index (rdisc->dns_domains, NMRDiscDNSDomain, i);
if (!g_strcmp0 (item->domain, new->domain)) {
gboolean changed;
if (new->lifetime == 0) {
g_array_remove_index (rdisc->dns_domains, i);
return TRUE;
}
changed = (item->timestamp != new->timestamp ||
item->lifetime != new->lifetime);
if (changed) {
item->timestamp = new->timestamp;
item->lifetime = new->lifetime;
}
return changed;
}
}
if (new->lifetime) {
g_array_insert_val (rdisc->dns_domains, i, *new);
item = &g_array_index (rdisc->dns_domains, NMRDiscDNSDomain, i);
item->domain = g_strdup (new->domain);
}
return !!new->lifetime;
}
/******************************************************************/
/**
* nm_rdisc_set_iid:
* @rdisc: the #NMRDisc
* @iid: the new interface ID
*
* Sets the "Modified EUI-64" interface ID to be used when generating
* IPv6 addresses using received prefixes. Identifiers are either generated
* from the hardware addresses or manually set by the operator with
* "ip token" command.
*
* Upon token change (or initial setting) all addresses generated using
* the old identifier are removed. The caller should ensure the addresses
* will be reset by soliciting router advertisements.
*
* Returns: %TRUE if the token was changed, %FALSE otherwise.
**/
gboolean
nm_rdisc_set_iid (NMRDisc *rdisc, const NMUtilsIPv6IfaceId iid)
{
g_return_val_if_fail (NM_IS_RDISC (rdisc), FALSE);
if (rdisc->iid.id != iid.id) {
rdisc->iid = iid;
if (rdisc->addresses->len) {
_LOGD ("IPv6 interface identifier changed, flushing addresses");
g_array_remove_range (rdisc->addresses, 0, rdisc->addresses->len);
g_signal_emit_by_name (rdisc, NM_RDISC_CONFIG_CHANGED, NM_RDISC_CONFIG_ADDRESSES);
}
return TRUE;
}
return FALSE;
}
static void
clear_ra_timeout (NMRDisc *rdisc)
{
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
if (priv->ra_timeout_id) {
g_source_remove (priv->ra_timeout_id);
priv->ra_timeout_id = 0;
}
}
static void
clear_rs_timeout (NMRDisc *rdisc)
{
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
if (priv->send_rs_id) {
g_source_remove (priv->send_rs_id);
priv->send_rs_id = 0;
}
}
static gboolean
send_rs (NMRDisc *rdisc)
{
NMRDiscClass *klass = NM_RDISC_GET_CLASS (rdisc);
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
_LOGD ("sending router solicitation");
if (klass->send_rs (rdisc))
priv->solicitations_left--;
priv->last_rs = nm_utils_get_monotonic_timestamp_s ();
if (priv->solicitations_left > 0) {
_LOGD ("scheduling router solicitation retry in %d seconds.",
rdisc->rtr_solicitation_interval);
priv->send_rs_id = g_timeout_add_seconds (rdisc->rtr_solicitation_interval,
(GSourceFunc) send_rs, rdisc);
} else {
_LOGD ("did not receive a router advertisement after %d solicitations.",
rdisc->rtr_solicitations);
priv->send_rs_id = 0;
}
return G_SOURCE_REMOVE;
}
static void
solicit (NMRDisc *rdisc)
{
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
guint32 now = nm_utils_get_monotonic_timestamp_s ();
gint64 next;
if (!priv->send_rs_id) {
priv->solicitations_left = rdisc->rtr_solicitations;
next = CLAMP (priv->last_rs + rdisc->rtr_solicitation_interval - now, 0, G_MAXINT32);
_LOGD ("scheduling explicit router solicitation request in %" G_GINT64_FORMAT " seconds.",
next);
priv->send_rs_id = g_timeout_add_seconds ((guint32) next, (GSourceFunc) send_rs, rdisc);
}
}
static gboolean
rdisc_ra_timeout_cb (gpointer user_data)
{
NMRDisc *rdisc = NM_RDISC (user_data);
NM_RDISC_GET_PRIVATE (rdisc)->ra_timeout_id = 0;
g_signal_emit_by_name (rdisc, NM_RDISC_RA_TIMEOUT);
return G_SOURCE_REMOVE;
}
void
nm_rdisc_start (NMRDisc *rdisc)
{
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
NMRDiscClass *klass = NM_RDISC_GET_CLASS (rdisc);
guint ra_wait_secs;
g_assert (klass->start);
_LOGD ("starting router discovery: %d", rdisc->ifindex);
clear_ra_timeout (rdisc);
ra_wait_secs = CLAMP (rdisc->rtr_solicitations * rdisc->rtr_solicitation_interval, 30, 120);
priv->ra_timeout_id = g_timeout_add_seconds (ra_wait_secs, rdisc_ra_timeout_cb, rdisc);
_LOGD ("scheduling RA timeout in %d seconds", ra_wait_secs);
if (klass->start)
klass->start (rdisc);
solicit (rdisc);
}
void
nm_rdisc_dad_failed (NMRDisc *rdisc, struct in6_addr *address)
{
int i;
gboolean changed = FALSE;
for (i = 0; i < rdisc->addresses->len; i++) {
NMRDiscAddress *item = &g_array_index (rdisc->addresses, NMRDiscAddress, i);
if (!IN6_ARE_ADDR_EQUAL (&item->address, address))
continue;
_LOGD ("DAD failed for discovered address %s", nm_utils_inet6_ntop (address, NULL));
if (!complete_address (rdisc, item))
g_array_remove_index (rdisc->addresses, i--);
changed = TRUE;
}
if (changed)
g_signal_emit_by_name (rdisc, NM_RDISC_CONFIG_CHANGED, NM_RDISC_CONFIG_ADDRESSES);
}
#define CONFIG_MAP_MAX_STR 7
static void
config_map_to_string (NMRDiscConfigMap map, char *p)
{
if (map & NM_RDISC_CONFIG_DHCP_LEVEL)
*p++ = 'd';
if (map & NM_RDISC_CONFIG_GATEWAYS)
*p++ = 'G';
if (map & NM_RDISC_CONFIG_ADDRESSES)
*p++ = 'A';
if (map & NM_RDISC_CONFIG_ROUTES)
*p++ = 'R';
if (map & NM_RDISC_CONFIG_DNS_SERVERS)
*p++ = 'S';
if (map & NM_RDISC_CONFIG_DNS_DOMAINS)
*p++ = 'D';
*p = '\0';
}
static const char *
dhcp_level_to_string (NMRDiscDHCPLevel dhcp_level)
{
switch (dhcp_level) {
case NM_RDISC_DHCP_LEVEL_NONE:
return "none";
case NM_RDISC_DHCP_LEVEL_OTHERCONF:
return "otherconf";
case NM_RDISC_DHCP_LEVEL_MANAGED:
return "managed";
default:
return "INVALID";
}
}
#define expiry(item) (item->timestamp + item->lifetime)
static void
config_changed (NMRDisc *rdisc, NMRDiscConfigMap changed)
{
int i;
char changedstr[CONFIG_MAP_MAX_STR];
char addrstr[INET6_ADDRSTRLEN];
if (_LOGD_ENABLED ()) {
config_map_to_string (changed, changedstr);
_LOGD ("router discovery configuration changed [%s]:", changedstr);
_LOGD (" dhcp-level %s", dhcp_level_to_string (rdisc->dhcp_level));
for (i = 0; i < rdisc->gateways->len; i++) {
NMRDiscGateway *gateway = &g_array_index (rdisc->gateways, NMRDiscGateway, i);
inet_ntop (AF_INET6, &gateway->address, addrstr, sizeof (addrstr));
_LOGD (" gateway %s pref %d exp %u", addrstr, gateway->preference, expiry (gateway));
}
for (i = 0; i < rdisc->addresses->len; i++) {
NMRDiscAddress *address = &g_array_index (rdisc->addresses, NMRDiscAddress, i);
inet_ntop (AF_INET6, &address->address, addrstr, sizeof (addrstr));
_LOGD (" address %s exp %u", addrstr, expiry (address));
}
for (i = 0; i < rdisc->routes->len; i++) {
NMRDiscRoute *route = &g_array_index (rdisc->routes, NMRDiscRoute, i);
inet_ntop (AF_INET6, &route->network, addrstr, sizeof (addrstr));
_LOGD (" route %s/%d via %s pref %d exp %u", addrstr, route->plen,
nm_utils_inet6_ntop (&route->gateway, NULL), route->preference,
expiry (route));
}
for (i = 0; i < rdisc->dns_servers->len; i++) {
NMRDiscDNSServer *dns_server = &g_array_index (rdisc->dns_servers, NMRDiscDNSServer, i);
inet_ntop (AF_INET6, &dns_server->address, addrstr, sizeof (addrstr));
_LOGD (" dns_server %s exp %u", addrstr, expiry (dns_server));
}
for (i = 0; i < rdisc->dns_domains->len; i++) {
NMRDiscDNSDomain *dns_domain = &g_array_index (rdisc->dns_domains, NMRDiscDNSDomain, i);
_LOGD (" dns_domain %s exp %u", dns_domain->domain, expiry (dns_domain));
}
}
}
static void
clean_gateways (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap *changed, guint32 *nextevent)
{
int i;
for (i = 0; i < rdisc->gateways->len; i++) {
NMRDiscGateway *item = &g_array_index (rdisc->gateways, NMRDiscGateway, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdisc->gateways, i--);
*changed |= NM_RDISC_CONFIG_GATEWAYS;
} else if (*nextevent > expiry)
*nextevent = expiry;
}
}
static void
clean_addresses (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap *changed, guint32 *nextevent)
{
int i;
for (i = 0; i < rdisc->addresses->len; i++) {
NMRDiscAddress *item = &g_array_index (rdisc->addresses, NMRDiscAddress, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdisc->addresses, i--);
*changed |= NM_RDISC_CONFIG_ADDRESSES;
} else if (*nextevent > expiry)
*nextevent = expiry;
}
}
static void
clean_routes (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap *changed, guint32 *nextevent)
{
int i;
for (i = 0; i < rdisc->routes->len; i++) {
NMRDiscRoute *item = &g_array_index (rdisc->routes, NMRDiscRoute, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdisc->routes, i--);
*changed |= NM_RDISC_CONFIG_ROUTES;
} else if (*nextevent > expiry)
*nextevent = expiry;
}
}
static void
clean_dns_servers (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap *changed, guint32 *nextevent)
{
int i;
for (i = 0; i < rdisc->dns_servers->len; i++) {
NMRDiscDNSServer *item = &g_array_index (rdisc->dns_servers, NMRDiscDNSServer, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
guint64 refresh = (guint64) item->timestamp + item->lifetime / 2;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdisc->dns_servers, i--);
*changed |= NM_RDISC_CONFIG_DNS_SERVERS;
} else if (now >= refresh)
solicit (rdisc);
else if (*nextevent > refresh)
*nextevent = refresh;
}
}
static void
clean_dns_domains (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap *changed, guint32 *nextevent)
{
int i;
for (i = 0; i < rdisc->dns_domains->len; i++) {
NMRDiscDNSDomain *item = &g_array_index (rdisc->dns_domains, NMRDiscDNSDomain, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
guint64 refresh = (guint64) item->timestamp + item->lifetime / 2;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdisc->dns_domains, i--);
*changed |= NM_RDISC_CONFIG_DNS_DOMAINS;
} else if (now >= refresh)
solicit (rdisc);
else if (*nextevent > refresh)
*nextevent = refresh;
}
}
static gboolean timeout_cb (gpointer user_data);
static void
check_timestamps (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap changed)
{
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
/* Use a magic date in the distant future (~68 years) */
guint32 never = G_MAXINT32;
guint32 nextevent = never;
if (priv->timeout_id) {
g_source_remove (priv->timeout_id);
priv->timeout_id = 0;
}
clean_gateways (rdisc, now, &changed, &nextevent);
clean_addresses (rdisc, now, &changed, &nextevent);
clean_routes (rdisc, now, &changed, &nextevent);
clean_dns_servers (rdisc, now, &changed, &nextevent);
clean_dns_domains (rdisc, now, &changed, &nextevent);
if (changed)
g_signal_emit_by_name (rdisc, NM_RDISC_CONFIG_CHANGED, changed);
if (nextevent != never) {
g_return_if_fail (nextevent > now);
_LOGD ("scheduling next now/lifetime check: %u seconds",
nextevent - now);
priv->timeout_id = g_timeout_add_seconds (nextevent - now, timeout_cb, rdisc);
}
}
static gboolean
timeout_cb (gpointer user_data)
{
NM_RDISC_GET_PRIVATE (user_data)->timeout_id = 0;
check_timestamps (NM_RDISC (user_data), nm_utils_get_monotonic_timestamp_s (), 0);
return G_SOURCE_REMOVE;
}
void
nm_rdisc_ra_received (NMRDisc *rdisc, guint32 now, NMRDiscConfigMap changed)
{
clear_ra_timeout (rdisc);
clear_rs_timeout (rdisc);
check_timestamps (rdisc, now, changed);
}
/******************************************************************/
static void
dns_domain_free (gpointer data)
{
g_free (((NMRDiscDNSDomain *)(data))->domain);
}
static void
nm_rdisc_init (NMRDisc *rdisc)
{
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
rdisc->gateways = g_array_new (FALSE, FALSE, sizeof (NMRDiscGateway));
rdisc->addresses = g_array_new (FALSE, FALSE, sizeof (NMRDiscAddress));
rdisc->routes = g_array_new (FALSE, FALSE, sizeof (NMRDiscRoute));
rdisc->dns_servers = g_array_new (FALSE, FALSE, sizeof (NMRDiscDNSServer));
rdisc->dns_domains = g_array_new (FALSE, FALSE, sizeof (NMRDiscDNSDomain));
g_array_set_clear_func (rdisc->dns_domains, dns_domain_free);
rdisc->hop_limit = 64;
/* Start at very low number so that last_rs - rtr_solicitation_interval
* is much lower than nm_utils_get_monotonic_timestamp_s() at startup.
*/
priv->last_rs = G_MININT32;
}
static void
dispose (GObject *object)
{
NMRDisc *rdisc = NM_RDISC (object);
NMRDiscPrivate *priv = NM_RDISC_GET_PRIVATE (rdisc);
clear_ra_timeout (rdisc);
clear_rs_timeout (rdisc);
if (priv->timeout_id) {
g_source_remove (priv->timeout_id);
priv->timeout_id = 0;
}
G_OBJECT_CLASS (nm_rdisc_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
NMRDisc *rdisc = NM_RDISC (object);
g_free (rdisc->ifname);
g_array_unref (rdisc->gateways);
g_array_unref (rdisc->addresses);
g_array_unref (rdisc->routes);
g_array_unref (rdisc->dns_servers);
g_array_unref (rdisc->dns_domains);
G_OBJECT_CLASS (nm_rdisc_parent_class)->finalize (object);
}
static void
nm_rdisc_class_init (NMRDiscClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
g_type_class_add_private (klass, sizeof (NMRDiscPrivate));
object_class->dispose = dispose;
object_class->finalize = finalize;
klass->config_changed = config_changed;
signals[CONFIG_CHANGED] = g_signal_new (
NM_RDISC_CONFIG_CHANGED,
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (NMRDiscClass, config_changed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_INT);
signals[RA_TIMEOUT] = g_signal_new (
NM_RDISC_RA_TIMEOUT,
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (NMRDiscClass, ra_timeout),
NULL, NULL, NULL,
G_TYPE_NONE, 0);
}