Files
NLnetLabs-krill/doc/development/05_repo_manager.md
T

8.6 KiB

Repository Manager

The RepositoryManager is the central component that is responsible for managing access to, and content of the RPKI Repository. It delegates those functions to an event-sourced component RepositoryAccess which is responsible for guarding access, and the RepositoryContent component which manages the current content.

Composition

There can only be one active repository, but since Krill storage (KeyValueStore) and eventsourcing support is based on the concept of multiple managed entities, it was convenient to create wrapper types to help access:

/// RepositoryManager is responsible for:
/// * verifying that a publisher is allowed to publish.
/// * publish content to RRDP and rsync.
pub struct RepositoryManager {
    access: Arc<RepositoryAccessProxy>,
    content: Arc<RepositoryContentProxy>,

    // Shared task queue, use to schedule RRDP updates when content is updated.
    tasks: Arc<TaskQueue>,

    config: Arc<Config>,
    signer: Arc<KrillSigner>,
}
/// We can only have one (1) RepositoryAccess, but it is an event-sourced
/// type which is stored in an AggregateStore which could theoretically
/// serve multiple RepositoryAccess instances. So, we use RepositoryAccessProxy
/// as a wrapper around this so that callers don't need to worry about storage details.
pub struct RepositoryAccessProxy {
    store: AggregateStore<RepositoryAccess>,
    key: MyHandle,
}
/// We can only have one (1) RepositoryContent, but it is stored
/// in a KeyValueStore. So this type provides a wrapper around this
/// so that callers don't need to worry about storage details.
#[derive(Debug)]
pub struct RepositoryContentProxy {
    store: Arc<WalStore<RepositoryContent>>,
    default_handle: MyHandle,
}

Initialization

The RepositoryManager is instantiated when Krill starts if the Publication Server function is enabled:

/// Builds a RepositoryManager. This will use a disk based KeyValueStore using the
/// the data directory specified in the supplied `Config`.
pub fn build(config: Arc<Config>, signer: Arc<KrillSigner>) -> Result<Self, Error> { ... }

When the RepositoryManager is created, it still needs to be initialized. This is done this way, because we cannot change the base URIs of an operational RPKI repository. There is no way to inform publishing CAs that the location of their objects has changed. Therefore we have an explicit function for this:

/// Create the publication server, will fail if it was already created.
pub fn init(&self, uris: PublicationServerUris) -> KrillResult<()> {
    info!("Initializing repository");
    self.access.init(uris.clone(), &self.signer)?;
    self.content.init(&self.config.data_dir, uris)?;
    self.content.write_repository(&self.config.repository_retention)?;

    Ok(())
}

Note that it writes the empty repository as part of the initialization process. We do this, because that gives users an opportunity to verify that the repository content can be accessed. In particular: expect an RRDP notification.xml and a snapshot.xml containing 0 entries to be published.

If it is found that the repository is not set up correctly, then it can be 'cleared' so it can be initialized again:

/// Clear the publication server. Will fail if it still
/// has publishers. Or if it does not exist
pub fn repository_clear(&self) -> KrillResult<()> {
    self.access.clear()?;
    self.content.clear()
}

Adding / Removing Publishers

To add a publisher we have to submit an rfc8183::PublisherRequest, and an Actor so that we attribute the change in the history of the RepositoryAccess component. The request type can be created from XML supplied by a remote publisher. The 'handle' is the local name that we will use for the publisher, and the XML contains a suggestion set by the remote party. But, it is possible (and perhaps recommended) to override this 'handle' in the request object with something that is unique to us, e.g. a UUID.

/// Adds a publisher. This will fail if a publisher already exists for the handle in the request.
pub fn create_publisher(&self, req: rfc8183::PublisherRequest, actor: &Actor) -> KrillResult<()> {
    let name = req.publisher_handle().clone();

    self.access.add_publisher(req, actor)?;
    self.content.add_publisher(name)
}

The publisher is going to need to get RFC8183 Repository Response XML. We can get an rfc8183::RepositoryResponse which can be printed (fmt::Display) as XML:

/// Returns the RFC8183 Repository Response for the publisher
pub fn repository_response(
    &self,
    rfc8181_uri: uri::Https,
    publisher: &PublisherHandle,
) -> KrillResult<rfc8183::RepositoryResponse> {
    self.access.repository_response(rfc8181_uri, publisher)
}

In order to remove a publisher we just need to tell the RepositoryManager its handle (name). This removes both the access information (handle, RFC8181 ID cert, ..) and all content for the publisher:

/// Removes a publisher and all of its content.
pub fn remove_publisher(&self, name: PublisherHandle, actor: &Actor) -> KrillResult<()> {
    let publisher = self.access.get_publisher(&name)?;
    let base_uri = publisher.base_uri();

    self.content.remove_publisher(&name, base_uri, &self.config.repository_retention)?;
    self.access.remove_publisher(name, actor)
}

Publisher Stats

We can get the stats for the complete repository, i.e. with all publishers, as follows:

pub fn repo_stats(&self) -> KrillResult<RepoStats> {
    self.content.stats()
}

The RepoStats structure includes the following information, and can be serialized to JSON:

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RepoStats {
    publishers: HashMap<PublisherHandle, PublisherStats>,
    session: RrdpSession,
    serial: u64,
    last_update: Option<Time>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PublisherStats {
    objects: usize,
    size: usize,
    last_update: Option<Time>,
}

NOTE: This includes a map of PublisherHandle -> PublisherStats. In JSON this can look confusing because there will be JSON members that are the publisher name in quotes. For other types we have been using an array with named items in the JSON representation. So, perhaps we should change this as well. We could just have a Vec here, since fast map access is not really a concern. The consumer can build their own map if they really want one.

Publishing / RFC 8181

Now, for the main purpose of course.. we have a function here that will take bytes submitted by a publisher, parse it and validate it as an RFC 8181 request and return the appropriate signed response.

pub fn rfc8181(&self, publisher_handle: PublisherHandle, msg_bytes: Bytes) -> KrillResult<Bytes>;

In case of issues that are part of the protocol (sections 2.4 and 2.5 of RFC 8181) this will return a signed RFC 8181 error response. In case of issues that we cannot recover from - e.g. there is I/O error - something is seriously broken in the server - then this will return an actual rust Error. It's up to the caller of this function to take appropriate action, e.g. give the publisher an HTTP response code, but possibly even crash this server - if it cannot function anymore.

Under the hood this function will first retrieve the publisher from RepositoryAccess to verify that the sender is known, and get their ID certificate (used for signing) and base_uri Rsync jail. The content of the query (list or send updates) is then passed on to RepositoryContent.

The RepositoryContent type is not event-sourced as it turned out that keeping the full history of changes from all CAs (especially their manifests and CRL updates) resulted in keeping way too much data.

Instead this relies on the Write-Ahead-Log support. This is very, very, similar to the event-sourcing used elsewhere, except that it does not guarantee that all changes are kept. The WalStore (Wal: Write-Ahead-Log) only keeps a snapshot and the latest 'change set's since that snapshot. As with the AggregateStore snapshots are only updated nightly for performance reasons. In fact, it was the 250MB+ nic.br repository content entity that triggered this code change.

The locking, retrieval and updating are similar to AggregateStore, but uses separate types for all this for historical reasons. A future improvement could be to merge the types and make it a per instance type (CertAuth, RepositoryAccess, RepositoryContent etc) choice whether it's fully event-sourced or only recent changes are kept.