diff options
author | Michael Brown <mcb30@ipxe.org> | 2015-08-17 13:16:37 +0100 |
---|---|---|
committer | Michael Brown <mcb30@ipxe.org> | 2015-08-17 13:24:39 +0100 |
commit | 4d032d5db8ef7e6713991afbc2388f0be21abd48 (patch) | |
tree | 9a5ea2f918c1c1f0714f6fb6966e8687992d4d10 /src/net | |
parent | dc9d24e7d29ae6926fcafadbc8ca4089f0aae616 (diff) | |
download | ipxe-4d032d5db8ef7e6713991afbc2388f0be21abd48.zip ipxe-4d032d5db8ef7e6713991afbc2388f0be21abd48.tar.gz ipxe-4d032d5db8ef7e6713991afbc2388f0be21abd48.tar.bz2 |
[peerdist] Add individual block download mechanism
Signed-off-by: Michael Brown <mcb30@ipxe.org>
Diffstat (limited to 'src/net')
-rw-r--r-- | src/net/peerblk.c | 1366 |
1 files changed, 1366 insertions, 0 deletions
diff --git a/src/net/peerblk.c b/src/net/peerblk.c new file mode 100644 index 0000000..fd7ea08 --- /dev/null +++ b/src/net/peerblk.c @@ -0,0 +1,1366 @@ +/* + * Copyright (C) 2015 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 (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * You can also choose to distribute this program under the terms of + * the Unmodified Binary Distribution Licence (as given in the file + * COPYING.UBDL), provided that you have satisfied its requirements. + */ + +FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <ipxe/http.h> +#include <ipxe/iobuf.h> +#include <ipxe/xfer.h> +#include <ipxe/uri.h> +#include <ipxe/timer.h> +#include <ipxe/profile.h> +#include <ipxe/fault.h> +#include <ipxe/pccrr.h> +#include <ipxe/peerblk.h> + +/** @file + * + * Peer Content Caching and Retrieval (PeerDist) protocol block downloads + * + */ + +/** PeerDist decryption chunksize + * + * This is a policy decision. + */ +#define PEERBLK_DECRYPT_CHUNKSIZE 2048 + +/** PeerDist raw block download attempt initial progress timeout + * + * This is a policy decision. + */ +#define PEERBLK_RAW_OPEN_TIMEOUT ( 10 * TICKS_PER_SEC ) + +/** PeerDist raw block download attempt ongoing progress timeout + * + * This is a policy decision. + */ +#define PEERBLK_RAW_RX_TIMEOUT ( 15 * TICKS_PER_SEC ) + +/** PeerDist retrieval protocol block download attempt initial progress timeout + * + * This is a policy decision. + */ +#define PEERBLK_RETRIEVAL_OPEN_TIMEOUT ( 3 * TICKS_PER_SEC ) + +/** PeerDist retrieval protocol block download attempt ongoing progress timeout + * + * This is a policy decision. + */ +#define PEERBLK_RETRIEVAL_RX_TIMEOUT ( 5 * TICKS_PER_SEC ) + +/** PeerDist maximum number of full download attempt cycles + * + * This is the maximum number of times that we will try a full cycle + * of download attempts (i.e. a retrieval protocol download attempt + * from each discovered peer plus a raw download attempt from the + * origin server). + * + * This is a policy decision. + */ +#define PEERBLK_MAX_ATTEMPT_CYCLES 4 + +/** PeerDist block download profiler */ +static struct profiler peerblk_download_profiler __profiler = + { .name = "peerblk.download" }; + +/** PeerDist block download attempt success profiler */ +static struct profiler peerblk_attempt_success_profiler __profiler = + { .name = "peerblk.attempt.success" }; + +/** PeerDist block download attempt failure profiler */ +static struct profiler peerblk_attempt_failure_profiler __profiler = + { .name = "peerblk.attempt.failure" }; + +/** PeerDist block download attempt timeout profiler */ +static struct profiler peerblk_attempt_timeout_profiler __profiler = + { .name = "peerblk.attempt.timeout" }; + +/** PeerDist block download discovery success profiler */ +static struct profiler peerblk_discovery_success_profiler __profiler = + { .name = "peerblk.discovery.success" }; + +/** PeerDist block download discovery timeout profiler */ +static struct profiler peerblk_discovery_timeout_profiler __profiler = + { .name = "peerblk.discovery.timeout" }; + +/** + * Get profiling timestamp + * + * @ret timestamp Timestamp + */ +static inline __attribute__ (( always_inline )) unsigned long +peerblk_timestamp ( void ) { + + if ( PROFILING ) { + return currticks(); + } else { + return 0; + } +} + +/** + * Free PeerDist block download + * + * @v refcnt Reference count + */ +static void peerblk_free ( struct refcnt *refcnt ) { + struct peerdist_block *peerblk = + container_of ( refcnt, struct peerdist_block, refcnt ); + + uri_put ( peerblk->uri ); + free ( peerblk->cipherctx ); + free ( peerblk ); +} + +/** + * Reset PeerDist block download attempt + * + * @v peerblk PeerDist block download + * @v rc Reason for reset + */ +static void peerblk_reset ( struct peerdist_block *peerblk, int rc ) { + + /* Stop decryption process */ + process_del ( &peerblk->process ); + + /* Stop timer */ + stop_timer ( &peerblk->timer ); + + /* Abort any current download attempt */ + intf_restart ( &peerblk->raw, rc ); + intf_restart ( &peerblk->retrieval, rc ); + + /* Empty received data buffer */ + xferbuf_free ( &peerblk->buffer ); + peerblk->pos = 0; + + /* Reset digest and free cipher context */ + digest_init ( peerblk->digest, peerblk->digestctx ); + free ( peerblk->cipherctx ); + peerblk->cipherctx = NULL; + peerblk->cipher = NULL; + + /* Reset trim thresholds */ + peerblk->start = ( peerblk->trim.start - peerblk->range.start ); + peerblk->end = ( peerblk->trim.end - peerblk->range.start ); + assert ( peerblk->start <= peerblk->end ); +} + +/** + * Close PeerDist block download + * + * @v peerblk PeerDist block download + * @v rc Reason for close + */ +static void peerblk_close ( struct peerdist_block *peerblk, int rc ) { + unsigned long now = peerblk_timestamp(); + + /* Profile overall block download */ + profile_custom ( &peerblk_download_profiler, + ( now - peerblk->started ) ); + + /* Reset download attempt */ + peerblk_reset ( peerblk, rc ); + + /* Close discovery */ + peerdisc_close ( &peerblk->discovery ); + + /* Shut down all interfaces */ + intf_shutdown ( &peerblk->retrieval, rc ); + intf_shutdown ( &peerblk->raw, rc ); + intf_shutdown ( &peerblk->xfer, rc ); +} + +/** + * Calculate offset within overall download + * + * @v peerblk PeerDist block download + * @v pos Position within incoming data stream + * @ret offset Offset within overall download + */ +static inline __attribute__ (( always_inline )) size_t +peerblk_offset ( struct peerdist_block *peerblk, size_t pos ) { + + return ( ( pos - peerblk->start ) + peerblk->offset ); +} + +/** + * Deliver download attempt data block + * + * @v peerblk PeerDist block download + * @v iobuf I/O buffer + * @v meta Original data transfer metadata + * @v pos Position within incoming data stream + * @ret rc Return status code + */ +static int peerblk_deliver ( struct peerdist_block *peerblk, + struct io_buffer *iobuf, + struct xfer_metadata *meta, size_t pos ) { + struct xfer_metadata xfer_meta; + size_t len = iob_len ( iobuf ); + size_t start = pos; + size_t end = ( pos + len ); + int rc; + + /* Discard zero-length packets and packets which lie entirely + * outside the trimmed range. + */ + if ( ( start >= peerblk->end ) || ( end <= peerblk->start ) || + ( len == 0 ) ) { + free_iob ( iobuf ); + return 0; + } + + /* Truncate data to within trimmed range */ + if ( start < peerblk->start ) { + iob_pull ( iobuf, ( peerblk->start - start ) ); + start = peerblk->start; + } + if ( end > peerblk->end ) { + iob_unput ( iobuf, ( end - peerblk->end ) ); + end = peerblk->end; + } + + /* Construct metadata */ + memcpy ( &xfer_meta, meta, sizeof ( xfer_meta ) ); + xfer_meta.flags |= XFER_FL_ABS_OFFSET; + xfer_meta.offset = peerblk_offset ( peerblk, start ); + + /* Deliver data */ + if ( ( rc = xfer_deliver ( &peerblk->xfer, iob_disown ( iobuf ), + &xfer_meta ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not deliver data: %s\n", + peerblk, peerblk->segment, peerblk->block, + strerror ( rc ) ); + return rc; + } + + return 0; +} + +/** + * Finish PeerDist block download attempt + * + * @v peerblk PeerDist block download + * @v rc Reason for close + */ +static void peerblk_done ( struct peerdist_block *peerblk, int rc ) { + struct digest_algorithm *digest = peerblk->digest; + uint8_t hash[digest->digestsize]; + unsigned long now = peerblk_timestamp(); + + /* Check for errors on completion */ + if ( rc != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d attempt failed: %s\n", + peerblk, peerblk->segment, peerblk->block, + strerror ( rc ) ); + goto err; + } + + /* Check digest */ + digest_final ( digest, peerblk->digestctx, hash ); + if ( memcmp ( hash, peerblk->hash, peerblk->digestsize ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d digest mismatch:\n", + peerblk, peerblk->segment, peerblk->block ); + DBGC_HDA ( peerblk, 0, hash, peerblk->digestsize ); + DBGC_HDA ( peerblk, 0, peerblk->hash, peerblk->digestsize ); + rc = -EIO; + goto err; + } + + /* Profile successful attempt */ + profile_custom ( &peerblk_attempt_success_profiler, + ( now - peerblk->attempted ) ); + + /* Close download */ + peerblk_close ( peerblk, 0 ); + return; + + err: + /* Record failure reason and schedule a retry attempt */ + profile_custom ( &peerblk_attempt_failure_profiler, + ( now - peerblk->attempted ) ); + peerblk_reset ( peerblk, rc ); + peerblk->rc = rc; + start_timer_nodelay ( &peerblk->timer ); +} + +/****************************************************************************** + * + * Raw block download attempts (using an HTTP range request) + * + ****************************************************************************** + */ + +/** + * Open PeerDist raw block download attempt + * + * @v peerblk PeerDist block download + * @ret rc Return status code + */ +static int peerblk_raw_open ( struct peerdist_block *peerblk ) { + struct http_request_range range; + int rc; + + DBGC2 ( peerblk, "PEERBLK %p %d.%d attempting raw range request\n", + peerblk, peerblk->segment, peerblk->block ); + + /* Construct HTTP range */ + memset ( &range, 0, sizeof ( range ) ); + range.start = peerblk->range.start; + range.len = ( peerblk->range.end - peerblk->range.start ); + + /* Initiate range request to retrieve block */ + if ( ( rc = http_open ( &peerblk->raw, &http_get, peerblk->uri, + &range, NULL ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not create range " + "request: %s\n", peerblk, peerblk->segment, + peerblk->block, strerror ( rc ) ); + return rc; + } + + /* Annul HTTP connection (for testing) if applicable. Do not + * report as an immediate error, in order to test our ability + * to recover from a totally unresponsive HTTP server. + */ + if ( inject_fault ( PEERBLK_ANNUL_RATE ) ) + intf_restart ( &peerblk->raw, 0 ); + + return 0; +} + +/** + * Receive PeerDist raw data + * + * @v peerblk PeerDist block download + * @v iobuf I/O buffer + * @v meta Data transfer metadata + * @ret rc Return status code + */ +static int peerblk_raw_rx ( struct peerdist_block *peerblk, + struct io_buffer *iobuf, + struct xfer_metadata *meta ) { + size_t len = iob_len ( iobuf ); + size_t pos = peerblk->pos; + size_t mid = ( ( peerblk->range.end - peerblk->range.start ) / 2 ); + int rc; + + /* Corrupt received data (for testing) if applicable */ + inject_corruption ( PEERBLK_CORRUPT_RATE, iobuf->data, len ); + + /* Fail if data is delivered out of order, since the streaming + * digest requires strict ordering. + */ + if ( ( rc = xfer_check_order ( meta, &peerblk->pos, len ) ) != 0 ) + goto err; + + /* Add data to digest */ + digest_update ( peerblk->digest, peerblk->digestctx, iobuf->data, len ); + + /* Deliver data */ + if ( ( rc = peerblk_deliver ( peerblk, iob_disown ( iobuf ), meta, + pos ) ) != 0 ) + goto err; + + /* Extend download attempt timer */ + start_timer_fixed ( &peerblk->timer, PEERBLK_RAW_RX_TIMEOUT ); + + /* Stall download attempt (for testing) if applicable */ + if ( ( pos < mid ) && ( ( pos + len ) >= mid ) && + ( ( rc = inject_fault ( PEERBLK_STALL_RATE ) ) != 0 ) ) { + intf_restart ( &peerblk->raw, rc ); + } + + return 0; + + err: + free_iob ( iobuf ); + peerblk_done ( peerblk, rc ); + return rc; +} + +/** + * Close PeerDist raw block download attempt + * + * @v peerblk PeerDist block download + * @v rc Reason for close + */ +static void peerblk_raw_close ( struct peerdist_block *peerblk, int rc ) { + + /* Restart interface */ + intf_restart ( &peerblk->raw, rc ); + + /* Fail immediately if we have an error */ + if ( rc != 0 ) + goto done; + + /* Abort download attempt (for testing) if applicable */ + if ( ( rc = inject_fault ( PEERBLK_ABORT_RATE ) ) != 0 ) + goto done; + + done: + /* Complete download attempt */ + peerblk_done ( peerblk, rc ); +} + +/****************************************************************************** + * + * Retrieval protocol block download attempts (using HTTP POST) + * + ****************************************************************************** + */ + +/** + * Construct PeerDist retrieval protocol URI + * + * @v location Peer location + * @ret uri Retrieval URI, or NULL on error + */ +static struct uri * peerblk_retrieval_uri ( const char *location ) { + char uri_string[ 7 /* "http://" */ + strlen ( location ) + + sizeof ( PEERDIST_MAGIC_PATH /* includes NUL */ ) ]; + + /* Construct URI string */ + snprintf ( uri_string, sizeof ( uri_string ), + ( "http://%s" PEERDIST_MAGIC_PATH ), location ); + + /* Parse URI string */ + return parse_uri ( uri_string ); +} + +/** + * Open PeerDist retrieval protocol block download attempt + * + * @v peerblk PeerDist block download + * @v location Peer location + * @ret rc Return status code + */ +static int peerblk_retrieval_open ( struct peerdist_block *peerblk, + const char *location ) { + size_t digestsize = peerblk->digestsize; + peerdist_msg_getblks_t ( digestsize, 1, 0 ) req; + peerblk_msg_blk_t ( digestsize, 0, 0, 0 ) *rsp; + struct http_request_content content; + struct uri *uri; + int rc; + + DBGC2 ( peerblk, "PEERBLK %p %d.%d attempting retrieval from %s\n", + peerblk, peerblk->segment, peerblk->block, location ); + + /* Construct block fetch request */ + memset ( &req, 0, sizeof ( req ) ); + req.getblks.hdr.version.raw = htonl ( PEERDIST_MSG_GETBLKS_VERSION ); + req.getblks.hdr.type = htonl ( PEERDIST_MSG_GETBLKS_TYPE ); + req.getblks.hdr.len = htonl ( sizeof ( req ) ); + req.getblks.hdr.algorithm = htonl ( PEERDIST_MSG_AES_128_CBC ); + req.segment.segment.digestsize = htonl ( digestsize ); + memcpy ( req.segment.id, peerblk->id, digestsize ); + req.ranges.ranges.count = htonl ( 1 ); + req.ranges.range[0].first = htonl ( peerblk->block ); + req.ranges.range[0].count = htonl ( 1 ); + + /* Construct POST request content */ + memset ( &content, 0, sizeof ( content ) ); + content.data = &req; + content.len = sizeof ( req ); + + /* Construct URI */ + if ( ( uri = peerblk_retrieval_uri ( location ) ) == NULL ) { + rc = -ENOMEM; + goto err_uri; + } + + /* Update trim thresholds */ + peerblk->start += offsetof ( typeof ( *rsp ), msg.vrf ); + peerblk->end += offsetof ( typeof ( *rsp ), msg.vrf ); + + /* Initiate HTTP POST to retrieve block */ + if ( ( rc = http_open ( &peerblk->retrieval, &http_post, uri, + NULL, &content ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not create retrieval " + "request: %s\n", peerblk, peerblk->segment, + peerblk->block, strerror ( rc ) ); + goto err_open; + } + + /* Annul HTTP connection (for testing) if applicable. Do not + * report as an immediate error, in order to test our ability + * to recover from a totally unresponsive HTTP server. + */ + if ( inject_fault ( PEERBLK_ANNUL_RATE ) ) + intf_restart ( &peerblk->retrieval, 0 ); + + err_open: + uri_put ( uri ); + err_uri: + return rc; +} + +/** + * Receive PeerDist retrieval protocol data + * + * @v peerblk PeerDist block download + * @v iobuf I/O buffer + * @v meta Data transfer metadata + * @ret rc Return status code + */ +static int peerblk_retrieval_rx ( struct peerdist_block *peerblk, + struct io_buffer *iobuf, + struct xfer_metadata *meta ) { + size_t len = iob_len ( iobuf ); + size_t start; + size_t end; + size_t before; + size_t after; + size_t cut; + int rc; + + /* Some genius at Microsoft thought it would be a great idea + * to place the AES-CBC initialisation vector *after* the + * encrypted data, thereby making it logically impossible to + * decrypt each packet as it arrives. + * + * To work around this mindless stupidity, we deliver the + * ciphertext as-is and later use xfer_buffer() to obtain + * access to the underlying data transfer buffer in order to + * perform the decryption. + * + * There will be some data both before and after the bytes + * corresponding to the trimmed plaintext: a MSG_BLK + * header/footer, some block padding for the AES-CBC cipher, + * and a possibly large quantity of unwanted ciphertext which + * is excluded from the trimmed content range. We store this + * data in a local data transfer buffer. If the amount of + * data to be stored is too large, we will fail allocation and + * so eventually fall back to using a range request (which + * does not require this kind of temporary storage + * allocation). + */ + + /* Corrupt received data (for testing) if applicable */ + inject_corruption ( PEERBLK_CORRUPT_RATE, iobuf->data, len ); + + /* Calculate start and end positions of this buffer */ + start = peerblk->pos; + if ( meta->flags & XFER_FL_ABS_OFFSET ) + start = 0; + start += meta->offset; + end = ( start + len ); + + /* Buffer any data before the trimmed content */ + if ( ( start < peerblk->start ) && ( len > 0 ) ) { + + /* Calculate length of data before the trimmed content */ + before = ( peerblk->start - start ); + if ( before > len ) + before = len; + + /* Buffer data before the trimmed content */ + if ( ( rc = xferbuf_write ( &peerblk->buffer, start, + iobuf->data, before ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not buffer " + "data: %s\n", peerblk, peerblk->segment, + peerblk->block, strerror ( rc ) ); + goto err; + } + } + + /* Buffer any data after the trimmed content */ + if ( ( end > peerblk->end ) && ( len > 0 ) ) { + + /* Calculate length of data after the trimmed content */ + after = ( end - peerblk->end ); + if ( after > len ) + after = len; + + /* Buffer data after the trimmed content */ + cut = ( peerblk->end - peerblk->start ); + if ( ( rc = xferbuf_write ( &peerblk->buffer, + ( end - after - cut ), + ( iobuf->data + len - after ), + after ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not buffer " + "data: %s\n", peerblk, peerblk->segment, + peerblk->block, strerror ( rc ) ); + goto err; + } + } + + /* Deliver any remaining data */ + if ( ( rc = peerblk_deliver ( peerblk, iob_disown ( iobuf ), meta, + start ) ) != 0 ) + goto err; + + /* Update position */ + peerblk->pos = end; + + /* Extend download attempt timer */ + start_timer_fixed ( &peerblk->timer, PEERBLK_RETRIEVAL_RX_TIMEOUT ); + + /* Stall download attempt (for testing) if applicable */ + if ( ( start < peerblk->end ) && ( end >= peerblk->end ) && + ( ( rc = inject_fault ( PEERBLK_STALL_RATE ) ) != 0 ) ) { + intf_restart ( &peerblk->retrieval, rc ); + } + + return 0; + + err: + free_iob ( iobuf ); + peerblk_done ( peerblk, rc ); + return rc; +} + +/** + * Parse retrieval protocol message header + * + * @v peerblk PeerDist block download + * @ret rc Return status code + */ +static int peerblk_parse_header ( struct peerdist_block *peerblk ) { + struct { + struct peerdist_msg_transport_header hdr; + struct peerdist_msg_header msg; + } __attribute__ (( packed )) *msg = peerblk->buffer.data; + struct cipher_algorithm *cipher; + size_t len = peerblk->buffer.len; + size_t keylen = 0; + int rc; + + /* Check message length */ + if ( len < sizeof ( *msg ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d message too short for header " + "(%zd bytes)\n", peerblk, peerblk->segment, + peerblk->block, len ); + return -ERANGE; + } + + /* Check message type */ + if ( msg->msg.type != htonl ( PEERDIST_MSG_BLK_TYPE ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d unexpected message type " + "%#08x\n", peerblk, peerblk->segment, peerblk->block, + ntohl ( msg->msg.type ) ); + return -EPROTO; + } + + /* Determine cipher algorithm and key length */ + cipher = &aes_cbc_algorithm; + switch ( msg->msg.algorithm ) { + case htonl ( PEERDIST_MSG_PLAINTEXT ) : + cipher = NULL; + break; + case htonl ( PEERDIST_MSG_AES_128_CBC ) : + keylen = ( 128 / 8 ); + break; + case htonl ( PEERDIST_MSG_AES_192_CBC ) : + keylen = ( 192 / 8 ); + break; + case htonl ( PEERDIST_MSG_AES_256_CBC ) : + keylen = ( 256 / 8 ); + break; + default: + DBGC ( peerblk, "PEERBLK %p %d.%d unrecognised algorithm " + "%#08x\n", peerblk, peerblk->segment, peerblk->block, + ntohl ( msg->msg.algorithm ) ); + return -ENOTSUP; + } + DBGC2 ( peerblk, "PEERBLK %p %d.%d using %s with %zd-bit key\n", + peerblk, peerblk->segment, peerblk->block, + ( cipher ? cipher->name : "plaintext" ), ( 8 * keylen ) ); + + /* Sanity check key length against maximum secret length */ + if ( keylen > peerblk->digestsize ) { + DBGC ( peerblk, "PEERBLK %p %d.%d %zd-byte secret too short " + "for %zd-bit key\n", peerblk, peerblk->segment, + peerblk->block, peerblk->digestsize, ( 8 * keylen ) ); + return -EPROTO; + } + + /* Allocate cipher context. Freeing the cipher context (on + * error or otherwise) is handled by peerblk_reset(). + */ + peerblk->cipher = cipher; + assert ( peerblk->cipherctx == NULL ); + peerblk->cipherctx = malloc ( cipher->ctxsize ); + if ( ! peerblk->cipherctx ) + return -ENOMEM; + + /* Initialise cipher */ + if ( ( rc = cipher_setkey ( cipher, peerblk->cipherctx, peerblk->secret, + keylen ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not set key: %s\n", + peerblk, peerblk->segment, peerblk->block, + strerror ( rc ) ); + return rc; + } + + return 0; +} + +/** + * Parse retrieval protocol message segment and block details + * + * @v peerblk PeerDist block download + * @v buf_len Length of buffered data to fill in + * @ret rc Return status code + */ +static int peerblk_parse_block ( struct peerdist_block *peerblk, + size_t *buf_len ) { + size_t digestsize = peerblk->digestsize; + peerblk_msg_blk_t ( digestsize, 0, 0, 0 ) *msg = peerblk->buffer.data; + size_t len = peerblk->buffer.len; + size_t data_len; + size_t total; + + /* Check message length */ + if ( len < offsetof ( typeof ( *msg ), msg.block.data ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d message too short for " + "zero-length data (%zd bytes)\n", peerblk, + peerblk->segment, peerblk->block, len ); + return -ERANGE; + } + + /* Check digest size */ + if ( ntohl ( msg->msg.segment.segment.digestsize ) != digestsize ) { + DBGC ( peerblk, "PEERBLK %p %d.%d incorrect digest size %d\n", + peerblk, peerblk->segment, peerblk->block, + ntohl ( msg->msg.segment.segment.digestsize ) ); + return -EPROTO; + } + + /* Check segment ID */ + if ( memcmp ( msg->msg.segment.id, peerblk->id, digestsize ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d segment ID mismatch\n", + peerblk, peerblk->segment, peerblk->block ); + return -EPROTO; + } + + /* Check block ID */ + if ( ntohl ( msg->msg.index ) != peerblk->block ) { + DBGC ( peerblk, "PEERBLK %p %d.%d block ID mismatch (got %d)\n", + peerblk, peerblk->segment, peerblk->block, + ntohl ( msg->msg.index ) ); + return -EPROTO; + } + + /* Check for missing blocks */ + data_len = be32_to_cpu ( msg->msg.block.block.len ); + if ( ! data_len ) { + DBGC ( peerblk, "PEERBLK %p %d.%d block not found\n", + peerblk, peerblk->segment, peerblk->block ); + return -ENOENT; + } + + /* Check for underlength blocks */ + if ( data_len < ( peerblk->range.end - peerblk->range.start ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d underlength block (%zd " + "bytes)\n", peerblk, peerblk->segment, peerblk->block, + data_len ); + return -ERANGE; + } + + /* Calculate buffered data length (i.e. excluding data which + * was delivered to the final data transfer buffer). + */ + *buf_len = ( data_len - ( peerblk->end - peerblk->start ) ); + + /* Describe data before the trimmed content */ + peerblk->decrypt[PEERBLK_BEFORE].xferbuf = &peerblk->buffer; + peerblk->decrypt[PEERBLK_BEFORE].offset = + offsetof ( typeof ( *msg ), msg.block.data ); + peerblk->decrypt[PEERBLK_BEFORE].len = + ( peerblk->start - + offsetof ( typeof ( *msg ), msg.block.data ) ); + total = peerblk->decrypt[PEERBLK_BEFORE].len; + + /* Describe data within the trimmed content */ + peerblk->decrypt[PEERBLK_DURING].offset = + peerblk_offset ( peerblk, peerblk->start ); + peerblk->decrypt[PEERBLK_DURING].len = + ( peerblk->end - peerblk->start ); + total += peerblk->decrypt[PEERBLK_DURING].len; + + /* Describe data after the trimmed content */ + peerblk->decrypt[PEERBLK_AFTER].xferbuf = &peerblk->buffer; + peerblk->decrypt[PEERBLK_AFTER].offset = peerblk->start; + peerblk->decrypt[PEERBLK_AFTER].len = + ( offsetof ( typeof ( *msg ), msg.block.data ) + + *buf_len - peerblk->start ); + total += peerblk->decrypt[PEERBLK_AFTER].len; + + /* Sanity check */ + assert ( total == be32_to_cpu ( msg->msg.block.block.len ) ); + + /* Initialise cipher and digest lengths */ + peerblk->cipher_remaining = total; + peerblk->digest_remaining = + ( peerblk->range.end - peerblk->range.start ); + assert ( peerblk->cipher_remaining >= peerblk->digest_remaining ); + + return 0; +} + +/** + * Parse retrieval protocol message useless details + * + * @v peerblk PeerDist block download + * @v buf_len Length of buffered data + * @v vrf_len Length of uselessness to fill in + * @ret rc Return status code + */ +static int peerblk_parse_useless ( struct peerdist_block *peerblk, + size_t buf_len, size_t *vrf_len ) { + size_t digestsize = peerblk->digestsize; + peerblk_msg_blk_t ( digestsize, buf_len, 0, 0 ) *msg = + peerblk->buffer.data; + size_t len = peerblk->buffer.len; + + /* Check message length */ + if ( len < offsetof ( typeof ( *msg ), msg.vrf.data ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d message too short for " + "zero-length uselessness (%zd bytes)\n", peerblk, + peerblk->segment, peerblk->block, len ); + return -ERANGE; + } + + /* Extract length of uselessness */ + *vrf_len = be32_to_cpu ( msg->msg.vrf.vrf.len ); + + return 0; +} + +/** + * Parse retrieval protocol message initialisation vector details + * + * @v peerblk PeerDist block download + * @v buf_len Length of buffered data + * @v vrf_len Length of uselessness + * @ret rc Return status code + */ +static int peerblk_parse_iv ( struct peerdist_block *peerblk, size_t buf_len, + size_t vrf_len ) { + size_t digestsize = peerblk->digestsize; + size_t blksize = peerblk->cipher->blocksize; + peerblk_msg_blk_t ( digestsize, buf_len, vrf_len, blksize ) *msg = + peerblk->buffer.data; + size_t len = peerblk->buffer.len; + + /* Check message length */ + if ( len < sizeof ( *msg ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d message too short for " + "initialisation vector (%zd bytes)\n", peerblk, + peerblk->segment, peerblk->block, len ); + return -ERANGE; + } + + /* Check initialisation vector size */ + if ( ntohl ( msg->msg.iv.iv.blksize ) != blksize ) { + DBGC ( peerblk, "PEERBLK %p %d.%d incorrect IV size %d\n", + peerblk, peerblk->segment, peerblk->block, + ntohl ( msg->msg.iv.iv.blksize ) ); + return -EPROTO; + } + + /* Set initialisation vector */ + cipher_setiv ( peerblk->cipher, peerblk->cipherctx, msg->msg.iv.data ); + + return 0; +} + +/** + * Read from decryption buffers + * + * @v peerblk PeerDist block download + * @v data Data buffer + * @v len Length to read + * @ret rc Return status code + */ +static int peerblk_decrypt_read ( struct peerdist_block *peerblk, + void *data, size_t len ) { + struct peerdist_block_decrypt *decrypt = peerblk->decrypt; + size_t frag_len; + int rc; + + /* Read from each decryption buffer in turn */ + for ( ; len ; decrypt++, data += frag_len, len -= frag_len ) { + + /* Calculate length to use from this buffer */ + frag_len = decrypt->len; + if ( frag_len > len ) + frag_len = len; + if ( ! frag_len ) + continue; + + /* Read from this buffer */ + if ( ( rc = xferbuf_read ( decrypt->xferbuf, decrypt->offset, + data, frag_len ) ) != 0 ) + return rc; + } + + return 0; +} + +/** + * Write to decryption buffers and update offsets and lengths + * + * @v peerblk PeerDist block download + * @v data Data buffer + * @v len Length to read + * @ret rc Return status code + */ +static int peerblk_decrypt_write ( struct peerdist_block *peerblk, + const void *data, size_t len ) { + struct peerdist_block_decrypt *decrypt = peerblk->decrypt; + size_t frag_len; + int rc; + + /* Write to each decryption buffer in turn */ + for ( ; len ; decrypt++, data += frag_len, len -= frag_len ) { + + /* Calculate length to use from this buffer */ + frag_len = decrypt->len; + if ( frag_len > len ) + frag_len = len; + if ( ! frag_len ) + continue; + + /* Write to this buffer */ + if ( ( rc = xferbuf_write ( decrypt->xferbuf, decrypt->offset, + data, frag_len ) ) != 0 ) + return rc; + + /* Update offset and length */ + decrypt->offset += frag_len; + decrypt->len -= frag_len; + } + + return 0; +} + +/** + * Decrypt one chunk of PeerDist retrieval protocol data + * + * @v peerblk PeerDist block download + */ +static void peerblk_decrypt ( struct peerdist_block *peerblk ) { + struct cipher_algorithm *cipher = peerblk->cipher; + struct digest_algorithm *digest = peerblk->digest; + struct xfer_buffer *xferbuf; + size_t cipher_len; + size_t digest_len; + void *data; + int rc; + + /* Sanity check */ + assert ( ( PEERBLK_DECRYPT_CHUNKSIZE % cipher->blocksize ) == 0 ); + + /* Get the underlying data transfer buffer */ + xferbuf = xfer_buffer ( &peerblk->xfer ); + if ( ! xferbuf ) { + DBGC ( peerblk, "PEERBLK %p %d.%d has no underlying data " + "transfer buffer\n", peerblk, peerblk->segment, + peerblk->block ); + rc = -ENOTSUP; + goto err_xfer_buffer; + } + peerblk->decrypt[PEERBLK_DURING].xferbuf = xferbuf; + + /* Calculate cipher and digest lengths */ + cipher_len = PEERBLK_DECRYPT_CHUNKSIZE; + if ( cipher_len > peerblk->cipher_remaining ) + cipher_len = peerblk->cipher_remaining; + digest_len = cipher_len; + if ( digest_len > peerblk->digest_remaining ) + digest_len = peerblk->digest_remaining; + assert ( ( cipher_len & ( cipher->blocksize - 1 ) ) == 0 ); + + /* Allocate temporary data buffer */ + data = malloc ( cipher_len ); + if ( ! data ) { + rc = -ENOMEM; + goto err_alloc_data; + } + + /* Read ciphertext */ + if ( ( rc = peerblk_decrypt_read ( peerblk, data, cipher_len ) ) != 0 ){ + DBGC ( peerblk, "PEERBLK %p %d.%d could not read ciphertext: " + "%s\n", peerblk, peerblk->segment, peerblk->block, + strerror ( rc ) ); + goto err_read; + } + + /* Decrypt data */ + cipher_decrypt ( cipher, peerblk->cipherctx, data, data, cipher_len ); + + /* Add data to digest */ + digest_update ( digest, peerblk->digestctx, data, digest_len ); + + /* Write plaintext */ + if ( ( rc = peerblk_decrypt_write ( peerblk, data, cipher_len ) ) != 0){ + DBGC ( peerblk, "PEERBLK %p %d.%d could not write plaintext: " + "%s\n", peerblk, peerblk->segment, peerblk->block, + strerror ( rc ) ); + goto err_write; + } + + /* Consume input */ + peerblk->cipher_remaining -= cipher_len; + peerblk->digest_remaining -= digest_len; + + /* Free temporary data buffer */ + free ( data ); + + /* Continue processing until all input is consumed */ + if ( peerblk->cipher_remaining ) + return; + + /* Complete download attempt */ + peerblk_done ( peerblk, 0 ); + return; + + err_write: + err_read: + free ( data ); + err_alloc_data: + err_xfer_buffer: + peerblk_done ( peerblk, rc ); +} + +/** + * Close PeerDist retrieval protocol block download attempt + * + * @v peerblk PeerDist block download + * @v rc Reason for close + */ +static void peerblk_retrieval_close ( struct peerdist_block *peerblk, int rc ) { + size_t buf_len; + size_t vrf_len; + + /* Restart interface */ + intf_restart ( &peerblk->retrieval, rc ); + + /* Fail immediately if we have an error */ + if ( rc != 0 ) + goto done; + + /* Abort download attempt (for testing) if applicable */ + if ( ( rc = inject_fault ( PEERBLK_ABORT_RATE ) ) != 0 ) + goto done; + + /* Parse message header */ + if ( ( rc = peerblk_parse_header ( peerblk ) ) != 0 ) + goto done; + + /* Parse message segment and block details */ + if ( ( rc = peerblk_parse_block ( peerblk, &buf_len ) ) != 0 ) + goto done; + + /* If the block was plaintext, then there is nothing more to do */ + if ( ! peerblk->cipher ) + goto done; + + /* Parse message useless details */ + if ( ( rc = peerblk_parse_useless ( peerblk, buf_len, &vrf_len ) ) != 0) + goto done; + + /* Parse message initialisation vector details */ + if ( ( rc = peerblk_parse_iv ( peerblk, buf_len, vrf_len ) ) != 0 ) + goto done; + + /* Fail if decryption length is not aligned to the cipher block size */ + if ( peerblk->cipher_remaining & ( peerblk->cipher->blocksize - 1 ) ) { + DBGC ( peerblk, "PEERBLK %p %d.%d unaligned data length %zd\n", + peerblk, peerblk->segment, peerblk->block, + peerblk->cipher_remaining ); + rc = -EPROTO; + goto done; + } + + /* Stop the download attempt timer: there is no point in + * timing out while decrypting. + */ + stop_timer ( &peerblk->timer ); + + /* Start decryption process */ + process_add ( &peerblk->process ); + return; + + done: + /* Complete download attempt */ + peerblk_done ( peerblk, rc ); +} + +/****************************************************************************** + * + * Retry policy + * + ****************************************************************************** + */ + +/** + * Handle PeerDist retry timer expiry + * + * @v timer Retry timer + * @v over Failure indicator + */ +static void peerblk_expired ( struct retry_timer *timer, int over __unused ) { + struct peerdist_block *peerblk = + container_of ( timer, struct peerdist_block, timer ); + struct peerdisc_segment *segment = peerblk->discovery.segment; + struct peerdisc_peer *head; + unsigned long now = peerblk_timestamp(); + const char *location; + int rc; + + /* Profile discovery timeout, if applicable */ + if ( ( peerblk->peer == NULL ) && ( timer->timeout != 0 ) ) { + profile_custom ( &peerblk_discovery_timeout_profiler, + ( now - peerblk->started ) ); + DBGC ( peerblk, "PEERBLK %p %d.%d discovery timed out after " + "%ld ticks\n", peerblk, peerblk->segment, + peerblk->block, timer->timeout ); + } + + /* Profile download timeout, if applicable */ + if ( ( peerblk->peer != NULL ) && ( timer->timeout != 0 ) ) { + profile_custom ( &peerblk_attempt_timeout_profiler, + ( now - peerblk->attempted ) ); + DBGC ( peerblk, "PEERBLK %p %d.%d timed out after %ld ticks\n", + peerblk, peerblk->segment, peerblk->block, + timer->timeout ); + } + + /* Abort any current download attempt */ + peerblk_reset ( peerblk, -ETIMEDOUT ); + + /* Record attempt start time */ + peerblk->attempted = now; + + /* If we have exceeded our maximum number of attempt cycles + * (each cycle comprising a retrieval protocol download from + * each peer in the list followed by a raw download from the + * origin server), then abort the overall download. + */ + head = list_entry ( &segment->peers, struct peerdisc_peer, list ); + if ( ( peerblk->peer == head ) && + ( ++peerblk->cycles >= PEERBLK_MAX_ATTEMPT_CYCLES ) ) { + rc = peerblk->rc; + assert ( rc != 0 ); + goto err; + } + + /* If we have not yet made any download attempts, then move to + * the start of the peer list. + */ + if ( peerblk->peer == NULL ) + peerblk->peer = head; + + /* Attempt retrieval protocol download from next usable peer */ + list_for_each_entry_continue ( peerblk->peer, &segment->peers, list ) { + + /* Attempt retrieval protocol download from this peer */ + location = peerblk->peer->location; + if ( ( rc = peerblk_retrieval_open ( peerblk, + location ) ) != 0 ) { + /* Non-fatal: continue to try next peer */ + continue; + } + + /* Start download attempt timer */ + peerblk->rc = -ETIMEDOUT; + start_timer_fixed ( &peerblk->timer, + PEERBLK_RETRIEVAL_OPEN_TIMEOUT ); + return; + } + + /* Attempt raw download */ + if ( ( rc = peerblk_raw_open ( peerblk ) ) != 0 ) + goto err; + + /* Start download attempt timer */ + peerblk->rc = -ETIMEDOUT; + start_timer_fixed ( &peerblk->timer, PEERBLK_RAW_OPEN_TIMEOUT ); + return; + + err: + peerblk_close ( peerblk, rc ); +} + +/** + * Handle PeerDist peer discovery + * + * @v discovery PeerDist discovery client + */ +static void peerblk_discovered ( struct peerdisc_client *discovery ) { + struct peerdist_block *peerblk = + container_of ( discovery, struct peerdist_block, discovery ); + unsigned long now = peerblk_timestamp(); + + /* Do nothing unless we are still waiting for the initial + * discovery timeout. + */ + if ( ( peerblk->peer != NULL ) || ( peerblk->timer.timeout == 0 ) ) + return; + + /* Schedule an immediate retry */ + start_timer_nodelay ( &peerblk->timer ); + + /* Profile discovery success */ + profile_custom ( &peerblk_discovery_success_profiler, + ( now - peerblk->started ) ); +} + +/****************************************************************************** + * + * Opener + * + ****************************************************************************** + */ + +/** PeerDist block download data transfer interface operations */ +static struct interface_operation peerblk_xfer_operations[] = { + INTF_OP ( intf_close, struct peerdist_block *, peerblk_close ), +}; + +/** PeerDist block download data transfer interface descriptor */ +static struct interface_descriptor peerblk_xfer_desc = + INTF_DESC ( struct peerdist_block, xfer, peerblk_xfer_operations ); + +/** PeerDist block download raw data interface operations */ +static struct interface_operation peerblk_raw_operations[] = { + INTF_OP ( xfer_deliver, struct peerdist_block *, peerblk_raw_rx ), + INTF_OP ( intf_close, struct peerdist_block *, peerblk_raw_close ), +}; + +/** PeerDist block download raw data interface descriptor */ +static struct interface_descriptor peerblk_raw_desc = + INTF_DESC ( struct peerdist_block, raw, peerblk_raw_operations ); + +/** PeerDist block download retrieval protocol interface operations */ +static struct interface_operation peerblk_retrieval_operations[] = { + INTF_OP ( xfer_deliver, struct peerdist_block *, peerblk_retrieval_rx ), + INTF_OP ( intf_close, struct peerdist_block *, peerblk_retrieval_close), +}; + +/** PeerDist block download retrieval protocol interface descriptor */ +static struct interface_descriptor peerblk_retrieval_desc = + INTF_DESC ( struct peerdist_block, retrieval, + peerblk_retrieval_operations ); + +/** PeerDist block download decryption process descriptor */ +static struct process_descriptor peerblk_process_desc = + PROC_DESC ( struct peerdist_block, process, peerblk_decrypt ); + +/** PeerDist block download discovery operations */ +static struct peerdisc_client_operations peerblk_discovery_operations = { + .discovered = peerblk_discovered, +}; + +/** + * Open PeerDist block download + * + * @v xfer Data transfer interface + * @v uri Original URI + * @v info Content information block + * @ret rc Return status code + */ +int peerblk_open ( struct interface *xfer, struct uri *uri, + struct peerdist_info_block *block ) { + const struct peerdist_info_segment *segment = block->segment; + const struct peerdist_info *info = segment->info; + struct digest_algorithm *digest = info->digest; + struct peerdist_block *peerblk; + unsigned long timeout; + size_t digestsize; + int rc; + + /* Allocate and initialise structure */ + peerblk = zalloc ( sizeof ( *peerblk ) + digest->ctxsize ); + if ( ! peerblk ) { + rc = -ENOMEM; + goto err_alloc; + } + ref_init ( &peerblk->refcnt, peerblk_free ); + intf_init ( &peerblk->xfer, &peerblk_xfer_desc, &peerblk->refcnt ); + intf_init ( &peerblk->raw, &peerblk_raw_desc, &peerblk->refcnt ); + intf_init ( &peerblk->retrieval, &peerblk_retrieval_desc, + &peerblk->refcnt ); + peerblk->uri = uri_get ( uri ); + memcpy ( &peerblk->range, &block->range, sizeof ( peerblk->range ) ); + memcpy ( &peerblk->trim, &block->trim, sizeof ( peerblk->trim ) ); + peerblk->offset = ( block->trim.start - info->trim.start ); + peerblk->digest = info->digest; + peerblk->digestsize = digestsize = info->digestsize; + peerblk->digestctx = ( ( ( void * ) peerblk ) + sizeof ( *peerblk ) ); + peerblk->segment = segment->index; + memcpy ( peerblk->id, segment->id, sizeof ( peerblk->id ) ); + memcpy ( peerblk->secret, segment->secret, sizeof ( peerblk->secret ) ); + peerblk->block = block->index; + memcpy ( peerblk->hash, block->hash, sizeof ( peerblk->hash ) ); + xferbuf_malloc_init ( &peerblk->buffer ); + process_init_stopped ( &peerblk->process, &peerblk_process_desc, + &peerblk->refcnt ); + peerdisc_init ( &peerblk->discovery, &peerblk_discovery_operations ); + timer_init ( &peerblk->timer, peerblk_expired, &peerblk->refcnt ); + DBGC2 ( peerblk, "PEERBLK %p %d.%d id %02x%02x%02x%02x%02x..." + "%02x%02x%02x [%08zx,%08zx)", peerblk, peerblk->segment, + peerblk->block, peerblk->id[0], peerblk->id[1], peerblk->id[2], + peerblk->id[3], peerblk->id[4], peerblk->id[ digestsize - 3 ], + peerblk->id[ digestsize - 2 ], peerblk->id[ digestsize - 1 ], + peerblk->range.start, peerblk->range.end ); + if ( ( peerblk->trim.start != peerblk->range.start ) || + ( peerblk->trim.end != peerblk->range.end ) ) { + DBGC2 ( peerblk, " covers [%08zx,%08zx)", + peerblk->trim.start, peerblk->trim.end ); + } + DBGC2 ( peerblk, "\n" ); + + /* Open discovery */ + if ( ( rc = peerdisc_open ( &peerblk->discovery, peerblk->id, + peerblk->digestsize ) ) != 0 ) + goto err_open_discovery; + + /* Schedule a retry attempt either immediately (if we already + * have some peers) or after the discovery timeout. + */ + timeout = ( list_empty ( &peerblk->discovery.segment->peers ) ? + ( peerdisc_timeout_secs * TICKS_PER_SEC ) : 0 ); + start_timer_fixed ( &peerblk->timer, timeout ); + + /* Record start time */ + peerblk->started = peerblk_timestamp(); + + /* Attach to parent interface, mortalise self, and return */ + intf_plug_plug ( xfer, &peerblk->xfer ); + ref_put ( &peerblk->refcnt ); + return 0; + + err_open_discovery: + peerblk_close ( peerblk, rc ); + err_alloc: + return rc; +} |