| /* |
| * 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; |
| /** 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_cmp ( setting, &ip6_setting ) == 0 ) ); |
| } |
| |
| /** |
| * Fetch value of DHCPv6 leased address |
| * |
| * @v dhcpset 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_lease ( struct dhcpv6_settings *dhcpv6set, |
| void *data, size_t len ) { |
| struct in6_addr *lease = &dhcpv6set->lease; |
| |
| /* Do nothing unless a leased address exists */ |
| if ( IN6_IS_ADDR_UNSPECIFIED ( lease ) ) |
| return -ENOENT; |
| |
| /* Copy leased address */ |
| if ( len > sizeof ( *lease ) ) |
| len = sizeof ( *lease ); |
| memcpy ( data, lease, len ); |
| |
| return sizeof ( *lease ); |
| } |
| |
| /** |
| * 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; |
| |
| /* Handle leased address */ |
| if ( setting_cmp ( setting, &ip6_setting ) == 0 ) |
| return dhcpv6_fetch_lease ( 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 options DHCPv6 option list |
| * @v parent Parent settings block |
| * @ret rc Return status code |
| */ |
| static int dhcpv6_register ( struct in6_addr *lease, |
| 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 ) ); |
| |
| /* 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; |
| /** 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, &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; |
| 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 ); |
| 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, |
| }; |