diff options
Diffstat (limited to 'roms/ipxe/src/net/udp')
| -rw-r--r-- | roms/ipxe/src/net/udp/dhcp.c | 1446 | ||||
| -rw-r--r-- | roms/ipxe/src/net/udp/dhcpv6.c | 989 | ||||
| -rw-r--r-- | roms/ipxe/src/net/udp/dns.c | 1152 | ||||
| -rw-r--r-- | roms/ipxe/src/net/udp/slam.c | 757 | ||||
| -rw-r--r-- | roms/ipxe/src/net/udp/syslog.c | 298 | ||||
| -rw-r--r-- | roms/ipxe/src/net/udp/tftp.c | 1236 | 
6 files changed, 5878 insertions, 0 deletions
| diff --git a/roms/ipxe/src/net/udp/dhcp.c b/roms/ipxe/src/net/udp/dhcp.c new file mode 100644 index 00000000..04fad04c --- /dev/null +++ b/roms/ipxe/src/net/udp/dhcp.c @@ -0,0 +1,1446 @@ +/* + * Copyright (C) 2006 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 <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <ctype.h> +#include <errno.h> +#include <assert.h> +#include <byteswap.h> +#include <ipxe/if_ether.h> +#include <ipxe/iobuf.h> +#include <ipxe/netdevice.h> +#include <ipxe/device.h> +#include <ipxe/xfer.h> +#include <ipxe/open.h> +#include <ipxe/job.h> +#include <ipxe/retry.h> +#include <ipxe/tcpip.h> +#include <ipxe/ip.h> +#include <ipxe/uuid.h> +#include <ipxe/timer.h> +#include <ipxe/settings.h> +#include <ipxe/dhcp.h> +#include <ipxe/dhcpopts.h> +#include <ipxe/dhcppkt.h> +#include <ipxe/dhcp_arch.h> +#include <ipxe/features.h> + +/** @file + * + * Dynamic Host Configuration Protocol + * + */ + +struct dhcp_session; +static int dhcp_tx ( struct dhcp_session *dhcp ); + +/** + * DHCP operation types + * + * This table maps from DHCP message types (i.e. values of the @c + * DHCP_MESSAGE_TYPE option) to values of the "op" field within a DHCP + * packet. + */ +static const uint8_t dhcp_op[] = { +	[DHCPDISCOVER]	= BOOTP_REQUEST, +	[DHCPOFFER]	= BOOTP_REPLY, +	[DHCPREQUEST]	= BOOTP_REQUEST, +	[DHCPDECLINE]	= BOOTP_REQUEST, +	[DHCPACK]	= BOOTP_REPLY, +	[DHCPNAK]	= BOOTP_REPLY, +	[DHCPRELEASE]	= BOOTP_REQUEST, +	[DHCPINFORM]	= BOOTP_REQUEST, +}; + +/** Raw option data for options common to all DHCP requests */ +static uint8_t dhcp_request_options_data[] = { +	DHCP_MESSAGE_TYPE, DHCP_BYTE ( 0 ), +	DHCP_MAX_MESSAGE_SIZE, +	DHCP_WORD ( ETH_MAX_MTU - 20 /* IP header */ - 8 /* UDP header */ ), +	DHCP_CLIENT_ARCHITECTURE, DHCP_ARCH_CLIENT_ARCHITECTURE, +	DHCP_CLIENT_NDI, DHCP_ARCH_CLIENT_NDI, +	DHCP_VENDOR_CLASS_ID, DHCP_ARCH_VENDOR_CLASS_ID, +	DHCP_USER_CLASS_ID, DHCP_STRING ( 'i', 'P', 'X', 'E' ), +	DHCP_PARAMETER_REQUEST_LIST, +	DHCP_OPTION ( DHCP_SUBNET_MASK, DHCP_ROUTERS, DHCP_DNS_SERVERS, +		      DHCP_LOG_SERVERS, DHCP_HOST_NAME, DHCP_DOMAIN_NAME, +		      DHCP_ROOT_PATH, DHCP_VENDOR_ENCAP, DHCP_VENDOR_CLASS_ID, +		      DHCP_TFTP_SERVER_NAME, DHCP_BOOTFILE_NAME, +		      DHCP_DOMAIN_SEARCH, +		      128, 129, 130, 131, 132, 133, 134, 135, /* for PXE */ +		      DHCP_EB_ENCAP, DHCP_ISCSI_INITIATOR_IQN ), +	DHCP_END +}; + +/** DHCP server address setting */ +const struct setting dhcp_server_setting __setting ( SETTING_MISC, +						     dhcp-server ) = { +	.name = "dhcp-server", +	.description = "DHCP server", +	.tag = DHCP_SERVER_IDENTIFIER, +	.type = &setting_type_ipv4, +}; + +/** + * Most recent DHCP transaction ID + * + * This is exposed for use by the fakedhcp code when reconstructing + * DHCP packets for PXE NBPs. + */ +uint32_t dhcp_last_xid; + +/** + * Name a DHCP packet type + * + * @v msgtype		DHCP message type + * @ret string		DHCP mesasge type name + */ +static inline const char * dhcp_msgtype_name ( unsigned int msgtype ) { +	switch ( msgtype ) { +	case DHCPNONE:		return "BOOTP"; /* Non-DHCP packet */ +	case DHCPDISCOVER:	return "DHCPDISCOVER"; +	case DHCPOFFER:		return "DHCPOFFER"; +	case DHCPREQUEST:	return "DHCPREQUEST"; +	case DHCPDECLINE:	return "DHCPDECLINE"; +	case DHCPACK:		return "DHCPACK"; +	case DHCPNAK:		return "DHCPNAK"; +	case DHCPRELEASE:	return "DHCPRELEASE"; +	case DHCPINFORM:	return "DHCPINFORM"; +	default:		return "DHCP<invalid>"; +	} +} + +/**************************************************************************** + * + * DHCP session + * + */ + +struct dhcp_session; + +/** DHCP session state operations */ +struct dhcp_session_state { +	/** State name */ +	const char *name; +	/** +	 * Construct transmitted packet +	 * +	 * @v dhcp		DHCP session +	 * @v dhcppkt		DHCP packet +	 * @v peer		Destination address +	 */ +	int ( * tx ) ( struct dhcp_session *dhcp, +		       struct dhcp_packet *dhcppkt, +		       struct sockaddr_in *peer ); +	/** Handle received packet +	 * +	 * @v dhcp		DHCP session +	 * @v dhcppkt		DHCP packet +	 * @v peer		DHCP server address +	 * @v msgtype		DHCP message type +	 * @v server_id		DHCP server ID +	 */ +	void ( * rx ) ( struct dhcp_session *dhcp, +			struct dhcp_packet *dhcppkt, +			struct sockaddr_in *peer, +			uint8_t msgtype, struct in_addr server_id ); +	/** Handle timer expiry +	 * +	 * @v dhcp		DHCP session +	 */ +	void ( * expired ) ( struct dhcp_session *dhcp ); +	/** Transmitted message type */ +	uint8_t tx_msgtype; +	/** Apply minimum timeout */ +	uint8_t apply_min_timeout; +}; + +static struct dhcp_session_state dhcp_state_discover; +static struct dhcp_session_state dhcp_state_request; +static struct dhcp_session_state dhcp_state_proxy; +static struct dhcp_session_state dhcp_state_pxebs; + +/** A DHCP session */ +struct dhcp_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; +	/** Local socket address */ +	struct sockaddr_in local; +	/** State of the session */ +	struct dhcp_session_state *state; +	/** Transaction ID (in network-endian order) */ +	uint32_t xid; + +	/** Offered IP address */ +	struct in_addr offer; +	/** DHCP server */ +	struct in_addr server; +	/** DHCP offer priority */ +	int priority; + +	/** ProxyDHCP protocol extensions should be ignored */ +	int no_pxedhcp; +	/** ProxyDHCP server */ +	struct in_addr proxy_server; +	/** ProxyDHCP offer */ +	struct dhcp_packet *proxy_offer; +	/** ProxyDHCP offer priority */ +	int proxy_priority; + +	/** PXE Boot Server type */ +	uint16_t pxe_type; +	/** List of PXE Boot Servers to attempt */ +	struct in_addr *pxe_attempt; +	/** List of PXE Boot Servers to accept */ +	struct in_addr *pxe_accept; + +	/** Retransmission timer */ +	struct retry_timer timer; +	/** Transmission counter */ +	unsigned int count; +	/** Start time of the current state (in ticks) */ +	unsigned long start; +}; + +/** + * Free DHCP session + * + * @v refcnt		Reference counter + */ +static void dhcp_free ( struct refcnt *refcnt ) { +	struct dhcp_session *dhcp = +		container_of ( refcnt, struct dhcp_session, refcnt ); + +	netdev_put ( dhcp->netdev ); +	dhcppkt_put ( dhcp->proxy_offer ); +	free ( dhcp ); +} + +/** + * Mark DHCP session as complete + * + * @v dhcp		DHCP session + * @v rc		Return status code + */ +static void dhcp_finished ( struct dhcp_session *dhcp, int rc ) { + +	/* Stop retry timer */ +	stop_timer ( &dhcp->timer ); + +	/* Shut down interfaces */ +	intf_shutdown ( &dhcp->xfer, rc ); +	intf_shutdown ( &dhcp->job, rc ); +} + +/** + * Transition to new DHCP session state + * + * @v dhcp		DHCP session + * @v state		New session state + */ +static void dhcp_set_state ( struct dhcp_session *dhcp, +			     struct dhcp_session_state *state ) { + +	DBGC ( dhcp, "DHCP %p entering %s state\n", dhcp, state->name ); +	dhcp->state = state; +	dhcp->start = currticks(); +	stop_timer ( &dhcp->timer ); +	dhcp->timer.min_timeout = +		( state->apply_min_timeout ? DHCP_MIN_TIMEOUT : 0 ); +	dhcp->timer.max_timeout = DHCP_MAX_TIMEOUT; +	start_timer_nodelay ( &dhcp->timer ); +} + +/** + * Check if DHCP packet contains PXE options + * + * @v dhcppkt		DHCP packet + * @ret has_pxeopts	DHCP packet contains PXE options + * + * It is assumed that the packet is already known to contain option 60 + * set to "PXEClient". + */ +static int dhcp_has_pxeopts ( struct dhcp_packet *dhcppkt ) { + +	/* Check for a boot filename */ +	if ( dhcppkt_fetch ( dhcppkt, DHCP_BOOTFILE_NAME, NULL, 0 ) > 0 ) +		return 1; + +	/* Check for a PXE boot menu */ +	if ( dhcppkt_fetch ( dhcppkt, DHCP_PXE_BOOT_MENU, NULL, 0 ) > 0 ) +		return 1; + +	return 0; +} + +/**************************************************************************** + * + * DHCP state machine + * + */ + +/** + * Construct transmitted packet for DHCP discovery + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		Destination address + */ +static int dhcp_discovery_tx ( struct dhcp_session *dhcp, +			       struct dhcp_packet *dhcppkt __unused, +			       struct sockaddr_in *peer ) { + +	DBGC ( dhcp, "DHCP %p DHCPDISCOVER\n", dhcp ); + +	/* Set server address */ +	peer->sin_addr.s_addr = INADDR_BROADCAST; +	peer->sin_port = htons ( BOOTPS_PORT ); + +	return 0; +} + +/** + * Handle received packet during DHCP discovery + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		DHCP server address + * @v msgtype		DHCP message type + * @v server_id		DHCP server ID + */ +static void dhcp_discovery_rx ( struct dhcp_session *dhcp, +				struct dhcp_packet *dhcppkt, +				struct sockaddr_in *peer, uint8_t msgtype, +				struct in_addr server_id ) { +	struct in_addr ip; +	char vci[9]; /* "PXEClient" */ +	int vci_len; +	int has_pxeclient; +	int8_t priority = 0; +	uint8_t no_pxedhcp = 0; +	unsigned long elapsed; + +	DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp, +	       dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ), +	       ntohs ( peer->sin_port ) ); +	if ( server_id.s_addr != peer->sin_addr.s_addr ) +		DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) ); + +	/* Identify offered IP address */ +	ip = dhcppkt->dhcphdr->yiaddr; +	if ( ip.s_addr ) +		DBGC ( dhcp, " for %s", inet_ntoa ( ip ) ); + +	/* Identify "PXEClient" vendor class */ +	vci_len = dhcppkt_fetch ( dhcppkt, DHCP_VENDOR_CLASS_ID, +				  vci, sizeof ( vci ) ); +	has_pxeclient = ( ( vci_len >= ( int ) sizeof ( vci ) ) && +			  ( strncmp ( "PXEClient", vci, sizeof (vci) ) == 0 )); +	if ( has_pxeclient ) { +		DBGC ( dhcp, "%s", +		       ( dhcp_has_pxeopts ( dhcppkt ) ? " pxe" : " proxy" ) ); +	} + +	/* Identify priority */ +	dhcppkt_fetch ( dhcppkt, DHCP_EB_PRIORITY, &priority, +			sizeof ( priority ) ); +	if ( priority ) +		DBGC ( dhcp, " pri %d", priority ); + +	/* Identify ignore-PXE flag */ +	dhcppkt_fetch ( dhcppkt, DHCP_EB_NO_PXEDHCP, &no_pxedhcp, +			sizeof ( no_pxedhcp ) ); +	if ( no_pxedhcp ) +		DBGC ( dhcp, " nopxe" ); +	DBGC ( dhcp, "\n" ); + +	/* Select as DHCP offer, if applicable */ +	if ( ip.s_addr && ( peer->sin_port == htons ( BOOTPS_PORT ) ) && +	     ( ( msgtype == DHCPOFFER ) || ( ! msgtype /* BOOTP */ ) ) && +	     ( priority >= dhcp->priority ) ) { +		dhcp->offer = ip; +		dhcp->server = server_id; +		dhcp->priority = priority; +		dhcp->no_pxedhcp = no_pxedhcp; +	} + +	/* Select as ProxyDHCP offer, if applicable */ +	if ( server_id.s_addr && has_pxeclient && +	     ( priority >= dhcp->proxy_priority ) ) { +		dhcppkt_put ( dhcp->proxy_offer ); +		dhcp->proxy_server = server_id; +		dhcp->proxy_offer = dhcppkt_get ( dhcppkt ); +		dhcp->proxy_priority = priority; +	} + +	/* We can exit the discovery state when we have a valid +	 * DHCPOFFER, and either: +	 * +	 *  o  The DHCPOFFER instructs us to ignore ProxyDHCPOFFERs, or +	 *  o  We have a valid ProxyDHCPOFFER, or +	 *  o  We have allowed sufficient time for ProxyDHCPOFFERs. +	 */ + +	/* If we don't yet have a DHCPOFFER, do nothing */ +	if ( ! dhcp->offer.s_addr ) +		return; + +	/* If we can't yet transition to DHCPREQUEST, do nothing */ +	elapsed = ( currticks() - dhcp->start ); +	if ( ! ( dhcp->no_pxedhcp || dhcp->proxy_offer || +		 ( elapsed > PROXYDHCP_MAX_TIMEOUT ) ) ) +		return; + +	/* Transition to DHCPREQUEST */ +	dhcp_set_state ( dhcp, &dhcp_state_request ); +} + +/** + * Handle timer expiry during DHCP discovery + * + * @v dhcp		DHCP session + */ +static void dhcp_discovery_expired ( struct dhcp_session *dhcp ) { +	unsigned long elapsed = ( currticks() - dhcp->start ); + +	/* Give up waiting for ProxyDHCP before we reach the failure point */ +	if ( dhcp->offer.s_addr && ( elapsed > PROXYDHCP_MAX_TIMEOUT ) ) { +		dhcp_set_state ( dhcp, &dhcp_state_request ); +		return; +	} + +	/* Otherwise, retransmit current packet */ +	dhcp_tx ( dhcp ); +} + +/** DHCP discovery state operations */ +static struct dhcp_session_state dhcp_state_discover = { +	.name			= "discovery", +	.tx			= dhcp_discovery_tx, +	.rx			= dhcp_discovery_rx, +	.expired		= dhcp_discovery_expired, +	.tx_msgtype		= DHCPDISCOVER, +	.apply_min_timeout	= 1, +}; + +/** + * Construct transmitted packet for DHCP request + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		Destination address + */ +static int dhcp_request_tx ( struct dhcp_session *dhcp, +			     struct dhcp_packet *dhcppkt, +			     struct sockaddr_in *peer ) { +	int rc; + +	DBGC ( dhcp, "DHCP %p DHCPREQUEST to %s:%d", +	       dhcp, inet_ntoa ( dhcp->server ), BOOTPS_PORT ); +	DBGC ( dhcp, " for %s\n", inet_ntoa ( dhcp->offer ) ); + +	/* Set server ID */ +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_SERVER_IDENTIFIER, +				    &dhcp->server, +				    sizeof ( dhcp->server ) ) ) != 0 ) +		return rc; + +	/* Set requested IP address */ +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_REQUESTED_ADDRESS, +				    &dhcp->offer, +				    sizeof ( dhcp->offer ) ) ) != 0 ) +		return rc; + +	/* Set server address */ +	peer->sin_addr.s_addr = INADDR_BROADCAST; +	peer->sin_port = htons ( BOOTPS_PORT ); + +	return 0; +} + +/** + * Handle received packet during DHCP request + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		DHCP server address + * @v msgtype		DHCP message type + * @v server_id		DHCP server ID + */ +static void dhcp_request_rx ( struct dhcp_session *dhcp, +			      struct dhcp_packet *dhcppkt, +			      struct sockaddr_in *peer, uint8_t msgtype, +			      struct in_addr server_id ) { +	struct in_addr ip; +	struct settings *parent; +	struct settings *settings; +	int rc; + +	DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp, +	       dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ), +	       ntohs ( peer->sin_port ) ); +	if ( server_id.s_addr != peer->sin_addr.s_addr ) +		DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) ); + +	/* Identify leased IP address */ +	ip = dhcppkt->dhcphdr->yiaddr; +	if ( ip.s_addr ) +		DBGC ( dhcp, " for %s", inet_ntoa ( ip ) ); +	DBGC ( dhcp, "\n" ); + +	/* Filter out unacceptable responses */ +	if ( peer->sin_port != htons ( BOOTPS_PORT ) ) +		return; +	if ( msgtype /* BOOTP */ && ( msgtype != DHCPACK ) ) +		return; +	if ( server_id.s_addr != dhcp->server.s_addr ) +		return; +	if ( ip.s_addr != dhcp->offer.s_addr ) +		return; + +	/* Record assigned address */ +	dhcp->local.sin_addr = ip; + +	/* Register settings */ +	parent = netdev_settings ( dhcp->netdev ); +	settings = &dhcppkt->settings; +	if ( ( rc = register_settings ( settings, parent, +					DHCP_SETTINGS_NAME ) ) != 0 ) { +		DBGC ( dhcp, "DHCP %p could not register settings: %s\n", +		       dhcp, strerror ( rc ) ); +		dhcp_finished ( dhcp, rc ); +		return; +	} + +	/* Perform ProxyDHCP if applicable */ +	if ( dhcp->proxy_offer /* Have ProxyDHCP offer */ && +	     ( ! dhcp->no_pxedhcp ) /* ProxyDHCP not disabled */ ) { +		if ( dhcp_has_pxeopts ( dhcp->proxy_offer ) ) { +			/* PXE options already present; register settings +			 * without performing a ProxyDHCPREQUEST +			 */ +			settings = &dhcp->proxy_offer->settings; +			if ( ( rc = register_settings ( settings, NULL, +					   PROXYDHCP_SETTINGS_NAME ) ) != 0 ) { +				DBGC ( dhcp, "DHCP %p could not register " +				       "proxy settings: %s\n", +				       dhcp, strerror ( rc ) ); +				dhcp_finished ( dhcp, rc ); +				return; +			} +		} else { +			/* PXE options not present; use a ProxyDHCPREQUEST */ +			dhcp_set_state ( dhcp, &dhcp_state_proxy ); +			return; +		} +	} + +	/* Terminate DHCP */ +	dhcp_finished ( dhcp, 0 ); +} + +/** + * Handle timer expiry during DHCP discovery + * + * @v dhcp		DHCP session + */ +static void dhcp_request_expired ( struct dhcp_session *dhcp ) { + +	/* Retransmit current packet */ +	dhcp_tx ( dhcp ); +} + +/** DHCP request state operations */ +static struct dhcp_session_state dhcp_state_request = { +	.name			= "request", +	.tx			= dhcp_request_tx, +	.rx			= dhcp_request_rx, +	.expired		= dhcp_request_expired, +	.tx_msgtype		= DHCPREQUEST, +	.apply_min_timeout	= 0, +}; + +/** + * Construct transmitted packet for ProxyDHCP request + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		Destination address + */ +static int dhcp_proxy_tx ( struct dhcp_session *dhcp, +			   struct dhcp_packet *dhcppkt, +			   struct sockaddr_in *peer ) { +	int rc; + +	DBGC ( dhcp, "DHCP %p ProxyDHCP REQUEST to %s\n", dhcp, +	       inet_ntoa ( dhcp->proxy_server ) ); + +	/* Set server ID */ +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_SERVER_IDENTIFIER, +				    &dhcp->proxy_server, +				    sizeof ( dhcp->proxy_server ) ) ) != 0 ) +		return rc; + +	/* Set server address */ +	peer->sin_addr = dhcp->proxy_server; +	peer->sin_port = htons ( PXE_PORT ); + +	return 0; +} + +/** + * Handle received packet during ProxyDHCP request + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		DHCP server address + * @v msgtype		DHCP message type + * @v server_id		DHCP server ID + */ +static void dhcp_proxy_rx ( struct dhcp_session *dhcp, +			    struct dhcp_packet *dhcppkt, +			    struct sockaddr_in *peer, uint8_t msgtype, +			    struct in_addr server_id ) { +	struct settings *settings = &dhcppkt->settings; +	int rc; + +	DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp, +	       dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ), +	       ntohs ( peer->sin_port ) ); +	if ( server_id.s_addr != peer->sin_addr.s_addr ) +		DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) ); +	DBGC ( dhcp, "\n" ); + +	/* Filter out unacceptable responses */ +	if ( peer->sin_port != ntohs ( PXE_PORT ) ) +		return; +	if ( ( msgtype != DHCPOFFER ) && ( msgtype != DHCPACK ) ) +		return; +	if ( server_id.s_addr /* Linux PXE server omits server ID */ && +	     ( server_id.s_addr != dhcp->proxy_server.s_addr ) ) +		return; + +	/* Register settings */ +	if ( ( rc = register_settings ( settings, NULL, +					PROXYDHCP_SETTINGS_NAME ) ) != 0 ) { +		DBGC ( dhcp, "DHCP %p could not register proxy settings: %s\n", +		       dhcp, strerror ( rc ) ); +		dhcp_finished ( dhcp, rc ); +		return; +	} + +	/* Terminate DHCP */ +	dhcp_finished ( dhcp, 0 ); +} + +/** + * Handle timer expiry during ProxyDHCP request + * + * @v dhcp		DHCP session + */ +static void dhcp_proxy_expired ( struct dhcp_session *dhcp ) { +	unsigned long elapsed = ( currticks() - dhcp->start ); + +	/* Give up waiting for ProxyDHCP before we reach the failure point */ +	if ( elapsed > PROXYDHCP_MAX_TIMEOUT ) { +		dhcp_finished ( dhcp, 0 ); +		return; +	} + +	/* Retransmit current packet */ +	dhcp_tx ( dhcp ); +} + +/** ProxyDHCP request state operations */ +static struct dhcp_session_state dhcp_state_proxy = { +	.name			= "ProxyDHCP", +	.tx			= dhcp_proxy_tx, +	.rx			= dhcp_proxy_rx, +	.expired		= dhcp_proxy_expired, +	.tx_msgtype		= DHCPREQUEST, +	.apply_min_timeout	= 0, +}; + +/** + * Construct transmitted packet for PXE Boot Server Discovery + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		Destination address + */ +static int dhcp_pxebs_tx ( struct dhcp_session *dhcp, +			   struct dhcp_packet *dhcppkt, +			   struct sockaddr_in *peer ) { +	struct dhcp_pxe_boot_menu_item menu_item = { 0, 0 }; +	int rc; + +	/* Set server address */ +	peer->sin_addr = *(dhcp->pxe_attempt); +	peer->sin_port = ( ( peer->sin_addr.s_addr == INADDR_BROADCAST ) ? +			   htons ( BOOTPS_PORT ) : htons ( PXE_PORT ) ); + +	DBGC ( dhcp, "DHCP %p PXEBS REQUEST to %s:%d for type %d\n", +	       dhcp, inet_ntoa ( peer->sin_addr ), ntohs ( peer->sin_port ), +	       le16_to_cpu ( dhcp->pxe_type ) ); + +	/* Set boot menu item */ +	menu_item.type = dhcp->pxe_type; +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_PXE_BOOT_MENU_ITEM, +				    &menu_item, sizeof ( menu_item ) ) ) != 0 ) +		return rc; + +	return 0; +} + +/** + * Check to see if PXE Boot Server address is acceptable + * + * @v dhcp		DHCP session + * @v bs		Boot Server address + * @ret accept		Boot Server is acceptable + */ +static int dhcp_pxebs_accept ( struct dhcp_session *dhcp, +			       struct in_addr bs ) { +	struct in_addr *accept; + +	/* Accept if we have no acceptance filter */ +	if ( ! dhcp->pxe_accept ) +		return 1; + +	/* Scan through acceptance list */ +	for ( accept = dhcp->pxe_accept ; accept->s_addr ; accept++ ) { +		if ( accept->s_addr == bs.s_addr ) +			return 1; +	} + +	DBGC ( dhcp, "DHCP %p rejecting server %s\n", +	       dhcp, inet_ntoa ( bs ) ); +	return 0; +} + +/** + * Handle received packet during PXE Boot Server Discovery + * + * @v dhcp		DHCP session + * @v dhcppkt		DHCP packet + * @v peer		DHCP server address + * @v msgtype		DHCP message type + * @v server_id		DHCP server ID + */ +static void dhcp_pxebs_rx ( struct dhcp_session *dhcp, +			    struct dhcp_packet *dhcppkt, +			    struct sockaddr_in *peer, uint8_t msgtype, +			    struct in_addr server_id ) { +	struct dhcp_pxe_boot_menu_item menu_item = { 0, 0 }; +	int rc; + +	DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp, +	       dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ), +	       ntohs ( peer->sin_port ) ); +	if ( server_id.s_addr != peer->sin_addr.s_addr ) +		DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) ); + +	/* Identify boot menu item */ +	dhcppkt_fetch ( dhcppkt, DHCP_PXE_BOOT_MENU_ITEM, +			&menu_item, sizeof ( menu_item ) ); +	if ( menu_item.type ) +		DBGC ( dhcp, " for type %d", ntohs ( menu_item.type ) ); +	DBGC ( dhcp, "\n" ); + +	/* Filter out unacceptable responses */ +	if ( ( peer->sin_port != htons ( BOOTPS_PORT ) ) && +	     ( peer->sin_port != htons ( PXE_PORT ) ) ) +		return; +	if ( msgtype != DHCPACK ) +		return; +	if ( menu_item.type != dhcp->pxe_type ) +		return; +	if ( ! dhcp_pxebs_accept ( dhcp, ( server_id.s_addr ? +					   server_id : peer->sin_addr ) ) ) +		return; + +	/* Register settings */ +	if ( ( rc = register_settings ( &dhcppkt->settings, NULL, +					PXEBS_SETTINGS_NAME ) ) != 0 ) { +		DBGC ( dhcp, "DHCP %p could not register settings: %s\n", +		       dhcp, strerror ( rc ) ); +		dhcp_finished ( dhcp, rc ); +		return; +	} + +	/* Terminate DHCP */ +	dhcp_finished ( dhcp, 0 ); +} + +/** + * Handle timer expiry during PXE Boot Server Discovery + * + * @v dhcp		DHCP session + */ +static void dhcp_pxebs_expired ( struct dhcp_session *dhcp ) { +	unsigned long elapsed = ( currticks() - dhcp->start ); + +	/* Give up waiting before we reach the failure point, and fail +	 * over to the next server in the attempt list +	 */ +	if ( elapsed > PXEBS_MAX_TIMEOUT ) { +		dhcp->pxe_attempt++; +		if ( dhcp->pxe_attempt->s_addr ) { +			dhcp_set_state ( dhcp, &dhcp_state_pxebs ); +			return; +		} else { +			dhcp_finished ( dhcp, -ETIMEDOUT ); +			return; +		} +	} + +	/* Retransmit current packet */ +	dhcp_tx ( dhcp ); +} + +/** PXE Boot Server Discovery state operations */ +static struct dhcp_session_state dhcp_state_pxebs = { +	.name			= "PXEBS", +	.tx			= dhcp_pxebs_tx, +	.rx			= dhcp_pxebs_rx, +	.expired		= dhcp_pxebs_expired, +	.tx_msgtype		= DHCPREQUEST, +	.apply_min_timeout	= 1, +}; + +/**************************************************************************** + * + * Packet construction + * + */ + +/** + * Create a DHCP packet + * + * @v dhcppkt		DHCP packet structure to fill in + * @v netdev		Network device + * @v msgtype		DHCP message type + * @v xid		Transaction ID (in network-endian order) + * @v options		Initial options to include (or NULL) + * @v options_len	Length of initial options + * @v data		Buffer for DHCP packet + * @v max_len		Size of DHCP packet buffer + * @ret rc		Return status code + * + * Creates a DHCP packet in the specified buffer, and initialise a + * DHCP packet structure. + */ +int dhcp_create_packet ( struct dhcp_packet *dhcppkt, +			 struct net_device *netdev, uint8_t msgtype, +			 uint32_t xid, const void *options, size_t options_len, +			 void *data, size_t max_len ) { +	struct dhcphdr *dhcphdr = data; +	int rc; + +	/* Sanity check */ +	if ( max_len < ( sizeof ( *dhcphdr ) + options_len ) ) +		return -ENOSPC; + +	/* Initialise DHCP packet content */ +	memset ( dhcphdr, 0, max_len ); +	dhcphdr->xid = xid; +	dhcphdr->magic = htonl ( DHCP_MAGIC_COOKIE ); +	dhcphdr->htype = ntohs ( netdev->ll_protocol->ll_proto ); +	dhcphdr->op = dhcp_op[msgtype]; +	dhcphdr->hlen = netdev->ll_protocol->ll_addr_len; +	memcpy ( dhcphdr->chaddr, netdev->ll_addr, +		 netdev->ll_protocol->ll_addr_len ); +	memcpy ( dhcphdr->options, options, options_len ); + +	/* If the local link-layer address functions only as a name +	 * (i.e. cannot be used as a destination address), then +	 * request broadcast responses. +	 */ +	if ( netdev->ll_protocol->flags & LL_NAME_ONLY ) +		dhcphdr->flags |= htons ( BOOTP_FL_BROADCAST ); + +	/* If the network device already has an IPv4 address then +	 * unicast responses from the DHCP server may be rejected, so +	 * request broadcast responses. +	 */ +	if ( ipv4_has_any_addr ( netdev ) ) +		dhcphdr->flags |= htons ( BOOTP_FL_BROADCAST ); + +	/* Initialise DHCP packet structure */ +	memset ( dhcppkt, 0, sizeof ( *dhcppkt ) ); +	dhcppkt_init ( dhcppkt, data, max_len ); +	 +	/* Set DHCP_MESSAGE_TYPE option */ +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_MESSAGE_TYPE, +				    &msgtype, sizeof ( msgtype ) ) ) != 0 ) +		return rc; + +	return 0; +} + +/** + * Create DHCP request packet + * + * @v dhcppkt		DHCP packet structure to fill in + * @v netdev		Network device + * @v msgtype		DHCP message type + * @v xid		Transaction ID (in network-endian order) + * @v ciaddr		Client IP address + * @v data		Buffer for DHCP packet + * @v max_len		Size of DHCP packet buffer + * @ret rc		Return status code + * + * Creates a DHCP request packet in the specified buffer, and + * initialise a DHCP packet structure. + */ +int dhcp_create_request ( struct dhcp_packet *dhcppkt, +			  struct net_device *netdev, unsigned int msgtype, +			  uint32_t xid, struct in_addr ciaddr, +			  void *data, size_t max_len ) { +	struct dhcp_netdev_desc dhcp_desc; +	struct dhcp_client_id client_id; +	struct dhcp_client_uuid client_uuid; +	uint8_t *dhcp_features; +	size_t dhcp_features_len; +	size_t ll_addr_len; +	void *user_class; +	ssize_t len; +	int rc; + +	/* Create DHCP packet */ +	if ( ( rc = dhcp_create_packet ( dhcppkt, netdev, msgtype, xid, +					 dhcp_request_options_data, +					 sizeof ( dhcp_request_options_data ), +					 data, max_len ) ) != 0 ) { +		DBG ( "DHCP could not create DHCP packet: %s\n", +		      strerror ( rc ) ); +		goto err_create_packet; +	} + +	/* Set client IP address */ +	dhcppkt->dhcphdr->ciaddr = ciaddr; + +	/* Add options to identify the feature list */ +	dhcp_features = table_start ( DHCP_FEATURES ); +	dhcp_features_len = table_num_entries ( DHCP_FEATURES ); +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_EB_ENCAP, dhcp_features, +				    dhcp_features_len ) ) != 0 ) { +		DBG ( "DHCP could not set features list option: %s\n", +		      strerror ( rc ) ); +		goto err_store_features; +	} + +	/* Add options to identify the network device */ +	fetch_raw_setting ( netdev_settings ( netdev ), &busid_setting, +			    &dhcp_desc, sizeof ( dhcp_desc ) ); +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_EB_BUS_ID, &dhcp_desc, +				    sizeof ( dhcp_desc ) ) ) != 0 ) { +		DBG ( "DHCP could not set bus ID option: %s\n", +		      strerror ( rc ) ); +		goto err_store_busid; +	} + +	/* Add DHCP client identifier.  Required for Infiniband, and +	 * doesn't hurt other link layers. +	 */ +	client_id.ll_proto = ntohs ( netdev->ll_protocol->ll_proto ); +	ll_addr_len = netdev->ll_protocol->ll_addr_len; +	assert ( ll_addr_len <= sizeof ( client_id.ll_addr ) ); +	memcpy ( client_id.ll_addr, netdev->ll_addr, ll_addr_len ); +	if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_CLIENT_ID, &client_id, +				    ( ll_addr_len + 1 ) ) ) != 0 ) { +		DBG ( "DHCP could not set client ID: %s\n", +		      strerror ( rc ) ); +		goto err_store_client_id; +	} + +	/* Add client UUID, if we have one.  Required for PXE.  The +	 * PXE spec does not specify a byte ordering for UUIDs, but +	 * RFC4578 suggests that it follows the EFI spec, in which the +	 * first three fields are little-endian. +	 */ +	client_uuid.type = DHCP_CLIENT_UUID_TYPE; +	if ( ( len = fetch_uuid_setting ( NULL, &uuid_setting, +					  &client_uuid.uuid ) ) >= 0 ) { +		uuid_mangle ( &client_uuid.uuid ); +		if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_CLIENT_UUID, +					    &client_uuid, +					    sizeof ( client_uuid ) ) ) != 0 ) { +			DBG ( "DHCP could not set client UUID: %s\n", +			      strerror ( rc ) ); +			goto err_store_client_uuid; +		} +	} + +	/* Add user class, if we have one. */ +	if ( ( len = fetch_raw_setting_copy ( NULL, &user_class_setting, +					      &user_class ) ) >= 0 ) { +		if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_USER_CLASS_ID, +					    user_class, len ) ) != 0 ) { +			DBG ( "DHCP could not set user class: %s\n", +			      strerror ( rc ) ); +			goto err_store_user_class; +		} +	} + + err_store_user_class: +	free ( user_class ); + err_store_client_uuid: + err_store_client_id: + err_store_busid: + err_store_features: + err_create_packet: +	return rc; +} + +/**************************************************************************** + * + * Data transfer interface + * + */ + +/** + * Transmit DHCP request + * + * @v dhcp		DHCP session + * @ret rc		Return status code + */ +static int dhcp_tx ( struct dhcp_session *dhcp ) { +	static struct sockaddr_in peer = { +		.sin_family = AF_INET, +	}; +	struct xfer_metadata meta = { +		.netdev = dhcp->netdev, +		.src = ( struct sockaddr * ) &dhcp->local, +		.dest = ( struct sockaddr * ) &peer, +	}; +	struct io_buffer *iobuf; +	uint8_t msgtype = dhcp->state->tx_msgtype; +	struct dhcp_packet dhcppkt; +	int rc; + +	/* Start retry timer.  Do this first so that failures to +	 * transmit will be retried. +	 */ +	start_timer ( &dhcp->timer ); + +	/* Allocate buffer for packet */ +	iobuf = xfer_alloc_iob ( &dhcp->xfer, DHCP_MIN_LEN ); +	if ( ! iobuf ) +		return -ENOMEM; + +	/* Create basic DHCP packet in temporary buffer */ +	if ( ( rc = dhcp_create_request ( &dhcppkt, dhcp->netdev, msgtype, +					  dhcp->xid, dhcp->local.sin_addr, +					  iobuf->data, +					  iob_tailroom ( iobuf ) ) ) != 0 ) { +		DBGC ( dhcp, "DHCP %p could not construct DHCP request: %s\n", +		       dhcp, strerror ( rc ) ); +		goto done; +	} + +	/* (Ab)use the "secs" field to convey metadata about the DHCP +	 * session state into packet traces.  Useful for extracting +	 * debug information from non-debug builds. +	 */ +	dhcppkt.dhcphdr->secs = htons ( ( ++(dhcp->count) << 2 ) | +					( dhcp->offer.s_addr ? 0x02 : 0 ) | +					( dhcp->proxy_offer ? 0x01 : 0 ) ); + +	/* Fill in packet based on current state */ +	if ( ( rc = dhcp->state->tx ( dhcp, &dhcppkt, &peer ) ) != 0 ) { +		DBGC ( dhcp, "DHCP %p could not fill DHCP request: %s\n", +		       dhcp, strerror ( rc ) ); +		goto done; +	} + +	/* Transmit the packet */ +	iob_put ( iobuf, dhcppkt_len ( &dhcppkt ) ); +	if ( ( rc = xfer_deliver ( &dhcp->xfer, iob_disown ( iobuf ), +				   &meta ) ) != 0 ) { +		DBGC ( dhcp, "DHCP %p could not transmit UDP packet: %s\n", +		       dhcp, strerror ( rc ) ); +		goto done; +	} + + done: +	free_iob ( iobuf ); +	return rc; +} + +/** + * Receive new data + * + * @v dhcp		DHCP session + * @v iobuf		I/O buffer + * @v meta		Transfer metadata + * @ret rc		Return status code + */ +static int dhcp_deliver ( struct dhcp_session *dhcp, +			  struct io_buffer *iobuf, +			  struct xfer_metadata *meta ) { +	struct net_device *netdev = dhcp->netdev; +	struct ll_protocol *ll_protocol = netdev->ll_protocol; +	struct sockaddr_in *peer; +	size_t data_len; +	struct dhcp_packet *dhcppkt; +	struct dhcphdr *dhcphdr; +	uint8_t msgtype = 0; +	struct in_addr server_id = { 0 }; +	int rc = 0; + +	/* Sanity checks */ +	if ( ! meta->src ) { +		DBGC ( dhcp, "DHCP %p received packet without source port\n", +		       dhcp ); +		rc = -EINVAL; +		goto err_no_src; +	} +	peer = ( struct sockaddr_in * ) meta->src; + +	/* Create a DHCP packet containing the I/O buffer contents. +	 * Whilst we could just use the original buffer in situ, that +	 * would waste the unused space in the packet buffer, and also +	 * waste a relatively scarce fully-aligned I/O buffer. +	 */ +	data_len = iob_len ( iobuf ); +	dhcppkt = zalloc ( sizeof ( *dhcppkt ) + data_len ); +	if ( ! dhcppkt ) { +		rc = -ENOMEM; +		goto err_alloc_dhcppkt; +	} +	dhcphdr = ( ( ( void * ) dhcppkt ) + sizeof ( *dhcppkt ) ); +	memcpy ( dhcphdr, iobuf->data, data_len ); +	dhcppkt_init ( dhcppkt, dhcphdr, data_len ); + +	/* Identify message type */ +	dhcppkt_fetch ( dhcppkt, DHCP_MESSAGE_TYPE, &msgtype, +			sizeof ( msgtype ) ); + +	/* Identify server ID */ +	dhcppkt_fetch ( dhcppkt, DHCP_SERVER_IDENTIFIER, +			&server_id, sizeof ( server_id ) ); + +	/* Check for matching transaction ID */ +	if ( dhcphdr->xid != dhcp->xid ) { +		DBGC ( dhcp, "DHCP %p %s from %s:%d has bad transaction " +		       "ID\n", dhcp, dhcp_msgtype_name ( msgtype ), +		       inet_ntoa ( peer->sin_addr ), +		       ntohs ( peer->sin_port ) ); +		rc = -EINVAL; +		goto err_xid; +	}; + +	/* Check for matching client hardware address */ +	if ( memcmp ( dhcphdr->chaddr, netdev->ll_addr, +		      ll_protocol->ll_addr_len ) != 0 ) { +		DBGC ( dhcp, "DHCP %p %s from %s:%d has bad chaddr %s\n", +		       dhcp, dhcp_msgtype_name ( msgtype ), +		       inet_ntoa ( peer->sin_addr ), ntohs ( peer->sin_port ), +		       ll_protocol->ntoa ( dhcphdr->chaddr ) ); +		rc = -EINVAL; +		goto err_chaddr; +	} + +	/* Handle packet based on current state */ +	dhcp->state->rx ( dhcp, dhcppkt, peer, msgtype, server_id ); + + err_chaddr: + err_xid: +	dhcppkt_put ( dhcppkt ); + err_alloc_dhcppkt: + err_no_src: +	free_iob ( iobuf ); +	return rc; +} + +/** DHCP data transfer interface operations */ +static struct interface_operation dhcp_xfer_operations[] = { +	INTF_OP ( xfer_deliver, struct dhcp_session *, dhcp_deliver ), +}; + +/** DHCP data transfer interface descriptor */ +static struct interface_descriptor dhcp_xfer_desc = +	INTF_DESC ( struct dhcp_session, xfer, dhcp_xfer_operations ); + +/** + * Handle DHCP retry timer expiry + * + * @v timer		DHCP retry timer + * @v fail		Failure indicator + */ +static void dhcp_timer_expired ( struct retry_timer *timer, int fail ) { +	struct dhcp_session *dhcp = +		container_of ( timer, struct dhcp_session, timer ); + +	/* If we have failed, terminate DHCP */ +	if ( fail ) { +		dhcp_finished ( dhcp, -ETIMEDOUT ); +		return; +	} + +	/* Handle timer expiry based on current state */ +	dhcp->state->expired ( dhcp ); +} + +/**************************************************************************** + * + * Job control interface + * + */ + +/** DHCP job control interface operations */ +static struct interface_operation dhcp_job_op[] = { +	INTF_OP ( intf_close, struct dhcp_session *, dhcp_finished ), +}; + +/** DHCP job control interface descriptor */ +static struct interface_descriptor dhcp_job_desc = +	INTF_DESC ( struct dhcp_session, job, dhcp_job_op ); + +/**************************************************************************** + * + * Instantiators + * + */ + +/** + * DHCP peer address for socket opening + * + * This is a dummy address; the only useful portion is the socket + * family (so that we get a UDP connection).  The DHCP client will set + * the IP address and source port explicitly on each transmission. + */ +static struct sockaddr dhcp_peer = { +	.sa_family = AF_INET, +}; + +/** + * Start DHCP state machine on a network device + * + * @v job		Job control interface + * @v netdev		Network device + * @ret rc		Return status code + * + * Starts DHCP on the specified network device.  If successful, the + * DHCPACK (and ProxyDHCPACK, if applicable) will be registered as + * option sources. + */ +int start_dhcp ( struct interface *job, struct net_device *netdev ) { +	struct dhcp_session *dhcp; +	int rc; + +	/* Allocate and initialise structure */ +	dhcp = zalloc ( sizeof ( *dhcp ) ); +	if ( ! dhcp ) +		return -ENOMEM; +	ref_init ( &dhcp->refcnt, dhcp_free ); +	intf_init ( &dhcp->job, &dhcp_job_desc, &dhcp->refcnt ); +	intf_init ( &dhcp->xfer, &dhcp_xfer_desc, &dhcp->refcnt ); +	timer_init ( &dhcp->timer, dhcp_timer_expired, &dhcp->refcnt ); +	dhcp->netdev = netdev_get ( netdev ); +	dhcp->local.sin_family = AF_INET; +	dhcp->local.sin_port = htons ( BOOTPC_PORT ); +	dhcp->xid = random(); + +	/* Store DHCP transaction ID for fakedhcp code */ +	dhcp_last_xid = dhcp->xid; + +	/* Instantiate child objects and attach to our interfaces */ +	if ( ( rc = xfer_open_socket ( &dhcp->xfer, SOCK_DGRAM, &dhcp_peer, +				  ( struct sockaddr * ) &dhcp->local ) ) != 0 ) +		goto err; + +	/* Enter DHCPDISCOVER state */ +	dhcp_set_state ( dhcp, &dhcp_state_discover ); + +	/* Attach parent interface, mortalise self, and return */ +	intf_plug_plug ( &dhcp->job, job ); +	ref_put ( &dhcp->refcnt ); +	return 0; + + err: +	dhcp_finished ( dhcp, rc ); +	ref_put ( &dhcp->refcnt ); +	return rc; +} + +/** + * Retrieve list of PXE boot servers for a given server type + * + * @v dhcp		DHCP session + * @v raw		DHCP PXE boot server list + * @v raw_len		Length of DHCP PXE boot server list + * @v ip		IP address list to fill in + * + * The caller must ensure that the IP address list has sufficient + * space. + */ +static void pxebs_list ( struct dhcp_session *dhcp, void *raw, +			 size_t raw_len, struct in_addr *ip ) { +	struct dhcp_pxe_boot_server *server = raw; +	size_t server_len; +	unsigned int i; + +	while ( raw_len ) { +		if ( raw_len < sizeof ( *server ) ) { +			DBGC ( dhcp, "DHCP %p malformed PXE server list\n", +			       dhcp ); +			break; +		} +		server_len = offsetof ( typeof ( *server ), +					ip[ server->num_ip ] ); +		if ( raw_len < server_len ) { +			DBGC ( dhcp, "DHCP %p malformed PXE server list\n", +			       dhcp ); +			break; +		} +		if ( server->type == dhcp->pxe_type ) { +			for ( i = 0 ; i < server->num_ip ; i++ ) +				*(ip++) = server->ip[i]; +		} +		server = ( ( ( void * ) server ) + server_len ); +		raw_len -= server_len; +	} +} + +/** + * Start PXE Boot Server Discovery on a network device + * + * @v job		Job control interface + * @v netdev		Network device + * @v pxe_type		PXE server type + * @ret rc		Return status code + * + * Starts PXE Boot Server Discovery on the specified network device. + * If successful, the Boot Server ACK will be registered as an option + * source. + */ +int start_pxebs ( struct interface *job, struct net_device *netdev, +		  unsigned int pxe_type ) { +	struct setting pxe_discovery_control_setting = +		{ .tag = DHCP_PXE_DISCOVERY_CONTROL }; +	struct setting pxe_boot_servers_setting = +		{ .tag = DHCP_PXE_BOOT_SERVERS }; +	struct setting pxe_boot_server_mcast_setting = +		{ .tag = DHCP_PXE_BOOT_SERVER_MCAST }; +	ssize_t pxebs_list_len; +	struct dhcp_session *dhcp; +	struct in_addr *ip; +	unsigned int pxe_discovery_control; +	int rc; + +	/* Get upper bound for PXE boot server IP address list */ +	pxebs_list_len = fetch_raw_setting ( NULL, &pxe_boot_servers_setting, +					     NULL, 0 ); +	if ( pxebs_list_len < 0 ) +		pxebs_list_len = 0; + +	/* Allocate and initialise structure */ +	dhcp = zalloc ( sizeof ( *dhcp ) + sizeof ( *ip ) /* mcast */ + +			sizeof ( *ip ) /* bcast */ + pxebs_list_len + +			sizeof ( *ip ) /* terminator */ ); +	if ( ! dhcp ) +		return -ENOMEM; +	ref_init ( &dhcp->refcnt, dhcp_free ); +	intf_init ( &dhcp->job, &dhcp_job_desc, &dhcp->refcnt ); +	intf_init ( &dhcp->xfer, &dhcp_xfer_desc, &dhcp->refcnt ); +	timer_init ( &dhcp->timer, dhcp_timer_expired, &dhcp->refcnt ); +	dhcp->netdev = netdev_get ( netdev ); +	dhcp->local.sin_family = AF_INET; +	fetch_ipv4_setting ( netdev_settings ( netdev ), &ip_setting, +			     &dhcp->local.sin_addr ); +	dhcp->local.sin_port = htons ( BOOTPC_PORT ); +	dhcp->pxe_type = cpu_to_le16 ( pxe_type ); + +	/* Construct PXE boot server IP address lists */ +	pxe_discovery_control = +		fetch_uintz_setting ( NULL, &pxe_discovery_control_setting ); +	ip = ( ( ( void * ) dhcp ) + sizeof ( *dhcp ) ); +	dhcp->pxe_attempt = ip; +	if ( ! ( pxe_discovery_control & PXEBS_NO_MULTICAST ) ) { +		fetch_ipv4_setting ( NULL, &pxe_boot_server_mcast_setting, ip); +		if ( ip->s_addr ) +			ip++; +	} +	if ( ! ( pxe_discovery_control & PXEBS_NO_BROADCAST ) ) +		(ip++)->s_addr = INADDR_BROADCAST; +	if ( pxe_discovery_control & PXEBS_NO_UNKNOWN_SERVERS ) +		dhcp->pxe_accept = ip; +	if ( pxebs_list_len ) { +		uint8_t buf[pxebs_list_len]; + +		fetch_raw_setting ( NULL, &pxe_boot_servers_setting, +				    buf, sizeof ( buf ) ); +		pxebs_list ( dhcp, buf, sizeof ( buf ), ip ); +	} +	if ( ! dhcp->pxe_attempt->s_addr ) { +		DBGC ( dhcp, "DHCP %p has no PXE boot servers for type %04x\n", +		       dhcp, pxe_type ); +		rc = -EINVAL; +		goto err; +	} + +	/* Dump out PXE server lists */ +	DBGC ( dhcp, "DHCP %p attempting", dhcp ); +	for ( ip = dhcp->pxe_attempt ; ip->s_addr ; ip++ ) +		DBGC ( dhcp, " %s", inet_ntoa ( *ip ) ); +	DBGC ( dhcp, "\n" ); +	if ( dhcp->pxe_accept ) { +		DBGC ( dhcp, "DHCP %p accepting", dhcp ); +		for ( ip = dhcp->pxe_accept ; ip->s_addr ; ip++ ) +			DBGC ( dhcp, " %s", inet_ntoa ( *ip ) ); +		DBGC ( dhcp, "\n" ); +	} + +	/* Instantiate child objects and attach to our interfaces */ +	if ( ( rc = xfer_open_socket ( &dhcp->xfer, SOCK_DGRAM, &dhcp_peer, +				  ( struct sockaddr * ) &dhcp->local ) ) != 0 ) +		goto err; + +	/* Enter PXEBS state */ +	dhcp_set_state ( dhcp, &dhcp_state_pxebs ); + +	/* Attach parent interface, mortalise self, and return */ +	intf_plug_plug ( &dhcp->job, job ); +	ref_put ( &dhcp->refcnt ); +	return 0; + + err: +	dhcp_finished ( dhcp, rc ); +	ref_put ( &dhcp->refcnt ); +	return rc; +} + +/** DHCP network device configurator */ +struct net_device_configurator dhcp_configurator __net_device_configurator = { +	.name = "dhcp", +	.start = start_dhcp, +}; diff --git a/roms/ipxe/src/net/udp/dhcpv6.c b/roms/ipxe/src/net/udp/dhcpv6.c new file mode 100644 index 00000000..f7736d08 --- /dev/null +++ b/roms/ipxe/src/net/udp/dhcpv6.c @@ -0,0 +1,989 @@ +/* + * 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/ipv6.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; +	/** 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 == &ipv6_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, &ipv6_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_SERVERS ), htons ( DHCPV6_DOMAIN_LIST ), +	htons ( DHCPV6_BOOTFILE_URL ), htons ( DHCPV6_BOOTFILE_PARAM ), +}; + +/** + * 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, +	/** Apply received IPv6 address */ +	DHCPV6_RX_APPLY_IAADDR = 0x10, +}; + +/** 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 | DHCPV6_RX_APPLY_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_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 ) + +			  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; +	} +	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, +		 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 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, +					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 ); +	} + +	/* Apply identity association address, if applicable */ +	if ( dhcpv6->state->flags & DHCPV6_RX_APPLY_IAADDR ) { +		if ( ( rc = ipv6_set_address ( dhcpv6->netdev, +					       &dhcpv6->lease ) ) != 0 ) { +			DBGC ( dhcpv6, "DHCPv6 %s could not apply %s: %s\n", +			       dhcpv6->netdev->name, +			       inet6_ntoa ( &dhcpv6->lease ), strerror ( rc ) ); +			/* This is plausibly the error we want to return */ +			dhcpv6->rc = rc; +			goto done; +		} +	} + +	/* 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; +	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->index; +	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 = &ipv6_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 = &ipv6_scope, +}; diff --git a/roms/ipxe/src/net/udp/dns.c b/roms/ipxe/src/net/udp/dns.c new file mode 100644 index 00000000..fffe6e69 --- /dev/null +++ b/roms/ipxe/src/net/udp/dns.c @@ -0,0 +1,1152 @@ +/* + * Copyright (C) 2006 Michael Brown <mbrown@fensystems.co.uk>. + * + * Portions copyright (C) 2004 Anselm M. Hoffmeister + * <stockholm@users.sourceforge.net>. + * + * 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 <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <ctype.h> +#include <errno.h> +#include <byteswap.h> +#include <ipxe/refcnt.h> +#include <ipxe/iobuf.h> +#include <ipxe/xfer.h> +#include <ipxe/open.h> +#include <ipxe/resolv.h> +#include <ipxe/retry.h> +#include <ipxe/tcpip.h> +#include <ipxe/settings.h> +#include <ipxe/features.h> +#include <ipxe/dhcp.h> +#include <ipxe/dhcpv6.h> +#include <ipxe/dns.h> + +/** @file + * + * DNS protocol + * + */ + +FEATURE ( FEATURE_PROTOCOL, "DNS", DHCP_EB_FEATURE_DNS, 1 ); + +/* Disambiguate the various error causes */ +#define ENXIO_NO_RECORD __einfo_error ( EINFO_ENXIO_NO_RECORD ) +#define EINFO_ENXIO_NO_RECORD \ +	__einfo_uniqify ( EINFO_ENXIO, 0x01, "DNS name does not exist" ) +#define ENXIO_NO_NAMESERVER __einfo_error ( EINFO_ENXIO_NO_NAMESERVER ) +#define EINFO_ENXIO_NO_NAMESERVER \ +	__einfo_uniqify ( EINFO_ENXIO, 0x02, "No DNS servers available" ) + +/** The DNS server */ +static union { +	struct sockaddr sa; +	struct sockaddr_tcpip st; +	struct sockaddr_in sin; +	struct sockaddr_in6 sin6; +} nameserver = { +	.st = { +		.st_port = htons ( DNS_PORT ), +	}, +}; + +/** The DNS search list */ +static struct dns_name dns_search; + +/** + * Encode a DNS name using RFC1035 encoding + * + * @v string		DNS name as a string + * @v name		DNS name to fill in + * @ret len		Length of DNS name, or negative error + */ +int dns_encode ( const char *string, struct dns_name *name ) { +	uint8_t *start = ( name->data + name->offset ); +	uint8_t *end = ( name->data + name->len ); +	uint8_t *dst = start; +	size_t len = 0; +	char c; + +	/* Encode name */ +	while ( ( c = *(string++) ) ) { + +		/* Handle '.' separators */ +		if ( c == '.' ) { + +			/* Reject consecutive '.' */ +			if ( ( len == 0 ) && ( dst != start ) ) +				return -EINVAL; + +			/* Terminate if this is the trailing '.' */ +			if ( *string == '\0' ) +				break; + +			/* Reject initial non-terminating '.' */ +			if ( len == 0 ) +				return -EINVAL; + +			/* Reset length */ +			len = 0; + +		} else { + +			/* Increment length */ +			len++; + +			/* Check for overflow */ +			if ( len > DNS_MAX_LABEL_LEN ) +				return -EINVAL; +		} + +		/* Copy byte, update length */ +		if ( ++dst < end ) { +			*dst = c; +			dst[-len] = len; +		} +	} + +	/* Add terminating root marker */ +	if ( len ) +		dst++; +	if ( dst < end ) +		*dst = '\0'; +	dst++; + +	return ( dst - start ); +} + +/** + * Find start of valid label within an RFC1035-encoded DNS name + * + * @v name		DNS name + * @v offset		Current offset + * @ret offset		Offset of label, or negative error + */ +static int dns_label ( struct dns_name *name, size_t offset ) { +	const uint8_t *byte; +	const uint16_t *word; +	size_t len; +	size_t ptr; + +	while ( 1 ) { + +		/* Fail if we have overrun the DNS name */ +		if ( ( offset + sizeof ( *byte) ) > name->len ) +			return -EINVAL; +		byte = ( name->data + offset ); + +		/* Follow compression pointer, if applicable */ +		if ( DNS_IS_COMPRESSED ( *byte ) ) { + +			/* Fail if we have overrun the DNS name */ +			if ( ( offset + sizeof ( *word ) ) > name->len ) +				return -EINVAL; +			word = ( name->data + offset ); + +			/* Extract pointer to new offset */ +			ptr = DNS_COMPRESSED_OFFSET ( ntohs ( *word ) ); + +			/* Fail if pointer does not point backwards. +			 * (This guarantees termination of the +			 * function.) +			 */ +			if ( ptr >= offset ) +				return -EINVAL; + +			/* Continue from new offset */ +			offset = ptr; +			continue; +		} + +		/* Fail if we have overrun the DNS name */ +		len = *byte; +		if ( ( offset + sizeof ( *byte ) + len ) > name->len ) +			return -EINVAL; + +		/* We have a valid label */ +		return offset; +	} +} + +/** + * Decode RFC1035-encoded DNS name + * + * @v name		DNS name + * @v data		Output buffer + * @v len		Length of output buffer + * @ret len		Length of decoded DNS name, or negative error + */ +int dns_decode ( struct dns_name *name, char *data, size_t len ) { +	unsigned int recursion_limit = name->len; /* Generous upper bound */ +	int offset = name->offset; +	const uint8_t *label; +	size_t decoded_len = 0; +	size_t label_len; +	size_t copy_len; + +	while ( recursion_limit-- ) { + +		/* Find valid DNS label */ +		offset = dns_label ( name, offset ); +		if ( offset < 0 ) +			return offset; + +		/* Terminate if we have reached the root */ +		label = ( name->data + offset ); +		label_len = *(label++); +		if ( label_len == 0 ) { +			if ( decoded_len < len ) +				*data = '\0'; +			return decoded_len; +		} + +		/* Prepend '.' if applicable */ +		if ( decoded_len && ( decoded_len++ < len ) ) +			*(data++) = '.'; + +		/* Copy label to output buffer */ +		copy_len = ( ( decoded_len < len ) ? ( len - decoded_len ) : 0); +		if ( copy_len > label_len ) +			copy_len = label_len; +		memcpy ( data, label, copy_len ); +		data += copy_len; +		decoded_len += label_len; + +		/* Move to next label */ +		offset += ( sizeof ( *label ) + label_len ); +	} + +	/* Recursion limit exceeded */ +	return -EINVAL; +} + +/** + * Compare DNS names for equality + * + * @v first		First DNS name + * @v second		Second DNS name + * @ret rc		Return status code + */ +int dns_compare ( struct dns_name *first, struct dns_name *second ) { +	unsigned int recursion_limit = first->len; /* Generous upper bound */ +	int first_offset = first->offset; +	int second_offset = second->offset; +	const uint8_t *first_label; +	const uint8_t *second_label; +	size_t label_len; +	size_t len; + +	while ( recursion_limit-- ) { + +		/* Find valid DNS labels */ +		first_offset = dns_label ( first, first_offset ); +		if ( first_offset < 0 ) +			return first_offset; +		second_offset = dns_label ( second, second_offset ); +		if ( second_offset < 0 ) +			return second_offset; + +		/* Compare label lengths */ +		first_label = ( first->data + first_offset ); +		second_label = ( second->data + second_offset ); +		label_len = *(first_label++); +		if ( label_len != *(second_label++) ) +			return -ENOENT; +		len = ( sizeof ( *first_label ) + label_len ); + +		/* Terminate if we have reached the root */ +		if ( label_len == 0 ) +			return 0; + +		/* Compare label contents (case-insensitively) */ +		while ( label_len-- ) { +			if ( tolower ( *(first_label++) ) != +			     tolower ( *(second_label++) ) ) +				return -ENOENT; +		} + +		/* Move to next labels */ +		first_offset += len; +		second_offset += len; +	} + +	/* Recursion limit exceeded */ +	return -EINVAL; +} + +/** + * Copy a DNS name + * + * @v src		Source DNS name + * @v dst		Destination DNS name + * @ret len		Length of copied DNS name, or negative error + */ +int dns_copy ( struct dns_name *src, struct dns_name *dst ) { +	unsigned int recursion_limit = src->len; /* Generous upper bound */ +	int src_offset = src->offset; +	size_t dst_offset = dst->offset; +	const uint8_t *label; +	size_t label_len; +	size_t copy_len; +	size_t len; + +	while ( recursion_limit-- ) { + +		/* Find valid DNS label */ +		src_offset = dns_label ( src, src_offset ); +		if ( src_offset < 0 ) +			return src_offset; + +		/* Copy as an uncompressed label */ +		label = ( src->data + src_offset ); +		label_len = *label; +		len = ( sizeof ( *label ) + label_len ); +		copy_len = ( ( dst_offset < dst->len ) ? +			     ( dst->len - dst_offset ) : 0 ); +		if ( copy_len > len ) +			copy_len = len; +		memcpy ( ( dst->data + dst_offset ), label, copy_len ); +		src_offset += len; +		dst_offset += len; + +		/* Terminate if we have reached the root */ +		if ( label_len == 0 ) +			return ( dst_offset - dst->offset ); +	} + +	/* Recursion limit exceeded */ +	return -EINVAL; +} + +/** + * Skip RFC1035-encoded DNS name + * + * @v name		DNS name + * @ret offset		Offset to next name, or negative error + */ +int dns_skip ( struct dns_name *name ) { +	unsigned int recursion_limit = name->len; /* Generous upper bound */ +	int offset = name->offset; +	int prev_offset; +	const uint8_t *label; +	size_t label_len; + +	while ( recursion_limit-- ) { + +		/* Find valid DNS label */ +		prev_offset = offset; +		offset = dns_label ( name, prev_offset ); +		if ( offset < 0 ) +			return offset; + +		/* Terminate if we have reached a compression pointer */ +		if ( offset != prev_offset ) +			return ( prev_offset + sizeof ( uint16_t ) ); + +		/* Skip this label */ +		label = ( name->data + offset ); +		label_len = *label; +		offset += ( sizeof ( *label ) + label_len ); + +		/* Terminate if we have reached the root */ +		if ( label_len == 0 ) +			return offset; +	} + +	/* Recursion limit exceeded */ +	return -EINVAL; +} + +/** + * Skip RFC1035-encoded DNS name in search list + * + * @v name		DNS name + * @ret offset		Offset to next non-empty name, or negative error + */ +static int dns_skip_search ( struct dns_name *name ) { +	int offset; + +	/* Find next name */ +	offset = dns_skip ( name ); +	if ( offset < 0 ) +		return offset; + +	/* Skip over any subsequent empty names (e.g. due to padding +	 * bytes used in the NDP DNSSL option). +	 */ +	while ( ( offset < ( ( int ) name->len ) ) && +		( *( ( uint8_t * ) ( name->data + offset ) ) == 0 ) ) { +		offset++; +	} + +	return offset; +} + +/** + * Transcribe DNS name (for debugging) + * + * @v name		DNS name + * @ret string		Transcribed DNS name + */ +static const char * dns_name ( struct dns_name *name ) { +	static char buf[256]; +	int len; + +	len = dns_decode ( name, buf, sizeof ( buf ) ); +	return ( ( len < 0 ) ? "<INVALID>" : buf ); +} + +/** + * Name a DNS query type (for debugging) + * + * @v type		Query type (in network byte order) + * @ret name		Type name + */ +static const char * dns_type ( uint16_t type ) { +	switch ( type ) { +	case htons ( DNS_TYPE_A ):	return "A"; +	case htons ( DNS_TYPE_AAAA ):	return "AAAA"; +	case htons ( DNS_TYPE_CNAME ):	return "CNAME"; +	default:			return "<UNKNOWN>"; +	} +} + +/** A DNS request */ +struct dns_request { +	/** Reference counter */ +	struct refcnt refcnt; +	/** Name resolution interface */ +	struct interface resolv; +	/** Data transfer interface */ +	struct interface socket; +	/** Retry timer */ +	struct retry_timer timer; + +	/** Socket address to fill in with resolved address */ +	union { +		struct sockaddr sa; +		struct sockaddr_in sin; +		struct sockaddr_in6 sin6; +	} address; +	/** Initial query type */ +	uint16_t qtype; +	/** Buffer for current query */ +	struct { +		/** Query header */ +		struct dns_header query; +		/** Name buffer */ +		char name[DNS_MAX_NAME_LEN]; +		/** Space for question */ +		struct dns_question padding; +	} __attribute__ (( packed )) buf; +	/** Current query name */ +	struct dns_name name; +	/** Question within current query */ +	struct dns_question *question; +	/** Length of current query */ +	size_t len; +	/** Offset of search suffix within current query */ +	size_t offset; +	/** Search list */ +	struct dns_name search; +	/** Recursion counter */ +	unsigned int recursion; +}; + +/** + * Mark DNS request as complete + * + * @v dns		DNS request + * @v rc		Return status code + */ +static void dns_done ( struct dns_request *dns, int rc ) { + +	/* Stop the retry timer */ +	stop_timer ( &dns->timer ); + +	/* Shut down interfaces */ +	intf_shutdown ( &dns->socket, rc ); +	intf_shutdown ( &dns->resolv, rc ); +} + +/** + * Mark DNS request as resolved and complete + * + * @v dns		DNS request + * @v rc		Return status code + */ +static void dns_resolved ( struct dns_request *dns ) { + +	DBGC ( dns, "DNS %p found address %s\n", +	       dns, sock_ntoa ( &dns->address.sa ) ); + +	/* Return resolved address */ +	resolv_done ( &dns->resolv, &dns->address.sa ); + +	/* Mark operation as complete */ +	dns_done ( dns, 0 ); +} + +/** + * Construct DNS question + * + * @v dns		DNS request + * @ret rc		Return status code + */ +static int dns_question ( struct dns_request *dns ) { +	static struct dns_name search_root = { +		.data = "", +		.len = 1, +	}; +	struct dns_name *search = &dns->search; +	int len; +	size_t offset; + +	/* Use root suffix if search list is empty */ +	if ( search->offset == search->len ) +		search = &search_root; + +	/* Overwrite current suffix */ +	dns->name.offset = dns->offset; +	len = dns_copy ( search, &dns->name ); +	if ( len < 0 ) +		return len; + +	/* Sanity check */ +	offset = ( dns->name.offset + len ); +	if ( offset > dns->name.len ) { +		DBGC ( dns, "DNS %p name is too long\n", dns ); +		return -EINVAL; +	} + +	/* Construct question */ +	dns->question = ( ( ( void * ) &dns->buf ) + offset ); +	dns->question->qtype = dns->qtype; +	dns->question->qclass = htons ( DNS_CLASS_IN ); + +	/* Store length */ +	dns->len = ( offset + sizeof ( *(dns->question) ) ); + +	/* Restore name */ +	dns->name.offset = offsetof ( typeof ( dns->buf ), name ); + +	DBGC2 ( dns, "DNS %p question is %s type %s\n", dns, +		dns_name ( &dns->name ), dns_type ( dns->question->qtype ) ); + +	return 0; +} + +/** + * Send DNS query + * + * @v dns		DNS request + * @ret rc		Return status code + */ +static int dns_send_packet ( struct dns_request *dns ) { +	struct dns_header *query = &dns->buf.query; + +	/* Start retransmission timer */ +	start_timer ( &dns->timer ); + +	/* Generate query identifier */ +	query->id = random(); + +	/* Send query */ +	DBGC ( dns, "DNS %p sending query ID %#04x for %s type %s\n", dns, +	       ntohs ( query->id ), dns_name ( &dns->name ), +	       dns_type ( dns->question->qtype ) ); + +	/* Send the data */ +	return xfer_deliver_raw ( &dns->socket, query, dns->len ); +} + +/** + * Handle DNS retransmission timer expiry + * + * @v timer		Retry timer + * @v fail		Failure indicator + */ +static void dns_timer_expired ( struct retry_timer *timer, int fail ) { +	struct dns_request *dns = +		container_of ( timer, struct dns_request, timer ); + +	if ( fail ) { +		dns_done ( dns, -ETIMEDOUT ); +	} else { +		dns_send_packet ( dns ); +	} +} + +/** + * Receive new data + * + * @v dns		DNS request + * @v iobuf		I/O buffer + * @v meta		Data transfer metadata + * @ret rc		Return status code + */ +static int dns_xfer_deliver ( struct dns_request *dns, +			      struct io_buffer *iobuf, +			      struct xfer_metadata *meta __unused ) { +	struct dns_header *response = iobuf->data; +	struct dns_header *query = &dns->buf.query; +	unsigned int qtype = dns->question->qtype; +	struct dns_name buf; +	union dns_rr *rr; +	int offset; +	size_t answer_offset; +	size_t next_offset; +	size_t rdlength; +	size_t name_len; +	int rc; + +	/* Sanity check */ +	if ( iob_len ( iobuf ) < sizeof ( *response ) ) { +		DBGC ( dns, "DNS %p received underlength packet length %zd\n", +		       dns, iob_len ( iobuf ) ); +		rc = -EINVAL; +		goto done; +	} + +	/* Check response ID matches query ID */ +	if ( response->id != query->id ) { +		DBGC ( dns, "DNS %p received unexpected response ID %#04x " +		       "(wanted %d)\n", dns, ntohs ( response->id ), +		       ntohs ( query->id ) ); +		rc = -EINVAL; +		goto done; +	} +	DBGC ( dns, "DNS %p received response ID %#04x\n", +	       dns, ntohs ( response->id ) ); + +	/* Check that we have exactly one question */ +	if ( response->qdcount != htons ( 1 ) ) { +		DBGC ( dns, "DNS %p received response with %d questions\n", +		       dns, ntohs ( response->qdcount ) ); +		rc = -EINVAL; +		goto done; +	} + +	/* Skip question section */ +	buf.data = iobuf->data; +	buf.offset = sizeof ( *response ); +	buf.len = iob_len ( iobuf ); +	offset = dns_skip ( &buf ); +	if ( offset < 0 ) { +		rc = offset; +		DBGC ( dns, "DNS %p received response with malformed " +		       "question: %s\n", dns, strerror ( rc ) ); +		goto done; +	} +	answer_offset = ( offset + sizeof ( struct dns_question ) ); + +	/* Search through response for useful answers.  Do this +	 * multiple times, to take advantage of useful nameservers +	 * which send us e.g. the CNAME *and* the A record for the +	 * pointed-to name. +	 */ +	for ( buf.offset = answer_offset ; buf.offset != buf.len ; +	      buf.offset = next_offset ) { + +		/* Check for valid name */ +		offset = dns_skip ( &buf ); +		if ( offset < 0 ) { +			rc = offset; +			DBGC ( dns, "DNS %p received response with malformed " +			       "answer: %s\n", dns, strerror ( rc ) ); +			goto done; +		} + +		/* Check for sufficient space for resource record */ +		rr = ( buf.data + offset ); +		if ( ( offset + sizeof ( rr->common ) ) > buf.len ) { +			DBGC ( dns, "DNS %p received response with underlength " +			       "RR\n", dns ); +			rc = -EINVAL; +			goto done; +		} +		rdlength = ntohs ( rr->common.rdlength ); +		next_offset = ( offset + sizeof ( rr->common ) + rdlength ); +		if ( next_offset > buf.len ) { +			DBGC ( dns, "DNS %p received response with underlength " +			       "RR\n", dns ); +			rc = -EINVAL; +			goto done; +		} + +		/* Skip non-matching names */ +		if ( dns_compare ( &buf, &dns->name ) != 0 ) { +			DBGC2 ( dns, "DNS %p ignoring response for %s type " +				"%s\n", dns, dns_name ( &buf ), +				dns_type ( rr->common.type ) ); +			continue; +		} + +		/* Handle answer */ +		switch ( rr->common.type ) { + +		case htons ( DNS_TYPE_AAAA ): + +			/* Found the target AAAA record */ +			if ( rdlength < sizeof ( dns->address.sin6.sin6_addr )){ +				DBGC ( dns, "DNS %p received response with " +				       "underlength AAAA\n", dns ); +				rc = -EINVAL; +				goto done; +			} +			dns->address.sin6.sin6_family = AF_INET6; +			memcpy ( &dns->address.sin6.sin6_addr, +				 &rr->aaaa.in6_addr, +				 sizeof ( dns->address.sin6.sin6_addr ) ); +			dns_resolved ( dns ); +			rc = 0; +			goto done; + +		case htons ( DNS_TYPE_A ): + +			/* Found the target A record */ +			if ( rdlength < sizeof ( dns->address.sin.sin_addr ) ) { +				DBGC ( dns, "DNS %p received response with " +				       "underlength A\n", dns ); +				rc = -EINVAL; +				goto done; +			} +			dns->address.sin.sin_family = AF_INET; +			dns->address.sin.sin_addr = rr->a.in_addr; +			dns_resolved ( dns ); +			rc = 0; +			goto done; + +		case htons ( DNS_TYPE_CNAME ): + +			/* Terminate the operation if we recurse too far */ +			if ( ++dns->recursion > DNS_MAX_CNAME_RECURSION ) { +				DBGC ( dns, "DNS %p recursion exceeded\n", +				       dns ); +				rc = -ELOOP; +				dns_done ( dns, rc ); +				goto done; +			} + +			/* Found a CNAME record; update query and recurse */ +			buf.offset = ( offset + sizeof ( rr->cname ) ); +			DBGC ( dns, "DNS %p found CNAME %s\n", +			       dns, dns_name ( &buf ) ); +			dns->search.offset = dns->search.len; +			name_len = dns_copy ( &buf, &dns->name ); +			dns->offset = ( offsetof ( typeof ( dns->buf ), name ) + +					name_len - 1 /* Strip root label */ ); +			if ( ( rc = dns_question ( dns ) ) != 0 ) { +				dns_done ( dns, rc ); +				goto done; +			} +			next_offset = answer_offset; +			break; + +		default: +			DBGC ( dns, "DNS %p got unknown record type %d\n", +			       dns, ntohs ( rr->common.type ) ); +			break; +		} +	} + +	/* Stop the retry timer.  After this point, each code path +	 * must either restart the timer by calling dns_send_packet(), +	 * or mark the DNS operation as complete by calling +	 * dns_done() +	 */ +	stop_timer ( &dns->timer ); + +	/* Determine what to do next based on the type of query we +	 * issued and the response we received +	 */ +	switch ( qtype ) { + +	case htons ( DNS_TYPE_AAAA ): +		/* We asked for an AAAA record and got nothing; try +		 * the A. +		 */ +		DBGC ( dns, "DNS %p found no AAAA record; trying A\n", dns ); +		dns->question->qtype = htons ( DNS_TYPE_A ); +		dns_send_packet ( dns ); +		rc = 0; +		goto done; + +	case htons ( DNS_TYPE_A ): +		/* We asked for an A record and got nothing; +		 * try the CNAME. +		 */ +		DBGC ( dns, "DNS %p found no A record; trying CNAME\n", dns ); +		dns->question->qtype = htons ( DNS_TYPE_CNAME ); +		dns_send_packet ( dns ); +		rc = 0; +		goto done; + +	case htons ( DNS_TYPE_CNAME ): +		/* We asked for a CNAME record.  If we got a response +		 * (i.e. if the next AAAA/A query is already set up), +		 * then issue it. +		 */ +		if ( qtype == dns->qtype ) { +			dns_send_packet ( dns ); +			rc = 0; +			goto done; +		} + +		/* If we have already reached the end of the search list, +		 * then terminate lookup. +		 */ +		if ( dns->search.offset == dns->search.len ) { +			DBGC ( dns, "DNS %p found no CNAME record\n", dns ); +			rc = -ENXIO_NO_RECORD; +			dns_done ( dns, rc ); +			goto done; +		} + +		/* Move to next entry in search list.  This can never fail, +		 * since we have already used this entry. +		 */ +		DBGC ( dns, "DNS %p found no CNAME record; trying next " +		       "suffix\n", dns ); +		dns->search.offset = dns_skip_search ( &dns->search ); +		if ( ( rc = dns_question ( dns ) ) != 0 ) { +			dns_done ( dns, rc ); +			goto done; +		} +		dns_send_packet ( dns ); +		goto done; + +	default: +		assert ( 0 ); +		rc = -EINVAL; +		dns_done ( dns, rc ); +		goto done; +	} + + done: +	/* Free I/O buffer */ +	free_iob ( iobuf ); +	return rc; +} + +/** + * Receive new data + * + * @v dns		DNS request + * @v rc		Reason for close + */ +static void dns_xfer_close ( struct dns_request *dns, int rc ) { + +	if ( ! rc ) +		rc = -ECONNABORTED; + +	dns_done ( dns, rc ); +} + +/** DNS socket interface operations */ +static struct interface_operation dns_socket_operations[] = { +	INTF_OP ( xfer_deliver, struct dns_request *, dns_xfer_deliver ), +	INTF_OP ( intf_close, struct dns_request *, dns_xfer_close ), +}; + +/** DNS socket interface descriptor */ +static struct interface_descriptor dns_socket_desc = +	INTF_DESC ( struct dns_request, socket, dns_socket_operations ); + +/** DNS resolver interface operations */ +static struct interface_operation dns_resolv_op[] = { +	INTF_OP ( intf_close, struct dns_request *, dns_done ), +}; + +/** DNS resolver interface descriptor */ +static struct interface_descriptor dns_resolv_desc = +	INTF_DESC ( struct dns_request, resolv, dns_resolv_op ); + +/** + * Resolve name using DNS + * + * @v resolv		Name resolution interface + * @v name		Name to resolve + * @v sa		Socket address to fill in + * @ret rc		Return status code + */ +static int dns_resolv ( struct interface *resolv, +			const char *name, struct sockaddr *sa ) { +	struct dns_request *dns; +	struct dns_header *query; +	size_t search_len; +	int name_len; +	int rc; + +	/* Fail immediately if no DNS servers */ +	if ( ! nameserver.sa.sa_family ) { +		DBG ( "DNS not attempting to resolve \"%s\": " +		      "no DNS servers\n", name ); +		rc = -ENXIO_NO_NAMESERVER; +		goto err_no_nameserver; +	} + +	/* Determine whether or not to use search list */ +	search_len = ( strchr ( name, '.' ) ? 0 : dns_search.len ); + +	/* Allocate DNS structure */ +	dns = zalloc ( sizeof ( *dns ) + search_len ); +	if ( ! dns ) { +		rc = -ENOMEM; +		goto err_alloc_dns; +	} +	ref_init ( &dns->refcnt, NULL ); +	intf_init ( &dns->resolv, &dns_resolv_desc, &dns->refcnt ); +	intf_init ( &dns->socket, &dns_socket_desc, &dns->refcnt ); +	timer_init ( &dns->timer, dns_timer_expired, &dns->refcnt ); +	memcpy ( &dns->address.sa, sa, sizeof ( dns->address.sa ) ); +	dns->search.data = ( ( ( void * ) dns ) + sizeof ( *dns ) ); +	dns->search.len = search_len; +	memcpy ( dns->search.data, dns_search.data, search_len ); + +	/* Determine initial query type */ +	switch ( nameserver.sa.sa_family ) { +	case AF_INET: +		dns->qtype = htons ( DNS_TYPE_A ); +		break; +	case AF_INET6: +		dns->qtype = htons ( DNS_TYPE_AAAA ); +		break; +	default: +		rc = -ENOTSUP; +		goto err_type; +	} + +	/* Construct query */ +	query = &dns->buf.query; +	query->flags = htons ( DNS_FLAG_RD ); +	query->qdcount = htons ( 1 ); +	dns->name.data = &dns->buf; +	dns->name.offset = offsetof ( typeof ( dns->buf ), name ); +	dns->name.len = offsetof ( typeof ( dns->buf ), padding ); +	name_len = dns_encode ( name, &dns->name ); +	if ( name_len < 0 ) { +		rc = name_len; +		goto err_encode; +	} +	dns->offset = ( offsetof ( typeof ( dns->buf ), name ) + +			name_len - 1 /* Strip root label */ ); +	if ( ( rc = dns_question ( dns ) ) != 0 ) +		goto err_question; + +	/* Open UDP connection */ +	if ( ( rc = xfer_open_socket ( &dns->socket, SOCK_DGRAM, +				       &nameserver.sa, NULL ) ) != 0 ) { +		DBGC ( dns, "DNS %p could not open socket: %s\n", +		       dns, strerror ( rc ) ); +		goto err_open_socket; +	} + +	/* Start timer to trigger first packet */ +	start_timer_nodelay ( &dns->timer ); + +	/* Attach parent interface, mortalise self, and return */ +	intf_plug_plug ( &dns->resolv, resolv ); +	ref_put ( &dns->refcnt ); +	return 0;	 + + err_open_socket: + err_question: + err_encode: + err_type: +	ref_put ( &dns->refcnt ); + err_alloc_dns: + err_no_nameserver: +	return rc; +} + +/** DNS name resolver */ +struct resolver dns_resolver __resolver ( RESOLV_NORMAL ) = { +	.name = "DNS", +	.resolv = dns_resolv, +}; + +/****************************************************************************** + * + * Settings + * + ****************************************************************************** + */ + +/** + * Format DNS search list setting + * + * @v type		Setting type + * @v raw		Raw setting value + * @v raw_len		Length of raw setting value + * @v buf		Buffer to contain formatted value + * @v len		Length of buffer + * @ret len		Length of formatted value, or negative error + */ +static int format_dnssl_setting ( const struct setting_type *type __unused, +				  const void *raw, size_t raw_len, +				  char *buf, size_t len ) { +	struct dns_name name = { +		.data = ( ( void * ) raw ), +		.len = raw_len, +	}; +	size_t remaining = len; +	size_t total = 0; +	int name_len; + +	while ( name.offset < raw_len ) { + +		/* Decode name */ +		remaining = ( ( total < len ) ? ( len - total ) : 0 ); +		name_len = dns_decode ( &name, ( buf + total ), remaining ); +		if ( name_len < 0 ) +			return name_len; +		total += name_len; + +		/* Move to next name */ +		name.offset = dns_skip_search ( &name ); + +		/* Add separator if applicable */ +		if ( name.offset != raw_len ) { +			if ( total < len ) +				buf[total] = ' '; +			total++; +		} +	} + +	return total; +} + +/** A DNS search list setting type */ +const struct setting_type setting_type_dnssl __setting_type = { +	.name = "dnssl", +	.format = format_dnssl_setting, +}; + +/** IPv4 DNS server setting */ +const struct setting dns_setting __setting ( SETTING_IP_EXTRA, dns ) = { +	.name = "dns", +	.description = "DNS server", +	.tag = DHCP_DNS_SERVERS, +	.type = &setting_type_ipv4, +}; + +/** IPv6 DNS server setting */ +const struct setting dns6_setting __setting ( SETTING_IP_EXTRA, dns6 ) = { +	.name = "dns6", +	.description = "DNS server", +	.tag = DHCPV6_DNS_SERVERS, +	.type = &setting_type_ipv6, +	.scope = &ipv6_scope, +}; + +/** DNS search list */ +const struct setting dnssl_setting __setting ( SETTING_IP_EXTRA, dnssl ) = { +	.name = "dnssl", +	.description = "DNS search list", +	.tag = DHCP_DOMAIN_SEARCH, +	.type = &setting_type_dnssl, +}; + +/** + * Apply DNS search list + * + */ +static void apply_dns_search ( void ) { +	char *localdomain; +	int len; + +	/* Free existing search list */ +	free ( dns_search.data ); +	memset ( &dns_search, 0, sizeof ( dns_search ) ); + +	/* Fetch DNS search list */ +	len = fetch_setting_copy ( NULL, &dnssl_setting, NULL, NULL, +				   &dns_search.data ); +	if ( len >= 0 ) { +		dns_search.len = len; +		return; +	} + +	/* If no DNS search list exists, try to fetch the local domain */ +	fetch_string_setting_copy ( NULL, &domain_setting, &localdomain ); +	if ( localdomain ) { +		len = dns_encode ( localdomain, &dns_search ); +		if ( len >= 0 ) { +			dns_search.data = malloc ( len ); +			if ( dns_search.data ) { +				dns_search.len = len; +				dns_encode ( localdomain, &dns_search ); +			} +		} +		free ( localdomain ); +		return; +	} +} + +/** + * Apply DNS settings + * + * @ret rc		Return status code + */ +static int apply_dns_settings ( void ) { + +	/* Fetch DNS server address */ +	nameserver.sa.sa_family = 0; +	if ( fetch_ipv6_setting ( NULL, &dns6_setting, +				  &nameserver.sin6.sin6_addr ) >= 0 ) { +		nameserver.sin6.sin6_family = AF_INET6; +	} else if ( fetch_ipv4_setting ( NULL, &dns_setting, +					 &nameserver.sin.sin_addr ) >= 0 ) { +		nameserver.sin.sin_family = AF_INET; +	} +	if ( nameserver.sa.sa_family ) { +		DBG ( "DNS using nameserver %s\n", +		      sock_ntoa ( &nameserver.sa ) ); +	} + +	/* Fetch DNS search list */ +	apply_dns_search(); +	if ( DBG_LOG && ( dns_search.len != 0 ) ) { +		struct dns_name name; +		int offset; + +		DBG ( "DNS search list:" ); +		memcpy ( &name, &dns_search, sizeof ( name ) ); +		while ( name.offset != name.len ) { +			DBG ( " %s", dns_name ( &name ) ); +			offset = dns_skip_search ( &name ); +			if ( offset < 0 ) +				break; +			name.offset = offset; +		} +		DBG ( "\n" ); +	} + +	return 0; +} + +/** DNS settings applicator */ +struct settings_applicator dns_applicator __settings_applicator = { +	.apply = apply_dns_settings, +}; diff --git a/roms/ipxe/src/net/udp/slam.c b/roms/ipxe/src/net/udp/slam.c new file mode 100644 index 00000000..3cb492d7 --- /dev/null +++ b/roms/ipxe/src/net/udp/slam.c @@ -0,0 +1,757 @@ +/* + * Copyright (C) 2008 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 <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <errno.h> +#include <assert.h> +#include <byteswap.h> +#include <ipxe/features.h> +#include <ipxe/iobuf.h> +#include <ipxe/bitmap.h> +#include <ipxe/xfer.h> +#include <ipxe/open.h> +#include <ipxe/uri.h> +#include <ipxe/tcpip.h> +#include <ipxe/timer.h> +#include <ipxe/retry.h> + +/** @file + * + * Scalable Local Area Multicast protocol + * + * The SLAM protocol is supported only by Etherboot; it was designed + * and implemented by Eric Biederman.  A server implementation is + * available in contrib/mini-slamd.  There does not appear to be any + * documentation beyond a few sparse comments in Etherboot's + * proto_slam.c. + * + * SLAM packets use three types of data field: + * + *  Nul : A single NUL (0) byte, used as a list terminator + * + *  Raw : A block of raw data + * + *  Int : A variable-length integer, in big-endian order.  The length + *        of the integer is encoded in the most significant three bits. + * + * Packets received by the client have the following layout: + * + *  Int : Transaction identifier.  This is an opaque value. + * + *  Int : Total number of bytes in the transfer. + * + *  Int : Block size, in bytes. + * + *  Int : Packet sequence number within the transfer (if this packet + *        contains data). + * + *  Raw : Packet data (if this packet contains data). + * + * Packets transmitted by the client consist of a run-length-encoded + * representation of the received-blocks bitmap, looking something + * like: + * + *  Int : Number of consecutive successfully-received packets + *  Int : Number of consecutive missing packets + *  Int : Number of consecutive successfully-received packets + *  Int : Number of consecutive missing packets + *  .... + *  Nul + * + */ + +FEATURE ( FEATURE_PROTOCOL, "SLAM", DHCP_EB_FEATURE_SLAM, 1 ); + +/** Default SLAM server port */ +#define SLAM_DEFAULT_PORT 10000 + +/** Default SLAM multicast IP address */ +#define SLAM_DEFAULT_MULTICAST_IP \ +	( ( 239 << 24 ) | ( 255 << 16 ) | ( 1 << 8 ) | ( 1 << 0 ) ) + +/** Default SLAM multicast port */ +#define SLAM_DEFAULT_MULTICAST_PORT 10000 + +/** Maximum SLAM header length */ +#define SLAM_MAX_HEADER_LEN ( 7 /* transaction id */ + 7 /* total_bytes */ + \ +			      7 /* block_size */ ) + +/** Maximum number of blocks to request per NACK + * + * This is a policy decision equivalent to selecting a TCP window + * size. + */ +#define SLAM_MAX_BLOCKS_PER_NACK 4 + +/** Maximum SLAM NACK length + * + * We only ever send a NACK for a single range of up to @c + * SLAM_MAX_BLOCKS_PER_NACK blocks. + */ +#define SLAM_MAX_NACK_LEN ( 7 /* block */ + 7 /* #blocks */ + 1 /* NUL */ ) + +/** SLAM slave timeout */ +#define SLAM_SLAVE_TIMEOUT ( 1 * TICKS_PER_SEC ) + +/** A SLAM request */ +struct slam_request { +	/** Reference counter */ +	struct refcnt refcnt; + +	/** Data transfer interface */ +	struct interface xfer; +	/** Unicast socket */ +	struct interface socket; +	/** Multicast socket */ +	struct interface mc_socket; + +	/** Master client retry timer */ +	struct retry_timer master_timer; +	/** Slave client retry timer */ +	struct retry_timer slave_timer; + +	/** Cached header */ +	uint8_t header[SLAM_MAX_HEADER_LEN]; +	/** Size of cached header */ +	size_t header_len; +	/** Total number of bytes in transfer */ +	unsigned long total_bytes; +	/** Transfer block size */ +	unsigned long block_size; +	/** Number of blocks in transfer */ +	unsigned long num_blocks; +	/** Block bitmap */ +	struct bitmap bitmap; +	/** NACK sent flag */ +	int nack_sent; +}; + +/** + * Free a SLAM request + * + * @v refcnt		Reference counter + */ +static void slam_free ( struct refcnt *refcnt ) { +	struct slam_request *slam = +		container_of ( refcnt, struct slam_request, refcnt ); + +	bitmap_free ( &slam->bitmap ); +	free ( slam ); +} + +/** + * Mark SLAM request as complete + * + * @v slam		SLAM request + * @v rc		Return status code + */ +static void slam_finished ( struct slam_request *slam, int rc ) { +	static const uint8_t slam_disconnect[] = { 0 }; + +	DBGC ( slam, "SLAM %p finished with status code %d (%s)\n", +	       slam, rc, strerror ( rc ) ); + +	/* Send a disconnect message if we ever sent anything to the +	 * server. +	 */ +	if ( slam->nack_sent ) { +		xfer_deliver_raw ( &slam->socket, slam_disconnect, +				   sizeof ( slam_disconnect ) ); +	} + +	/* Stop the retry timers */ +	stop_timer ( &slam->master_timer ); +	stop_timer ( &slam->slave_timer ); + +	/* Close all data transfer interfaces */ +	intf_shutdown ( &slam->socket, rc ); +	intf_shutdown ( &slam->mc_socket, rc ); +	intf_shutdown ( &slam->xfer, rc ); +} + +/**************************************************************************** + * + * TX datapath + * + */ + +/** + * Add a variable-length value to a SLAM packet + * + * @v slam		SLAM request + * @v iobuf		I/O buffer + * @v value		Value to add + * @ret rc		Return status code + * + * Adds a variable-length value to the end of an I/O buffer.  Will + * always leave at least one byte of tailroom in the I/O buffer (to + * allow space for the terminating NUL). + */ +static int slam_put_value ( struct slam_request *slam, +			    struct io_buffer *iobuf, unsigned long value ) { +	uint8_t *data; +	size_t len; +	unsigned int i; + +	/* Calculate variable length required to store value.  Always +	 * leave at least one byte in the I/O buffer. +	 */ +	len = ( ( flsl ( value ) + 10 ) / 8 ); +	if ( len >= iob_tailroom ( iobuf ) ) { +		DBGC2 ( slam, "SLAM %p cannot add %zd-byte value\n", +			slam, len ); +		return -ENOBUFS; +	} +	/* There is no valid way within the protocol that we can end +	 * up trying to push a full-sized long (i.e. without space for +	 * the length encoding). +	 */ +	assert ( len <= sizeof ( value ) ); + +	/* Add value */ +	data = iob_put ( iobuf, len ); +	for ( i = len ; i-- ; ) { +		data[i] = value; +		value >>= 8; +	} +	*data |= ( len << 5 ); +	assert ( value == 0 ); + +	return 0; +} + +/** + * Send SLAM NACK packet + * + * @v slam		SLAM request + * @ret rc		Return status code + */ +static int slam_tx_nack ( struct slam_request *slam ) { +	struct io_buffer *iobuf; +	unsigned long first_block; +	unsigned long num_blocks; +	uint8_t *nul; +	int rc; + +	/* Mark NACK as sent, so that we know we have to disconnect later */ +	slam->nack_sent = 1; + +	/* Allocate I/O buffer */ +	iobuf = xfer_alloc_iob ( &slam->socket,	SLAM_MAX_NACK_LEN ); +	if ( ! iobuf ) { +		DBGC ( slam, "SLAM %p could not allocate I/O buffer\n", +		       slam ); +		return -ENOMEM; +	} + +	/* Construct NACK.  We always request only a single packet; +	 * this allows us to force multicast-TFTP-style flow control +	 * on the SLAM server, which will otherwise just blast the +	 * data out as fast as it can.  On a gigabit network, without +	 * RX checksumming, this would inevitably cause packet drops. +	 */ +	first_block = bitmap_first_gap ( &slam->bitmap ); +	for ( num_blocks = 1 ; ; num_blocks++ ) { +		if ( num_blocks >= SLAM_MAX_BLOCKS_PER_NACK ) +			break; +		if ( ( first_block + num_blocks ) >= slam->num_blocks ) +			break; +		if ( bitmap_test ( &slam->bitmap, +				   ( first_block + num_blocks ) ) ) +			break; +	} +	if ( first_block ) { +		DBGCP ( slam, "SLAM %p transmitting NACK for blocks " +			"%ld-%ld\n", slam, first_block, +			( first_block + num_blocks - 1 ) ); +	} else { +		DBGC ( slam, "SLAM %p transmitting initial NACK for blocks " +		       "0-%ld\n", slam, ( num_blocks - 1 ) ); +	} +	if ( ( rc = slam_put_value ( slam, iobuf, first_block ) ) != 0 ) +		return rc; +	if ( ( rc = slam_put_value ( slam, iobuf, num_blocks ) ) != 0 ) +		return rc; +	nul = iob_put ( iobuf, 1 ); +	*nul = 0; + +	/* Transmit packet */ +	return xfer_deliver_iob ( &slam->socket, iobuf ); +} + +/** + * Handle SLAM master client retry timer expiry + * + * @v timer		Master retry timer + * @v fail		Failure indicator + */ +static void slam_master_timer_expired ( struct retry_timer *timer, +					int fail ) { +	struct slam_request *slam = +		container_of ( timer, struct slam_request, master_timer ); + +	if ( fail ) { +		/* Allow timer to stop running.  We will terminate the +		 * connection only if the slave timer times out. +		 */ +		DBGC ( slam, "SLAM %p giving up acting as master client\n", +		       slam ); +	} else { +		/* Retransmit NACK */ +		start_timer ( timer ); +		slam_tx_nack ( slam ); +	} +} + +/** + * Handle SLAM slave client retry timer expiry + * + * @v timer		Master retry timer + * @v fail		Failure indicator + */ +static void slam_slave_timer_expired ( struct retry_timer *timer, +					int fail ) { +	struct slam_request *slam = +		container_of ( timer, struct slam_request, slave_timer ); + +	if ( fail ) { +		/* Terminate connection */ +		slam_finished ( slam, -ETIMEDOUT ); +	} else { +		/* Try sending a NACK */ +		DBGC ( slam, "SLAM %p trying to become master client\n", +		       slam ); +		start_timer ( timer ); +		slam_tx_nack ( slam ); +	} +} + +/**************************************************************************** + * + * RX datapath + * + */ + +/** + * Read and strip a variable-length value from a SLAM packet + * + * @v slam		SLAM request + * @v iobuf		I/O buffer + * @v value		Value to fill in, or NULL to ignore value + * @ret rc		Return status code + * + * Reads a variable-length value from the start of the I/O buffer.   + */ +static int slam_pull_value ( struct slam_request *slam, +			     struct io_buffer *iobuf, +			     unsigned long *value ) { +	uint8_t *data; +	size_t len; + +	/* Sanity check */ +	if ( iob_len ( iobuf ) == 0 ) { +		DBGC ( slam, "SLAM %p empty value\n", slam ); +		return -EINVAL; +	} + +	/* Read and verify length of value */ +	data = iobuf->data; +	len = ( *data >> 5 ); +	if ( ( len == 0 ) || +	     ( value && ( len > sizeof ( *value ) ) ) ) { +		DBGC ( slam, "SLAM %p invalid value length %zd bytes\n", +		       slam, len ); +		return -EINVAL; +	} +	if ( len > iob_len ( iobuf ) ) { +		DBGC ( slam, "SLAM %p value extends beyond I/O buffer\n", +		       slam ); +		return -EINVAL; +	} + +	/* Read value */ +	iob_pull ( iobuf, len ); +	*value = ( *data & 0x1f ); +	while ( --len ) { +		*value <<= 8; +		*value |= *(++data); +	} + +	return 0; +} + +/** + * Read and strip SLAM header + * + * @v slam		SLAM request + * @v iobuf		I/O buffer + * @ret rc		Return status code + */ +static int slam_pull_header ( struct slam_request *slam, +			      struct io_buffer *iobuf ) { +	void *header = iobuf->data; +	int rc; + +	/* If header matches cached header, just pull it and return */ +	if ( ( slam->header_len <= iob_len ( iobuf ) ) && +	     ( memcmp ( slam->header, iobuf->data, slam->header_len ) == 0 )){ +		iob_pull ( iobuf, slam->header_len ); +		return 0; +	} + +	DBGC ( slam, "SLAM %p detected changed header; resetting\n", slam ); + +	/* Read and strip transaction ID, total number of bytes, and +	 * block size. +	 */ +	if ( ( rc = slam_pull_value ( slam, iobuf, NULL ) ) != 0 ) +		return rc; +	if ( ( rc = slam_pull_value ( slam, iobuf, +				      &slam->total_bytes ) ) != 0 ) +		return rc; +	if ( ( rc = slam_pull_value ( slam, iobuf, +				      &slam->block_size ) ) != 0 ) +		return rc; + +	/* Update the cached header */ +	slam->header_len = ( iobuf->data - header ); +	assert ( slam->header_len <= sizeof ( slam->header ) ); +	memcpy ( slam->header, header, slam->header_len ); + +	/* Calculate number of blocks */ +	slam->num_blocks = ( ( slam->total_bytes + slam->block_size - 1 ) / +			     slam->block_size ); + +	DBGC ( slam, "SLAM %p has total bytes %ld, block size %ld, num " +	       "blocks %ld\n", slam, slam->total_bytes, slam->block_size, +	       slam->num_blocks ); + +	/* Discard and reset the bitmap */ +	bitmap_free ( &slam->bitmap ); +	memset ( &slam->bitmap, 0, sizeof ( slam->bitmap ) ); + +	/* Allocate a new bitmap */ +	if ( ( rc = bitmap_resize ( &slam->bitmap, +				    slam->num_blocks ) ) != 0 ) { +		/* Failure to allocate a bitmap is fatal */ +		DBGC ( slam, "SLAM %p could not allocate bitmap for %ld " +		       "blocks: %s\n", slam, slam->num_blocks, +		       strerror ( rc ) ); +		slam_finished ( slam, rc ); +		return rc; +	} + +	/* Notify recipient of file size */ +	xfer_seek ( &slam->xfer, slam->total_bytes ); + +	return 0; +} + +/** + * Receive SLAM data packet + * + * @v slam		SLAM request + * @v iobuf		I/O buffer + * @ret rc		Return status code + */ +static int slam_mc_socket_deliver ( struct slam_request *slam, +				    struct io_buffer *iobuf, +				    struct xfer_metadata *rx_meta __unused ) { +	struct xfer_metadata meta; +	unsigned long packet; +	size_t len; +	int rc; + +	/* Stop the master client timer.  Restart the slave client timer. */ +	stop_timer ( &slam->master_timer ); +	stop_timer ( &slam->slave_timer ); +	start_timer_fixed ( &slam->slave_timer, SLAM_SLAVE_TIMEOUT ); + +	/* Read and strip packet header */ +	if ( ( rc = slam_pull_header ( slam, iobuf ) ) != 0 ) +		goto err_discard; + +	/* Read and strip packet number */ +	if ( ( rc = slam_pull_value ( slam, iobuf, &packet ) ) != 0 ) +		goto err_discard; + +	/* Sanity check packet number */ +	if ( packet >= slam->num_blocks ) { +		DBGC ( slam, "SLAM %p received out-of-range packet %ld " +		       "(num_blocks=%ld)\n", slam, packet, slam->num_blocks ); +		rc = -EINVAL; +		goto err_discard; +	} + +	/* Sanity check length */ +	len = iob_len ( iobuf ); +	if ( len > slam->block_size ) { +		DBGC ( slam, "SLAM %p received oversize packet of %zd bytes " +		       "(block_size=%ld)\n", slam, len, slam->block_size ); +		rc = -EINVAL; +		goto err_discard; +	} +	if ( ( packet != ( slam->num_blocks - 1 ) ) && +	     ( len < slam->block_size ) ) { +		DBGC ( slam, "SLAM %p received short packet of %zd bytes " +		       "(block_size=%ld)\n", slam, len, slam->block_size ); +		rc = -EINVAL; +		goto err_discard; +	} + +	/* If we have already seen this packet, discard it */ +	if ( bitmap_test ( &slam->bitmap, packet ) ) { +		goto discard; +	} + +	/* Pass to recipient */ +	memset ( &meta, 0, sizeof ( meta ) ); +	meta.flags = XFER_FL_ABS_OFFSET; +	meta.offset = ( packet * slam->block_size ); +	if ( ( rc = xfer_deliver ( &slam->xfer, iobuf, &meta ) ) != 0 ) +		goto err; + +	/* Mark block as received */ +	bitmap_set ( &slam->bitmap, packet ); + +	/* If we have received all blocks, terminate */ +	if ( bitmap_full ( &slam->bitmap ) ) +		slam_finished ( slam, 0 ); + +	return 0; + + err_discard: + discard: +	free_iob ( iobuf ); + err: +	return rc; +} + +/** + * Receive SLAM non-data packet + * + * @v slam		SLAM request + * @v iobuf		I/O buffer + * @ret rc		Return status code + */ +static int slam_socket_deliver ( struct slam_request *slam, +				 struct io_buffer *iobuf, +				 struct xfer_metadata *rx_meta __unused ) { +	int rc; + +	/* Restart the master client timer */ +	stop_timer ( &slam->master_timer ); +	start_timer ( &slam->master_timer ); + +	/* Read and strip packet header */ +	if ( ( rc = slam_pull_header ( slam, iobuf ) ) != 0 ) +		goto discard; + +	/* Sanity check */ +	if ( iob_len ( iobuf ) != 0 ) { +		DBGC ( slam, "SLAM %p received trailing garbage:\n", slam ); +		DBGC_HD ( slam, iobuf->data, iob_len ( iobuf ) ); +		rc = -EINVAL; +		goto discard; +	} + +	/* Discard packet */ +	free_iob ( iobuf ); + +	/* Send NACK in reply */ +	slam_tx_nack ( slam ); + +	return 0; + + discard: +	free_iob ( iobuf ); +	return rc; + +} + +/** SLAM unicast socket interface operations */ +static struct interface_operation slam_socket_operations[] = { +	INTF_OP ( xfer_deliver, struct slam_request *, slam_socket_deliver ), +	INTF_OP ( intf_close, struct slam_request *, slam_finished ), +}; + +/** SLAM unicast socket interface descriptor */ +static struct interface_descriptor slam_socket_desc = +	INTF_DESC ( struct slam_request, socket, slam_socket_operations ); + +/** SLAM multicast socket interface operations */ +static struct interface_operation slam_mc_socket_operations[] = { +	INTF_OP ( xfer_deliver, struct slam_request *, slam_mc_socket_deliver ), +	INTF_OP ( intf_close, struct slam_request *, slam_finished ), +}; + +/** SLAM multicast socket interface descriptor */ +static struct interface_descriptor slam_mc_socket_desc = +	INTF_DESC ( struct slam_request, mc_socket, slam_mc_socket_operations ); + +/**************************************************************************** + * + * Data transfer interface + * + */ + +/** SLAM data transfer interface operations */ +static struct interface_operation slam_xfer_operations[] = { +	INTF_OP ( intf_close, struct slam_request *, slam_finished ), +}; + +/** SLAM data transfer interface descriptor */ +static struct interface_descriptor slam_xfer_desc = +	INTF_DESC ( struct slam_request, xfer, slam_xfer_operations ); + +/** + * Parse SLAM URI multicast address + * + * @v slam		SLAM request + * @v path		Path portion of x-slam:// URI + * @v address		Socket address to fill in + * @ret rc		Return status code + */ +static int slam_parse_multicast_address ( struct slam_request *slam, +					  const char *path, +					  struct sockaddr_in *address ) { +	char path_dup[ strlen ( path ) /* no +1 */ ]; +	char *sep; +	char *end; + +	/* Create temporary copy of path, minus the leading '/' */ +	assert ( *path == '/' ); +	memcpy ( path_dup, ( path + 1 ) , sizeof ( path_dup ) ); + +	/* Parse port, if present */ +	sep = strchr ( path_dup, ':' ); +	if ( sep ) { +		*(sep++) = '\0'; +		address->sin_port = htons ( strtoul ( sep, &end, 0 ) ); +		if ( *end != '\0' ) { +			DBGC ( slam, "SLAM %p invalid multicast port " +			       "\"%s\"\n", slam, sep ); +			return -EINVAL; +		} +	} + +	/* Parse address */ +	if ( inet_aton ( path_dup, &address->sin_addr ) == 0 ) { +		DBGC ( slam, "SLAM %p invalid multicast address \"%s\"\n", +		       slam, path_dup ); +		return -EINVAL; +	} + +	return 0; +} + +/** + * Initiate a SLAM request + * + * @v xfer		Data transfer interface + * @v uri		Uniform Resource Identifier + * @ret rc		Return status code + */ +static int slam_open ( struct interface *xfer, struct uri *uri ) { +	static const struct sockaddr_in default_multicast = { +		.sin_family = AF_INET, +		.sin_port = htons ( SLAM_DEFAULT_MULTICAST_PORT ), +		.sin_addr = { htonl ( SLAM_DEFAULT_MULTICAST_IP ) }, +	}; +	struct slam_request *slam; +	struct sockaddr_tcpip server; +	struct sockaddr_in multicast; +	int rc; + +	/* Sanity checks */ +	if ( ! uri->host ) +		return -EINVAL; + +	/* Allocate and populate structure */ +	slam = zalloc ( sizeof ( *slam ) ); +	if ( ! slam ) +		return -ENOMEM; +	ref_init ( &slam->refcnt, slam_free ); +	intf_init ( &slam->xfer, &slam_xfer_desc, &slam->refcnt ); +	intf_init ( &slam->socket, &slam_socket_desc, &slam->refcnt ); +	intf_init ( &slam->mc_socket, &slam_mc_socket_desc, &slam->refcnt ); +	timer_init ( &slam->master_timer, slam_master_timer_expired, +		     &slam->refcnt ); +	timer_init ( &slam->slave_timer, slam_slave_timer_expired, +		     &slam->refcnt ); +	/* Fake an invalid cached header of { 0x00, ... } */ +	slam->header_len = 1; +	/* Fake parameters for initial NACK */ +	slam->num_blocks = 1; +	if ( ( rc = bitmap_resize ( &slam->bitmap, 1 ) ) != 0 ) { +		DBGC ( slam, "SLAM %p could not allocate initial bitmap: " +		       "%s\n", slam, strerror ( rc ) ); +		goto err; +	} + +	/* Open unicast socket */ +	memset ( &server, 0, sizeof ( server ) ); +	server.st_port = htons ( uri_port ( uri, SLAM_DEFAULT_PORT ) ); +	if ( ( rc = xfer_open_named_socket ( &slam->socket, SOCK_DGRAM, +					     ( struct sockaddr * ) &server, +					     uri->host, NULL ) ) != 0 ) { +		DBGC ( slam, "SLAM %p could not open unicast socket: %s\n", +		       slam, strerror ( rc ) ); +		goto err; +	} + +	/* Open multicast socket */ +	memcpy ( &multicast, &default_multicast, sizeof ( multicast ) ); +	if ( uri->path && +	     ( ( rc = slam_parse_multicast_address ( slam, uri->path, +						     &multicast ) ) != 0 ) ) { +		goto err; +	} +	if ( ( rc = xfer_open_socket ( &slam->mc_socket, SOCK_DGRAM, +				 ( struct sockaddr * ) &multicast, +				 ( struct sockaddr * ) &multicast ) ) != 0 ) { +		DBGC ( slam, "SLAM %p could not open multicast socket: %s\n", +		       slam, strerror ( rc ) ); +		goto err; +	} + +	/* Start slave retry timer */ +	start_timer_fixed ( &slam->slave_timer, SLAM_SLAVE_TIMEOUT ); + +	/* Attach to parent interface, mortalise self, and return */ +	intf_plug_plug ( &slam->xfer, xfer ); +	ref_put ( &slam->refcnt ); +	return 0; + + err: +	slam_finished ( slam, rc ); +	ref_put ( &slam->refcnt ); +	return rc; +} + +/** SLAM URI opener */ +struct uri_opener slam_uri_opener __uri_opener = { +	.scheme	= "x-slam", +	.open	= slam_open, +}; diff --git a/roms/ipxe/src/net/udp/syslog.c b/roms/ipxe/src/net/udp/syslog.c new file mode 100644 index 00000000..d65d19ab --- /dev/null +++ b/roms/ipxe/src/net/udp/syslog.c @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2011 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 ); + +/** @file + * + * Syslog protocol + * + */ + +#include <stdint.h> +#include <stdlib.h> +#include <ctype.h> +#include <byteswap.h> +#include <ipxe/xfer.h> +#include <ipxe/open.h> +#include <ipxe/tcpip.h> +#include <ipxe/dhcp.h> +#include <ipxe/dhcpv6.h> +#include <ipxe/settings.h> +#include <ipxe/console.h> +#include <ipxe/lineconsole.h> +#include <ipxe/syslog.h> +#include <config/console.h> + +/* Set default console usage if applicable */ +#if ! ( defined ( CONSOLE_SYSLOG ) && CONSOLE_EXPLICIT ( CONSOLE_SYSLOG ) ) +#undef CONSOLE_SYSLOG +#define CONSOLE_SYSLOG ( CONSOLE_USAGE_ALL & ~CONSOLE_USAGE_TUI ) +#endif + +/** The syslog server */ +static union { +	struct sockaddr sa; +	struct sockaddr_tcpip st; +	struct sockaddr_in sin; +	struct sockaddr_in6 sin6; +} logserver = { +	.st = { +		.st_port = htons ( SYSLOG_PORT ), +	}, +}; + +/** Syslog UDP interface operations */ +static struct interface_operation syslogger_operations[] = {}; + +/** Syslog UDP interface descriptor */ +static struct interface_descriptor syslogger_desc = +	INTF_DESC_PURE ( syslogger_operations ); + +/** The syslog UDP interface */ +static struct interface syslogger = INTF_INIT ( syslogger_desc ); + +/****************************************************************************** + * + * Console driver + * + ****************************************************************************** + */ + +/** Host name (for log messages) */ +static char *syslog_hostname; + +/** Domain name (for log messages) */ +static char *syslog_domain; + +/** + * Transmit formatted syslog message + * + * @v xfer		Data transfer interface + * @v severity		Severity + * @v message		Message + * @v terminator	Message terminator + * @ret rc		Return status code + */ +int syslog_send ( struct interface *xfer, unsigned int severity, +		  const char *message, const char *terminator ) { +	const char *hostname = ( syslog_hostname ? syslog_hostname : "" ); +	const char *domain = ( ( hostname[0] && syslog_domain ) ? +			       syslog_domain : "" ); + +	return xfer_printf ( xfer, "<%d>%s%s%s%sipxe: %s%s", +			     SYSLOG_PRIORITY ( SYSLOG_DEFAULT_FACILITY, +					       severity ), hostname, +			     ( domain[0] ? "." : "" ), domain, +			     ( hostname[0] ? " " : "" ), message, terminator ); +} + +/****************************************************************************** + * + * Console driver + * + ****************************************************************************** + */ + +/** Syslog line buffer */ +static char syslog_buffer[SYSLOG_BUFSIZE]; + +/** Syslog severity */ +static unsigned int syslog_severity = SYSLOG_DEFAULT_SEVERITY; + +/** + * Handle ANSI set syslog priority (private sequence) + * + * @v ctx		ANSI escape sequence context + * @v count		Parameter count + * @v params		List of graphic rendition aspects + */ +static void syslog_handle_priority ( struct ansiesc_context *ctx __unused, +				     unsigned int count __unused, +				     int params[] ) { +	if ( params[0] >= 0 ) { +		syslog_severity = params[0]; +	} else { +		syslog_severity = SYSLOG_DEFAULT_SEVERITY; +	} +} + +/** Syslog ANSI escape sequence handlers */ +static struct ansiesc_handler syslog_handlers[] = { +	{ ANSIESC_LOG_PRIORITY, syslog_handle_priority }, +	{ 0, NULL } +}; + +/** Syslog line console */ +static struct line_console syslog_line = { +	.buffer = syslog_buffer, +	.len = sizeof ( syslog_buffer ), +	.ctx = { +		.handlers = syslog_handlers, +	}, +}; + +/** Syslog recursion marker */ +static int syslog_entered; + +/** + * Print a character to syslog console + * + * @v character		Character to be printed + */ +static void syslog_putchar ( int character ) { +	int rc; + +	/* Ignore if we are already mid-logging */ +	if ( syslog_entered ) +		return; + +	/* Fill line buffer */ +	if ( line_putchar ( &syslog_line, character ) == 0 ) +		return; + +	/* Guard against re-entry */ +	syslog_entered = 1; + +	/* Send log message */ +	if ( ( rc = syslog_send ( &syslogger, syslog_severity, +				  syslog_buffer, "" ) ) != 0 ) { +		DBG ( "SYSLOG could not send log message: %s\n", +		      strerror ( rc ) ); +	} + +	/* Clear re-entry flag */ +	syslog_entered = 0; +} + +/** Syslog console driver */ +struct console_driver syslog_console __console_driver = { +	.putchar = syslog_putchar, +	.disabled = CONSOLE_DISABLED, +	.usage = CONSOLE_SYSLOG, +}; + +/****************************************************************************** + * + * Settings + * + ****************************************************************************** + */ + +/** IPv4 syslog server setting */ +const struct setting syslog_setting __setting ( SETTING_MISC, syslog ) = { +	.name = "syslog", +	.description = "Syslog server", +	.tag = DHCP_LOG_SERVERS, +	.type = &setting_type_ipv4, +}; + +/** IPv6 syslog server setting */ +const struct setting syslog6_setting __setting ( SETTING_MISC, syslog6 ) = { +	.name = "syslog6", +	.description = "Syslog server", +	.tag = DHCPV6_LOG_SERVERS, +	.type = &setting_type_ipv6, +	.scope = &ipv6_scope, +}; + +/** + * Strip invalid characters from host/domain name + * + * @v name		Name to strip + */ +static void syslog_fix_name ( char *name ) { +	char *fixed = name; +	int c; + +	/* Do nothing if name does not exist */ +	if ( ! name ) +		return; + +	/* Strip any non-printable or whitespace characters from the name */ +	do { +		c = *(name++); +		*fixed = c; +		if ( isprint ( c ) && ! isspace ( c ) ) +			fixed++; +	} while ( c ); +} + +/** + * Apply syslog settings + * + * @ret rc		Return status code + */ +static int apply_syslog_settings ( void ) { +	struct sockaddr old_logserver; +	int rc; + +	/* Fetch hostname and domain name */ +	free ( syslog_hostname ); +	fetch_string_setting_copy ( NULL, &hostname_setting, &syslog_hostname ); +	syslog_fix_name ( syslog_hostname ); +	free ( syslog_domain ); +	fetch_string_setting_copy ( NULL, &domain_setting, &syslog_domain ); +	syslog_fix_name ( syslog_domain ); + +	/* Fetch log server */ +	syslog_console.disabled = CONSOLE_DISABLED; +	memcpy ( &old_logserver, &logserver, sizeof ( old_logserver ) ); +	logserver.sa.sa_family = 0; +	if ( fetch_ipv6_setting ( NULL, &syslog6_setting, +				  &logserver.sin6.sin6_addr ) >= 0 ) { +		logserver.sin6.sin6_family = AF_INET6; +	} else if ( fetch_ipv4_setting ( NULL, &syslog_setting, +					 &logserver.sin.sin_addr ) >= 0 ) { +		logserver.sin.sin_family = AF_INET; +	} +	if ( logserver.sa.sa_family ) { +		syslog_console.disabled = 0; +		DBG ( "SYSLOG using log server %s\n", +		      sock_ntoa ( &logserver.sa ) ); +	} + +	/* Do nothing unless log server has changed */ +	if ( memcmp ( &logserver, &old_logserver, sizeof ( logserver ) ) == 0 ) +		return 0; + +	/* Reset syslog connection */ +	intf_restart ( &syslogger, 0 ); + +	/* Do nothing unless we have a log server */ +	if ( syslog_console.disabled ) { +		DBG ( "SYSLOG has no log server\n" ); +		return 0; +	} + +	/* Connect to log server */ +	if ( ( rc = xfer_open_socket ( &syslogger, SOCK_DGRAM, +				       &logserver.sa, NULL ) ) != 0 ) { +		DBG ( "SYSLOG cannot connect to log server: %s\n", +		      strerror ( rc ) ); +		return rc; +	} + +	return 0; +} + +/** Syslog settings applicator */ +struct settings_applicator syslog_applicator __settings_applicator = { +	.apply = apply_syslog_settings, +}; diff --git a/roms/ipxe/src/net/udp/tftp.c b/roms/ipxe/src/net/udp/tftp.c new file mode 100644 index 00000000..ee827ae3 --- /dev/null +++ b/roms/ipxe/src/net/udp/tftp.c @@ -0,0 +1,1236 @@ +/* + * Copyright (C) 2006 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 <stdint.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <strings.h> +#include <byteswap.h> +#include <errno.h> +#include <assert.h> +#include <ipxe/refcnt.h> +#include <ipxe/iobuf.h> +#include <ipxe/xfer.h> +#include <ipxe/open.h> +#include <ipxe/uri.h> +#include <ipxe/tcpip.h> +#include <ipxe/retry.h> +#include <ipxe/features.h> +#include <ipxe/bitmap.h> +#include <ipxe/settings.h> +#include <ipxe/dhcp.h> +#include <ipxe/uri.h> +#include <ipxe/tftp.h> + +/** @file + * + * TFTP protocol + * + */ + +FEATURE ( FEATURE_PROTOCOL, "TFTP", DHCP_EB_FEATURE_TFTP, 1 ); + +/* TFTP-specific error codes */ +#define EINVAL_BLKSIZE 	__einfo_error ( EINFO_EINVAL_BLKSIZE ) +#define EINFO_EINVAL_BLKSIZE __einfo_uniqify \ +	( EINFO_EINVAL, 0x01, "Invalid blksize" ) +#define EINVAL_TSIZE __einfo_error ( EINFO_EINVAL_TSIZE ) +#define EINFO_EINVAL_TSIZE __einfo_uniqify \ +	( EINFO_EINVAL, 0x02, "Invalid tsize" ) +#define EINVAL_MC_NO_PORT __einfo_error ( EINFO_EINVAL_MC_NO_PORT ) +#define EINFO_EINVAL_MC_NO_PORT __einfo_uniqify \ +	( EINFO_EINVAL, 0x03, "Missing multicast port" ) +#define EINVAL_MC_NO_MC __einfo_error ( EINFO_EINVAL_MC_NO_MC ) +#define EINFO_EINVAL_MC_NO_MC __einfo_uniqify \ +	( EINFO_EINVAL, 0x04, "Missing multicast mc" ) +#define EINVAL_MC_INVALID_MC __einfo_error ( EINFO_EINVAL_MC_INVALID_MC ) +#define EINFO_EINVAL_MC_INVALID_MC __einfo_uniqify \ +	( EINFO_EINVAL, 0x05, "Missing multicast IP" ) +#define EINVAL_MC_INVALID_IP __einfo_error ( EINFO_EINVAL_MC_INVALID_IP ) +#define EINFO_EINVAL_MC_INVALID_IP __einfo_uniqify \ +	( EINFO_EINVAL, 0x06, "Invalid multicast IP" ) +#define EINVAL_MC_INVALID_PORT __einfo_error ( EINFO_EINVAL_MC_INVALID_PORT ) +#define EINFO_EINVAL_MC_INVALID_PORT __einfo_uniqify \ +	( EINFO_EINVAL, 0x07, "Invalid multicast port" ) + +/** + * A TFTP request + * + * This data structure holds the state for an ongoing TFTP transfer. + */ +struct tftp_request { +	/** Reference count */ +	struct refcnt refcnt; +	/** Data transfer interface */ +	struct interface xfer; + +	/** URI being fetched */ +	struct uri *uri; +	/** Transport layer interface */ +	struct interface socket; +	/** Multicast transport layer interface */ +	struct interface mc_socket; + +	/** Data block size +	 * +	 * This is the "blksize" option negotiated with the TFTP +	 * server.  (If the TFTP server does not support TFTP options, +	 * this will default to 512). +	 */ +	unsigned int blksize; +	/** File size +	 * +	 * This is the value returned in the "tsize" option from the +	 * TFTP server.  If the TFTP server does not support the +	 * "tsize" option, this value will be zero. +	 */ +	unsigned long tsize; +	 +	/** Server port +	 * +	 * This is the port to which RRQ packets are sent. +	 */ +	unsigned int port; +	/** Peer address +	 * +	 * The peer address is determined by the first response +	 * received to the TFTP RRQ. +	 */ +	struct sockaddr_tcpip peer; +	/** Request flags */ +	unsigned int flags; +	/** MTFTP timeout count */ +	unsigned int mtftp_timeouts; + +	/** Block bitmap */ +	struct bitmap bitmap; +	/** Maximum known length +	 * +	 * We don't always know the file length in advance.  In +	 * particular, if the TFTP server doesn't support the tsize +	 * option, or we are using MTFTP, then we don't know the file +	 * length until we see the end-of-file block (which, in the +	 * case of MTFTP, may not be the last block we see). +	 * +	 * This value is updated whenever we obtain information about +	 * the file length. +	 */ +	size_t filesize; +	/** Retransmission timer */ +	struct retry_timer timer; +}; + +/** TFTP request flags */ +enum { +	/** Send ACK packets */ +	TFTP_FL_SEND_ACK = 0x0001, +	/** Request blksize and tsize options */ +	TFTP_FL_RRQ_SIZES = 0x0002, +	/** Request multicast option */ +	TFTP_FL_RRQ_MULTICAST = 0x0004, +	/** Perform MTFTP recovery on timeout */ +	TFTP_FL_MTFTP_RECOVERY = 0x0008, +	/** Only get filesize and then abort the transfer */ +	TFTP_FL_SIZEONLY = 0x0010, +}; + +/** Maximum number of MTFTP open requests before falling back to TFTP */ +#define MTFTP_MAX_TIMEOUTS 3 + +/** + * Free TFTP request + * + * @v refcnt		Reference counter + */ +static void tftp_free ( struct refcnt *refcnt ) { +	struct tftp_request *tftp = +		container_of ( refcnt, struct tftp_request, refcnt ); + +	uri_put ( tftp->uri ); +	bitmap_free ( &tftp->bitmap ); +	free ( tftp ); +} + +/** + * Mark TFTP request as complete + * + * @v tftp		TFTP connection + * @v rc		Return status code + */ +static void tftp_done ( struct tftp_request *tftp, int rc ) { + +	DBGC ( tftp, "TFTP %p finished with status %d (%s)\n", +	       tftp, rc, strerror ( rc ) ); + +	/* Stop the retry timer */ +	stop_timer ( &tftp->timer ); + +	/* Close all data transfer interfaces */ +	intf_shutdown ( &tftp->socket, rc ); +	intf_shutdown ( &tftp->mc_socket, rc ); +	intf_shutdown ( &tftp->xfer, rc ); +} + +/** + * Reopen TFTP socket + * + * @v tftp		TFTP connection + * @ret rc		Return status code + */ +static int tftp_reopen ( struct tftp_request *tftp ) { +	struct sockaddr_tcpip server; +	int rc; + +	/* Close socket */ +	intf_restart ( &tftp->socket, 0 ); + +	/* Disable ACK sending. */ +	tftp->flags &= ~TFTP_FL_SEND_ACK; + +	/* Reset peer address */ +	memset ( &tftp->peer, 0, sizeof ( tftp->peer ) ); + +	/* Open socket */ +	memset ( &server, 0, sizeof ( server ) ); +	server.st_port = htons ( tftp->port ); +	if ( ( rc = xfer_open_named_socket ( &tftp->socket, SOCK_DGRAM, +					     ( struct sockaddr * ) &server, +					     tftp->uri->host, NULL ) ) != 0 ) { +		DBGC ( tftp, "TFTP %p could not open socket: %s\n", +		       tftp, strerror ( rc ) ); +		return rc; +	} + +	return 0; +} + +/** + * Reopen TFTP multicast socket + * + * @v tftp		TFTP connection + * @v local		Local socket address + * @ret rc		Return status code + */ +static int tftp_reopen_mc ( struct tftp_request *tftp, +			    struct sockaddr *local ) { +	int rc; + +	/* Close multicast socket */ +	intf_restart ( &tftp->mc_socket, 0 ); + +	/* Open multicast socket.  We never send via this socket, so +	 * use the local address as the peer address (since the peer +	 * address cannot be NULL). +	 */ +	if ( ( rc = xfer_open_socket ( &tftp->mc_socket, SOCK_DGRAM, +				       local, local ) ) != 0 ) { +		DBGC ( tftp, "TFTP %p could not open multicast " +		       "socket: %s\n", tftp, strerror ( rc ) ); +		return rc; +	} + +	return 0; +} + +/** + * Presize TFTP receive buffers and block bitmap + * + * @v tftp		TFTP connection + * @v filesize		Known minimum file size + * @ret rc		Return status code + */ +static int tftp_presize ( struct tftp_request *tftp, size_t filesize ) { +	unsigned int num_blocks; +	int rc; + +	/* Do nothing if we are already large enough */ +	if ( filesize <= tftp->filesize ) +		return 0; + +	/* Record filesize */ +	tftp->filesize = filesize; + +	/* Notify recipient of file size */ +	xfer_seek ( &tftp->xfer, filesize ); +	xfer_seek ( &tftp->xfer, 0 ); + +	/* Calculate expected number of blocks.  Note that files whose +	 * length is an exact multiple of the blocksize will have a +	 * trailing zero-length block, which must be included. +	 */ +	num_blocks = ( ( filesize / tftp->blksize ) + 1 ); +	if ( ( rc = bitmap_resize ( &tftp->bitmap, num_blocks ) ) != 0 ) { +		DBGC ( tftp, "TFTP %p could not resize bitmap to %d blocks: " +		       "%s\n", tftp, num_blocks, strerror ( rc ) ); +		return rc; +	} + +	return 0; +} + +/** + * MTFTP multicast receive address + * + * This is treated as a global configuration parameter. + */ +static struct sockaddr_in tftp_mtftp_socket = { +	.sin_family = AF_INET, +	.sin_addr.s_addr = htonl ( 0xefff0101 ), +	.sin_port = htons ( 3001 ), +}; + +/** + * Set MTFTP multicast address + * + * @v address		Multicast IPv4 address + */ +void tftp_set_mtftp_address ( struct in_addr address ) { +	tftp_mtftp_socket.sin_addr = address; +} + +/** + * Set MTFTP multicast port + * + * @v port		Multicast port + */ +void tftp_set_mtftp_port ( unsigned int port ) { +	tftp_mtftp_socket.sin_port = htons ( port ); +} + +/** + * Transmit RRQ + * + * @v tftp		TFTP connection + * @ret rc		Return status code + */ +static int tftp_send_rrq ( struct tftp_request *tftp ) { +	const char *path = tftp->uri->path; +	struct tftp_rrq *rrq; +	size_t len; +	struct io_buffer *iobuf; +	size_t blksize; + +	DBGC ( tftp, "TFTP %p requesting \"%s\"\n", tftp, path ); + +	/* Allocate buffer */ +	len = ( sizeof ( *rrq ) + strlen ( path ) + 1 /* NUL */ +		+ 5 + 1 /* "octet" + NUL */ +		+ 7 + 1 + 5 + 1 /* "blksize" + NUL + ddddd + NUL */ +		+ 5 + 1 + 1 + 1 /* "tsize" + NUL + "0" + NUL */  +		+ 9 + 1 + 1 /* "multicast" + NUL + NUL */ ); +	iobuf = xfer_alloc_iob ( &tftp->socket, len ); +	if ( ! iobuf ) +		return -ENOMEM; + +	/* Determine block size */ +	blksize = xfer_window ( &tftp->xfer ); +	if ( blksize > TFTP_MAX_BLKSIZE ) +		blksize = TFTP_MAX_BLKSIZE; + +	/* Build request */ +	rrq = iob_put ( iobuf, sizeof ( *rrq ) ); +	rrq->opcode = htons ( TFTP_RRQ ); +	iob_put ( iobuf, snprintf ( iobuf->tail, iob_tailroom ( iobuf ), +				    "%s%coctet", path, 0 ) + 1 ); +	if ( tftp->flags & TFTP_FL_RRQ_SIZES ) { +		iob_put ( iobuf, snprintf ( iobuf->tail, +					    iob_tailroom ( iobuf ), +					    "blksize%c%zd%ctsize%c0", +					    0, blksize, 0, 0 ) + 1 ); +	} +	if ( tftp->flags & TFTP_FL_RRQ_MULTICAST ) { +		iob_put ( iobuf, snprintf ( iobuf->tail, +					    iob_tailroom ( iobuf ), +					    "multicast%c", 0 ) + 1 ); +	} + +	/* RRQ always goes to the address specified in the initial +	 * xfer_open() call +	 */ +	return xfer_deliver_iob ( &tftp->socket, iobuf ); +} + +/** + * Transmit ACK + * + * @v tftp		TFTP connection + * @ret rc		Return status code + */ +static int tftp_send_ack ( struct tftp_request *tftp ) { +	struct tftp_ack *ack; +	struct io_buffer *iobuf; +	struct xfer_metadata meta = { +		.dest = ( struct sockaddr * ) &tftp->peer, +	}; +	unsigned int block; + +	/* Determine next required block number */ +	block = bitmap_first_gap ( &tftp->bitmap ); +	DBGC2 ( tftp, "TFTP %p sending ACK for block %d\n", tftp, block ); + +	/* Allocate buffer */ +	iobuf = xfer_alloc_iob ( &tftp->socket, sizeof ( *ack ) ); +	if ( ! iobuf ) +		return -ENOMEM; + +	/* Build ACK */ +	ack = iob_put ( iobuf, sizeof ( *ack ) ); +	ack->opcode = htons ( TFTP_ACK ); +	ack->block = htons ( block ); + +	/* ACK always goes to the peer recorded from the RRQ response */ +	return xfer_deliver ( &tftp->socket, iobuf, &meta ); +} + +/** + * Transmit ERROR (Abort) + * + * @v tftp		TFTP connection + * @v errcode		TFTP error code + * @v errmsg		Error message string + * @ret rc		Return status code + */ +static int tftp_send_error ( struct tftp_request *tftp, int errcode, +			     const char *errmsg ) { +	struct tftp_error *err; +	struct io_buffer *iobuf; +	struct xfer_metadata meta = { +		.dest = ( struct sockaddr * ) &tftp->peer, +	}; +	size_t msglen; + +	DBGC2 ( tftp, "TFTP %p sending ERROR %d: %s\n", tftp, errcode, +		errmsg ); + +	/* Allocate buffer */ +	msglen = sizeof ( *err ) + strlen ( errmsg ) + 1 /* NUL */; +	iobuf = xfer_alloc_iob ( &tftp->socket, msglen ); +	if ( ! iobuf ) +		return -ENOMEM; + +	/* Build ERROR */ +	err = iob_put ( iobuf, msglen ); +	err->opcode = htons ( TFTP_ERROR ); +	err->errcode = htons ( errcode ); +	strcpy ( err->errmsg, errmsg ); + +	/* ERR always goes to the peer recorded from the RRQ response */ +	return xfer_deliver ( &tftp->socket, iobuf, &meta ); +} + +/** + * Transmit next relevant packet + * + * @v tftp		TFTP connection + * @ret rc		Return status code + */ +static int tftp_send_packet ( struct tftp_request *tftp ) { + +	/* Update retransmission timer.  While name resolution takes place the +	 * window is zero.  Avoid unnecessary delay after name resolution +	 * completes by retrying immediately. +	 */ +	stop_timer ( &tftp->timer ); +	if ( xfer_window ( &tftp->socket ) ) { +		start_timer ( &tftp->timer ); +	} else { +		start_timer_nodelay ( &tftp->timer ); +	} + +	/* Send RRQ or ACK as appropriate */ +	if ( ! tftp->peer.st_family ) { +		return tftp_send_rrq ( tftp ); +	} else { +		if ( tftp->flags & TFTP_FL_SEND_ACK ) { +			return tftp_send_ack ( tftp ); +		} else { +			return 0; +		} +	} +} + +/** + * Handle TFTP retransmission timer expiry + * + * @v timer		Retry timer + * @v fail		Failure indicator + */ +static void tftp_timer_expired ( struct retry_timer *timer, int fail ) { +	struct tftp_request *tftp = +		container_of ( timer, struct tftp_request, timer ); +	int rc; + +	/* If we are doing MTFTP, attempt the various recovery strategies */ +	if ( tftp->flags & TFTP_FL_MTFTP_RECOVERY ) { +		if ( tftp->peer.st_family ) { +			/* If we have received any response from the server, +			 * try resending the RRQ to restart the download. +			 */ +			DBGC ( tftp, "TFTP %p attempting reopen\n", tftp ); +			if ( ( rc = tftp_reopen ( tftp ) ) != 0 ) +				goto err; +		} else { +			/* Fall back to plain TFTP after several attempts */ +			tftp->mtftp_timeouts++; +			DBGC ( tftp, "TFTP %p timeout %d waiting for MTFTP " +			       "open\n", tftp, tftp->mtftp_timeouts ); + +			if ( tftp->mtftp_timeouts > MTFTP_MAX_TIMEOUTS ) { +				DBGC ( tftp, "TFTP %p falling back to plain " +				       "TFTP\n", tftp ); +				tftp->flags = TFTP_FL_RRQ_SIZES; + +				/* Close multicast socket */ +				intf_restart ( &tftp->mc_socket, 0 ); + +				/* Reset retry timer */ +				start_timer_nodelay ( &tftp->timer ); + +				/* The blocksize may change: discard +				 * the block bitmap +				 */ +				bitmap_free ( &tftp->bitmap ); +				memset ( &tftp->bitmap, 0, +					 sizeof ( tftp->bitmap ) ); + +				/* Reopen on standard TFTP port */ +				tftp->port = TFTP_PORT; +				if ( ( rc = tftp_reopen ( tftp ) ) != 0 ) +					goto err; +			} +		} +	} else { +		/* Not doing MTFTP (or have fallen back to plain +		 * TFTP); fail as per normal. +		 */ +		if ( fail ) { +			rc = -ETIMEDOUT; +			goto err; +		} +	} +	tftp_send_packet ( tftp ); +	return; + + err: +	tftp_done ( tftp, rc ); +} + +/** + * Process TFTP "blksize" option + * + * @v tftp		TFTP connection + * @v value		Option value + * @ret rc		Return status code + */ +static int tftp_process_blksize ( struct tftp_request *tftp, +				  const char *value ) { +	char *end; + +	tftp->blksize = strtoul ( value, &end, 10 ); +	if ( *end ) { +		DBGC ( tftp, "TFTP %p got invalid blksize \"%s\"\n", +		       tftp, value ); +		return -EINVAL_BLKSIZE; +	} +	DBGC ( tftp, "TFTP %p blksize=%d\n", tftp, tftp->blksize ); + +	return 0; +} + +/** + * Process TFTP "tsize" option + * + * @v tftp		TFTP connection + * @v value		Option value + * @ret rc		Return status code + */ +static int tftp_process_tsize ( struct tftp_request *tftp, +				const char *value ) { +	char *end; + +	tftp->tsize = strtoul ( value, &end, 10 ); +	if ( *end ) { +		DBGC ( tftp, "TFTP %p got invalid tsize \"%s\"\n", +		       tftp, value ); +		return -EINVAL_TSIZE; +	} +	DBGC ( tftp, "TFTP %p tsize=%ld\n", tftp, tftp->tsize ); + +	return 0; +} + +/** + * Process TFTP "multicast" option + * + * @v tftp		TFTP connection + * @v value		Option value + * @ret rc		Return status code + */ +static int tftp_process_multicast ( struct tftp_request *tftp, +				    const char *value ) { +	union { +		struct sockaddr sa; +		struct sockaddr_in sin; +	} socket; +	char buf[ strlen ( value ) + 1 ]; +	char *addr; +	char *port; +	char *port_end; +	char *mc; +	char *mc_end; +	int rc; + +	/* Split value into "addr,port,mc" fields */ +	memcpy ( buf, value, sizeof ( buf ) ); +	addr = buf; +	port = strchr ( addr, ',' ); +	if ( ! port ) { +		DBGC ( tftp, "TFTP %p multicast missing port,mc\n", tftp ); +		return -EINVAL_MC_NO_PORT; +	} +	*(port++) = '\0'; +	mc = strchr ( port, ',' ); +	if ( ! mc ) { +		DBGC ( tftp, "TFTP %p multicast missing mc\n", tftp ); +		return -EINVAL_MC_NO_MC; +	} +	*(mc++) = '\0'; + +	/* Parse parameters */ +	if ( strtoul ( mc, &mc_end, 0 ) == 0 ) +		tftp->flags &= ~TFTP_FL_SEND_ACK; +	if ( *mc_end ) { +		DBGC ( tftp, "TFTP %p multicast invalid mc %s\n", tftp, mc ); +		return -EINVAL_MC_INVALID_MC; +	} +	DBGC ( tftp, "TFTP %p is%s the master client\n", +	       tftp, ( ( tftp->flags & TFTP_FL_SEND_ACK ) ? "" : " not" ) ); +	if ( *addr && *port ) { +		socket.sin.sin_family = AF_INET; +		if ( inet_aton ( addr, &socket.sin.sin_addr ) == 0 ) { +			DBGC ( tftp, "TFTP %p multicast invalid IP address " +			       "%s\n", tftp, addr ); +			return -EINVAL_MC_INVALID_IP; +		} +		DBGC ( tftp, "TFTP %p multicast IP address %s\n", +		       tftp, inet_ntoa ( socket.sin.sin_addr ) ); +		socket.sin.sin_port = htons ( strtoul ( port, &port_end, 0 ) ); +		if ( *port_end ) { +			DBGC ( tftp, "TFTP %p multicast invalid port %s\n", +			       tftp, port ); +			return -EINVAL_MC_INVALID_PORT; +		} +		DBGC ( tftp, "TFTP %p multicast port %d\n", +		       tftp, ntohs ( socket.sin.sin_port ) ); +		if ( ( rc = tftp_reopen_mc ( tftp, &socket.sa ) ) != 0 ) +			return rc; +	} + +	return 0; +} + +/** A TFTP option */ +struct tftp_option { +	/** Option name */ +	const char *name; +	/** Option processor +	 * +	 * @v tftp	TFTP connection +	 * @v value	Option value +	 * @ret rc	Return status code +	 */ +	int ( * process ) ( struct tftp_request *tftp, const char *value ); +}; + +/** Recognised TFTP options */ +static struct tftp_option tftp_options[] = { +	{ "blksize", tftp_process_blksize }, +	{ "tsize", tftp_process_tsize }, +	{ "multicast", tftp_process_multicast }, +	{ NULL, NULL } +}; + +/** + * Process TFTP option + * + * @v tftp		TFTP connection + * @v name		Option name + * @v value		Option value + * @ret rc		Return status code + */ +static int tftp_process_option ( struct tftp_request *tftp, +				 const char *name, const char *value ) { +	struct tftp_option *option; + +	for ( option = tftp_options ; option->name ; option++ ) { +		if ( strcasecmp ( name, option->name ) == 0 ) +			return option->process ( tftp, value ); +	} + +	DBGC ( tftp, "TFTP %p received unknown option \"%s\" = \"%s\"\n", +	       tftp, name, value ); + +	/* Unknown options should be silently ignored */ +	return 0; +} + +/** + * Receive OACK + * + * @v tftp		TFTP connection + * @v buf		Temporary data buffer + * @v len		Length of temporary data buffer + * @ret rc		Return status code + */ +static int tftp_rx_oack ( struct tftp_request *tftp, void *buf, size_t len ) { +	struct tftp_oack *oack = buf; +	char *end = buf + len; +	char *name; +	char *value; +	char *next; +	int rc = 0; + +	/* Sanity check */ +	if ( len < sizeof ( *oack ) ) { +		DBGC ( tftp, "TFTP %p received underlength OACK packet " +		       "length %zd\n", tftp, len ); +		rc = -EINVAL; +		goto done; +	} + +	/* Process each option in turn */ +	for ( name = oack->data ; name < end ; name = next ) { + +		/* Parse option name and value +		 * +		 * We treat parsing errors as non-fatal, because there +		 * exists at least one TFTP server (IBM Tivoli PXE +		 * Server 5.1.0.3) that has been observed to send +		 * malformed OACKs containing trailing garbage bytes. +		 */ +		value = ( name + strnlen ( name, ( end - name ) ) + 1 ); +		if ( value > end ) { +			DBGC ( tftp, "TFTP %p received OACK with malformed " +			       "option name:\n", tftp ); +			DBGC_HD ( tftp, oack, len ); +			break; +		} +		if ( value == end ) { +			DBGC ( tftp, "TFTP %p received OACK missing value " +			       "for option \"%s\"\n", tftp, name ); +			DBGC_HD ( tftp, oack, len ); +			break; +		} +		next = ( value + strnlen ( value, ( end - value ) ) + 1 ); +		if ( next > end ) { +			DBGC ( tftp, "TFTP %p received OACK with malformed " +			       "value for option \"%s\":\n", tftp, name ); +			DBGC_HD ( tftp, oack, len ); +			break; +		} + +		/* Process option */ +		if ( ( rc = tftp_process_option ( tftp, name, value ) ) != 0 ) +			goto done; +	} + +	/* Process tsize information, if available */ +	if ( tftp->tsize ) { +		if ( ( rc = tftp_presize ( tftp, tftp->tsize ) ) != 0 ) +			goto done; +	} + +	/* Abort request if only trying to determine file size */ +	if ( tftp->flags & TFTP_FL_SIZEONLY ) { +		rc = 0; +		tftp_send_error ( tftp, 0, "TFTP Aborted" ); +		tftp_done ( tftp, rc ); +		return rc; +	} + +	/* Request next data block */ +	tftp_send_packet ( tftp ); + + done: +	if ( rc ) +		tftp_done ( tftp, rc ); +	return rc; +} + +/** + * Receive DATA + * + * @v tftp		TFTP connection + * @v iobuf		I/O buffer + * @ret rc		Return status code + * + * Takes ownership of I/O buffer. + */ +static int tftp_rx_data ( struct tftp_request *tftp, +			  struct io_buffer *iobuf ) { +	struct tftp_data *data = iobuf->data; +	struct xfer_metadata meta; +	unsigned int block; +	off_t offset; +	size_t data_len; +	int rc; + +	if ( tftp->flags & TFTP_FL_SIZEONLY ) { +		/* If we get here then server doesn't support SIZE option */ +		rc = -ENOTSUP; +		tftp_send_error ( tftp, 0, "TFTP Aborted" ); +		goto done; +	} + +	/* Sanity check */ +	if ( iob_len ( iobuf ) < sizeof ( *data ) ) { +		DBGC ( tftp, "TFTP %p received underlength DATA packet " +		       "length %zd\n", tftp, iob_len ( iobuf ) ); +		rc = -EINVAL; +		goto done; +	} + +	/* Calculate block number */ +	block = ( ( bitmap_first_gap ( &tftp->bitmap ) + 1 ) & ~0xffff ); +	if ( data->block == 0 && block == 0 ) { +		DBGC ( tftp, "TFTP %p received data block 0\n", tftp ); +		rc = -EINVAL; +		goto done; +	} +	block += ( ntohs ( data->block ) - 1 ); + +	/* Extract data */ +	offset = ( block * tftp->blksize ); +	iob_pull ( iobuf, sizeof ( *data ) ); +	data_len = iob_len ( iobuf ); +	if ( data_len > tftp->blksize ) { +		DBGC ( tftp, "TFTP %p received overlength DATA packet " +		       "length %zd\n", tftp, data_len ); +		rc = -EINVAL; +		goto done; +	} + +	/* Deliver data */ +	memset ( &meta, 0, sizeof ( meta ) ); +	meta.flags = XFER_FL_ABS_OFFSET; +	meta.offset = offset; +	if ( ( rc = xfer_deliver ( &tftp->xfer, iob_disown ( iobuf ), +				   &meta ) ) != 0 ) { +		DBGC ( tftp, "TFTP %p could not deliver data: %s\n", +		       tftp, strerror ( rc ) ); +		goto done; +	} + +	/* Ensure block bitmap is ready */ +	if ( ( rc = tftp_presize ( tftp, ( offset + data_len ) ) ) != 0 ) +		goto done; + +	/* Mark block as received */ +	bitmap_set ( &tftp->bitmap, block ); + +	/* Acknowledge block */ +	tftp_send_packet ( tftp ); + +	/* If all blocks have been received, finish. */ +	if ( bitmap_full ( &tftp->bitmap ) ) +		tftp_done ( tftp, 0 ); + + done: +	free_iob ( iobuf ); +	if ( rc ) +		tftp_done ( tftp, rc ); +	return rc; +} + +/** + * Convert TFTP error code to return status code + * + * @v errcode		TFTP error code + * @ret rc		Return status code + */ +static int tftp_errcode_to_rc ( unsigned int errcode ) { +	switch ( errcode ) { +	case TFTP_ERR_FILE_NOT_FOUND:	return -ENOENT; +	case TFTP_ERR_ACCESS_DENIED:	return -EACCES; +	case TFTP_ERR_ILLEGAL_OP:	return -ENOTTY; +	default:			return -ENOTSUP; +	} +} + +/** + * Receive ERROR + * + * @v tftp		TFTP connection + * @v buf		Temporary data buffer + * @v len		Length of temporary data buffer + * @ret rc		Return status code + */ +static int tftp_rx_error ( struct tftp_request *tftp, void *buf, size_t len ) { +	struct tftp_error *error = buf; +	int rc; + +	/* Sanity check */ +	if ( len < sizeof ( *error ) ) { +		DBGC ( tftp, "TFTP %p received underlength ERROR packet " +		       "length %zd\n", tftp, len ); +		return -EINVAL; +	} + +	DBGC ( tftp, "TFTP %p received ERROR packet with code %d, message " +	       "\"%s\"\n", tftp, ntohs ( error->errcode ), error->errmsg ); +	 +	/* Determine final operation result */ +	rc = tftp_errcode_to_rc ( ntohs ( error->errcode ) ); + +	/* Close TFTP request */ +	tftp_done ( tftp, rc ); + +	return 0; +} + +/** + * Receive new data + * + * @v tftp		TFTP connection + * @v iobuf		I/O buffer + * @v meta		Transfer metadata + * @ret rc		Return status code + */ +static int tftp_rx ( struct tftp_request *tftp, +		     struct io_buffer *iobuf, +		     struct xfer_metadata *meta ) { +	struct sockaddr_tcpip *st_src; +	struct tftp_common *common = iobuf->data; +	size_t len = iob_len ( iobuf ); +	int rc = -EINVAL; +	 +	/* Sanity checks */ +	if ( len < sizeof ( *common ) ) { +		DBGC ( tftp, "TFTP %p received underlength packet length " +		       "%zd\n", tftp, len ); +		goto done; +	} +	if ( ! meta->src ) { +		DBGC ( tftp, "TFTP %p received packet without source port\n", +		       tftp ); +		goto done; +	} + +	/* Filter by TID.  Set TID on first response received */ +	st_src = ( struct sockaddr_tcpip * ) meta->src; +	if ( ! tftp->peer.st_family ) { +		memcpy ( &tftp->peer, st_src, sizeof ( tftp->peer ) ); +		DBGC ( tftp, "TFTP %p using remote port %d\n", tftp, +		       ntohs ( tftp->peer.st_port ) ); +	} else if ( memcmp ( &tftp->peer, st_src, +			     sizeof ( tftp->peer ) ) != 0 ) { +		DBGC ( tftp, "TFTP %p received packet from wrong source (got " +		       "%d, wanted %d)\n", tftp, ntohs ( st_src->st_port ), +		       ntohs ( tftp->peer.st_port ) ); +		goto done; +	} + +	switch ( common->opcode ) { +	case htons ( TFTP_OACK ): +		rc = tftp_rx_oack ( tftp, iobuf->data, len ); +		break; +	case htons ( TFTP_DATA ): +		rc = tftp_rx_data ( tftp, iob_disown ( iobuf ) ); +		break; +	case htons ( TFTP_ERROR ): +		rc = tftp_rx_error ( tftp, iobuf->data, len ); +		break; +	default: +		DBGC ( tftp, "TFTP %p received strange packet type %d\n", +		       tftp, ntohs ( common->opcode ) ); +		break; +	}; + + done: +	free_iob ( iobuf ); +	return rc; +} + +/** + * Receive new data via socket + * + * @v tftp		TFTP connection + * @v iobuf		I/O buffer + * @v meta		Transfer metadata + * @ret rc		Return status code + */ +static int tftp_socket_deliver ( struct tftp_request *tftp, +				 struct io_buffer *iobuf, +				 struct xfer_metadata *meta ) { + +	/* Enable sending ACKs when we receive a unicast packet.  This +	 * covers three cases: +	 * +	 * 1. Standard TFTP; we should always send ACKs, and will +	 *    always receive a unicast packet before we need to send the +	 *    first ACK. +	 * +	 * 2. RFC2090 multicast TFTP; the only unicast packets we will +         *    receive are the OACKs; enable sending ACKs here (before +         *    processing the OACK) and disable it when processing the +         *    multicast option if we are not the master client. +	 * +	 * 3. MTFTP; receiving a unicast datagram indicates that we +	 *    are the "master client" and should send ACKs. +	 */ +	tftp->flags |= TFTP_FL_SEND_ACK; + +	return tftp_rx ( tftp, iobuf, meta ); +} + +/** TFTP socket operations */ +static struct interface_operation tftp_socket_operations[] = { +	INTF_OP ( xfer_deliver, struct tftp_request *, tftp_socket_deliver ), +}; + +/** TFTP socket interface descriptor */ +static struct interface_descriptor tftp_socket_desc = +	INTF_DESC ( struct tftp_request, socket, tftp_socket_operations ); + +/** TFTP multicast socket operations */ +static struct interface_operation tftp_mc_socket_operations[] = { +	INTF_OP ( xfer_deliver, struct tftp_request *, tftp_rx ), +}; + +/** TFTP multicast socket interface descriptor */ +static struct interface_descriptor tftp_mc_socket_desc = +	INTF_DESC ( struct tftp_request, mc_socket, tftp_mc_socket_operations ); + +/** + * Check flow control window + * + * @v tftp		TFTP connection + * @ret len		Length of window + */ +static size_t tftp_xfer_window ( struct tftp_request *tftp ) { + +	/* We abuse this data-xfer method to convey the blocksize to +	 * the caller.  This really should be done using some kind of +	 * stat() method, but we don't yet have the facility to do +	 * that. +	 */ +	return tftp->blksize; +} + +/** TFTP data transfer interface operations */ +static struct interface_operation tftp_xfer_operations[] = { +	INTF_OP ( xfer_window, struct tftp_request *, tftp_xfer_window ), +	INTF_OP ( intf_close, struct tftp_request *, tftp_done ), +}; + +/** TFTP data transfer interface descriptor */ +static struct interface_descriptor tftp_xfer_desc = +	INTF_DESC ( struct tftp_request, xfer, tftp_xfer_operations ); + +/** + * Initiate TFTP/TFTM/MTFTP download + * + * @v xfer		Data transfer interface + * @v uri		Uniform Resource Identifier + * @ret rc		Return status code + */ +static int tftp_core_open ( struct interface *xfer, struct uri *uri, +			    unsigned int default_port, +			    struct sockaddr *multicast, +			    unsigned int flags ) { +	struct tftp_request *tftp; +	int rc; + +	/* Sanity checks */ +	if ( ! uri->host ) +		return -EINVAL; +	if ( ! uri->path ) +		return -EINVAL; + +	/* Allocate and populate TFTP structure */ +	tftp = zalloc ( sizeof ( *tftp ) ); +	if ( ! tftp ) +		return -ENOMEM; +	ref_init ( &tftp->refcnt, tftp_free ); +	intf_init ( &tftp->xfer, &tftp_xfer_desc, &tftp->refcnt ); +	intf_init ( &tftp->socket, &tftp_socket_desc, &tftp->refcnt ); +	intf_init ( &tftp->mc_socket, &tftp_mc_socket_desc, &tftp->refcnt ); +	timer_init ( &tftp->timer, tftp_timer_expired, &tftp->refcnt ); +	tftp->uri = uri_get ( uri ); +	tftp->blksize = TFTP_DEFAULT_BLKSIZE; +	tftp->flags = flags; + +	/* Open socket */ +	tftp->port = uri_port ( tftp->uri, default_port ); +	if ( ( rc = tftp_reopen ( tftp ) ) != 0 ) +		goto err; + +	/* Open multicast socket */ +	if ( multicast ) { +		if ( ( rc = tftp_reopen_mc ( tftp, multicast ) ) != 0 ) +			goto err; +	} + +	/* Start timer to initiate RRQ */ +	start_timer_nodelay ( &tftp->timer ); + +	/* Attach to parent interface, mortalise self, and return */ +	intf_plug_plug ( &tftp->xfer, xfer ); +	ref_put ( &tftp->refcnt ); +	return 0; + + err: +	DBGC ( tftp, "TFTP %p could not create request: %s\n", +	       tftp, strerror ( rc ) ); +	tftp_done ( tftp, rc ); +	ref_put ( &tftp->refcnt ); +	return rc; +} + +/** + * Initiate TFTP download + * + * @v xfer		Data transfer interface + * @v uri		Uniform Resource Identifier + * @ret rc		Return status code + */ +static int tftp_open ( struct interface *xfer, struct uri *uri ) { +	return tftp_core_open ( xfer, uri, TFTP_PORT, NULL, +				TFTP_FL_RRQ_SIZES ); + +} + +/** TFTP URI opener */ +struct uri_opener tftp_uri_opener __uri_opener = { +	.scheme	= "tftp", +	.open	= tftp_open, +}; + +/** + * Initiate TFTP-size request + * + * @v xfer		Data transfer interface + * @v uri		Uniform Resource Identifier + * @ret rc		Return status code + */ +static int tftpsize_open ( struct interface *xfer, struct uri *uri ) { +	return tftp_core_open ( xfer, uri, TFTP_PORT, NULL, +				( TFTP_FL_RRQ_SIZES | +				  TFTP_FL_SIZEONLY ) ); + +} + +/** TFTP URI opener */ +struct uri_opener tftpsize_uri_opener __uri_opener = { +	.scheme	= "tftpsize", +	.open	= tftpsize_open, +}; + +/** + * Initiate TFTM download + * + * @v xfer		Data transfer interface + * @v uri		Uniform Resource Identifier + * @ret rc		Return status code + */ +static int tftm_open ( struct interface *xfer, struct uri *uri ) { +	return tftp_core_open ( xfer, uri, TFTP_PORT, NULL, +				( TFTP_FL_RRQ_SIZES | +				  TFTP_FL_RRQ_MULTICAST ) ); + +} + +/** TFTM URI opener */ +struct uri_opener tftm_uri_opener __uri_opener = { +	.scheme	= "tftm", +	.open	= tftm_open, +}; + +/** + * Initiate MTFTP download + * + * @v xfer		Data transfer interface + * @v uri		Uniform Resource Identifier + * @ret rc		Return status code + */ +static int mtftp_open ( struct interface *xfer, struct uri *uri ) { +	return tftp_core_open ( xfer, uri, MTFTP_PORT, +				( struct sockaddr * ) &tftp_mtftp_socket, +				TFTP_FL_MTFTP_RECOVERY ); +} + +/** MTFTP URI opener */ +struct uri_opener mtftp_uri_opener __uri_opener = { +	.scheme	= "mtftp", +	.open	= mtftp_open, +}; + +/****************************************************************************** + * + * Settings + * + ****************************************************************************** + */ + +/** + * Apply TFTP configuration settings + * + * @ret rc		Return status code + */ +static int tftp_apply_settings ( void ) { +	static struct in_addr tftp_server = { 0 }; +	struct in_addr last_tftp_server; +	char uri_string[32]; +	struct uri *uri; + +	/* Retrieve TFTP server setting */ +	last_tftp_server = tftp_server; +	fetch_ipv4_setting ( NULL, &next_server_setting, &tftp_server ); + +	/* If TFTP server setting has changed, set the current working +	 * URI to match.  Do it only when the TFTP server has changed +	 * to try to minimise surprises to the user, who probably +	 * won't expect the CWURI to change just because they updated +	 * an unrelated setting and triggered all the settings +	 * applicators. +	 */ +	if ( tftp_server.s_addr != last_tftp_server.s_addr ) { +		if ( tftp_server.s_addr ) { +			snprintf ( uri_string, sizeof ( uri_string ), +				   "tftp://%s/", inet_ntoa ( tftp_server ) ); +			uri = parse_uri ( uri_string ); +			if ( ! uri ) +				return -ENOMEM; +		} else { +			uri = NULL; +		} +		churi ( uri ); +		uri_put ( uri ); +	} + +	return 0; +} + +/** TFTP settings applicator */ +struct settings_applicator tftp_settings_applicator __settings_applicator = { +	.apply = tftp_apply_settings, +}; | 
