diff --git a/src/include/ipxe/errfile.h b/src/include/ipxe/errfile.h index 18367f464..90a792b8c 100644 --- a/src/include/ipxe/errfile.h +++ b/src/include/ipxe/errfile.h @@ -327,6 +327,7 @@ FILE_SECBOOT ( PERMITTED ); #define ERRFILE_eap_md5 ( ERRFILE_NET | 0x004d0000 ) #define ERRFILE_eap_mschapv2 ( ERRFILE_NET | 0x004e0000 ) #define ERRFILE_syslogs ( ERRFILE_NET | 0x004f0000 ) +#define ERRFILE_pccrr ( ERRFILE_NET | 0x00500000 ) #define ERRFILE_image ( ERRFILE_IMAGE | 0x00000000 ) #define ERRFILE_elf ( ERRFILE_IMAGE | 0x00010000 ) diff --git a/src/include/ipxe/pccrr.h b/src/include/ipxe/pccrr.h index 92522d0b7..b023f5dcb 100644 --- a/src/include/ipxe/pccrr.h +++ b/src/include/ipxe/pccrr.h @@ -14,6 +14,8 @@ FILE_SECBOOT ( PERMITTED ); #include +struct interface; + /** Magic retrieval URI path */ #define PEERDIST_MAGIC_PATH "/116B50EB-ECE2-41ac-8429-9F9E963361B7/" @@ -350,4 +352,26 @@ struct peerdist_msg_blk { /** Retrieval protocol block fetch response type */ #define PEERDIST_MSG_BLK_TYPE 0x00000005UL +/** A retrieval protocol mechanism */ +struct peerdist_retrieval { + /** Name */ + const char *name; + /** + * Open retrieval protocol connection + * + * @v xfer Data transfer interface + * @v location Peer location + * @v digestsize Digest size + * @v id Segment identifier + * @v block Block index + * @ret rc Return status code + */ + int ( * open ) ( struct interface *xfer, const char *location, + size_t digestsize, const uint8_t *id, + unsigned int block ); +}; + +extern struct peerdist_retrieval peerdist_post; +extern struct peerdist_retrieval peerdist_get; + #endif /* _IPXE_PCCRR_H */ diff --git a/src/include/ipxe/peerdisc.h b/src/include/ipxe/peerdisc.h index 9a8f13ecf..0d86c3930 100644 --- a/src/include/ipxe/peerdisc.h +++ b/src/include/ipxe/peerdisc.h @@ -72,6 +72,8 @@ struct peerdisc_segment { struct peerdisc_peer { /** List of peers */ struct list_head list; + /** Retrieval protocol */ + struct peerdist_retrieval *retrieval; /** Peer location */ char location[0]; }; diff --git a/src/net/pccrr.c b/src/net/pccrr.c new file mode 100644 index 000000000..087202859 --- /dev/null +++ b/src/net/pccrr.c @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2026 Michael Brown . + * + * 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 ); +FILE_SECBOOT ( PERMITTED ); + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @file + * + * Peer Content Caching and Retrieval: Retrieval Protocol [MS-PCCRR] + * + * The MS-PCCRR specification defines an HTTP POST request/response + * pair for retrieving an encrypted block from a peer (with the + * decryption keys provided separately via the content information). + * + * The HTTP POST request parameters serve only to identify the block: + * the actual response body is effectively a static encrypted blob. + * + * We define an additional (non-standard) retrieval protocol in which + * the block is identified solely using the request URI, with the + * response being the same content that would be returned as the HTTP + * POST response body. This allows for encrypted blocks to be + * retrieved from a static source such as AWS S3 or a local FAT + * filesystem, without requiring a peer that can understand the + * retrieval protocol HTTP POST request format. + * + * We define the static request URI format as: + * + * /xx/xxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-b.blk + * + * where + * + * - `xxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` is the segment ID (HoHoDK) + * as a lower-case hexadecimal string + * + * - `xx` is the first two characters (i.e. the first byte) of the + * segment ID + * + * - `b` is the block index within the segment (which will always + * be zero when using MS-PCCRC version 2 content information), as + * an unpadded decimal string + * + * - the extension `.blk` represents a block file + * + * This path format is designed to allow for efficient storage in a + * local FAT filesystem (by limiting both the number of directories + * and the number of entries within each directory). + * + */ + +/** + * Open retrieval protocol connection using HTTP POST + * + * @v xfer Data transfer interface + * @v location Peer location + * @v digestsize Digest size + * @v id Segment identifier + * @v block Block index + * @ret rc Return status code + */ +static int peerdist_open_post ( struct interface *xfer, const char *location, + size_t digestsize, const uint8_t *id, + unsigned int block ) { + char uri_string[ 7 /* "http://" */ + strlen ( location ) + + sizeof ( PEERDIST_MAGIC_PATH /* includes NUL */ ) ]; + peerdist_msg_getblks_t ( digestsize, 1, 0 ) req; + struct http_request_content content; + struct uri *uri; + int rc; + + /* 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, id, digestsize ); + req.ranges.ranges.count = htonl ( 1 ); + req.ranges.range[0].first = htonl ( 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 string */ + snprintf ( uri_string, sizeof ( uri_string ), + ( "http://%s" PEERDIST_MAGIC_PATH ), location ); + + /* Parse URI */ + uri = parse_uri ( uri_string ); + if ( ! uri ) { + rc = -ENOMEM; + goto err_uri; + } + + /* Initiate HTTP POST to retrieve block */ + if ( ( rc = http_open ( xfer, &http_post, uri, NULL, + &content ) ) != 0 ) { + DBGC ( xfer, "PCCRR %p could not open %s: %s\n", + xfer, uri_string, strerror ( rc ) ); + goto err_open; + } + + err_open: + uri_put ( uri ); + err_uri: + return rc; +} + +/** PeerDist retrieval protocol using HTTP POST */ +struct peerdist_retrieval peerdist_post = { + .name = "POST", + .open = peerdist_open_post, +}; + +/** + * Open retrieval protocol connection using HTTP GET or local file + * + * @v xfer Data transfer interface + * @v location Peer location + * @v digestsize Digest size + * @v id Segment identifier + * @v block Block index + * @ret rc Return status code + */ +static int peerdist_open_get ( struct interface *xfer, const char *location, + size_t digestsize, const uint8_t *id, + unsigned int block ) { + char uri_string[ strlen ( location ) + 4 /* "/xx/" */ + + ( 2 * PEERDIST_DIGEST_MAX_SIZE ) + 1 /* "-" */ + + 10 /* block number */ + 4 /* ".blk" */ + + 1 /* NUL */ ]; + size_t len; + int rc; + + /* Construct URI string */ + assert ( digestsize <= PEERDIST_DIGEST_MAX_SIZE ); + len = snprintf ( uri_string, sizeof ( uri_string ), "%s/%02x/", + location, id[0] ); + assert ( len < sizeof ( uri_string ) ); + len += base16_encode ( id, digestsize, ( uri_string + len ), + ( sizeof ( uri_string ) - len ) ); + assert ( len < sizeof ( uri_string ) ); + snprintf ( ( uri_string + len ), ( sizeof ( uri_string ) - len ), + "-%d.blk", block ); + + /* Open URI */ + if ( ( rc = xfer_open_uri_string ( xfer, uri_string ) ) != 0 ) { + DBGC ( xfer, "PCCRR %p could not open %s: %s\n", + xfer, uri_string, strerror ( rc ) ); + return rc; + } + + return 0; +} + +/** PeerDist retrieval protocol using HTTP GET or local file */ +struct peerdist_retrieval peerdist_get = { + .name = "GET", + .open = peerdist_open_get, +}; diff --git a/src/net/peerblk.c b/src/net/peerblk.c index 670566a51..e04cc3d36 100644 --- a/src/net/peerblk.c +++ b/src/net/peerblk.c @@ -571,85 +571,45 @@ static struct peerdist_block_queue peerblk_raw_queue = { /****************************************************************************** * - * Retrieval protocol block download attempts (using HTTP POST) + * Retrieval protocol block download attempts * ****************************************************************************** */ -/** - * 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 retrieval Retrieval protocol * @v location Peer location * @ret rc Return status code */ static int peerblk_retrieval_open ( struct peerdist_block *peerblk, + struct peerdist_retrieval *retrieval, 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 ); + DBGC2 ( peerblk, "PEERBLK %p %d.%d attempting %s retrieval from %s\n", + peerblk, peerblk->segment, peerblk->block, retrieval->name, + 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; + /* Open connection */ + if ( ( rc = retrieval->open ( &peerblk->retrieval, location, + digestsize, peerblk->id, + peerblk->block ) ) != 0 ) { + DBGC ( peerblk, "PEERBLK %p %d.%d could not create %s " + "retrieval request: %s\n", peerblk, peerblk->segment, + peerblk->block, retrieval->name, strerror ( rc ) ); + return rc; } - /* Update trim thresholds */ + /* Update trim thresholds for retrieval protocol format */ 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 + /* Annul 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. */ @@ -660,10 +620,7 @@ static int peerblk_retrieval_open ( struct peerdist_block *peerblk, peerblk->rc = -ETIMEDOUT; start_timer_fixed ( &peerblk->timer, PEERBLK_RETRIEVAL_OPEN_TIMEOUT ); - err_open: - uri_put ( uri ); - err_uri: - return rc; + return 0; } /** @@ -1291,6 +1248,7 @@ static void peerblk_expired ( struct retry_timer *timer, int over __unused ) { struct peerdisc_segment *segment = peerblk->discovery.segment; struct peerdisc_peer *head; unsigned long now = peerblk_timestamp(); + struct peerdist_retrieval *retrieval; const char *location; int rc; @@ -1341,8 +1299,9 @@ static void peerblk_expired ( struct retry_timer *timer, int over __unused ) { list_for_each_entry_continue ( peerblk->peer, &segment->peers, list ) { /* Attempt retrieval protocol download from this peer */ + retrieval = peerblk->peer->retrieval; location = peerblk->peer->location; - if ( ( rc = peerblk_retrieval_open ( peerblk, + if ( ( rc = peerblk_retrieval_open ( peerblk, retrieval, location ) ) != 0 ) { /* Non-fatal: continue to try next peer */ continue; diff --git a/src/net/peerdisc.c b/src/net/peerdisc.c index d03c8f343..588a23023 100644 --- a/src/net/peerdisc.c +++ b/src/net/peerdisc.c @@ -39,6 +39,7 @@ FILE_SECBOOT ( PERMITTED ); #include #include #include +#include #include #include @@ -80,8 +81,12 @@ static char *peerdisc_recent; /** Hosted cache server */ static char *peerhost; +/** Local cache directory */ +static char *peerpath; + static struct peerdisc_segment * peerdisc_find ( const char *id ); static int peerdisc_discovered ( struct peerdisc_segment *segment, + struct peerdist_retrieval *retrieval, const char *location ); /****************************************************************************** @@ -271,8 +276,10 @@ static int peerdisc_socket_rx ( struct peerdisc_socket *socket, /* Report discovered peer location */ if ( ( rc = peerdisc_discovered ( segment, - location ) ) != 0 ) + &peerdist_post, + location ) ) != 0 ) { goto err; + } } } @@ -379,10 +386,12 @@ static struct peerdisc_segment * peerdisc_find ( const char *id ) { * Add discovered PeerDist peer * * @v segment PeerDist discovery segment + * @v retrieval PeerDist retrieval protocol * @v location Peer location * @ret rc Return status code */ static int peerdisc_discovered ( struct peerdisc_segment *segment, + struct peerdist_retrieval *retrieval, const char *location ) { struct peerdisc_peer *peer; struct peerdisc_client *peerdisc; @@ -403,13 +412,16 @@ static int peerdisc_discovered ( struct peerdisc_segment *segment, peer = zalloc ( sizeof ( *peer ) + strlen ( location ) + 1 /* NUL */ ); if ( ! peer ) return -ENOMEM; + peer->retrieval = retrieval; strcpy ( peer->location, location ); /* Add to end of list of peers */ list_add_tail ( &peer->list, &segment->peers ); - /* Record as most recently discovered peer */ - if ( location != peerdisc_recent ) { + /* Record as most recently discovered peer, if applicable */ + if ( ( location != peerdisc_recent ) && + ( location != peerhost ) && + ( location != peerpath ) ) { recent = strdup ( location ); if ( recent ) { free ( peerdisc_recent ); @@ -489,12 +501,18 @@ static struct peerdisc_segment * peerdisc_create ( const char *id ) { INIT_LIST_HEAD ( &segment->clients ); timer_init ( &segment->timer, peerdisc_expired, &segment->refcnt ); + /* Add local cache directory, if any */ + if ( peerpath ) + peerdisc_discovered ( segment, &peerdist_get, peerpath ); + /* Add hosted cache server or initiate discovery */ if ( peerhost ) { /* Add hosted cache server to list of peers */ - if ( ( rc = peerdisc_discovered ( segment, peerhost ) ) != 0 ) + if ( ( rc = peerdisc_discovered ( segment, &peerdist_post, + peerhost ) ) != 0 ) { goto err_peerhost; + } } else { @@ -505,8 +523,10 @@ static struct peerdisc_segment * peerdisc_create ( const char *id ) { * a high probability of also having a copy of the * next block that we attempt to discover. */ - if ( peerdisc_recent ) - peerdisc_discovered ( segment, peerdisc_recent ); + if ( peerdisc_recent ) { + peerdisc_discovered ( segment, &peerdist_post, + peerdisc_recent ); + } /* Start discovery timer */ start_timer_nodelay ( &segment->timer ); @@ -639,23 +659,39 @@ const struct setting peerhost_setting __setting ( SETTING_MISC, peerhost ) = { .type = &setting_type_string, }; +/** PeerDist local cache directory setting */ +const struct setting peerpath_setting __setting ( SETTING_MISC, peerpath ) = { + .name = "peerpath", + .description = "PeerDist local cache", + .type = &setting_type_string, +}; + /** * Apply PeerDist discovery settings * * @ret rc Return status code */ static int apply_peerdisc_settings ( void ) { + char *host; + char *path; - /* Free any existing hosted cache server */ - free ( peerhost ); - peerhost = NULL; - - /* Fetch hosted cache server */ - fetch_string_setting_copy ( NULL, &peerhost_setting, &peerhost ); - if ( peerhost ) { - DBGC ( &peerhost, "PEERDISC using hosted cache %s\n", - peerhost ); + /* Set hosted cache server, if any */ + fetch_string_setting_copy ( NULL, &peerhost_setting, &host ); + if ( host && ! ( peerhost && ( strcmp ( host, peerhost ) == 0 ) ) ) { + DBGC ( &peerdisc_segments, "PEERDISC using hosted cache %s\n", + host ); } + free ( peerhost ); + peerhost = host; + + /* Set local cache directory, if any */ + fetch_string_setting_copy ( NULL, &peerpath_setting, &path ); + if ( path && ! ( peerpath && ( strcmp ( path, peerpath ) == 0 ) ) ) { + DBGC ( &peerdisc_segments, "PEERDISC using local cache %s\n", + path ); + } + free ( peerpath ); + peerpath = path; return 0; }