[peerdist] Allow for retrieving blocks from a local filesystem cache

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.

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.

Define the static request URI format as:

   <base>/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).

Allow a base cache directory to be specified via the ${peerpath}
setting, e.g.

    set peerpath file:/peercache

Signed-off-by: Michael Brown <mcb30@ipxe.org>
This commit is contained in:
Michael Brown
2026-09-27 17:41:57 +01:00
parent 744cdb451e
commit 629e28b56c
6 changed files with 291 additions and 76 deletions
+1
View File
@@ -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 )
+24
View File
@@ -14,6 +14,8 @@ FILE_SECBOOT ( PERMITTED );
#include <stdint.h>
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 */
+2
View File
@@ -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];
};
+193
View File
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2026 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 );
FILE_SECBOOT ( PERMITTED );
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <ipxe/uri.h>
#include <ipxe/open.h>
#include <ipxe/http.h>
#include <ipxe/base16.h>
#include <ipxe/pccrc.h>
#include <ipxe/pccrr.h>
/** @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:
*
* <base>/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,
};
+20 -61
View File
@@ -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;
+51 -15
View File
@@ -39,6 +39,7 @@ FILE_SECBOOT ( PERMITTED );
#include <ipxe/timer.h>
#include <ipxe/fault.h>
#include <ipxe/settings.h>
#include <ipxe/pccrr.h>
#include <ipxe/pccrd.h>
#include <ipxe/peerdisc.h>
@@ -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;
}