Files
ipxe/src/net/ndp.c
Michael Brown b66e27d9b2 [ipv6] Expose router address for DHCPv6 leased addresses
The DHCPv6 protocol does not itself provide a router address or a
prefix length.  This information is instead obtained from the router
advertisements.

Our IPv6 minirouting table construction logic will first construct an
entry for each advertised prefix, and later update the entry to
include an address assigned within that prefix via stateful DHCPv6 (if
applicable).

This logic fails if the address assigned via stateful DHCPv6 does not
fall within any of the advertised prefixes (e.g. if the network is
configured to use DHCPv6-assigned /128 addresses with no advertised
on-link prefixes).  We will currently treat this situation as
equivalent to having a manually assigned address with no corresponding
router address or prefix length: the routing table entry will use the
default /64 prefix length and will not include the router address.

DHCPv6 is triggered only in response to a router advertisement with
the "Managed Address Configuration (M)" or "Other Configuration (O)"
flags set, and a router address is therefore available at the point
that we initiate DHCPv6.

Record the router address when initiating DHCPv6, and expose this
router address as part of the DHCPv6 settings block.  This allows the
routing table entry for any address assigned via stateful DHCPv6 to
correctly include the router address, even if the assigned address
does not fall within an advertised prefix.

Also provide a fixed /128 prefix length as part of the DHCPv6 settings
block.  When an address assigned via stateful DHCPv6 does not fall
within an advertised prefix, this will cause the routing table entry
to have a /128 prefix length as expected.  (When such an address does
fall within an advertised prefix, it will continue to use the
advertised prefix length.)

Originally-fixed-by: Guvenc Gulce <guevenc.guelce@sap.com>
Signed-off-by: Michael Brown <mcb30@ipxe.org>
2024-06-27 13:43:37 +01:00

1295 lines
36 KiB
C

/*
* Copyright (C) 2013 Michael Brown <mbrown@fensystems.co.uk>.
*
* 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 of the
* License, or 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.
*/
FILE_LICENCE ( GPL2_OR_LATER );
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <byteswap.h>
#include <ipxe/in.h>
#include <ipxe/iobuf.h>
#include <ipxe/tcpip.h>
#include <ipxe/ipv6.h>
#include <ipxe/icmpv6.h>
#include <ipxe/neighbour.h>
#include <ipxe/dhcpv6.h>
#include <ipxe/timer.h>
#include <ipxe/ndp.h>
/** @file
*
* IPv6 neighbour discovery protocol
*
*/
/** Router discovery minimum timeout */
#define IPV6CONF_MIN_TIMEOUT ( TICKS_PER_SEC / 8 )
/** Router discovery maximum timeout */
#define IPV6CONF_MAX_TIMEOUT ( TICKS_PER_SEC * 3 )
/** Router discovery blocked link retry timeout */
#define IPV6CONF_BLOCK_TIMEOUT ( TICKS_PER_SEC )
/** Router discovery maximum number of deferrals */
#define IPV6CONF_MAX_DEFERRALS 180
static struct ipv6conf * ipv6conf_demux ( struct net_device *netdev );
static int
ipv6conf_rx_router_advertisement ( struct net_device *netdev,
struct in6_addr *router,
struct ndp_router_advertisement_header *radv,
size_t len );
/**
* Transmit NDP packet with link-layer address option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v sin6_dest Destination socket address
* @v data NDP header
* @v len Size of NDP header
* @v option_type NDP option type
* @ret rc Return status code
*/
static int ndp_tx_ll_addr ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
struct sockaddr_in6 *sin6_dest,
const void *data, size_t len,
unsigned int option_type ) {
struct sockaddr_tcpip *st_src =
( ( struct sockaddr_tcpip * ) sin6_src );
struct sockaddr_tcpip *st_dest =
( ( struct sockaddr_tcpip * ) sin6_dest );
struct ll_protocol *ll_protocol = netdev->ll_protocol;
struct io_buffer *iobuf;
struct ndp_ll_addr_option *ll_addr_opt;
union ndp_header *ndp;
size_t option_len;
int rc;
/* Allocate and populate buffer */
option_len = ( ( sizeof ( *ll_addr_opt ) +
ll_protocol->ll_addr_len + NDP_OPTION_BLKSZ - 1 ) &
~( NDP_OPTION_BLKSZ - 1 ) );
iobuf = alloc_iob ( MAX_LL_NET_HEADER_LEN + len + option_len );
if ( ! iobuf )
return -ENOMEM;
iob_reserve ( iobuf, MAX_LL_NET_HEADER_LEN );
memcpy ( iob_put ( iobuf, len ), data, len );
ll_addr_opt = iob_put ( iobuf, option_len );
ll_addr_opt->header.type = option_type;
ll_addr_opt->header.blocks = ( option_len / NDP_OPTION_BLKSZ );
memcpy ( ll_addr_opt->ll_addr, netdev->ll_addr,
ll_protocol->ll_addr_len );
ndp = iobuf->data;
ndp->icmp.chksum = tcpip_chksum ( ndp, ( len + option_len ) );
/* Transmit packet */
if ( ( rc = tcpip_tx ( iobuf, &icmpv6_protocol, st_src, st_dest,
netdev, &ndp->icmp.chksum ) ) != 0 ) {
DBGC ( netdev, "NDP %s could not transmit packet: %s\n",
netdev->name, strerror ( rc ) );
return rc;
}
return 0;
}
/**
* Transmit NDP neighbour discovery request
*
* @v netdev Network device
* @v net_protocol Network-layer protocol
* @v net_dest Destination network-layer address
* @v net_source Source network-layer address
* @ret rc Return status code
*/
static int ndp_tx_request ( struct net_device *netdev,
struct net_protocol *net_protocol __unused,
const void *net_dest, const void *net_source ) {
struct sockaddr_in6 sin6_src;
struct sockaddr_in6 sin6_dest;
struct ndp_neighbour_header neigh;
int rc;
/* Construct source address */
memset ( &sin6_src, 0, sizeof ( sin6_src ) );
sin6_src.sin6_family = AF_INET6;
memcpy ( &sin6_src.sin6_addr, net_source,
sizeof ( sin6_src.sin6_addr ) );
/* Construct multicast destination address */
memset ( &sin6_dest, 0, sizeof ( sin6_dest ) );
sin6_dest.sin6_family = AF_INET6;
sin6_dest.sin6_scope_id = netdev->scope_id;
ipv6_solicited_node ( &sin6_dest.sin6_addr, net_dest );
/* Construct neighbour header */
memset ( &neigh, 0, sizeof ( neigh ) );
neigh.icmp.type = ICMPV6_NEIGHBOUR_SOLICITATION;
memcpy ( &neigh.target, net_dest, sizeof ( neigh.target ) );
/* Transmit neighbour discovery packet */
if ( ( rc = ndp_tx_ll_addr ( netdev, &sin6_src, &sin6_dest, &neigh,
sizeof ( neigh ),
NDP_OPT_LL_SOURCE ) ) != 0 )
return rc;
return 0;
}
/** NDP neighbour discovery protocol */
struct neighbour_discovery ndp_discovery = {
.name = "NDP",
.tx_request = ndp_tx_request,
};
/**
* Transmit NDP router solicitation
*
* @v netdev Network device
* @ret rc Return status code
*/
static int ndp_tx_router_solicitation ( struct net_device *netdev ) {
struct ndp_router_solicitation_header rsol;
struct sockaddr_in6 sin6_dest;
int rc;
/* Construct multicast destination address */
memset ( &sin6_dest, 0, sizeof ( sin6_dest ) );
sin6_dest.sin6_family = AF_INET6;
sin6_dest.sin6_scope_id = netdev->scope_id;
ipv6_all_routers ( &sin6_dest.sin6_addr );
/* Construct router solicitation */
memset ( &rsol, 0, sizeof ( rsol ) );
rsol.icmp.type = ICMPV6_ROUTER_SOLICITATION;
/* Transmit packet */
if ( ( rc = ndp_tx_ll_addr ( netdev, NULL, &sin6_dest, &rsol,
sizeof ( rsol ), NDP_OPT_LL_SOURCE ) ) !=0)
return rc;
return 0;
}
/**
* Process NDP neighbour solicitation source link-layer address option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP packet
* @v option NDP option
* @v len NDP option length
* @ret rc Return status code
*/
static int
ndp_rx_neighbour_solicitation_ll_source ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
union ndp_header *ndp,
union ndp_option *option,
size_t len ) {
struct ndp_neighbour_header *neigh = &ndp->neigh;
struct ndp_ll_addr_option *ll_addr_opt = &option->ll_addr;
struct ll_protocol *ll_protocol = netdev->ll_protocol;
int rc;
/* Silently ignore neighbour solicitations for addresses we do
* not own.
*/
if ( ! ipv6_has_addr ( netdev, &neigh->target ) )
return 0;
/* Sanity check */
if ( offsetof ( typeof ( *ll_addr_opt ),
ll_addr[ll_protocol->ll_addr_len] ) > len ) {
DBGC ( netdev, "NDP %s neighbour solicitation link-layer "
"address option too short at %zd bytes\n",
netdev->name, len );
return -EINVAL;
}
/* Create or update neighbour cache entry */
if ( ( rc = neighbour_define ( netdev, &ipv6_protocol,
&sin6_src->sin6_addr,
ll_addr_opt->ll_addr ) ) != 0 ) {
DBGC ( netdev, "NDP %s could not define %s => %s: %s\n",
netdev->name, inet6_ntoa ( &sin6_src->sin6_addr ),
ll_protocol->ntoa ( ll_addr_opt->ll_addr ),
strerror ( rc ) );
return rc;
}
/* Convert neighbour header to advertisement */
memset ( neigh, 0, offsetof ( typeof ( *neigh ), target ) );
neigh->icmp.type = ICMPV6_NEIGHBOUR_ADVERTISEMENT;
neigh->flags = ( NDP_NEIGHBOUR_SOLICITED | NDP_NEIGHBOUR_OVERRIDE );
/* Send neighbour advertisement */
if ( ( rc = ndp_tx_ll_addr ( netdev, NULL, sin6_src, neigh,
sizeof ( *neigh ),
NDP_OPT_LL_TARGET ) ) != 0 )
return rc;
return 0;
}
/**
* Process NDP neighbour advertisement target link-layer address option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP packet
* @v option NDP option
* @v len NDP option length
* @ret rc Return status code
*/
static int
ndp_rx_neighbour_advertisement_ll_target ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src
__unused,
union ndp_header *ndp,
union ndp_option *option,
size_t len ) {
struct ndp_neighbour_header *neigh = &ndp->neigh;
struct ndp_ll_addr_option *ll_addr_opt = &option->ll_addr;
struct ll_protocol *ll_protocol = netdev->ll_protocol;
int rc;
/* Sanity check */
if ( offsetof ( typeof ( *ll_addr_opt ),
ll_addr[ll_protocol->ll_addr_len] ) > len ) {
DBGC ( netdev, "NDP %s neighbour advertisement link-layer "
"address option too short at %zd bytes\n",
netdev->name, len );
return -EINVAL;
}
/* Update neighbour cache entry, if any */
if ( ( rc = neighbour_update ( netdev, &ipv6_protocol, &neigh->target,
ll_addr_opt->ll_addr ) ) != 0 ) {
DBGC ( netdev, "NDP %s could not update %s => %s: %s\n",
netdev->name, inet6_ntoa ( &neigh->target ),
ll_protocol->ntoa ( ll_addr_opt->ll_addr ),
strerror ( rc ) );
return rc;
}
return 0;
}
/**
* Process NDP router advertisement source link-layer address option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP packet
* @v option NDP option
* @v len NDP option length
* @ret rc Return status code
*/
static int
ndp_rx_router_advertisement_ll_source ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
union ndp_header *ndp __unused,
union ndp_option *option, size_t len ) {
struct ndp_ll_addr_option *ll_addr_opt = &option->ll_addr;
struct ll_protocol *ll_protocol = netdev->ll_protocol;
int rc;
/* Sanity check */
if ( offsetof ( typeof ( *ll_addr_opt ),
ll_addr[ll_protocol->ll_addr_len] ) > len ) {
DBGC ( netdev, "NDP %s router advertisement link-layer address "
"option too short at %zd bytes\n", netdev->name, len );
return -EINVAL;
}
/* Define neighbour cache entry */
if ( ( rc = neighbour_define ( netdev, &ipv6_protocol,
&sin6_src->sin6_addr,
ll_addr_opt->ll_addr ) ) != 0 ) {
DBGC ( netdev, "NDP %s could not define %s => %s: %s\n",
netdev->name, inet6_ntoa ( &sin6_src->sin6_addr ),
ll_protocol->ntoa ( ll_addr_opt->ll_addr ),
strerror ( rc ) );
return rc;
}
return 0;
}
/**
* Process NDP router advertisement prefix information option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP packet
* @v option NDP option
* @v len NDP option length
* @ret rc Return status code
*/
static int
ndp_rx_router_advertisement_prefix ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
union ndp_header *ndp,
union ndp_option *option, size_t len ) {
struct ndp_router_advertisement_header *radv = &ndp->radv;
struct ndp_prefix_information_option *prefix_opt = &option->prefix;
/* Sanity check */
if ( sizeof ( *prefix_opt ) > len ) {
DBGC ( netdev, "NDP %s router advertisement prefix option too "
"short at %zd bytes\n", netdev->name, len );
return -EINVAL;
}
DBGC ( netdev, "NDP %s found %sdefault router %s ",
netdev->name, ( radv->lifetime ? "" : "non-" ),
inet6_ntoa ( &sin6_src->sin6_addr ) );
DBGC ( netdev, "for %s-link %sautonomous prefix %s/%d\n",
( ( prefix_opt->flags & NDP_PREFIX_ON_LINK ) ? "on" : "off" ),
( ( prefix_opt->flags & NDP_PREFIX_AUTONOMOUS ) ? "" : "non-" ),
inet6_ntoa ( &prefix_opt->prefix ), prefix_opt->prefix_len );
return 0;
}
/** An NDP option handler */
struct ndp_option_handler {
/** ICMPv6 type */
uint8_t icmp_type;
/** Option type */
uint8_t option_type;
/**
* Handle received option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP packet
* @v option NDP option
* @ret rc Return status code
*/
int ( * rx ) ( struct net_device *netdev, struct sockaddr_in6 *sin6_src,
union ndp_header *ndp, union ndp_option *option,
size_t len );
};
/** NDP option handlers */
static struct ndp_option_handler ndp_option_handlers[] = {
{
.icmp_type = ICMPV6_NEIGHBOUR_SOLICITATION,
.option_type = NDP_OPT_LL_SOURCE,
.rx = ndp_rx_neighbour_solicitation_ll_source,
},
{
.icmp_type = ICMPV6_NEIGHBOUR_ADVERTISEMENT,
.option_type = NDP_OPT_LL_TARGET,
.rx = ndp_rx_neighbour_advertisement_ll_target,
},
{
.icmp_type = ICMPV6_ROUTER_ADVERTISEMENT,
.option_type = NDP_OPT_LL_SOURCE,
.rx = ndp_rx_router_advertisement_ll_source,
},
{
.icmp_type = ICMPV6_ROUTER_ADVERTISEMENT,
.option_type = NDP_OPT_PREFIX,
.rx = ndp_rx_router_advertisement_prefix,
},
};
/**
* Process received NDP option
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP packet
* @v option NDP option
* @v len Option length
* @ret rc Return status code
*/
static int ndp_rx_option ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src, union ndp_header *ndp,
union ndp_option *option, size_t len ) {
struct ndp_option_handler *handler;
unsigned int i;
/* Locate a suitable option handler, if any */
for ( i = 0 ; i < ( sizeof ( ndp_option_handlers ) /
sizeof ( ndp_option_handlers[0] ) ) ; i++ ) {
handler = &ndp_option_handlers[i];
if ( ( handler->icmp_type == ndp->icmp.type ) &&
( handler->option_type == option->header.type ) ) {
return handler->rx ( netdev, sin6_src, ndp,
option, len );
}
}
/* Silently ignore unknown options as per RFC 4861 */
return 0;
}
/**
* Process received NDP packet options
*
* @v netdev Network device
* @v sin6_src Source socket address
* @v ndp NDP header
* @v offset Offset to NDP options
* @v len Length of NDP packet
* @ret rc Return status code
*/
static int ndp_rx_options ( struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
union ndp_header *ndp, size_t offset, size_t len ) {
union ndp_option *option;
size_t remaining;
size_t option_len;
int rc;
/* Sanity check */
if ( len < offset ) {
DBGC ( netdev, "NDP %s packet too short at %zd bytes (min %zd "
"bytes)\n", netdev->name, len, offset );
return -EINVAL;
}
/* Search for option */
option = ( ( ( void * ) ndp ) + offset );
remaining = ( len - offset );
while ( remaining ) {
/* Sanity check */
if ( ( remaining < sizeof ( option->header ) ) ||
( option->header.blocks == 0 ) ||
( remaining < ( option->header.blocks *
NDP_OPTION_BLKSZ ) ) ) {
DBGC ( netdev, "NDP %s bad option length:\n",
netdev->name );
DBGC_HDA ( netdev, 0, option, remaining );
return -EINVAL;
}
option_len = ( option->header.blocks * NDP_OPTION_BLKSZ );
/* Handle option */
if ( ( rc = ndp_rx_option ( netdev, sin6_src, ndp, option,
option_len ) ) != 0 )
return rc;
/* Move to next option */
option = ( ( ( void * ) option ) + option_len );
remaining -= option_len;
}
return 0;
}
/**
* Process received NDP neighbour solicitation or advertisement
*
* @v iobuf I/O buffer
* @v netdev Network device
* @v sin6_src Source socket address
* @v sin6_dest Destination socket address
* @ret rc Return status code
*/
static int ndp_rx_neighbour ( struct io_buffer *iobuf,
struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
struct sockaddr_in6 *sin6_dest __unused ) {
union ndp_header *ndp = iobuf->data;
struct ndp_neighbour_header *neigh = &ndp->neigh;
size_t len = iob_len ( iobuf );
int rc;
/* Process options */
if ( ( rc = ndp_rx_options ( netdev, sin6_src, ndp,
offsetof ( typeof ( *neigh ), option ),
len ) ) != 0 )
goto err_options;
err_options:
free_iob ( iobuf );
return rc;
}
/**
* Process received NDP router advertisement
*
* @v iobuf I/O buffer
* @v netdev Network device
* @v sin6_src Source socket address
* @v sin6_dest Destination socket address
* @ret rc Return status code
*/
static int
ndp_rx_router_advertisement ( struct io_buffer *iobuf,
struct net_device *netdev,
struct sockaddr_in6 *sin6_src,
struct sockaddr_in6 *sin6_dest __unused ) {
union ndp_header *ndp = iobuf->data;
struct ndp_router_advertisement_header *radv = &ndp->radv;
struct in6_addr *router = &sin6_src->sin6_addr;
size_t len = iob_len ( iobuf );
int rc;
/* Process options */
if ( ( rc = ndp_rx_options ( netdev, sin6_src, ndp,
offsetof ( typeof ( *radv ), option ),
len ) ) != 0 )
goto err_options;
/* Pass to IPv6 autoconfiguration */
if ( ( rc = ipv6conf_rx_router_advertisement ( netdev, router,
radv, len ) ) != 0 )
goto err_ipv6conf;
err_ipv6conf:
err_options:
free_iob ( iobuf );
return rc;
}
/** NDP ICMPv6 handlers */
struct icmpv6_handler ndp_handlers[] __icmpv6_handler = {
{
.type = ICMPV6_NEIGHBOUR_SOLICITATION,
.rx = ndp_rx_neighbour,
},
{
.type = ICMPV6_NEIGHBOUR_ADVERTISEMENT,
.rx = ndp_rx_neighbour,
},
{
.type = ICMPV6_ROUTER_ADVERTISEMENT,
.rx = ndp_rx_router_advertisement,
},
};
/****************************************************************************
*
* NDP settings
*
*/
/** An NDP prefix settings block */
struct ndp_prefix_settings {
/** Settings interface */
struct settings settings;
/** Name */
char name[4];
/** Prefix information option */
struct ndp_prefix_information_option *prefix;
};
/** An NDP settings block */
struct ndp_settings {
/** Reference counter */
struct refcnt refcnt;
/** Settings interface */
struct settings settings;
/** Router address */
struct in6_addr router;
/** Router lifetime */
unsigned int lifetime;
/** Length of NDP options */
size_t len;
/** NDP options */
union ndp_option options[0];
};
/** NDP settings scope */
static const struct settings_scope ndp_settings_scope;
/**
* Construct NDP tag
*
* @v type NDP option type
* @v offset Starting offset of data
* @v len Length of data (or 0 to use all remaining data)
* @ret tag NDP tag
*/
#define NDP_TAG( type, offset, len ) \
( ( (len) << 16 ) | ( (offset) << 8 ) | (type) )
/**
* Extract NDP tag type
*
* @v tag NDP tag
* @ret type NDP option type
*/
#define NDP_TAG_TYPE( tag ) ( ( (tag) >> 0 ) & 0xff )
/**
* Extract NDP tag offset
*
* @v tag NDP tag
* @ret offset Starting offset of data
*/
#define NDP_TAG_OFFSET( tag ) ( ( (tag) >> 8 ) & 0xff )
/**
* Extract NDP tag length
*
* @v tag NDP tag
* @ret len Length of data (or 0 to use all remaining data)
*/
#define NDP_TAG_LEN( tag ) ( ( (tag) >> 16 ) & 0xff )
/**
* Extract NDP tag instance
*
* @v tag NDP tag
* @ret instance Instance
*/
#define NDP_TAG_INSTANCE( tag ) ( ( (tag) >> 24 ) & 0xff )
/**
* Check applicability of NDP setting
*
* @v settings Settings block
* @v setting Setting to fetch
* @ret applies Setting applies within this settings block
*/
static int ndp_applies ( struct settings *settings __unused,
const struct setting *setting ) {
return ( setting->scope == &ndp_settings_scope );
}
/**
* Fetch value of NDP setting
*
* @v settings Settings block
* @v setting Setting to fetch
* @v data Buffer to fill with setting data
* @v len Length of buffer
* @ret len Length of setting data, or negative error
*/
static int ndp_fetch ( struct settings *settings,
struct setting *setting,
void *data, size_t len ) {
struct ndp_settings *ndpset =
container_of ( settings, struct ndp_settings, settings );
struct net_device *netdev =
container_of ( settings->parent, struct net_device,
settings.settings );
union ndp_option *option;
unsigned int tag_type;
unsigned int tag_offset;
unsigned int tag_len;
unsigned int tag_instance;
size_t offset;
size_t option_len;
void *option_data;
/* Parse setting tag */
tag_type = NDP_TAG_TYPE ( setting->tag );
tag_offset = NDP_TAG_OFFSET ( setting->tag );
tag_len = NDP_TAG_LEN ( setting->tag );
tag_instance = NDP_TAG_INSTANCE ( setting->tag );
/* Scan through NDP options for requested type. We can assume
* that the options are well-formed, otherwise they would have
* been rejected prior to being stored.
*/
for ( offset = 0 ; offset < ndpset->len ; offset += option_len ) {
/* Calculate option length */
option = ( ( ( void * ) ndpset->options ) + offset );
option_len = ( option->header.blocks * NDP_OPTION_BLKSZ );
/* Skip options that do not match this tag */
if ( option->header.type != tag_type )
continue;
/* Skip previous instances of this option */
if ( tag_instance-- != 0 )
continue;
/* Sanity check */
if ( ( tag_offset + tag_len ) > option_len ) {
DBGC ( netdev, "NDP %s option %d too short\n",
netdev->name, tag_type );
return -EINVAL;
}
if ( ! tag_len )
tag_len = ( option_len - tag_offset );
option_data = ( ( ( void * ) option ) + tag_offset );
/* Copy data to output buffer */
if ( len > tag_len )
len = tag_len;
memcpy ( data, option_data, len );
/* Default to hex if no type is specified */
if ( ! setting->type )
setting->type = &setting_type_hex;
return tag_len;
}
return -ENOENT;
}
/** NDP settings operations */
static struct settings_operations ndp_settings_operations = {
.applies = ndp_applies,
.fetch = ndp_fetch,
};
/**
* Check applicability of NDP per-prefix setting
*
* @v settings Settings block
* @v setting Setting to fetch
* @ret applies Setting applies within this settings block
*/
static int ndp_prefix_applies ( struct settings *settings __unused,
const struct setting *setting ) {
return ( setting->scope == &ipv6_settings_scope );
}
/**
* Fetch value of NDP IPv6 address setting
*
* @v settings Settings block
* @v data Buffer to fill with setting data
* @v len Length of buffer
* @ret len Length of setting data, or negative error
*/
static int ndp_prefix_fetch_ip6 ( struct settings *settings, void *data,
size_t len ) {
struct ndp_prefix_settings *prefset =
container_of ( settings, struct ndp_prefix_settings, settings );
struct ndp_settings *ndpset =
container_of ( settings->parent, struct ndp_settings, settings);
struct net_device *netdev =
container_of ( ndpset->settings.parent, struct net_device,
settings.settings );
struct ndp_prefix_information_option *prefix = prefset->prefix;
struct in6_addr *ip6 = &prefix->prefix;
struct in6_addr slaac;
int prefix_len;
int rc;
/* Skip dead prefixes */
if ( ! prefix->valid )
return -ENOENT;
/* Construct IPv6 address via SLAAC, if applicable */
if ( prefix->flags & NDP_PREFIX_AUTONOMOUS ) {
memcpy ( &slaac, ip6, sizeof ( slaac ) );
prefix_len = ipv6_eui64 ( &slaac, netdev );
if ( prefix_len == prefix->prefix_len ) {
/* Correctly configured prefix: use SLAAC address */
ip6 = &slaac;
} else if ( prefix_len < 0 ) {
/* Link layer does not support SLAAC */
rc = prefix_len;
DBGC ( netdev, "NDP %s does not support SLAAC: %s\n",
netdev->name, strerror ( rc ) );
} else {
/* Prefix length incorrect: assume a badly
* configured router and ignore SLAAC address.
*/
DBGC ( netdev, "NDP %s ignoring misconfigured SLAAC "
"on prefix %s/%d\n", netdev->name,
inet6_ntoa ( ip6 ), prefix->prefix_len );
}
}
/* Fill in IPv6 address */
if ( len > sizeof ( *ip6 ) )
len = sizeof ( *ip6 );
memcpy ( data, ip6, len );
return sizeof ( *ip6 );
}
/**
* Fetch value of NDP prefix length setting
*
* @v settings Settings block
* @v data Buffer to fill with setting data
* @v len Length of buffer
* @ret len Length of setting data, or negative error
*/
static int ndp_prefix_fetch_len6 ( struct settings *settings, void *data,
size_t len ) {
struct ndp_prefix_settings *prefset =
container_of ( settings, struct ndp_prefix_settings, settings );
struct ndp_prefix_information_option *prefix = prefset->prefix;
uint8_t *len6;
/* Fill in prefix length */
if ( len >= sizeof ( *len6 ) ) {
/* We treat an off-link prefix as having a prefix
* length covering the entire IPv6 address.
*/
len6 = data;
*len6 = ( ( prefix->flags & NDP_PREFIX_ON_LINK ) ?
prefix->prefix_len : -1UL );
}
return sizeof ( *len6 );
}
/**
* Fetch value of NDP router address setting
*
* @v settings Settings block
* @v data Buffer to fill with setting data
* @v len Length of buffer
* @ret len Length of setting data, or negative error
*/
static int ndp_prefix_fetch_gateway6 ( struct settings *settings,
void *data, size_t len ) {
struct ndp_settings *ndpset =
container_of ( settings->parent, struct ndp_settings, settings);
/* Treat non-routing router as non-existent */
if ( ! ndpset->lifetime )
return -ENOENT;
/* Fill in router address */
if ( len > sizeof ( ndpset->router ) )
len = sizeof ( ndpset->router );
memcpy ( data, &ndpset->router, len );
return sizeof ( ndpset->router );
}
/** An NDP per-prefix setting operation */
struct ndp_prefix_operation {
/** Generic setting */
const struct setting *setting;
/**
* Fetch value of setting
*
* @v settings Settings block
* @v data Buffer to fill with setting data
* @v len Length of buffer
* @ret len Length of setting data, or negative error
*/
int ( * fetch ) ( struct settings *settings, void *data, size_t len );
};
/** NDP per-prefix settings operations */
static struct ndp_prefix_operation ndp_prefix_operations[] = {
{ &ip6_setting, ndp_prefix_fetch_ip6 },
{ &len6_setting, ndp_prefix_fetch_len6 },
{ &gateway6_setting, ndp_prefix_fetch_gateway6 },
};
/**
* Fetch value of NDP pre-prefix setting
*
* @v settings Settings block
* @v setting Setting to fetch
* @v data Buffer to fill with setting data
* @v len Length of buffer
* @ret len Length of setting data, or negative error
*/
static int ndp_prefix_fetch ( struct settings *settings,
struct setting *setting,
void *data, size_t len ) {
struct ndp_prefix_operation *op;
unsigned int i;
/* Handle per-prefix settings */
for ( i = 0 ; i < ( sizeof ( ndp_prefix_operations ) /
sizeof ( ndp_prefix_operations[0] ) ) ; i++ ) {
op = &ndp_prefix_operations[i];
if ( setting_cmp ( setting, op->setting ) == 0 )
return op->fetch ( settings, data, len );
}
return -ENOENT;
}
/** NDP per-prefix settings operations */
static struct settings_operations ndp_prefix_settings_operations = {
.applies = ndp_prefix_applies,
.fetch = ndp_prefix_fetch,
};
/**
* Register NDP settings
*
* @v netdev Network device
* @v router Router address
* @v lifetime Router lifetime
* @v options NDP options
* @v len Length of options
* @ret rc Return status code
*/
static int ndp_register_settings ( struct net_device *netdev,
struct in6_addr *router,
unsigned int lifetime,
union ndp_option *options, size_t len ) {
struct settings *parent = netdev_settings ( netdev );
union ndp_option *option;
struct ndp_settings *ndpset;
struct ndp_prefix_settings *prefset;
size_t offset;
size_t option_len;
unsigned int prefixes;
unsigned int instance;
int order;
int rc;
/* Count number of prefix options. We can assume that the
* options are well-formed, otherwise they would have been
* rejected prior to being stored.
*/
order = IPV6_ORDER_PREFIX_ONLY;
for ( prefixes = 0, offset = 0 ; offset < len ; offset += option_len ) {
/* Skip non-prefix options */
option = ( ( ( void * ) options ) + offset );
option_len = ( option->header.blocks * NDP_OPTION_BLKSZ );
if ( option->header.type != NDP_OPT_PREFIX )
continue;
/* Count number of prefixes */
prefixes++;
/* Increase overall order if we have SLAAC addresses */
if ( option->prefix.flags & NDP_PREFIX_AUTONOMOUS )
order = IPV6_ORDER_SLAAC;
}
/* Allocate and initialise structure */
ndpset = zalloc ( sizeof ( *ndpset ) + len +
( prefixes * sizeof ( *prefset ) ) );
if ( ! ndpset ) {
rc = -ENOMEM;
goto err_alloc;
}
ref_init ( &ndpset->refcnt, NULL );
settings_init ( &ndpset->settings, &ndp_settings_operations,
&ndpset->refcnt, &ndp_settings_scope );
ndpset->settings.order = order;
memcpy ( &ndpset->router, router, sizeof ( ndpset->router ) );
ndpset->lifetime = lifetime;
ndpset->len = len;
memcpy ( ndpset->options, options, len );
prefset = ( ( ( void * ) ndpset->options ) + len );
/* Register settings */
if ( ( rc = register_settings ( &ndpset->settings, parent,
NDP_SETTINGS_NAME ) ) != 0 )
goto err_register;
/* Construct and register per-prefix settings */
for ( instance = 0, offset = 0 ; offset < len ; offset += option_len ) {
/* Skip non-prefix options */
option = ( ( ( void * ) ndpset->options ) + offset );
option_len = ( option->header.blocks * NDP_OPTION_BLKSZ );
if ( option->header.type != NDP_OPT_PREFIX )
continue;
/* Initialise structure */
settings_init ( &prefset->settings,
&ndp_prefix_settings_operations,
&ndpset->refcnt, &ndp_settings_scope );
prefset->settings.order =
( ( option->prefix.flags & NDP_PREFIX_AUTONOMOUS ) ?
IPV6_ORDER_SLAAC : IPV6_ORDER_PREFIX_ONLY );
prefset->prefix = &option->prefix;
snprintf ( prefset->name, sizeof ( prefset->name ), "%d",
instance++ );
/* Register settings */
if ( ( rc = register_settings ( &prefset->settings,
&ndpset->settings,
prefset->name ) ) != 0 )
goto err_register_prefix;
/* Move to next per-prefix settings */
prefset++;
}
assert ( instance == prefixes );
ref_put ( &ndpset->refcnt );
return 0;
err_register_prefix:
unregister_settings ( &ndpset->settings );
err_register:
ref_put ( &ndpset->refcnt );
err_alloc:
return rc;
}
/** DNS server setting */
const struct setting ndp_dns6_setting __setting ( SETTING_IP6_EXTRA, dns6 ) = {
.name = "dns6",
.description = "DNS server",
.tag = NDP_TAG ( NDP_OPT_RDNSS,
offsetof ( struct ndp_rdnss_option, addresses ), 0 ),
.type = &setting_type_ipv6,
.scope = &ndp_settings_scope,
};
/** DNS search list setting */
const struct setting ndp_dnssl_setting __setting ( SETTING_IP_EXTRA, dnssl ) = {
.name = "dnssl",
.description = "DNS search list",
.tag = NDP_TAG ( NDP_OPT_DNSSL,
offsetof ( struct ndp_dnssl_option, names ), 0 ),
.type = &setting_type_dnssl,
.scope = &ndp_settings_scope,
};
/****************************************************************************
*
* IPv6 autoconfiguration
*
*/
/** An IPv6 configurator */
struct ipv6conf {
/** Reference count */
struct refcnt refcnt;
/** List of configurators */
struct list_head list;
/** Job control interface */
struct interface job;
/** DHCPv6 interface */
struct interface dhcp;
/** Network device being configured */
struct net_device *netdev;
/** Retransmission timer */
struct retry_timer timer;
/** Deferred discovery counter */
unsigned int deferred;
};
/** List of IPv6 configurators */
static LIST_HEAD ( ipv6confs );
/**
* Free IPv6 configurator
*
* @v refcnt Reference count
*/
static void ipv6conf_free ( struct refcnt *refcnt ) {
struct ipv6conf *ipv6conf =
container_of ( refcnt, struct ipv6conf, refcnt );
netdev_put ( ipv6conf->netdev );
free ( ipv6conf );
}
/**
* Identify IPv6 configurator by network device
*
* @v netdev Network device
* @ret ipv6 IPv6 configurator, or NULL
*/
static struct ipv6conf * ipv6conf_demux ( struct net_device *netdev ) {
struct ipv6conf *ipv6conf;
list_for_each_entry ( ipv6conf, &ipv6confs, list ) {
if ( ipv6conf->netdev == netdev )
return ipv6conf;
}
return NULL;
}
/**
* Finish IPv6 autoconfiguration
*
* @v ipv6 IPv6 configurator
* @v rc Reason for finishing
*/
static void ipv6conf_done ( struct ipv6conf *ipv6conf, int rc ) {
/* Shut down interfaces */
intf_shutdown ( &ipv6conf->job, rc );
intf_shutdown ( &ipv6conf->dhcp, rc );
/* Stop timer */
stop_timer ( &ipv6conf->timer );
/* Remove from list and drop list's reference */
list_del ( &ipv6conf->list );
ref_put ( &ipv6conf->refcnt );
}
/**
* Handle IPv6 configurator timer expiry
*
* @v timer Retry timer
* @v fail Failure indicator
*/
static void ipv6conf_expired ( struct retry_timer *timer, int fail ) {
struct ipv6conf *ipv6conf =
container_of ( timer, struct ipv6conf, timer );
struct net_device *netdev = ipv6conf->netdev;
/* If we have failed, terminate autoconfiguration */
if ( fail ) {
ipv6conf_done ( ipv6conf, -ETIMEDOUT );
return;
}
/* Otherwise, transmit router solicitation and restart timer */
start_timer ( &ipv6conf->timer );
ndp_tx_router_solicitation ( netdev );
/* If link is blocked, defer router discovery timeout */
if ( netdev_link_blocked ( netdev ) &&
( ipv6conf->deferred++ <= IPV6CONF_MAX_DEFERRALS ) ) {
DBGC ( netdev, "NDP %s deferring discovery timeout\n",
netdev->name );
start_timer_fixed ( &ipv6conf->timer, IPV6CONF_BLOCK_TIMEOUT );
}
}
/**
* Handle router advertisement during IPv6 autoconfiguration
*
* @v netdev Network device
* @v router Router address
* @v radv Router advertisement
* @v len Length of router advertisement
* @ret rc Return status code
*
* This function assumes that the router advertisement is well-formed,
* since it must have already passed through option processing.
*/
static int
ipv6conf_rx_router_advertisement ( struct net_device *netdev,
struct in6_addr *router,
struct ndp_router_advertisement_header *radv,
size_t len ) {
struct ipv6conf *ipv6conf;
size_t option_len;
int stateful;
int rc;
/* Identify IPv6 configurator, if any */
ipv6conf = ipv6conf_demux ( netdev );
/* Do nothing unless IPv6 autoconfiguration is in progress */
if ( ! ipv6conf )
return 0;
/* If this is not the first solicited router advertisement, ignore it */
if ( ! timer_running ( &ipv6conf->timer ) )
return 0;
/* Stop router solicitation timer */
stop_timer ( &ipv6conf->timer );
/* Register NDP settings */
option_len = ( len - offsetof ( typeof ( *radv ), option ) );
if ( ( rc = ndp_register_settings ( netdev, router,
ntohl ( radv->lifetime ),
radv->option, option_len ) ) != 0 )
return rc;
/* Start DHCPv6 if required */
if ( radv->flags & ( NDP_ROUTER_MANAGED | NDP_ROUTER_OTHER ) ) {
stateful = ( radv->flags & NDP_ROUTER_MANAGED );
if ( ( rc = start_dhcpv6 ( &ipv6conf->dhcp, netdev, router,
stateful ) ) != 0 ) {
DBGC ( netdev, "NDP %s could not start state%s DHCPv6: "
"%s\n", netdev->name,
( stateful ? "ful" : "less" ), strerror ( rc ) );
ipv6conf_done ( ipv6conf, rc );
return rc;
}
return 0;
}
/* Otherwise, terminate autoconfiguration */
ipv6conf_done ( ipv6conf, 0 );
return 0;
}
/** IPv6 configurator job interface operations */
static struct interface_operation ipv6conf_job_op[] = {
INTF_OP ( intf_close, struct ipv6conf *, ipv6conf_done ),
};
/** IPv6 configurator job interface descriptor */
static struct interface_descriptor ipv6conf_job_desc =
INTF_DESC ( struct ipv6conf, job, ipv6conf_job_op );
/** IPv6 configurator DHCPv6 interface operations */
static struct interface_operation ipv6conf_dhcp_op[] = {
INTF_OP ( intf_close, struct ipv6conf *, ipv6conf_done ),
};
/** IPv6 configurator DHCPv6 interface descriptor */
static struct interface_descriptor ipv6conf_dhcp_desc =
INTF_DESC ( struct ipv6conf, dhcp, ipv6conf_dhcp_op );
/**
* Start IPv6 autoconfiguration
*
* @v job Job control interface
* @v netdev Network device
* @ret rc Return status code
*/
int start_ipv6conf ( struct interface *job, struct net_device *netdev ) {
struct ipv6conf *ipv6conf;
/* Allocate and initialise structure */
ipv6conf = zalloc ( sizeof ( *ipv6conf ) );
if ( ! ipv6conf )
return -ENOMEM;
ref_init ( &ipv6conf->refcnt, ipv6conf_free );
intf_init ( &ipv6conf->job, &ipv6conf_job_desc, &ipv6conf->refcnt );
intf_init ( &ipv6conf->dhcp, &ipv6conf_dhcp_desc, &ipv6conf->refcnt );
timer_init ( &ipv6conf->timer, ipv6conf_expired, &ipv6conf->refcnt );
set_timer_limits ( &ipv6conf->timer, IPV6CONF_MIN_TIMEOUT,
IPV6CONF_MAX_TIMEOUT );
ipv6conf->netdev = netdev_get ( netdev );
/* Start timer to initiate router solicitation */
start_timer_nodelay ( &ipv6conf->timer );
/* Attach parent interface, transfer reference to list, and return */
intf_plug_plug ( &ipv6conf->job, job );
list_add ( &ipv6conf->list, &ipv6confs );
return 0;
}
/** IPv6 network device configurator */
struct net_device_configurator ipv6_configurator __net_device_configurator = {
.name = "ipv6",
.start = start_ipv6conf,
};