blob: a49109894184287cbbbf6d048ed33057490f4903 [file] [log] [blame]
/*
* 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.
*
* You can also choose to distribute this program under the terms of
* the Unmodified Binary Distribution Licence (as given in the file
* COPYING.UBDL), provided that you have satisfied its requirements.
*/
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#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/ipv6.h>
#include <ipxe/dhcparch.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
*
*/
/** A DHCPv6 settings block */
struct dhcpv6_settings {
/** Reference count */
struct refcnt refcnt;
/** Settings block */
struct settings settings;
/** Leased address */
struct in6_addr lease;
/** Router address */
struct in6_addr router;
/** 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,
const struct setting *setting ) {
return ( ( setting->scope == &dhcpv6_scope ) ||
( setting->scope == &ipv6_settings_scope ) );
}
/**
* Fetch value of DHCPv6 leased address
*
* @v dhcpv6set DHCPv6 settings
* @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_ip6 ( struct dhcpv6_settings *dhcpv6set,
void *data, size_t len ) {
struct in6_addr *lease = &dhcpv6set->lease;
/* Copy leased address */
if ( len > sizeof ( *lease ) )
len = sizeof ( *lease );
memcpy ( data, lease, len );
return sizeof ( *lease );
}
/**
* Fetch value of DHCPv6 implicit address prefix length
*
* @v dhcpv6set DHCPv6 settings
* @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_len6 ( struct dhcpv6_settings *dhcpv6set __unused,
void *data, size_t len ) {
uint8_t *len6 = data;
/* Default to assuming this is the only address on the link.
* If the address falls within a known prefix, then the IPv6
* routing table construction logic will match it against that
* prefix.
*/
if ( len )
*len6 = IPV6_MAX_PREFIX_LEN;
return sizeof ( *len6 );
}
/**
* Fetch value of DHCPv6 router address
*
* @v dhcpv6set DHCPv6 settings
* @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_gateway6 ( struct dhcpv6_settings *dhcpv6set,
void *data, size_t len ) {
struct in6_addr *router = &dhcpv6set->router;
/* Copy router address */
if ( len > sizeof ( *router ) )
len = sizeof ( *router );
memcpy ( data, router, len );
return sizeof ( *router );
}
/** A DHCPv6 address setting operation */
struct dhcpv6_address_operation {
/** Generic setting */
const struct setting *setting;
/**
* Fetch value of setting
*
* @v dhcpv6set DHCPv6 settings
* @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 dhcpv6_settings *dhcpv6set,
void *data, size_t len );
};
/** DHCPv6 address settings operations */
static struct dhcpv6_address_operation dhcpv6_address_operations[] = {
{ &ip6_setting, dhcpv6_fetch_ip6 },
{ &len6_setting, dhcpv6_fetch_len6 },
{ &gateway6_setting, dhcpv6_fetch_gateway6 },
};
/**
* 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;
struct dhcpv6_address_operation *op;
size_t option_len;
unsigned int i;
/* Handle address settings */
for ( i = 0 ; i < ( sizeof ( dhcpv6_address_operations ) /
sizeof ( dhcpv6_address_operations[0] ) ) ; i++ ) {
op = &dhcpv6_address_operations[i];
if ( setting_cmp ( setting, op->setting ) != 0 )
continue;
if ( IN6_IS_ADDR_UNSPECIFIED ( &dhcpv6set->lease ) )
return -ENOENT;
return op->fetch ( dhcpv6set, data, 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 lease DHCPv6 leased address
* @v router DHCPv6 router address
* @v options DHCPv6 option list
* @v parent Parent settings block
* @ret rc Return status code
*/
static int dhcpv6_register ( struct in6_addr *lease, struct in6_addr *router,
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_scope );
dhcpv6set->settings.order = IPV6_ORDER_DHCPV6;
data = ( ( ( void * ) dhcpv6set ) + sizeof ( *dhcpv6set ) );
len = options->len;
memcpy ( data, options->data, len );
dhcpv6set->options.data = data;
dhcpv6set->options.len = len;
memcpy ( &dhcpv6set->lease, lease, sizeof ( dhcpv6set->lease ) );
memcpy ( &dhcpv6set->router, router, sizeof ( dhcpv6set->router ) );
/* 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
*
*/
/** Raw option data for options common to all DHCPv6 requests */
static uint8_t dhcpv6_request_options_data[] = {
DHCPV6_CODE ( DHCPV6_OPTION_REQUEST ),
DHCPV6_OPTION ( DHCPV6_CODE ( DHCPV6_DNS_SERVERS ),
DHCPV6_CODE ( DHCPV6_DOMAIN_LIST ),
DHCPV6_CODE ( DHCPV6_BOOTFILE_URL ),
DHCPV6_CODE ( DHCPV6_BOOTFILE_PARAM ) ),
DHCPV6_CODE ( DHCPV6_VENDOR_CLASS ),
DHCPV6_OPTION ( DHCPV6_DWORD_VALUE ( DHCPV6_VENDOR_CLASS_PXE ),
DHCPV6_STRING (
DHCP_VENDOR_PXECLIENT ( DHCP_ARCH_CLIENT_ARCHITECTURE,
DHCP_ARCH_CLIENT_NDI ) ) ),
DHCPV6_CODE ( DHCPV6_CLIENT_ARCHITECTURE ),
DHCPV6_WORD ( DHCP_ARCH_CLIENT_ARCHITECTURE ),
DHCPV6_CODE ( DHCPV6_CLIENT_NDI ),
DHCPV6_OPTION ( DHCP_ARCH_CLIENT_NDI )
};
/**
* 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_RECORD_SERVER_ID = 0x04,
/** Record received IPv6 address */
DHCPV6_RX_RECORD_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_RECORD_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_RECORD_SERVER_ID |
DHCPV6_RX_RECORD_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;
/** Router address */
struct in6_addr router;
/** Transaction ID */
uint8_t xid[3];
/** Identity association ID */
uint32_t iaid;
/** Start time (in ticks) */
unsigned long start;
/** Client DUID */
struct dhcpv6_duid_uuid client_duid;
/** 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_raw_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_user_class_option *user_class;
struct dhcpv6_elapsed_time_option *elapsed;
struct dhcpv6_header *dhcphdr;
struct io_buffer *iobuf;
void *options;
size_t client_id_len;
size_t server_id_len;
size_t ia_na_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 ) +
sizeof ( dhcpv6->client_duid ) );
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;
}
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 + sizeof ( dhcpv6_request_options_data ) +
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,
sizeof ( dhcpv6->client_duid ) );
/* 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 fixed request options */
options = iob_put ( iobuf, sizeof ( dhcpv6_request_options_data ) );
memcpy ( options, dhcpv6_request_options_data,
sizeof ( dhcpv6_request_options_data ) );
/* 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,
sizeof ( dhcpv6->client_duid ) ) ) !=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_RECORD_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_RECORD_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, if applicable */
if ( dhcpv6->state->next ) {
dhcpv6_set_state ( dhcpv6, dhcpv6->state->next );
rc = 0;
goto done;
}
/* Register settings */
if ( ( rc = dhcpv6_register ( &dhcpv6->lease, &dhcpv6->router,
&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 router Router address
* @v stateful Perform stateful address autoconfiguration
* @ret rc Return status code
*/
int start_dhcpv6 ( struct interface *job, struct net_device *netdev,
struct in6_addr *router, 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;
uint32_t xid;
int len;
int rc;
/* Allocate and initialise structure */
dhcpv6 = zalloc ( sizeof ( *dhcpv6 ) );
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 );
memcpy ( &dhcpv6->router, router, sizeof ( dhcpv6->router ) );
xid = random();
memcpy ( dhcpv6->xid, &xid, sizeof ( dhcpv6->xid ) );
dhcpv6->start = currticks();
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_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->scope_id;
addresses.server.sin6.sin6_port = htons ( DHCPV6_SERVER_PORT );
/* Construct client DUID from system UUID */
dhcpv6->client_duid.type = htons ( DHCPV6_DUID_UUID );
if ( ( len = fetch_uuid_setting ( NULL, &uuid_setting,
&dhcpv6->client_duid.uuid ) ) < 0 ) {
rc = len;
DBGC ( dhcpv6, "DHCPv6 %s could not create DUID-UUID: %s\n",
dhcpv6->netdev->name, strerror ( rc ) );
goto err_client_duid;
}
/* Construct IAID from link-layer address */
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 );
err_client_duid:
ref_put ( &dhcpv6->refcnt );
return rc;
}
/** Boot filename setting */
const struct setting filename6_setting __setting ( SETTING_BOOT, filename ) = {
.name = "filename",
.description = "Boot filename",
.tag = DHCPV6_BOOTFILE_URL,
.type = &setting_type_string,
.scope = &dhcpv6_scope,
};
/** DNS search list setting */
const struct setting dnssl6_setting __setting ( SETTING_IP_EXTRA, dnssl ) = {
.name = "dnssl",
.description = "DNS search list",
.tag = DHCPV6_DOMAIN_LIST,
.type = &setting_type_dnssl,
.scope = &dhcpv6_scope,
};