diff options
author | Michael Brown <mcb30@ipxe.org> | 2013-11-08 14:35:29 +0000 |
---|---|---|
committer | Michael Brown <mcb30@ipxe.org> | 2013-11-14 12:35:43 +0000 |
commit | 2fa34085e28992dd7409496709af822776bd8fc6 (patch) | |
tree | 39a5bccb33c9b67eb2c93c950be39927f146a543 /src/net | |
parent | c1570d3dfb24c08377cc6cf5a335892e39bd9bd2 (diff) | |
download | ipxe-2fa34085e28992dd7409496709af822776bd8fc6.zip ipxe-2fa34085e28992dd7409496709af822776bd8fc6.tar.gz ipxe-2fa34085e28992dd7409496709af822776bd8fc6.tar.bz2 |
[dhcpv6] Add basic support for stateful and stateless DHCPv6
Add support for the stateful and stateless variants of the DHCPv6
protocol. The resulting settings block is registered as
"net<x>.dhcpv6", and DHCPv6 options can be obtained using
e.g. "${net0.dhcpv6/23:ipv6}" to obtain the IPv6 DNS server address.
IPv6 addresses obtained via stateful DHCPv6 are not yet applied to the
network device.
Signed-off-by: Michael Brown <mcb30@ipxe.org>
Diffstat (limited to 'src/net')
-rw-r--r-- | src/net/ndp.c | 41 | ||||
-rw-r--r-- | src/net/udp/dhcpv6.c | 955 |
2 files changed, 991 insertions, 5 deletions
diff --git a/src/net/ndp.c b/src/net/ndp.c index 1b1109e..f56bbe9 100644 --- a/src/net/ndp.c +++ b/src/net/ndp.c @@ -29,6 +29,7 @@ FILE_LICENCE ( GPL2_OR_LATER ); #include <ipxe/ipv6.h> #include <ipxe/icmpv6.h> #include <ipxe/neighbour.h> +#include <ipxe/dhcpv6.h> #include <ipxe/ndp.h> /** @file @@ -596,6 +597,8 @@ struct ipv6conf { /** Job control interface */ struct interface job; + /** DHCPv6 interface */ + struct interface dhcp; /** Network device being configured */ struct net_device *netdev; @@ -646,6 +649,7 @@ 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 ); @@ -686,6 +690,8 @@ static void ipv6conf_expired ( struct retry_timer *timer, int fail ) { static int ipv6conf_rx_router_advertisement ( struct net_device *netdev, unsigned int flags ) { struct ipv6conf *ipv6conf; + int stateful; + int rc; /* Identify IPv6 configurator, if any */ ipv6conf = ipv6conf_demux ( netdev ); @@ -697,13 +703,28 @@ static int ipv6conf_rx_router_advertisement ( struct net_device *netdev, return 0; } - /* Fail if stateful address autoconfiguration is required */ - if ( flags & NDP_ROUTER_MANAGED ) { - ipv6conf_done ( ipv6conf, -ENOTSUP ); - return -ENOTSUP; + /* 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 ); + + /* Start DHCPv6 if required */ + if ( flags & ( NDP_ROUTER_MANAGED | NDP_ROUTER_OTHER ) ) { + stateful = ( flags & NDP_ROUTER_MANAGED ); + if ( ( rc = start_dhcpv6 ( &ipv6conf->dhcp, netdev, + stateful ) ) != 0 ) { + DBGC ( netdev, "NDP could not start state%s DHCPv6: " + "%s\n", ( stateful ? "ful" : "less" ), + strerror ( rc ) ); + ipv6conf_done ( ipv6conf, rc ); + return rc; + } + return 0; } - /* Mark autoconfiguration as complete */ + /* Otherwise, terminate autoconfiguration */ ipv6conf_done ( ipv6conf, 0 ); return 0; @@ -718,6 +739,15 @@ static struct interface_operation ipv6conf_job_op[] = { 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 * @@ -734,6 +764,7 @@ int start_ipv6conf ( struct interface *job, struct net_device *netdev ) { 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 ); ipv6conf->netdev = netdev_get ( netdev ); diff --git a/src/net/udp/dhcpv6.c b/src/net/udp/dhcpv6.c new file mode 100644 index 0000000..ebd7221 --- /dev/null +++ b/src/net/udp/dhcpv6.c @@ -0,0 +1,955 @@ +/* + * 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/interface.h> +#include <ipxe/xfer.h> +#include <ipxe/iobuf.h> +#include <ipxe/open.h> +#include <ipxe/netdevice.h> +#include <ipxe/settings.h> +#include <ipxe/retry.h> +#include <ipxe/timer.h> +#include <ipxe/in.h> +#include <ipxe/crc32.h> +#include <ipxe/errortab.h> +#include <ipxe/dhcpv6.h> + +/** @file + * + * Dynamic Host Configuration Protocol for IPv6 + * + */ + +/* Disambiguate the various error causes */ +#define EPROTO_UNSPECFAIL __einfo_error ( EINFO_EPROTO_UNSPECFAIL ) +#define EINFO_EPROTO_UNSPECFAIL \ + __einfo_uniqify ( EINFO_EPROTO, 1, "Unspecified server failure" ) +#define EPROTO_NOADDRSAVAIL __einfo_error ( EINFO_EPROTO_NOADDRSAVAIL ) +#define EINFO_EPROTO_NOADDRSAVAIL \ + __einfo_uniqify ( EINFO_EPROTO, 2, "No addresses available" ) +#define EPROTO_NOBINDING __einfo_error ( EINFO_EPROTO_NOBINDING ) +#define EINFO_EPROTO_NOBINDING \ + __einfo_uniqify ( EINFO_EPROTO, 3, "Client record unavailable" ) +#define EPROTO_NOTONLINK __einfo_error ( EINFO_EPROTO_NOTONLINK ) +#define EINFO_EPROTO_NOTONLINK \ + __einfo_uniqify ( EINFO_EPROTO, 4, "Prefix not on link" ) +#define EPROTO_USEMULTICAST __einfo_error ( EINFO_EPROTO_USEMULTICAST ) +#define EINFO_EPROTO_USEMULTICAST \ + __einfo_uniqify ( EINFO_EPROTO, 5, "Use multicast address" ) +#define EPROTO_STATUS( status ) \ + EUNIQ ( EINFO_EPROTO, ( (status) & 0x0f ), EPROTO_UNSPECFAIL, \ + EPROTO_NOADDRSAVAIL, EPROTO_NOBINDING, \ + EPROTO_NOTONLINK, EPROTO_USEMULTICAST ) + +/** Human-readable error messages */ +struct errortab dhcpv6_errors[] __errortab = { + __einfo_errortab ( EINFO_EPROTO_NOADDRSAVAIL ), +}; + +/**************************************************************************** + * + * DHCPv6 option lists + * + */ + +/** A DHCPv6 option list */ +struct dhcpv6_option_list { + /** Data buffer */ + const void *data; + /** Length of data buffer */ + size_t len; +}; + +/** + * Find DHCPv6 option + * + * @v options DHCPv6 option list + * @v code Option code + * @ret option DHCPv6 option, or NULL if not found + */ +static const union dhcpv6_any_option * +dhcpv6_option ( struct dhcpv6_option_list *options, unsigned int code ) { + const union dhcpv6_any_option *option = options->data; + size_t remaining = options->len; + size_t data_len; + + /* Scan through list of options */ + while ( remaining >= sizeof ( option->header ) ) { + + /* Calculate and validate option length */ + remaining -= sizeof ( option->header ); + data_len = ntohs ( option->header.len ); + if ( data_len > remaining ) { + /* Malformed option list */ + return NULL; + } + + /* Return if we have found the specified option */ + if ( option->header.code == htons ( code ) ) + return option; + + /* Otherwise, move to the next option */ + option = ( ( ( void * ) option->header.data ) + data_len ); + remaining -= data_len; + } + + return NULL; +} + +/** + * Check DHCPv6 client or server identifier + * + * @v options DHCPv6 option list + * @v code Option code + * @v expected Expected value + * @v len Length of expected value + * @ret rc Return status code + */ +static int dhcpv6_check_duid ( struct dhcpv6_option_list *options, + unsigned int code, const void *expected, + size_t len ) { + const union dhcpv6_any_option *option; + const struct dhcpv6_duid_option *duid; + + /* Find option */ + option = dhcpv6_option ( options, code ); + if ( ! option ) + return -ENOENT; + duid = &option->duid; + + /* Check option length */ + if ( ntohs ( duid->header.len ) != len ) + return -EINVAL; + + /* Compare option value */ + if ( memcmp ( duid->duid, expected, len ) != 0 ) + return -EINVAL; + + return 0; +} + +/** + * Get DHCPv6 status code + * + * @v options DHCPv6 option list + * @ret rc Return status code + */ +static int dhcpv6_status_code ( struct dhcpv6_option_list *options ) { + const union dhcpv6_any_option *option; + const struct dhcpv6_status_code_option *status_code; + unsigned int status; + + /* Find status code option, if present */ + option = dhcpv6_option ( options, DHCPV6_STATUS_CODE ); + if ( ! option ) { + /* Omitted status code should be treated as "success" */ + return 0; + } + status_code = &option->status_code; + + /* Sanity check */ + if ( ntohs ( status_code->header.len ) < + ( sizeof ( *status_code ) - sizeof ( status_code->header ) ) ) { + return -EINVAL; + } + + /* Calculate iPXE error code from DHCPv6 status code */ + status = ntohs ( status_code->status ); + return ( status ? -EPROTO_STATUS ( status ) : 0 ); +} + +/** + * Get DHCPv6 identity association address + * + * @v options DHCPv6 option list + * @v iaid Identity association ID + * @v address IPv6 address to fill in + * @ret rc Return status code + */ +static int dhcpv6_iaaddr ( struct dhcpv6_option_list *options, uint32_t iaid, + struct in6_addr *address ) { + const union dhcpv6_any_option *option; + const struct dhcpv6_ia_na_option *ia_na; + const struct dhcpv6_iaaddr_option *iaaddr; + struct dhcpv6_option_list suboptions; + size_t len; + int rc; + + /* Find identity association option, if present */ + option = dhcpv6_option ( options, DHCPV6_IA_NA ); + if ( ! option ) + return -ENOENT; + ia_na = &option->ia_na; + + /* Sanity check */ + len = ntohs ( ia_na->header.len ); + if ( len < ( sizeof ( *ia_na ) - sizeof ( ia_na->header ) ) ) + return -EINVAL; + + /* Check identity association ID */ + if ( ia_na->iaid != htonl ( iaid ) ) + return -EINVAL; + + /* Construct IA_NA sub-options list */ + suboptions.data = ia_na->options; + suboptions.len = ( len + sizeof ( ia_na->header ) - + offsetof ( typeof ( *ia_na ), options ) ); + + /* Check IA_NA status code */ + if ( ( rc = dhcpv6_status_code ( &suboptions ) ) != 0 ) + return rc; + + /* Find identity association address, if present */ + option = dhcpv6_option ( &suboptions, DHCPV6_IAADDR ); + if ( ! option ) + return -ENOENT; + iaaddr = &option->iaaddr; + + /* Sanity check */ + len = ntohs ( iaaddr->header.len ); + if ( len < ( sizeof ( *iaaddr ) - sizeof ( iaaddr->header ) ) ) + return -EINVAL; + + /* Construct IAADDR sub-options list */ + suboptions.data = iaaddr->options; + suboptions.len = ( len + sizeof ( iaaddr->header ) - + offsetof ( typeof ( *iaaddr ), options ) ); + + /* Check IAADDR status code */ + if ( ( rc = dhcpv6_status_code ( &suboptions ) ) != 0 ) + return rc; + + /* Extract IPv6 address */ + memcpy ( address, &iaaddr->address, sizeof ( *address ) ); + + return 0; +} + +/**************************************************************************** + * + * DHCPv6 settings blocks + * + */ + +/** DHCPv6 settings scope */ +static struct settings_scope dhcpv6_settings_scope; + +/** A DHCPv6 settings block */ +struct dhcpv6_settings { + /** Reference count */ + struct refcnt refcnt; + /** Settings block */ + struct settings settings; + /** Option list */ + struct dhcpv6_option_list options; +}; + +/** + * Check applicability of DHCPv6 setting + * + * @v settings Settings block + * @v setting Setting + * @ret applies Setting applies within this settings block + */ +static int dhcpv6_applies ( struct settings *settings __unused, + struct setting *setting ) { + + return ( setting->scope == &dhcpv6_settings_scope ); +} + +/** + * Fetch value of DHCPv6 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 dhcpv6_fetch ( struct settings *settings, + struct setting *setting, + void *data, size_t len ) { + struct dhcpv6_settings *dhcpv6set = + container_of ( settings, struct dhcpv6_settings, settings ); + const union dhcpv6_any_option *option; + size_t option_len; + + /* Find option */ + option = dhcpv6_option ( &dhcpv6set->options, setting->tag ); + if ( ! option ) + return -ENOENT; + + /* Copy option to data buffer */ + option_len = ntohs ( option->header.len ); + if ( len > option_len ) + len = option_len; + memcpy ( data, option->header.data, len ); + return option_len; +} + +/** DHCPv6 settings operations */ +static struct settings_operations dhcpv6_settings_operations = { + .applies = dhcpv6_applies, + .fetch = dhcpv6_fetch, +}; + +/** + * Register DHCPv6 options as network device settings + * + * @v options DHCPv6 option list + * @v parent Parent settings block + * @ret rc Return status code + */ +static int dhcpv6_register ( struct dhcpv6_option_list *options, + struct settings *parent ) { + struct dhcpv6_settings *dhcpv6set; + void *data; + size_t len; + int rc; + + /* Allocate and initialise structure */ + dhcpv6set = zalloc ( sizeof ( *dhcpv6set ) + options->len ); + if ( ! dhcpv6set ) { + rc = -ENOMEM; + goto err_alloc; + } + ref_init ( &dhcpv6set->refcnt, NULL ); + settings_init ( &dhcpv6set->settings, &dhcpv6_settings_operations, + &dhcpv6set->refcnt, &dhcpv6_settings_scope ); + data = ( ( ( void * ) dhcpv6set ) + sizeof ( *dhcpv6set ) ); + len = options->len; + memcpy ( data, options->data, len ); + dhcpv6set->options.data = data; + dhcpv6set->options.len = len; + + /* Register settings */ + if ( ( rc = register_settings ( &dhcpv6set->settings, parent, + DHCPV6_SETTINGS_NAME ) ) != 0 ) + goto err_register; + + err_register: + ref_put ( &dhcpv6set->refcnt ); + err_alloc: + return rc; +} + +/**************************************************************************** + * + * DHCPv6 protocol + * + */ + +/** Options to be requested */ +static uint16_t dhcpv6_requested_options[] = { + htons ( DHCPV6_DNS_SERVER ), htons ( DHCPV6_DOMAIN_SEARCH ), +}; + +/** + * Name a DHCPv6 packet type + * + * @v type DHCPv6 packet type + * @ret name DHCPv6 packet type name + */ +static __attribute__ (( unused )) const char * +dhcpv6_type_name ( unsigned int type ) { + static char buf[ 12 /* "UNKNOWN-xxx" + NUL */ ]; + + switch ( type ) { + case DHCPV6_SOLICIT: return "SOLICIT"; + case DHCPV6_ADVERTISE: return "ADVERTISE"; + case DHCPV6_REQUEST: return "REQUEST"; + case DHCPV6_REPLY: return "REPLY"; + case DHCPV6_INFORMATION_REQUEST: return "INFORMATION-REQUEST"; + default: + snprintf ( buf, sizeof ( buf ), "UNKNOWN-%d", type ); + return buf; + } +} + +/** A DHCPv6 session state */ +struct dhcpv6_session_state { + /** Current transmitted packet type */ + uint8_t tx_type; + /** Current expected received packet type */ + uint8_t rx_type; + /** Flags */ + uint8_t flags; + /** Next state (or NULL to terminate) */ + struct dhcpv6_session_state *next; +}; + +/** DHCPv6 session state flags */ +enum dhcpv6_session_state_flags { + /** Include identity association within request */ + DHCPV6_TX_IA_NA = 0x01, + /** Include leased IPv6 address within request */ + DHCPV6_TX_IAADDR = 0x02, + /** Record received server ID */ + DHCPV6_RX_SERVER_ID = 0x04, + /** Record received IPv6 address */ + DHCPV6_RX_IAADDR = 0x08, +}; + +/** DHCPv6 request state */ +static struct dhcpv6_session_state dhcpv6_request = { + .tx_type = DHCPV6_REQUEST, + .rx_type = DHCPV6_REPLY, + .flags = ( DHCPV6_TX_IA_NA | DHCPV6_TX_IAADDR | DHCPV6_RX_IAADDR ), + .next = NULL, +}; + +/** DHCPv6 solicitation state */ +static struct dhcpv6_session_state dhcpv6_solicit = { + .tx_type = DHCPV6_SOLICIT, + .rx_type = DHCPV6_ADVERTISE, + .flags = ( DHCPV6_TX_IA_NA | DHCPV6_RX_SERVER_ID | DHCPV6_RX_IAADDR ), + .next = &dhcpv6_request, +}; + +/** DHCPv6 information request state */ +static struct dhcpv6_session_state dhcpv6_information_request = { + .tx_type = DHCPV6_INFORMATION_REQUEST, + .rx_type = DHCPV6_REPLY, + .flags = 0, + .next = NULL, +}; + +/** A DHCPv6 session */ +struct dhcpv6_session { + /** Reference counter */ + struct refcnt refcnt; + /** Job control interface */ + struct interface job; + /** Data transfer interface */ + struct interface xfer; + + /** Network device being configured */ + struct net_device *netdev; + /** Transaction ID */ + uint8_t xid[3]; + /** Identity association ID */ + uint32_t iaid; + /** Start time (in ticks) */ + unsigned long start; + /** Client DUID */ + void *client_duid; + /** Client DUID length */ + size_t client_duid_len; + /** Server DUID, if known */ + void *server_duid; + /** Server DUID length */ + size_t server_duid_len; + /** Leased IPv6 address */ + struct in6_addr lease; + + /** Retransmission timer */ + struct retry_timer timer; + + /** Current session state */ + struct dhcpv6_session_state *state; + /** Current timeout status code */ + int rc; +}; + +/** + * Free DHCPv6 session + * + * @v refcnt Reference count + */ +static void dhcpv6_free ( struct refcnt *refcnt ) { + struct dhcpv6_session *dhcpv6 = + container_of ( refcnt, struct dhcpv6_session, refcnt ); + + netdev_put ( dhcpv6->netdev ); + free ( dhcpv6->server_duid ); + free ( dhcpv6 ); +} + +/** + * Terminate DHCPv6 session + * + * @v dhcpv6 DHCPv6 session + * @v rc Reason for close + */ +static void dhcpv6_finished ( struct dhcpv6_session *dhcpv6, int rc ) { + + /* Stop timer */ + stop_timer ( &dhcpv6->timer ); + + /* Shut down interfaces */ + intf_shutdown ( &dhcpv6->xfer, rc ); + intf_shutdown ( &dhcpv6->job, rc ); +} + +/** + * Transition to new DHCPv6 session state + * + * @v dhcpv6 DHCPv6 session + * @v state New session state + */ +static void dhcpv6_set_state ( struct dhcpv6_session *dhcpv6, + struct dhcpv6_session_state *state ) { + + DBGC ( dhcpv6, "DHCPv6 %s entering %s state\n", dhcpv6->netdev->name, + dhcpv6_type_name ( state->tx_type ) ); + + /* Record state */ + dhcpv6->state = state; + + /* Default to -ETIMEDOUT if no more specific error is recorded */ + dhcpv6->rc = -ETIMEDOUT; + + /* Start timer to trigger transmission */ + start_timer_nodelay ( &dhcpv6->timer ); +} + +/** + * Get DHCPv6 user class + * + * @v data Data buffer + * @v len Length of data buffer + * @ret len Length of user class + */ +static size_t dhcpv6_user_class ( void *data, size_t len ) { + static const char default_user_class[4] = { 'i', 'P', 'X', 'E' }; + int actual_len; + + /* Fetch user-class setting, if defined */ + actual_len = fetch_setting ( NULL, &user_class_setting, data, len ); + if ( actual_len >= 0 ) + return actual_len; + + /* Otherwise, use the default user class ("iPXE") */ + if ( len > sizeof ( default_user_class ) ) + len = sizeof ( default_user_class ); + memcpy ( data, default_user_class, len ); + return sizeof ( default_user_class ); +} + +/** + * Transmit current request + * + * @v dhcpv6 DHCPv6 session + * @ret rc Return status code + */ +static int dhcpv6_tx ( struct dhcpv6_session *dhcpv6 ) { + struct dhcpv6_duid_option *client_id; + struct dhcpv6_duid_option *server_id; + struct dhcpv6_ia_na_option *ia_na; + struct dhcpv6_iaaddr_option *iaaddr; + struct dhcpv6_option_request_option *option_request; + struct dhcpv6_user_class_option *user_class; + struct dhcpv6_elapsed_time_option *elapsed; + struct dhcpv6_header *dhcphdr; + struct io_buffer *iobuf; + size_t client_id_len; + size_t server_id_len; + size_t ia_na_len; + size_t option_request_len; + size_t user_class_string_len; + size_t user_class_len; + size_t elapsed_len; + size_t total_len; + int rc; + + /* Calculate lengths */ + client_id_len = ( sizeof ( *client_id ) + dhcpv6->client_duid_len ); + server_id_len = ( dhcpv6->server_duid ? ( sizeof ( *server_id ) + + dhcpv6->server_duid_len ) :0); + if ( dhcpv6->state->flags & DHCPV6_TX_IA_NA ) { + ia_na_len = sizeof ( *ia_na ); + if ( dhcpv6->state->flags & DHCPV6_TX_IAADDR ) + ia_na_len += sizeof ( *iaaddr ); + } else { + ia_na_len = 0; + } + option_request_len = ( sizeof ( *option_request ) + + sizeof ( dhcpv6_requested_options ) ); + user_class_string_len = dhcpv6_user_class ( NULL, 0 ); + user_class_len = ( sizeof ( *user_class ) + + sizeof ( user_class->user_class[0] ) + + user_class_string_len ); + elapsed_len = sizeof ( *elapsed ); + total_len = ( sizeof ( *dhcphdr ) + client_id_len + server_id_len + + ia_na_len + option_request_len + user_class_len + + elapsed_len ); + + /* Allocate packet */ + iobuf = xfer_alloc_iob ( &dhcpv6->xfer, total_len ); + if ( ! iobuf ) + return -ENOMEM; + + /* Construct header */ + dhcphdr = iob_put ( iobuf, sizeof ( *dhcphdr ) ); + dhcphdr->type = dhcpv6->state->tx_type; + memcpy ( dhcphdr->xid, dhcpv6->xid, sizeof ( dhcphdr->xid ) ); + + /* Construct client identifier */ + client_id = iob_put ( iobuf, client_id_len ); + client_id->header.code = htons ( DHCPV6_CLIENT_ID ); + client_id->header.len = htons ( client_id_len - + sizeof ( client_id->header ) ); + memcpy ( client_id->duid, dhcpv6->client_duid, + dhcpv6->client_duid_len ); + + /* Construct server identifier, if applicable */ + if ( server_id_len ) { + server_id = iob_put ( iobuf, server_id_len ); + server_id->header.code = htons ( DHCPV6_SERVER_ID ); + server_id->header.len = htons ( server_id_len - + sizeof ( server_id->header ) ); + memcpy ( server_id->duid, dhcpv6->server_duid, + dhcpv6->server_duid_len ); + } + + /* Construct identity association, if applicable */ + if ( ia_na_len ) { + ia_na = iob_put ( iobuf, ia_na_len ); + ia_na->header.code = htons ( DHCPV6_IA_NA ); + ia_na->header.len = htons ( ia_na_len - + sizeof ( ia_na->header ) ); + ia_na->iaid = htonl ( dhcpv6->iaid ); + ia_na->renew = htonl ( 0 ); + ia_na->rebind = htonl ( 0 ); + if ( dhcpv6->state->flags & DHCPV6_TX_IAADDR ) { + iaaddr = ( ( void * ) ia_na->options ); + iaaddr->header.code = htons ( DHCPV6_IAADDR ); + iaaddr->header.len = htons ( sizeof ( *iaaddr ) - + sizeof ( iaaddr->header )); + memcpy ( &iaaddr->address, &dhcpv6->lease, + sizeof ( iaaddr->address ) ); + iaaddr->preferred = htonl ( 0 ); + iaaddr->valid = htonl ( 0 ); + } + } + + /* Construct option request */ + option_request = iob_put ( iobuf, option_request_len ); + option_request->header.code = htons ( DHCPV6_OPTION_REQUEST ); + option_request->header.len = htons ( option_request_len - + sizeof ( option_request->header )); + memcpy ( option_request->requested, dhcpv6_requested_options, + sizeof ( dhcpv6_requested_options ) ); + + /* Construct user class */ + user_class = iob_put ( iobuf, user_class_len ); + user_class->header.code = htons ( DHCPV6_USER_CLASS ); + user_class->header.len = htons ( user_class_len - + sizeof ( user_class->header ) ); + user_class->user_class[0].len = htons ( user_class_string_len ); + dhcpv6_user_class ( user_class->user_class[0].string, + user_class_string_len ); + + /* Construct elapsed time */ + elapsed = iob_put ( iobuf, elapsed_len ); + elapsed->header.code = htons ( DHCPV6_ELAPSED_TIME ); + elapsed->header.len = htons ( elapsed_len - + sizeof ( elapsed->header ) ); + elapsed->elapsed = htons ( ( ( currticks() - dhcpv6->start ) * 100 ) / + TICKS_PER_SEC ); + + /* Sanity check */ + assert ( iob_len ( iobuf ) == total_len ); + + /* Transmit packet */ + if ( ( rc = xfer_deliver_iob ( &dhcpv6->xfer, iobuf ) ) != 0 ) { + DBGC ( dhcpv6, "DHCPv6 %s could not transmit: %s\n", + dhcpv6->netdev->name, strerror ( rc ) ); + return rc; + } + + return 0; +} + +/** + * Handle timer expiry + * + * @v timer Retransmission timer + * @v fail Failure indicator + */ +static void dhcpv6_timer_expired ( struct retry_timer *timer, int fail ) { + struct dhcpv6_session *dhcpv6 = + container_of ( timer, struct dhcpv6_session, timer ); + + /* If we have failed, terminate DHCPv6 */ + if ( fail ) { + dhcpv6_finished ( dhcpv6, dhcpv6->rc ); + return; + } + + /* Restart timer */ + start_timer ( &dhcpv6->timer ); + + /* (Re)transmit current request */ + dhcpv6_tx ( dhcpv6 ); +} + +/** + * Receive new data + * + * @v dhcpv6 DHCPv6 session + * @v iobuf I/O buffer + * @v meta Data transfer metadata + * @ret rc Return status code + */ +static int dhcpv6_rx ( struct dhcpv6_session *dhcpv6, + struct io_buffer *iobuf, + struct xfer_metadata *meta ) { + struct settings *parent = netdev_settings ( dhcpv6->netdev ); + struct sockaddr_in6 *src = ( ( struct sockaddr_in6 * ) meta->src ); + struct dhcpv6_header *dhcphdr = iobuf->data; + struct dhcpv6_option_list options; + const union dhcpv6_any_option *option; + int rc; + + /* Sanity checks */ + if ( iob_len ( iobuf ) < sizeof ( *dhcphdr ) ) { + DBGC ( dhcpv6, "DHCPv6 %s received packet too short (%zd " + "bytes, min %zd bytes)\n", dhcpv6->netdev->name, + iob_len ( iobuf ), sizeof ( *dhcphdr ) ); + rc = -EINVAL; + goto done; + } + assert ( src != NULL ); + assert ( src->sin6_family == AF_INET6 ); + DBGC ( dhcpv6, "DHCPv6 %s received %s from %s\n", + dhcpv6->netdev->name, dhcpv6_type_name ( dhcphdr->type ), + inet6_ntoa ( &src->sin6_addr ) ); + + /* Construct option list */ + options.data = dhcphdr->options; + options.len = ( iob_len ( iobuf ) - + offsetof ( typeof ( *dhcphdr ), options ) ); + + /* Verify client identifier */ + if ( ( rc = dhcpv6_check_duid ( &options, DHCPV6_CLIENT_ID, + dhcpv6->client_duid, + dhcpv6->client_duid_len ) ) != 0 ) { + DBGC ( dhcpv6, "DHCPv6 %s received %s without correct client " + "ID: %s\n", dhcpv6->netdev->name, + dhcpv6_type_name ( dhcphdr->type ), strerror ( rc ) ); + goto done; + } + + /* Verify server identifier, if applicable */ + if ( dhcpv6->server_duid && + ( ( rc = dhcpv6_check_duid ( &options, DHCPV6_SERVER_ID, + dhcpv6->server_duid, + dhcpv6->server_duid_len ) ) != 0 ) ) { + DBGC ( dhcpv6, "DHCPv6 %s received %s without correct server " + "ID: %s\n", dhcpv6->netdev->name, + dhcpv6_type_name ( dhcphdr->type ), strerror ( rc ) ); + goto done; + } + + /* Check message type */ + if ( dhcphdr->type != dhcpv6->state->rx_type ) { + DBGC ( dhcpv6, "DHCPv6 %s received %s while expecting %s\n", + dhcpv6->netdev->name, dhcpv6_type_name ( dhcphdr->type ), + dhcpv6_type_name ( dhcpv6->state->rx_type ) ); + rc = -ENOTTY; + goto done; + } + + /* Fetch status code, if present */ + if ( ( rc = dhcpv6_status_code ( &options ) ) != 0 ) { + DBGC ( dhcpv6, "DHCPv6 %s received %s with error status: %s\n", + dhcpv6->netdev->name, dhcpv6_type_name ( dhcphdr->type ), + strerror ( rc ) ); + /* This is plausibly the error we want to return */ + dhcpv6->rc = rc; + goto done; + } + + /* Record identity association address, if applicable */ + if ( dhcpv6->state->flags & DHCPV6_RX_IAADDR ) { + if ( ( rc = dhcpv6_iaaddr ( &options, dhcpv6->iaid, + &dhcpv6->lease ) ) != 0 ) { + DBGC ( dhcpv6, "DHCPv6 %s received %s with unusable " + "IAADDR: %s\n", dhcpv6->netdev->name, + dhcpv6_type_name ( dhcphdr->type ), + strerror ( rc ) ); + /* This is plausibly the error we want to return */ + dhcpv6->rc = rc; + goto done; + } + DBGC ( dhcpv6, "DHCPv6 %s received %s is for %s\n", + dhcpv6->netdev->name, dhcpv6_type_name ( dhcphdr->type ), + inet6_ntoa ( &dhcpv6->lease ) ); + } + + /* Record server ID, if applicable */ + if ( dhcpv6->state->flags & DHCPV6_RX_SERVER_ID ) { + assert ( dhcpv6->server_duid == NULL ); + option = dhcpv6_option ( &options, DHCPV6_SERVER_ID ); + if ( ! option ) { + DBGC ( dhcpv6, "DHCPv6 %s received %s missing server " + "ID\n", dhcpv6->netdev->name, + dhcpv6_type_name ( dhcphdr->type ) ); + rc = -EINVAL; + goto done; + } + dhcpv6->server_duid_len = ntohs ( option->duid.header.len ); + dhcpv6->server_duid = malloc ( dhcpv6->server_duid_len ); + if ( ! dhcpv6->server_duid ) { + rc = -ENOMEM; + goto done; + } + memcpy ( dhcpv6->server_duid, option->duid.duid, + dhcpv6->server_duid_len ); + } + + /* Transition to next state or complete DHCPv6, as applicable */ + if ( dhcpv6->state->next ) { + + /* Transition to next state */ + dhcpv6_set_state ( dhcpv6, dhcpv6->state->next ); + rc = 0; + + } else { + + /* Register settings */ + if ( ( rc = dhcpv6_register ( &options, parent ) ) != 0 ) { + DBGC ( dhcpv6, "DHCPv6 %s could not register " + "settings: %s\n", dhcpv6->netdev->name, + strerror ( rc ) ); + goto done; + } + + /* Mark as complete */ + dhcpv6_finished ( dhcpv6, 0 ); + DBGC ( dhcpv6, "DHCPv6 %s complete\n", dhcpv6->netdev->name ); + } + + done: + free_iob ( iobuf ); + return rc; +} + +/** DHCPv6 job control interface operations */ +static struct interface_operation dhcpv6_job_op[] = { + INTF_OP ( intf_close, struct dhcpv6_session *, dhcpv6_finished ), +}; + +/** DHCPv6 job control interface descriptor */ +static struct interface_descriptor dhcpv6_job_desc = + INTF_DESC ( struct dhcpv6_session, job, dhcpv6_job_op ); + +/** DHCPv6 data transfer interface operations */ +static struct interface_operation dhcpv6_xfer_op[] = { + INTF_OP ( xfer_deliver, struct dhcpv6_session *, dhcpv6_rx ), +}; + +/** DHCPv6 data transfer interface descriptor */ +static struct interface_descriptor dhcpv6_xfer_desc = + INTF_DESC ( struct dhcpv6_session, xfer, dhcpv6_xfer_op ); + +/** + * Start DHCPv6 + * + * @v job Job control interface + * @v netdev Network device + * @v stateful Perform stateful address autoconfiguration + * @ret rc Return status code + */ +int start_dhcpv6 ( struct interface *job, struct net_device *netdev, + int stateful ) { + struct ll_protocol *ll_protocol = netdev->ll_protocol; + struct dhcpv6_session *dhcpv6; + struct { + union { + struct sockaddr_in6 sin6; + struct sockaddr sa; + } client; + union { + struct sockaddr_in6 sin6; + struct sockaddr sa; + } server; + } addresses; + struct dhcpv6_duid_ll *client_duid; + size_t client_duid_len; + uint32_t xid; + int rc; + + /* Allocate and initialise structure */ + client_duid_len = ( sizeof ( *client_duid ) + ll_protocol->ll_addr_len); + dhcpv6 = zalloc ( sizeof ( *dhcpv6 ) + client_duid_len ); + if ( ! dhcpv6 ) + return -ENOMEM; + ref_init ( &dhcpv6->refcnt, dhcpv6_free ); + intf_init ( &dhcpv6->job, &dhcpv6_job_desc, &dhcpv6->refcnt ); + intf_init ( &dhcpv6->xfer, &dhcpv6_xfer_desc, &dhcpv6->refcnt ); + dhcpv6->netdev = netdev_get ( netdev ); + xid = random(); + memcpy ( dhcpv6->xid, &xid, sizeof ( dhcpv6->xid ) ); + dhcpv6->start = currticks(); + dhcpv6->client_duid = ( ( ( void * ) dhcpv6 ) + sizeof ( *dhcpv6 ) ); + dhcpv6->client_duid_len = client_duid_len; + timer_init ( &dhcpv6->timer, dhcpv6_timer_expired, &dhcpv6->refcnt ); + + /* Construct client and server addresses */ + memset ( &addresses, 0, sizeof ( addresses ) ); + addresses.client.sin6.sin6_family = AF_INET6; + addresses.client.sin6.sin6_scope_id = netdev->index; + addresses.client.sin6.sin6_port = htons ( DHCPV6_CLIENT_PORT ); + addresses.server.sin6.sin6_family = AF_INET6; + ipv6_all_dhcp_relay_and_servers ( &addresses.server.sin6.sin6_addr ); + addresses.server.sin6.sin6_scope_id = netdev->index; + addresses.server.sin6.sin6_port = htons ( DHCPV6_SERVER_PORT ); + + /* Construct client DUID and IAID from link-layer address */ + client_duid = dhcpv6->client_duid; + client_duid->type = htons ( DHCPV6_DUID_LL ); + client_duid->htype = ll_protocol->ll_proto; + memcpy ( client_duid->ll_addr, netdev->ll_addr, + ll_protocol->ll_addr_len ); + dhcpv6->iaid = crc32_le ( 0, netdev->ll_addr, ll_protocol->ll_addr_len); + DBGC ( dhcpv6, "DHCPv6 %s has XID %02x%02x%02x\n", dhcpv6->netdev->name, + dhcpv6->xid[0], dhcpv6->xid[1], dhcpv6->xid[2] ); + + /* Enter initial state */ + dhcpv6_set_state ( dhcpv6, ( stateful ? &dhcpv6_solicit : + &dhcpv6_information_request ) ); + + /* Open socket */ + if ( ( rc = xfer_open_socket ( &dhcpv6->xfer, SOCK_DGRAM, + &addresses.server.sa, + &addresses.client.sa ) ) != 0 ) { + DBGC ( dhcpv6, "DHCPv6 %s could not open socket: %s\n", + dhcpv6->netdev->name, strerror ( rc ) ); + goto err_open_socket; + } + + /* Attach parent interface, mortalise self, and return */ + intf_plug_plug ( &dhcpv6->job, job ); + ref_put ( &dhcpv6->refcnt ); + return 0; + + err_open_socket: + dhcpv6_finished ( dhcpv6, rc ); + ref_put ( &dhcpv6->refcnt ); + return rc; +} |