diff options
Diffstat (limited to 'src/rdisc/nm-lndp-rdisc.c')
| -rw-r--r-- | src/rdisc/nm-lndp-rdisc.c | 700 |
1 files changed, 700 insertions, 0 deletions
diff --git a/src/rdisc/nm-lndp-rdisc.c b/src/rdisc/nm-lndp-rdisc.c new file mode 100644 index 00000000..cf0267d1 --- /dev/null +++ b/src/rdisc/nm-lndp-rdisc.c @@ -0,0 +1,700 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* nm-lndp-rdisc.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 <string.h> +#include <arpa/inet.h> +/* stdarg.h included because of a bug in ndp.h */ +#include <stdarg.h> +#include <ndp.h> + +#include "nm-lndp-rdisc.h" + +#include "NetworkManagerUtils.h" +#include "nm-logging.h" + +#define debug(...) nm_log_dbg (LOGD_IP6, __VA_ARGS__) +#define warning(...) nm_log_warn (LOGD_IP6, __VA_ARGS__) +#define error(...) nm_log_err (LOGD_IP6, __VA_ARGS__) + +typedef struct { + struct ndp *ndp; + + guint send_rs_id; + GIOChannel *event_channel; + guint event_id; + guint timeout_id; + + int solicitations_left; +} NMLNDPRDiscPrivate; + +#define NM_LNDP_RDISC_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_LNDP_RDISC, NMLNDPRDiscPrivate)) + +G_DEFINE_TYPE (NMLNDPRDisc, nm_lndp_rdisc, NM_TYPE_RDISC) + +/******************************************************************/ + +static inline gint32 +ipv6_sysctl_get (const char *ifname, const char *property, gint32 defval) +{ + return nm_platform_sysctl_get_int32 (nm_utils_ip6_property_path (ifname, property), defval); +} + +NMRDisc * +nm_lndp_rdisc_new (int ifindex, const char *ifname) +{ + NMRDisc *rdisc; + NMLNDPRDiscPrivate *priv; + int error; + + rdisc = g_object_new (NM_TYPE_LNDP_RDISC, NULL); + + rdisc->ifindex = ifindex; + rdisc->ifname = g_strdup (ifname); + + rdisc->max_addresses = ipv6_sysctl_get (ifname, "max_addresses", + NM_RDISC_MAX_ADDRESSES_DEFAULT); + rdisc->rtr_solicitations = ipv6_sysctl_get (ifname, "router_solicitations", + NM_RDISC_RTR_SOLICITATIONS_DEFAULT); + rdisc->rtr_solicitation_interval = ipv6_sysctl_get (ifname, "router_solicitation_interval", + NM_RDISC_RTR_SOLICITATION_INTERVAL_DEFAULT); + + priv = NM_LNDP_RDISC_GET_PRIVATE (rdisc); + error = ndp_open (&priv->ndp); + if (error != 0) { + g_object_unref (rdisc); + debug ("(%s): error creating socket for NDP; errno=%d", ifname, -error); + return NULL; + } + return rdisc; +} + +static gboolean +add_gateway (NMRDisc *rdisc, const NMRDiscGateway *new) +{ + int i; + + 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 (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) + break; + } + + g_array_insert_val (rdisc->gateways, i, *new); + return TRUE; +} + +static gboolean +add_address (NMRDisc *rdisc, const NMRDiscAddress *new) +{ + int i; + + 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 = 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; + + g_array_insert_val (rdisc->addresses, i, *new); + return TRUE; +} + +static gboolean +add_route (NMRDisc *rdisc, const NMRDiscRoute *new) +{ + int i; + + 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 (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) + break; + } + + g_array_insert_val (rdisc->routes, i, *new); + return TRUE; +} + +static gboolean +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)) { + gboolean changed = item->timestamp != new->timestamp || + item->lifetime != new->lifetime; + if (changed) { + item->timestamp = new->timestamp; + item->lifetime = new->lifetime; + } + return changed; + } + } + + /* DNS server should no longer be used */ + if (new->lifetime == 0) + return FALSE; + + g_array_insert_val (rdisc->dns_servers, i, *new); + return TRUE; +} + +/* Copies new->domain if 'new' is added to the dns_domains list */ +static gboolean +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 = item->timestamp != new->timestamp || + item->lifetime != new->lifetime; + if (changed) { + item->timestamp = new->timestamp; + item->lifetime = new->lifetime; + } + return changed; + } + } + + /* Domain should no longer be used */ + if (new->lifetime == 0) + return FALSE; + + 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 TRUE; +} + +static gboolean +send_rs (NMRDisc *rdisc) +{ + NMLNDPRDiscPrivate *priv = NM_LNDP_RDISC_GET_PRIVATE (rdisc); + struct ndp_msg *msg; + int error; + + error = ndp_msg_new (&msg, NDP_MSG_RS); + g_assert (!error); + ndp_msg_ifindex_set (msg, rdisc->ifindex); + + debug ("(%s): sending router solicitation", rdisc->ifname); + + error = ndp_msg_send (priv->ndp, msg); + if (error) + error ("(%s): cannot send router solicitation: %d.", rdisc->ifname, error); + else + priv->solicitations_left--; + + ndp_msg_destroy (msg); + + if (priv->solicitations_left > 0) { + debug ("(%s): scheduling router solicitation retry in %d seconds.", + rdisc->ifname, rdisc->rtr_solicitation_interval); + priv->send_rs_id = g_timeout_add_seconds (rdisc->rtr_solicitation_interval, + (GSourceFunc) send_rs, rdisc); + } else { + debug ("(%s): did not receive a router advertisement after %d solicitations.", + rdisc->ifname, rdisc->rtr_solicitations); + priv->send_rs_id = 0; + } + + return G_SOURCE_REMOVE; +} + +static void +solicit (NMRDisc *rdisc) +{ + NMLNDPRDiscPrivate *priv = NM_LNDP_RDISC_GET_PRIVATE (rdisc); + + if (!priv->send_rs_id) { + debug ("(%s): scheduling router solicitation.", rdisc->ifname); + priv->send_rs_id = g_idle_add ((GSourceFunc) send_rs, rdisc); + priv->solicitations_left = rdisc->rtr_solicitations; + } +} + +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_free (item->domain); + 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) +{ + NMLNDPRDiscPrivate *priv = NM_LNDP_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); + debug ("(%s): scheduling next now/lifetime check: %u seconds", + rdisc->ifname, nextevent - now); + priv->timeout_id = g_timeout_add_seconds (nextevent - now, timeout_cb, rdisc); + } +} + +static gboolean +timeout_cb (gpointer user_data) +{ + check_timestamps (user_data, nm_utils_get_monotonic_timestamp_s (), 0); + + return TRUE; +} + +static NMRDiscPreference +translate_preference (enum ndp_route_preference preference) +{ + switch (preference) { + case NDP_ROUTE_PREF_LOW: + return NM_RDISC_PREFERENCE_LOW; + case NDP_ROUTE_PREF_MEDIUM: + return NM_RDISC_PREFERENCE_MEDIUM; + case NDP_ROUTE_PREF_HIGH: + return NM_RDISC_PREFERENCE_HIGH; + default: + return NM_RDISC_PREFERENCE_INVALID; + } +} + +static void +fill_address_from_mac (struct in6_addr *address, const char *mac) +{ + unsigned char *identifier = address->s6_addr + 8; + + if (!mac) + return; + + /* Translate 48-bit MAC address to a 64-bit modified interface identifier + * and write it to the second half of the IPv6 address. + * + * See http://tools.ietf.org/html/rfc3513#page-21 + */ + memcpy (identifier, mac, 3); + identifier[0] ^= 0x02; + identifier[3] = 0xff; + identifier[4] = 0xfe; + memcpy (identifier + 5, mac + 3, 3); +} + +static int +receive_ra (struct ndp *ndp, struct ndp_msg *msg, gpointer user_data) +{ + NMRDisc *rdisc = (NMRDisc *) user_data; + NMLNDPRDiscPrivate *priv = NM_LNDP_RDISC_GET_PRIVATE (rdisc); + NMRDiscConfigMap changed = 0; + size_t lladdrlen = 0; + const char *lladdr = NULL; + struct ndp_msgra *msgra = ndp_msgra (msg); + NMRDiscGateway gateway; + guint32 now = nm_utils_get_monotonic_timestamp_s (); + int offset; + int hop_limit; + + if (rdisc->lladdr) + lladdr = g_bytes_get_data (rdisc->lladdr, &lladdrlen); + + /* 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. + */ + debug ("(%s): received router advertisement at %u", rdisc->ifname, now); + + if (priv->send_rs_id) { + g_source_remove (priv->send_rs_id); + priv->send_rs_id = 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. + */ + { + NMRDiscDHCPLevel dhcp_level; + + if (ndp_msgra_flag_managed (msgra)) + dhcp_level = NM_RDISC_DHCP_LEVEL_MANAGED; + else if (ndp_msgra_flag_other (msgra)) + dhcp_level = NM_RDISC_DHCP_LEVEL_OTHERCONF; + else + dhcp_level = NM_RDISC_DHCP_LEVEL_NONE; + + if (dhcp_level != rdisc->dhcp_level) { + rdisc->dhcp_level = dhcp_level; + changed |= NM_RDISC_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. + */ + memset (&gateway, 0, sizeof (gateway)); + gateway.address = *ndp_msg_addrto (msg); + gateway.timestamp = now; + gateway.lifetime = ndp_msgra_router_lifetime (msgra); + gateway.preference = translate_preference (ndp_msgra_route_preference (msgra)); + if (add_gateway (rdisc, &gateway)) + changed |= NM_RDISC_CONFIG_GATEWAYS; + + /* Addresses & Routes */ + ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREFIX) { + NMRDiscRoute route; + NMRDiscAddress address; + + /* Device route */ + memset (&route, 0, sizeof (route)); + route.plen = ndp_msg_opt_prefix_len (msg, offset); + nm_utils_ip6_address_clear_host_address (&route.network, ndp_msg_opt_prefix (msg, offset), route.plen); + route.timestamp = now; + if (ndp_msg_opt_prefix_flag_on_link (msg, offset)) { + route.lifetime = ndp_msg_opt_prefix_valid_time (msg, offset); + if (add_route (rdisc, &route)) + changed |= NM_RDISC_CONFIG_ROUTES; + } + + /* Address */ + if (ndp_msg_opt_prefix_flag_auto_addr_conf (msg, offset)) { + if (route.plen == 64 && lladdrlen == 6) { + memset (&address, 0, sizeof (address)); + address.address = route.network; + address.timestamp = now; + address.lifetime = ndp_msg_opt_prefix_valid_time (msg, offset); + address.preferred = ndp_msg_opt_prefix_preferred_time (msg, offset); + if (address.preferred > address.lifetime) + address.preferred = address.lifetime; + + fill_address_from_mac (&address.address, lladdr); + + if (add_address (rdisc, &address)) + changed |= NM_RDISC_CONFIG_ADDRESSES; + } + } + } + ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_ROUTE) { + NMRDiscRoute route; + + /* Routers through this particular gateway */ + memset (&route, 0, sizeof (route)); + route.gateway = gateway.address; + route.plen = ndp_msg_opt_route_prefix_len (msg, offset); + nm_utils_ip6_address_clear_host_address (&route.network, ndp_msg_opt_route_prefix (msg, offset), route.plen); + route.timestamp = now; + route.lifetime = ndp_msg_opt_route_lifetime (msg, offset); + route.preference = translate_preference (ndp_msg_opt_route_preference (msg, offset)); + if (add_route (rdisc, &route)) + changed |= NM_RDISC_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) { + NMRDiscDNSServer dns_server; + + memset (&dns_server, 0, sizeof (dns_server)); + dns_server.address = *addr; + dns_server.timestamp = now; + dns_server.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 (add_dns_server (rdisc, &dns_server)) + changed |= NM_RDISC_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) { + NMRDiscDNSDomain dns_domain; + + memset (&dns_domain, 0, sizeof (dns_domain)); + dns_domain.domain = domain; + dns_domain.timestamp = now; + dns_domain.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 (add_dns_domain (rdisc, &dns_domain)) + changed |= NM_RDISC_CONFIG_DNS_DOMAINS; + } + } + + hop_limit = ndp_msgra_curhoplimit (msgra); + if (rdisc->hop_limit != hop_limit) { + rdisc->hop_limit = hop_limit; + changed |= NM_RDISC_CONFIG_HOP_LIMIT; + } + + check_timestamps (rdisc, now, changed); + + return 0; +} + +static void +process_events (NMRDisc *rdisc) +{ + NMLNDPRDiscPrivate *priv = NM_LNDP_RDISC_GET_PRIVATE (rdisc); + + debug ("(%s): processing libndp events.", rdisc->ifname); + ndp_callall_eventfd_handler (priv->ndp); +} + +static gboolean +event_ready (GIOChannel *source, GIOCondition condition, NMRDisc *rdisc) +{ + process_events (rdisc); + + return TRUE; +} + +static void +start (NMRDisc *rdisc) +{ + NMLNDPRDiscPrivate *priv = NM_LNDP_RDISC_GET_PRIVATE (rdisc); + int fd = ndp_get_eventfd (priv->ndp); + + 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, rdisc); + + /* Flush any pending messages to avoid using obsolete information */ + process_events (rdisc); + + ndp_msgrcv_handler_register (priv->ndp, &receive_ra, NDP_MSG_RA, rdisc->ifindex, rdisc); + solicit (rdisc); +} + +/******************************************************************/ + +static void +nm_lndp_rdisc_init (NMLNDPRDisc *lndp_rdisc) +{ +} + +static void +nm_lndp_rdisc_finalize (GObject *object) +{ + NMLNDPRDiscPrivate *priv = NM_LNDP_RDISC_GET_PRIVATE (object); + + if (priv->send_rs_id) + g_source_remove (priv->send_rs_id); + if (priv->timeout_id) + g_source_remove (priv->timeout_id); + if (priv->event_channel) + g_io_channel_unref (priv->event_channel); + if (priv->event_id) + g_source_remove (priv->event_id); + + if (priv->ndp) + ndp_close (priv->ndp); +} + +static void +nm_lndp_rdisc_class_init (NMLNDPRDiscClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + NMRDiscClass *rdisc_class = NM_RDISC_CLASS (klass); + + g_type_class_add_private (klass, sizeof (NMLNDPRDiscPrivate)); + + object_class->finalize = nm_lndp_rdisc_finalize; + rdisc_class->start = start; +} |