Merge pull request #265 from NLnetLabs/0.6.0-plus

0.6.0 plus
This commit is contained in:
Tim Bruijnzeels
2020-06-23 16:24:09 +02:00
committed by GitHub
112 changed files with 971720 additions and 553 deletions
Generated
+51 -1
View File
@@ -1,5 +1,11 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "adler32"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
[[package]]
name = "aho-corasick"
version = "0.7.10"
@@ -206,6 +212,15 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "crc32fast"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.4.2"
@@ -625,6 +640,15 @@ dependencies = [
"autocfg 1.0.0",
]
[[package]]
name = "intervaltree"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566d5aa3b5cc5c5809cc1a9c9588d917a634248bfc58f7ea14e354e71595a32c"
dependencies = [
"smallvec 1.2.0",
]
[[package]]
name = "iovec"
version = "0.1.4"
@@ -661,7 +685,7 @@ dependencies = [
[[package]]
name = "krill"
version = "0.6.3"
version = "0.7.0"
dependencies = [
"base64 0.10.1",
"bcder",
@@ -676,7 +700,9 @@ dependencies = [
"hex",
"hyper",
"ignore",
"intervaltree",
"libc",
"libflate",
"log 0.4.8",
"openssl",
"pretty",
@@ -706,6 +732,24 @@ version = "0.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
[[package]]
name = "libflate"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1fbe6b967a94346446d37ace319ae85be7eca261bb8149325811ac435d35d64"
dependencies = [
"adler32",
"crc32fast",
"libflate_lz77",
"rle-decode-fast",
]
[[package]]
name = "libflate_lz77"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b"
[[package]]
name = "lock_api"
version = "0.3.3"
@@ -1342,6 +1386,12 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "rle-decode-fast"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac"
[[package]]
name = "rpki"
version = "0.9.0"
+3 -1
View File
@@ -1,7 +1,7 @@
[package]
# Note: some of these values are also used when building Debian packages below.
name = "krill"
version = "0.6.3"
version = "0.7.0"
edition = "2018"
authors = [ "The NLnet Labs RPKI team <rpki-team@nlnetlabs.nl>" ]
description = "Resource Public Key Infrastructure (RPKI) daemon"
@@ -24,6 +24,8 @@ futures = "0.3.4"
futures-util = "0.3.4"
hex = "^0.3"
hyper = "^0.13"
intervaltree = "0.2.6"
libflate = "1.0.0"
log = "^0.4"
openssl = { version = "^0.10", features = ["v110"] }
pretty = "0.5.2" # for testing
+38 -4
View File
@@ -3,7 +3,41 @@
Please see [here](https://github.com/NLnetLabs/krill/projects?query=is%3Aopen+sort%3Aname-asc)
for planned releases.
## 0.6.3 Release 'Play it again, Sam'
## 0.7.0 'Escondidinho de Lagosta'
This release brings significant improvements aimed at maintaining your ROAs. For now, Krill
will download aggregated BGP dumps from the RIPE NCC Routing Information Service (*) and
analyse how your ROAs affect announcements seen for your resources. In future we will extend
this system, so that it can use near-real-time data, or even a local feed with your own BGP
information instead.
For these changes to work well we needed to do some work on cleaning up existing ROAs. Until
now Krill has allowed the creation of essentially duplicate, or nonsensical ROAs, such as:
* ROAs for an ASN and prefix with and without an explicit max length matching the prefix
* ROAs for a prefix and ASN which were already permitted by another ROA.
On upgrade Krill will clean up such redundant authorizations for ROAs. For example if the
following authorizations would exist:
192.168.0.0/16 => 64496
192.168.0.0/24 => 64496
192.168.0.0/16-24 => 64496
Then only this last authorization needs to be kept, the first two are also covered by it.
Before this release it was also possible to have the same authorization with, and without, using
an explicit max length. For example:
192.168.0.0/16 => 64496
192.168.0.0/16-16 => 64496
Now Krill will *always* use an explicit max length in the definitions. Note however, that it is
still best practice to use the same max length as the announced prefix length, so Krill will just
set this by default if it is not specified.
*: https://www.ripe.net/analyse/internet-measurements/routing-information-service-ris
## 0.6.3 'Play it again, Sam'
This release addresses an issue where users with a CA that has delegated children, which in turn
had performed a key roll over in the past, could not upgrade to Release 0.6.2.
@@ -18,7 +52,7 @@ Release which is due in 2-4 weeks:
* Inconsistent use of "cas" in `krillc bulk` subcommand summary text (#254)
* Be consistent when referring to ending with a / (#255)
## 0.6.2 Release 'That was even faster!'
## 0.6.2 'That was even faster!'
So, as it turns out.. the code used to determine the age of snapshot files used in the previous
release was not safe on all platforms. This release fixes this!
@@ -26,7 +60,7 @@ release was not safe on all platforms. This release fixes this!
Users who upgraded to 0.6.1 and see messages like: "Creation time is not available on this
platform currently" in their logs, please upgrade!
## 0.6.1 Release 'That was fast!'
## 0.6.1 'That was fast!'
This release fixes an issue where the Krill Repository Server deleted RRDP snapshot files as soon
as a new notification file was published. This leads to issues in case a cached notification file
@@ -37,7 +71,7 @@ Users who use Krill as their own Repository Server are advised to upgrade.
Users who publish at a repository provided to them by a third party (e.g. nic.br) can safely skip
this release.
## 0.6.0 Release 'Go with the Flow'
## 0.6.0 'Go with the Flow'
The most visible change in this release is that the embedded Lagosta UI now includes French, Greek
and Spanish translations. But, the vast majority of the work went into making Krill use asynchronous
+42 -28
View File
@@ -28,6 +28,30 @@
#
### pid_file = "./data/krill.pid"
# Specify the base public service URI hostname and port.
#
# The default service URI is set to https://localhost:3000/ regardless of the
# IP and port configured above (but matching their default). This is fine for
# simple setups where you use Krill to run your own CA only and you use the
# CLI from localhost.
#
# However, if you need to access Krill remotely, or if you are serving as a
# parent CA, or Publication Server, to others, then make sure that you use a
# public URI here *and* make sure that you use a proxy server with a proper
# HTTPS certificate in front of Krill.
#
# At present this MUST be an https URI with a hostname and optional port number only.
# It is not allowed to use a Krill specific path prefix. If you have a strong
# motivation for this, then please commont on the following github issue:
# https://github.com/NLnetLabs/krill/issues/263
#
# Krill UI, API and service URIs will be derived as follows:
# <service_uri>api/v1/... (api)
# <service_uri>rfc8181 (for remote publishers)
# <service_uri>rfc6492 (for remote children)
# <service_uri>rrdp/.. (override with rddp_service_uri, see below)
# <service_uri>... (various UI resources)
### service_uri = "https://localhost:3000/"
# Specify whether an embedded repository should be started. For many users
# it will be better to use a repository server provided by a third party, e.g.
@@ -37,44 +61,28 @@
# setting is set to 'false' (default) AND there are no current publishers (i.e.
# all CAs use an external repository).
#
# For more information on running Krill as Publication Server see:
# https://rpki.readthedocs.io/en/latest/krill/publication-server.html
#
### repo_enabled = false
# Specify the base rsync repository for this server. Publishers will get
# a base URI that is based on the 'publisher_handle' in the XML file.
#
# Note, you should set up an rsync daemon to expose $data_dir/rsync to serve
# this data. The uri defined here should match the module name in your rsync
# configuration.
# Note, you need to set this parameter if (and only if) you chose to enable
# the repository function above (repo_enabled). If you did, you should set up
# an rsync daemon to expose $data_dir/rsync to serve this data. The uri defined
# here should match the module name in your rsync configuration.
#
# Furthemore.. note that the default 'localhost' is only allowed to be used
# when the KRILL_TEST ENV variable has been set.
#
### rsync_base = "rsync://localhost/repo/"
# Specify the base public URI to this service. Other URIs will be derived
# from this:
# <BASE_URI>api/v1/... (api)
# <BASE_URI>rfc8181 (for remote publishers)
# <BASE_URI>rfc6492 (for remote children)
# <BASE_URI>rrdp/.. (override with rddp_service_uri)
# <BASE_URI>ta/ta.cer (on TAL for embedded TA)
#
# MUST end with a slash.
#
# Note that the default service URI is set to https://localhost:3000/ regardless of
# the IP and port configured above (but matching their default).
#
# This service URI is fine for simple setups where you use Krill to run your own CA
# only and you use the CLI from localhost.
#
# However, if you need to access Krill remotely then make sure that you use a public
# URI here *and* make sure that you use a proxy server with a proper HTTPS certificate
# in front of Krill.
#
### service_uri = "https://localhost:3000/"
# Use the following if you want to use another public URI to access the RRDP files,
# e.g. because you serve them as raw files from another machine with a web server.
# Note, you may need to set this parameter if you chose to enable the repository
# function above (repo_enabled). By default Krill will use a public RRDP URI
# which is based on the service_uri. Use this directive use a different public
# URI to access the RRDP files.
#
### rrdp_service_uri = "$service_uri/rrdp/"
@@ -170,4 +178,10 @@
#
# Defaults to NO logging!
#
### rfc6492_log_dir = </some/path>
### rfc6492_log_dir = </some/path>
# Enable loading BGP Dumps from RIS for ROA vs BGP analysis.
#
# bgp_risdump_enable = true
# bgp_risdump_v4_uri = http://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz
# bgp_risdump_v6_uri = http://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz
+177 -131
View File
@@ -1,7 +1,7 @@
openapi: "3.0.2"
info:
title: Krill RPKI Server API
version: 0.6.3
version: 0.7.0
description: |
# Introduction
Welcome to the documentation for the Krill server API, a JSON based
@@ -248,7 +248,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/rfc8181'
$ref: '#/components/schemas/Rfc8181'
application/xml:
schema:
$ref: '#/components/schemas/RFC8183RepositoryResponseXML'
@@ -358,7 +358,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/rfc8183'
$ref: '#/components/schemas/Rfc8183'
application/xml:
schema:
$ref: '#/components/schemas/RFC8183RequestXML'
@@ -374,8 +374,7 @@ paths:
- "Certificate Authorities"
summary: Add a child CA.
description: |
This adds an embedded (in the same Krill instance) or remote child CA.
For a remote child CA the request body must include a JSON
This adds a child CA. The request body must include a JSON
representation of the [RFC 8183 Child Request XML](https://tools.ietf.org/html/rfc8183#section-5.2.1),
though the 'tag' and 'child_handle' attributes are ignored.
parameters:
@@ -387,15 +386,7 @@ paths:
schema:
$ref: '#/components/schemas/AddCAChildRequest'
examples:
embedded:
value:
handle: 'ca'
resources:
asn: 'AS1'
v4: '10.0.0.0/8'
v6: '::'
auth: 'embedded'
remote:
child:
value:
handle: 'ca'
resources:
@@ -409,7 +400,7 @@ paths:
id_cert: '<base64>'
responses:
'200':
$ref: '#/components/responses/Rfc6492OrEmbedded'
$ref: '#/components/responses/Rfc6492'
'400':
description: Bad request parameters.
content:
@@ -539,7 +530,7 @@ paths:
- $ref: '#/components/parameters/child_handle'
responses:
'200':
$ref: '#/components/responses/Rfc8183OrEmbedded'
$ref: '#/components/responses/Rfc8183O'
'403':
$ref: '#/components/responses/Forbidden'
'404':
@@ -560,16 +551,7 @@ paths:
- "Certificate Authorities"
summary: Get the RFC 8183 Parent Response for a child.
description: |
To get the RFC 8183 Parent Response for a child, regardless of whether
the parent knows the ID Cert.
**Note:** You can use 'update child' to add a new ID cert to a child,
however, this is a real corner case that can only happen if you
mistakenly added a remote child as embedded, rather than using its
RFC 8183 Child Request.
For "embedded" CAs you will not need this parent response when you add
the "embedded" parent.
To get the RFC 8183 Parent Response for a child.
parameters:
- $ref: '#/components/parameters/ca_handle'
- $ref: '#/components/parameters/child_handle'
@@ -580,7 +562,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/rfc6492'
$ref: '#/components/schemas/Rfc6492'
application/xml:
schema:
$ref: '#/components/schemas/RFC8183ParentResponseXML'
@@ -627,13 +609,8 @@ paths:
- "Certificate Authorities"
summary: Add a parent to a CA.
description: |
Adds an embedded or remote parents. For remote parents the RFC 6492
will be used. In principle CAs can also use this to talk to a local
parent CA in the same krill server, but this is inefficient. Therefore
it is also possible to add an 'embedded' parent in this case.
Note that you MUST specify a repository for your CA before you are
allowed to add a parent to it.
Add a parent to the CA. Note that your CA will only request a certificate
from any of its parents after a repository has been configured for it.
parameters:
- $ref: '#/components/parameters/ca_handle'
requestBody:
@@ -673,7 +650,7 @@ paths:
- $ref: '#/components/parameters/parent_handle'
responses:
'200':
$ref: '#/components/responses/Rfc6492OrEmbedded'
$ref: '#/components/responses/Rfc6492'
'403':
$ref: '#/components/responses/Forbidden'
'400':
@@ -699,7 +676,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Rfc6492OrEmbedded'
$ref: '#/components/schemas/Rfc6492'
responses:
'200':
$ref: '#/components/responses/Success'
@@ -777,12 +754,6 @@ paths:
- Request new certificates with SIA entries pointing to the new
locations.
- (best effort) Clean up of the old repository.
The new repository can be embedded, or remote. To use a remote
repository, the RFC 8181 Repository Response must be encoded into JSON.
Note: for most users it's better to use a remote repository, e.g. provided
by your RIR or NIR.
parameters:
- $ref: '#/components/parameters/ca_handle'
requestBody:
@@ -790,7 +761,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Rfc8181OrEmbedded'
$ref: '#/components/schemas/Rfc8181'
responses:
'200':
$ref: '#/components/responses/Success'
@@ -1011,6 +982,26 @@ paths:
'500':
$ref: '#/components/responses/GeneralErrorResponse'
/cas/{ca_handle}/routes/analysis/full:
get:
operationId: bgp_analysis_full
tags:
- "Route Authorizations"
summary: Show full analysis of all ROAs and Announcments for CA resources
description: |
Show all known announcements for the CA's resources and their RPKI validity
state compared to the CA's ROAs, as well as all the CA's ROAs and how they
affect the validatity state of known announcements.
parameters:
- $ref: '#/components/parameters/ca_handle'
responses:
'200':
$ref: '#/components/responses/BgpFull'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/GeneralErrorResponse'
/cas/{ca_handle}/issues:
get:
operationId: show_ca_issues
@@ -1145,9 +1136,6 @@ components:
ServiceUri:
type: string
format: uri
embedded:
type: string
enum: ['embedded']
rfcCommon:
type: object
properties:
@@ -1156,7 +1144,7 @@ components:
nullable: true
id_cert:
$ref: '#/components/schemas/IdCert'
rfc6492:
Rfc6492:
allOf:
- $ref: '#/components/schemas/rfcCommon'
- type: object
@@ -1174,7 +1162,7 @@ components:
properties:
publisher_handle:
$ref: '#/components/schemas/Handle'
rfc8181:
Rfc8181:
allOf:
- $ref: '#/components/schemas/rfc8181Base'
- type: object
@@ -1183,25 +1171,13 @@ components:
$ref: '#/components/schemas/ServiceUri'
repo_info:
$ref: '#/components/schemas/RepoInfo'
rfc8183:
Rfc8183:
allOf:
- $ref: '#/components/schemas/rfcCommon'
- type: object
properties:
child_handle:
$ref: '#/components/schemas/Handle'
Rfc6492OrEmbedded:
oneOf:
- $ref: '#/components/schemas/embedded'
- $ref: '#/components/schemas/rfc6492'
Rfc8181OrEmbedded:
oneOf:
- $ref: '#/components/schemas/embedded'
- $ref: '#/components/schemas/rfc8181'
Rfc8183OrEmbedded:
oneOf:
- $ref: '#/components/schemas/embedded'
- $ref: '#/components/schemas/rfc8183'
RFC8183PublisherRequestXML:
type: object
properties:
@@ -1386,7 +1362,7 @@ components:
resources:
$ref: '#/components/schemas/Resources'
auth:
$ref: '#/components/schemas/Rfc8183OrEmbedded'
$ref: '#/components/schemas/Rfc8183'
UpdateCAChildRequest:
type: object
properties:
@@ -1412,7 +1388,7 @@ components:
- $ref: '#/components/schemas/Handle'
- description: The local handle by which your ca refers to this parent.
contact:
$ref: '#/components/schemas/Rfc6492OrEmbedded'
$ref: '#/components/schemas/Rfc6492'
AddPublisherRequest:
type: object
required:
@@ -1694,6 +1670,86 @@ components:
type: string
example: cause
BgpFullSchema:
type: array
items:
type: object
required:
- asn
- prefix
- max_length
- state
- allowed_by
- disallowed_by
- authorizes
- disallows
properties:
asn:
type: integer
prefix:
type: string
max_length:
type: integer
nullable: true
state:
type: string
allowed_by:
type: array
items:
type: object
required:
- asn
- prefix
- max_length
properties:
asn:
type: integer
prefix:
type: string
max_length:
type: integer
nullable: true
disallowed_by:
type: array
items:
type: object
required:
- asn
- prefix
- max_length
properties:
asn:
type: integer
prefix:
type: string
max_length:
type: integer
nullable: true
allows:
type: array
items:
type: object
required:
- asn
- prefix
properties:
asn:
type: integer
prefix:
type: string
disallows:
type: array
items:
type: object
required:
- asn
- prefix
properties:
asn:
type: integer
prefix:
type: string
CaRoaUnknownResponse:
type: object
required:
@@ -1727,7 +1783,7 @@ components:
type: string
nullable: true
example: 24
CaRoaDuplicateResponse:
type: object
@@ -1795,8 +1851,8 @@ components:
max_length:
type: string
nullable: true
example: 24
example: 24
CaRoaNotEntitledResponse:
type: object
required:
@@ -1851,7 +1907,7 @@ components:
ca:
type: string
example: ca
CaUnknownResponse:
type: object
required:
@@ -1897,10 +1953,13 @@ components:
responses:
Success:
description: Success.
Forbidden:
description: Forbidden (incorrect authentication token).
NotFound:
description: Not Found.
CertAuthIssues:
description: Issues for a CA (e.g. cannot reach parent)
content:
@@ -1929,6 +1988,7 @@ components:
application/json:
schema:
$ref: '#/components/schemas/PubDuplicateResponse'
PubUnknownResponse:
description: Unknown publisher
content:
@@ -2000,6 +2060,28 @@ components:
schema:
$ref: '#/components/schemas/CaParentIssueResponse'
BgpFull:
description: |
Full ROAs vs known BGP Announcements report for this CA.
This is an array of both ROAs and Announcements, including all optional
information that may be applicable to either. I.e. even though the schema
includes max_length this value will not be present for announcements. The
arrays for 'allows' and 'disallows' will always be empty for announcements,
as will the arrays for 'allowed_by' and 'disallowed_by' for ROAs.
Confusing as this might be, this allows for parsing all kinds of states
in a single table. If you want to look at announcements or ROAs only, please
look at the other more specific BGP analysis endpoints for those.
Possible state values are: roa_seen, roa_stale, announcement_valid,
announcement_invalid_length, announcement_invalid_asn, announcement_not_found,
roa_no_announcement_info
content:
application/json:
schema:
$ref: '#/components/schemas/BgpFullSchema'
CaRoaUnknownResponse:
description: 'Invalid ROA delta: removing a definition which is unknown'
content:
@@ -2049,7 +2131,6 @@ components:
schema:
$ref: '#/components/schemas/GeneralErrorResponse'
GetCA:
description: Success.
content:
@@ -2189,6 +2270,7 @@ components:
expires: '2019-11-27T10:56:16.172248Z'
children:
- 'child'
ListCAs:
description: Success.
content:
@@ -2205,6 +2287,7 @@ components:
allOf:
- $ref: '#/components/schemas/Handle'
- description: The RFC-8183 handle that uniquely identifies the CA in this Krill instance.
GetChildCA:
description: Success.
content:
@@ -2216,6 +2299,7 @@ components:
$ref: '#/components/schemas/PemIdCert'
entitled_resources:
$ref: '#/components/schemas/Resources'
ListPublishers:
description: Success.
content:
@@ -2247,6 +2331,7 @@ components:
links:
rel: 'self'
link: '/api/v1/publishers/ca'
GetPublisher:
description: Success.
content:
@@ -2283,12 +2368,14 @@ components:
uri: 'rsync://<KRILL_SERVER_FQDN>/repo/child/0/A815..A01C.crl'
- base64: 'MIIJ..Cg4='
uri: 'rsync://<KRILL_SERVER_FQDN>/repo/child/0/3130..3936.roa'
AddPublisher:
description: Success.
content:
application/json:
schema:
$ref: '#/components/schemas/rfc8181'
$ref: '#/components/schemas/Rfc8181'
StalePublishers:
description: Success.
content:
@@ -2320,6 +2407,7 @@ components:
links:
rel: 'self'
link: '/api/v1/publishers/ca'
GetRepositoryForCA:
description: Success.
content:
@@ -2328,23 +2416,12 @@ components:
type: object
properties:
contact:
oneOf:
- type: object
properties:
embedded:
$ref: '#/components/schemas/RepoInfo'
- type: object
properties:
rfc8181:
$ref: '#/components/schemas/rfc8181'
type: object
properties:
rfc8181:
$ref: '#/components/schemas/Rfc8181'
examples:
embedded:
value:
contact:
embedded:
base_uri: 'rsync://localhost/repo/ca/'
rpki_notify: 'https://localhost:3000/rrdp/notification.xml'
rfc8181:
contact:
value:
contact:
rfc8181:
@@ -2354,7 +2431,8 @@ components:
service_uri: 'https://localhost:3000/rfc8181/ca'
repo_info:
base_uri: 'rsync://localhost/repo/ca/'
rpki_notify: 'https://localhost:3000/rrdp/notification.xml'
rpki_notify: 'https://localhost:3000/rrdp/notificati.xml'
GetCAHistory:
description: Success.
content:
@@ -2382,49 +2460,17 @@ components:
type: integer
details:
type: object
Rfc6492OrEmbedded:
description: Success.
content:
application/json:
schema:
$ref: '#/components/schemas/Rfc6492OrEmbedded'
examples:
rfc6492:
value:
rfc6492:
tag: null
id_cert: '<base64>'
parent_handle: 'ta'
child_handle: 'ca'
service_uri: 'https://localhost:3000/rfc6492/ta'
embedded:
value:
'embedded'
Rfc8183OrEmbedded:
description: Success.
content:
application/json:
schema:
$ref: '#/components/schemas/Rfc8183OrEmbedded'
examples:
rfc6492:
value:
rfc6492:
tag: null
id_cert: '<base64>'
parent_handle: 'ta'
child_handle: 'ca'
service_uri: 'https://localhost:3000/rfc6492/ta'
embedded:
value:
'embedded'
examples:
rfc6492:
value:
rfc6492:
tag: null
id_cert: '<base64>'
parent_handle: 'ta'
child_handle: 'ca'
service_uri: 'https://localhost:3000/rfc6492/ta'
Rfc6492:
description: Success.
content:
application/json:
schema:
$ref: '#/components/schemas/Rfc6492'
Rfc8183O:
description: Success.
content:
application/json:
schema:
$ref: '#/components/schemas/Rfc8183'
+3 -3
View File
@@ -36,11 +36,11 @@ log_info() {
}
if [ "$1" == "krill" ]; then
# Does the opreator want to use their own API token? If so they must
# Does the operator want to use their own API token? If so they must
# supply the KRILL_AUTH_TOKEN env var.
if [ "${KRILL_AUTH_TOKEN}" == "None" ]; then
# Generate a unique hard to guess authorisation token and export it
# so that the Krill daemon uses it (unless overriden by the Krill
# Generate a unique hard to guess authorization token and export it
# so that the Krill daemon uses it (unless overridden by the Krill
# daemon config file). Only do this if the operator didn't already
# supply a token when launching the Docker container.
export KRILL_AUTH_TOKEN=$(uuidgen)
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Lagosta - the Krill UI</title><style>noscript {
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Krill - RPKI</title><style>noscript {
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode",
Geneva, Verdana, sans-serif;
display: grid;
+10 -8
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+19
View File
@@ -13,6 +13,7 @@ use crate::commons::api::{
AllCertAuthIssues, CaRepoDetails, CertAuthIssues, ChildCaInfo, CurrentRepoState,
ParentCaContact, PublisherDetails, PublisherList, Token,
};
use crate::commons::bgp::BgpAnalysisReport;
use crate::commons::remote::rfc8183;
use crate::commons::util::httpclient;
use crate::constants::KRILL_CLI_API_ENV;
@@ -211,6 +212,24 @@ impl KrillClient {
Ok(ApiResponse::Empty)
}
CaCommand::BgpAnalysisFull(handle) => {
let uri = format!("api/v1/cas/{}/routes/analysis/full", handle);
let report = self.get_json(&uri).await?;
Ok(ApiResponse::BgpAnalysisFull(report))
}
CaCommand::BgpAnalysisAnnouncements(handle) => {
let uri = format!("api/v1/cas/{}/routes/analysis/full", handle);
let report: BgpAnalysisReport = self.get_json(&uri).await?;
Ok(ApiResponse::BgpAnalysisAnnouncements(report.into()))
}
CaCommand::BgpAnalysisRoas(handle) => {
let uri = format!("api/v1/cas/{}/routes/analysis/full", handle);
let report: BgpAnalysisReport = self.get_json(&uri).await?;
Ok(ApiResponse::BgpAnalysisRoas(report.into()))
}
CaCommand::Show(handle) => {
let uri = format!("api/v1/cas/{}", handle);
let ca_info = self.get_json(&uri).await?;
+184 -147
View File
@@ -251,7 +251,11 @@ impl Options {
Arg::with_name("rrdp")
.long("rrdp")
.value_name("uri")
.help("Specify the base https URI for your RRDP (excluding notify.xml), must end with '/'")
.help(
"Specify the base https URI for your RRDP (excluding notification.xml), \
must \
end with '/'",
)
.required(true),
)
}
@@ -371,30 +375,22 @@ impl Options {
}
fn make_cas_children_add_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let sub = SubCommand::with_name("add").about("Add a child to a CA.");
let mut sub = SubCommand::with_name("add").about("Add a child to a CA.");
let mut embedded =
SubCommand::with_name("embedded").about("Add a child in *this* Krill server");
embedded = Self::add_general_args(embedded);
embedded = Self::add_my_ca_arg(embedded);
embedded = Self::add_child_arg(embedded);
embedded = Self::add_child_resource_args(embedded);
let mut remote = SubCommand::with_name("remote")
.about("Add a remote child, and return the parent response");
remote = Self::add_general_args(remote);
remote = Self::add_my_ca_arg(remote);
remote = Self::add_child_arg(remote);
remote = Self::add_child_resource_args(remote);
let remote = remote.arg(
Arg::with_name("rfc8183")
.long("rfc8183")
.help("The RFC8183 Child Request XML file.")
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
sub = Self::add_child_arg(sub);
sub = Self::add_child_resource_args(sub);
let sub = sub.arg(
Arg::with_name("request")
.long("request")
.short("r")
.help("The location of the RFC8183 Child Request XML file.")
.value_name("<XML file>")
.required(true),
);
app.subcommand(sub.subcommand(embedded).subcommand(remote))
app.subcommand(sub)
}
fn make_cas_children_update_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
@@ -469,40 +465,35 @@ impl Options {
}
fn make_cas_parents_add_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let sub = SubCommand::with_name("add").about("Add a parent to this CA.");
let mut embedded = SubCommand::with_name("embedded")
.about("Add a parent that you manage in *this* Krill server");
embedded = Self::add_general_args(embedded);
embedded = Self::add_my_ca_arg(embedded);
embedded = Self::add_parent_arg(embedded);
let mut remote = SubCommand::with_name("remote").about("Add a remote parent");
remote = Self::add_general_args(remote);
remote = Self::add_my_ca_arg(remote);
remote = Self::add_parent_arg(remote);
remote = remote.arg(
Arg::with_name("rfc8183")
.long("rfc8183")
.help("The RFC8183 Parent Response XML")
.value_name("<XML file>")
.required(true),
);
app.subcommand(sub.subcommand(remote).subcommand(embedded))
}
fn make_cas_parents_update_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub =
SubCommand::with_name("update").about("Update an existing remote parent of this CA.");
let mut sub = SubCommand::with_name("add").about("Add a parent to this CA.");
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
sub = Self::add_parent_arg(sub);
sub = sub.arg(
Arg::with_name("rfc8183")
.long("rfc8183")
.help("The RFC8183 Parent Response XML")
Arg::with_name("response")
.long("response")
.short("r")
.help("The location of the RFC8183 Parent Response XML file.")
.value_name("<XML file>")
.required(true),
);
app.subcommand(sub)
}
fn make_cas_parents_update_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub =
SubCommand::with_name("update").about("Update an existing parent of this CA.");
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
sub = Self::add_parent_arg(sub);
sub = sub.arg(
Arg::with_name("response")
.long("response")
.short("r")
.help("The location of the RFC8183 Parent Response XML file.")
.value_name("<XML file>")
.required(true),
);
@@ -607,11 +598,48 @@ impl Options {
app.subcommand(sub)
}
fn make_cas_routes_bgp_full_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub = SubCommand::with_name("full").about("Show full report.");
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
app.subcommand(sub)
}
fn make_cas_routes_bgp_announcements_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub =
SubCommand::with_name("announcements").about("Show announcement centric report.");
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
app.subcommand(sub)
}
fn make_cas_routes_bgp_roas_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub = SubCommand::with_name("roas").about("Show ROA centric report.");
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
app.subcommand(sub)
}
fn make_cas_routes_bgp_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub = SubCommand::with_name("bgp")
.about("Show current authorizations in relation to known announcements.");
sub = Self::make_cas_routes_bgp_full_sc(sub);
sub = Self::make_cas_routes_bgp_announcements_sc(sub);
sub = Self::make_cas_routes_bgp_roas_sc(sub);
app.subcommand(sub)
}
fn make_cas_routes_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub = SubCommand::with_name("roas").about("Manage ROAs for your CA.");
sub = Self::make_cas_routes_list_sc(sub);
sub = Self::make_cas_routes_update_sc(sub);
sub = Self::make_cas_routes_bgp_sc(sub);
app.subcommand(sub)
}
@@ -647,25 +675,17 @@ impl Options {
let mut sub =
SubCommand::with_name("update").about("Change which repository this CA uses.");
let mut embedded =
SubCommand::with_name("embedded").about("Use the embedded server in krill");
embedded = Self::add_general_args(embedded);
embedded = Self::add_my_ca_arg(embedded);
let mut remote = SubCommand::with_name("remote").about("Use a remote server (RECOMMENDED)");
remote = Self::add_general_args(remote);
remote = Self::add_my_ca_arg(remote);
remote = remote.arg(
Arg::with_name("rfc8183")
sub = Self::add_general_args(sub);
sub = Self::add_my_ca_arg(sub);
sub = sub.arg(
Arg::with_name("response")
.value_name("file")
.long("rfc8183")
.help("File containing the RFC8183 XML. Defaults to reading from STDIN")
.long("response")
.short("r")
.help("The location of the RFC8183 Publisher Response XML file. Defaults to reading from STDIN")
.required(false),
);
sub = sub.subcommand(embedded);
sub = sub.subcommand(remote);
app.subcommand(sub)
}
@@ -730,20 +750,23 @@ impl Options {
let mut sub = SubCommand::with_name("add").about("Add a publisher.");
sub = Self::add_general_args(sub);
sub = sub.arg(
Arg::with_name("rfc8183")
.value_name("file")
.long("rfc8183")
.help("RFC8183 Publisher Request XML file containing a certificate (tag is ignored)")
.required(true)
).arg(
Arg::with_name("publisher")
.value_name("handle")
.short("p")
.long("publisher")
.help("Override the publisher handle in the XML.")
.required(false),
);
sub = sub
.arg(
Arg::with_name("request")
.value_name("file")
.long("request")
.short("r")
.help("The location of the RFC8183 Publisher Request XML file.")
.required(true),
)
.arg(
Arg::with_name("publisher")
.value_name("handle")
.short("p")
.long("publisher")
.help("Override the publisher handle in the XML.")
.required(false),
);
app.subcommand(sub)
}
@@ -763,8 +786,8 @@ impl Options {
}
fn make_publishers_response_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let mut sub = SubCommand::with_name("response")
.about("Show RFC8183 Repository Response XML.");
let mut sub =
SubCommand::with_name("response").about("Show RFC8183 Repository Response XML.");
sub = Self::add_general_args(sub);
sub = Self::add_publisher_arg(sub);
app.subcommand(sub)
@@ -810,7 +833,8 @@ impl Options {
}
fn make_health_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
let health = SubCommand::with_name("health").about("Perform an authenticated health check.");
let health =
SubCommand::with_name("health").about("Perform an authenticated health check.");
let health = Self::add_general_args(health);
app.subcommand(health)
}
@@ -1031,10 +1055,12 @@ impl Options {
Ok(Options::make(general_args, command))
}
fn parse_matches_cas_children_add_general(
matches: &ArgMatches,
rfc8183_opt: Option<rfc8183::ChildRequest>,
) -> Result<Options, Error> {
fn parse_matches_cas_children_add(matches: &ArgMatches) -> Result<Options, Error> {
let path = matches.value_of("request").unwrap();
let bytes = Self::read_file_arg(path)?;
let request = rfc8183::ChildRequest::validate(bytes.as_ref())?;
let auth_request = ChildAuthRequest::Rfc8183(request);
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
@@ -1044,29 +1070,11 @@ impl Options {
let resources =
Self::parse_resource_args(matches)?.ok_or_else(|| Error::MissingResources)?;
let auth_request = match rfc8183_opt {
Some(rfc8183) => ChildAuthRequest::Rfc8183(rfc8183),
None => ChildAuthRequest::Embedded,
};
let child_request = AddChildRequest::new(child, resources, auth_request);
let command = Command::CertAuth(CaCommand::ChildAdd(my_ca, child_request));
Ok(Options::make(general_args, command))
}
fn parse_matches_cas_children_add(matches: &ArgMatches) -> Result<Options, Error> {
if let Some(m) = matches.subcommand_matches("embedded") {
Self::parse_matches_cas_children_add_general(m, None)
} else if let Some(m) = matches.subcommand_matches("remote") {
let path = m.value_of("rfc8183").unwrap();
let bytes = Self::read_file_arg(path)?;
let request = rfc8183::ChildRequest::validate(bytes.as_ref())?;
Self::parse_matches_cas_children_add_general(m, Some(request))
} else {
Err(Error::UnrecognisedSubCommand)
}
}
fn parse_matches_cas_children_update(matches: &ArgMatches) -> Result<Options, Error> {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
@@ -1149,38 +1157,23 @@ impl Options {
Ok(Options::make(general_args, command))
}
fn parse_matches_cas_parents_add_general(
matches: &ArgMatches,
response_opt: Option<rfc8183::ParentResponse>,
) -> Result<Options, Error> {
fn parse_matches_cas_parents_add(matches: &ArgMatches) -> Result<Options, Error> {
let path = matches.value_of("response").unwrap();
let bytes = Self::read_file_arg(path)?;
let response = rfc8183::ParentResponse::validate(bytes.as_ref())?;
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
let parent = matches.value_of("parent").unwrap();
let parent = Handle::from_str(parent).map_err(|_| Error::InvalidHandle)?;
let contact = match response_opt {
Some(response) => ParentCaContact::for_rfc6492(response),
None => ParentCaContact::Embedded,
};
let contact = ParentCaContact::for_rfc6492(response);
let parent_req = ParentCaReq::new(parent, contact);
let command = Command::CertAuth(CaCommand::AddParent(my_ca, parent_req));
Ok(Options::make(general_args, command))
}
fn parse_matches_cas_parents_add(matches: &ArgMatches) -> Result<Options, Error> {
if let Some(m) = matches.subcommand_matches("embedded") {
Self::parse_matches_cas_parents_add_general(m, None)
} else if let Some(m) = matches.subcommand_matches("remote") {
let path = m.value_of("rfc8183").unwrap();
let bytes = Self::read_file_arg(path)?;
let response = rfc8183::ParentResponse::validate(bytes.as_ref())?;
Self::parse_matches_cas_parents_add_general(m, Some(response))
} else {
Err(Error::UnrecognisedSubCommand)
}
}
fn parse_matches_cas_parents_update(matches: &ArgMatches) -> Result<Options, Error> {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
@@ -1188,7 +1181,7 @@ impl Options {
let parent = matches.value_of("parent").unwrap();
let parent = Handle::from_str(parent).map_err(|_| Error::InvalidHandle)?;
let path = matches.value_of("rfc8183").unwrap();
let path = matches.value_of("response").unwrap();
let bytes = Self::read_file_arg(path)?;
let response = rfc8183::ParentResponse::validate(bytes.as_ref())?;
@@ -1287,11 +1280,52 @@ impl Options {
Ok(Options::make(general_args, command))
}
fn parse_matches_cas_routes_bgp_full(matches: &ArgMatches) -> Result<Options, Error> {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
Ok(Options::make(
general_args,
Command::CertAuth(CaCommand::BgpAnalysisFull(my_ca)),
))
}
fn parse_matches_cas_routes_bgp_announcements(matches: &ArgMatches) -> Result<Options, Error> {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
Ok(Options::make(
general_args,
Command::CertAuth(CaCommand::BgpAnalysisAnnouncements(my_ca)),
))
}
fn parse_matches_cas_routes_bgp_roas(matches: &ArgMatches) -> Result<Options, Error> {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
Ok(Options::make(
general_args,
Command::CertAuth(CaCommand::BgpAnalysisRoas(my_ca)),
))
}
fn parse_matches_cas_routes_bgp(matches: &ArgMatches) -> Result<Options, Error> {
if let Some(m) = matches.subcommand_matches("full") {
Self::parse_matches_cas_routes_bgp_full(m)
} else if let Some(m) = matches.subcommand_matches("announcements") {
Self::parse_matches_cas_routes_bgp_announcements(m)
} else if let Some(m) = matches.subcommand_matches("roas") {
Self::parse_matches_cas_routes_bgp_roas(m)
} else {
Err(Error::UnrecognisedSubCommand)
}
}
fn parse_matches_cas_routes(matches: &ArgMatches) -> Result<Options, Error> {
if let Some(m) = matches.subcommand_matches("list") {
Self::parse_matches_cas_routes_list(m)
} else if let Some(m) = matches.subcommand_matches("update") {
Self::parse_matches_cas_routes_update(m)
} else if let Some(m) = matches.subcommand_matches("bgp") {
Self::parse_matches_cas_routes_bgp(m)
} else {
Err(Error::UnrecognisedSubCommand)
}
@@ -1325,26 +1359,16 @@ impl Options {
}
fn parse_matches_cas_repo_update(matches: &ArgMatches) -> Result<Options, Error> {
if let Some(matches) = matches.subcommand_matches("embedded") {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
let update = RepositoryUpdate::embedded();
let command = Command::CertAuth(CaCommand::RepoUpdate(my_ca, update));
Ok(Options::make(general_args, command))
} else if let Some(matches) = matches.subcommand_matches("remote") {
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
let general_args = GeneralArgs::from_matches(matches)?;
let my_ca = Self::parse_my_ca(matches)?;
let path = matches.value_of("rfc8183").unwrap();
let bytes = Self::read_file_arg(path)?;
let response = rfc8183::RepositoryResponse::validate(bytes.as_ref())?;
let path = matches.value_of("response").unwrap();
let bytes = Self::read_file_arg(path)?;
let response = rfc8183::RepositoryResponse::validate(bytes.as_ref())?;
let update = RepositoryUpdate::rfc8181(response);
let command = Command::CertAuth(CaCommand::RepoUpdate(my_ca, update));
Ok(Options::make(general_args, command))
} else {
Err(Error::UnrecognisedSubCommand)
}
let update = RepositoryUpdate::rfc8181(response);
let command = Command::CertAuth(CaCommand::RepoUpdate(my_ca, update));
Ok(Options::make(general_args, command))
}
fn parse_matches_cas_repo(matches: &ArgMatches) -> Result<Options, Error> {
@@ -1399,7 +1423,7 @@ impl Options {
fn parse_matches_publishers_add(matches: &ArgMatches) -> Result<Options, Error> {
let general_args = GeneralArgs::from_matches(matches)?;
let path = matches.value_of("rfc8183").unwrap();
let path = matches.value_of("request").unwrap();
let path = PathBuf::from(path);
let bytes = file::read(&path)?;
let mut req = rfc8183::PublisherRequest::validate(bytes.as_ref())?;
@@ -1618,12 +1642,28 @@ pub enum CaCommand {
#[display(fmt = "activate key roll for ca: '{}'", _0)]
KeyRollActivate(Handle),
// Authorizations
#[display(fmt = "list ROAS for ca: '{}'", _0)]
RouteAuthorizationsList(Handle),
#[display(fmt = "Update ROAS for ca: '{}' -> {}", _0, _1)]
RouteAuthorizationsUpdate(Handle, RoaDefinitionUpdates),
#[display(fmt = "Show detailed ROA vs BGP analysis for ca: '{}'", _0)]
BgpAnalysisFull(Handle),
#[display(
fmt = "Show announcement centric summary of ROA vs BGP analysis for ca: '{}'",
_0
)]
BgpAnalysisAnnouncements(Handle),
#[display(
fmt = "Show ROA centric summary of ROA vs BGP analysis for ca: '{}'",
_0
)]
BgpAnalysisRoas(Handle),
// Show details for this CA
#[display(fmt = "Show details for ca: '{}'", _0)]
Show(Handle),
@@ -1824,9 +1864,6 @@ pub enum Error {
#[display(fmt = "You must specify resources when adding a CA (--asn, --ipv4, --ipv6)")]
MissingResources,
#[display(fmt = "You must specify either --embedded or --rfc8183 when adding a child")]
MissingChildAuth,
#[display(fmt = "Invalid ID cert for child.")]
InvalidChildIdCert,
+25
View File
@@ -11,6 +11,7 @@ use crate::commons::api::{
ParentCaContact, PublisherDetails, PublisherList, RepositoryContact, RoaDefinition, ServerInfo,
StoredEffect,
};
use crate::commons::bgp::{AnnouncementReport, BgpAnalysisReport, RoaReport};
use crate::commons::eventsourcing::WithStorableDetails;
use crate::commons::remote::api::ClientInfo;
use crate::commons::remote::rfc8183;
@@ -30,6 +31,9 @@ pub enum ApiResponse {
CertAuthAction(CaCommandDetails),
CertAuths(CertAuthList),
RouteAuthorizations(Vec<RoaDefinition>),
BgpAnalysisFull(BgpAnalysisReport),
BgpAnalysisAnnouncements(AnnouncementReport),
BgpAnalysisRoas(RoaReport),
ParentCaContact(ParentCaContact),
@@ -69,6 +73,9 @@ impl ApiResponse {
ApiResponse::CertAuthIssues(issues) => Ok(Some(issues.report(fmt)?)),
ApiResponse::AllCertAuthIssues(issues) => Ok(Some(issues.report(fmt)?)),
ApiResponse::RouteAuthorizations(auths) => Ok(Some(auths.report(fmt)?)),
ApiResponse::BgpAnalysisFull(table) => Ok(Some(table.report(fmt)?)),
ApiResponse::BgpAnalysisAnnouncements(summary) => Ok(Some(summary.report(fmt)?)),
ApiResponse::BgpAnalysisRoas(summary) => Ok(Some(summary.report(fmt)?)),
ApiResponse::ParentCaContact(contact) => Ok(Some(contact.report(fmt)?)),
ApiResponse::ChildInfo(info) => Ok(Some(info.report(fmt)?)),
ApiResponse::PublisherList(list) => Ok(Some(list.report(fmt)?)),
@@ -407,6 +414,24 @@ impl Report for Vec<RoaDefinition> {
}
}
impl Report for BgpAnalysisReport {
fn text(&self) -> Result<String, ReportError> {
Ok(self.to_string())
}
}
impl Report for AnnouncementReport {
fn text(&self) -> Result<String, ReportError> {
Ok(self.to_string())
}
}
impl Report for RoaReport {
fn text(&self) -> Result<String, ReportError> {
Ok(self.to_string())
}
}
impl Report for CaRepoDetails {
fn text(&self) -> Result<String, ReportError> {
let mut res = String::new();
+52 -1
View File
@@ -1891,13 +1891,15 @@ impl CertAuthIssues {
pub struct CertAuthStats {
roa_count: usize,
child_count: usize,
bgp_stats: BgpStats,
}
impl CertAuthStats {
pub fn new(roa_count: usize, child_count: usize) -> Self {
pub fn new(roa_count: usize, child_count: usize, bgp_stats: BgpStats) -> Self {
CertAuthStats {
roa_count,
child_count,
bgp_stats,
}
}
@@ -1908,6 +1910,55 @@ impl CertAuthStats {
pub fn child_count(&self) -> usize {
self.child_count
}
pub fn bgp_stats(&self) -> &BgpStats {
&self.bgp_stats
}
}
//------------ BgpStats ------------------------------------------------------
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BgpStats {
pub announcements_valid: usize,
pub announcements_invalid_asn: usize,
pub announcements_invalid_length: usize,
pub announcements_not_found: usize,
pub roas_stale: usize,
}
impl Default for BgpStats {
fn default() -> Self {
BgpStats {
announcements_valid: 0,
announcements_invalid_asn: 0,
announcements_invalid_length: 0,
announcements_not_found: 0,
roas_stale: 0,
}
}
}
impl BgpStats {
pub fn increment_valid(&mut self) {
self.announcements_valid += 1;
}
pub fn increment_invalid_asn(&mut self) {
self.announcements_invalid_asn += 1;
}
pub fn increment_invalid_length(&mut self) {
self.announcements_invalid_length += 1;
}
pub fn increment_not_found(&mut self) {
self.announcements_not_found += 1;
}
pub fn increment_stale(&mut self) {
self.roas_stale += 1;
}
}
//------------ ResSetErr -----------------------------------------------------
+170 -14
View File
@@ -1,3 +1,4 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::fmt;
use std::net::IpAddr;
@@ -15,7 +16,7 @@ use crate::daemon::ca::RouteAuthorizationUpdates;
/// This type defines the definition of a Route Origin Authorization (ROA), i.e.
/// the originating asn, IPv4 or IPv6 prefix, and optionally a max length.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RoaDefinition {
asn: AsNumber,
prefix: TypedPrefix,
@@ -32,6 +33,14 @@ impl RoaDefinition {
}
}
pub fn explicit_max_length(self) -> Self {
RoaDefinition {
asn: self.asn,
prefix: self.prefix,
max_length: Some(self.effective_max_length()),
}
}
pub fn asn(&self) -> AsNumber {
self.asn
}
@@ -61,6 +70,13 @@ impl RoaDefinition {
true
}
}
/// Returns `true` if the this definition includes the other definition.
pub fn includes(&self, other: &RoaDefinition) -> bool {
self.asn == other.asn
&& self.prefix.matching_or_less_specific(&other.prefix)
&& self.effective_max_length() >= other.effective_max_length()
}
}
impl FromStr for RoaDefinition {
@@ -99,6 +115,12 @@ impl FromStr for RoaDefinition {
}
}
impl fmt::Debug for RoaDefinition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self)
}
}
impl fmt::Display for RoaDefinition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.max_length {
@@ -108,6 +130,36 @@ impl fmt::Display for RoaDefinition {
}
}
impl Ord for RoaDefinition {
fn cmp(&self, other: &Self) -> Ordering {
let mut ordering = self.prefix.cmp(&other.prefix);
if ordering == Ordering::Equal {
ordering = self
.effective_max_length()
.cmp(&other.effective_max_length());
}
if ordering == Ordering::Equal {
ordering = self.asn.cmp(&other.asn);
}
ordering
}
}
impl PartialOrd for RoaDefinition {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl AsRef<TypedPrefix> for RoaDefinition {
fn as_ref(&self) -> &TypedPrefix {
&self.prefix
}
}
//------------ RouteAuthorizationUpdates -----------------------------------
/// This type defines a delta of Route Authorizations, i.e. additions or removals
@@ -116,7 +168,7 @@ impl fmt::Display for RoaDefinition {
/// on its resource certificates.
///
/// Multiple updates are sent as a single delta, because it's important that
/// all authorisations for a given prefix are published together in order to
/// all authorizations for a given prefix are published together in order to
/// avoid invalidating announcements.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RoaDefinitionUpdates {
@@ -222,7 +274,7 @@ impl From<RouteAuthorizationUpdates> for RoaDefinitionUpdates {
}
//------------ TypedPrefix -------------------------------------------------
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum TypedPrefix {
V4(Ipv4Prefix),
V6(Ipv6Prefix),
@@ -239,6 +291,25 @@ impl TypedPrefix {
TypedPrefix::V6(v6) => IpAddr::V6(v6.0.to_v6()),
}
}
fn matches_type(&self, other: &TypedPrefix) -> bool {
match &self {
TypedPrefix::V4(_) => match other {
TypedPrefix::V4(_) => true,
TypedPrefix::V6(_) => false,
},
TypedPrefix::V6(_) => match other {
TypedPrefix::V4(_) => false,
TypedPrefix::V6(_) => true,
},
}
}
pub fn matching_or_less_specific(&self, other: &TypedPrefix) -> bool {
self.matches_type(other)
&& self.prefix().min().le(&other.prefix().min())
&& self.prefix().max().ge(&other.prefix().max())
}
}
impl FromStr for TypedPrefix {
@@ -259,6 +330,12 @@ impl FromStr for TypedPrefix {
}
}
impl fmt::Debug for TypedPrefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self)
}
}
impl fmt::Display for TypedPrefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
@@ -268,6 +345,22 @@ impl fmt::Display for TypedPrefix {
}
}
impl Ord for TypedPrefix {
fn cmp(&self, other: &Self) -> Ordering {
let mut ordering = self.addr().cmp(&other.addr());
if ordering == Ordering::Equal {
ordering = self.addr_len().cmp(&other.addr_len())
}
ordering
}
}
impl PartialOrd for TypedPrefix {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl AsRef<Prefix> for TypedPrefix {
fn as_ref(&self) -> &Prefix {
match self {
@@ -326,28 +419,52 @@ impl From<TypedPrefix> for ResourceSet {
}
//------------ Ipv4Prefix --------------------------------------------------
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Ipv4Prefix(Prefix);
impl AsRef<Prefix> for Ipv4Prefix {
fn as_ref(&self) -> &Prefix {
&self.0
}
}
impl fmt::Display for Ipv4Prefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}/{}", self.0.to_v4(), self.0.addr_len())
}
}
impl fmt::Debug for Ipv4Prefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self)
}
}
//------------ Ipv6Prefix --------------------------------------------------
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Ipv6Prefix(Prefix);
impl AsRef<Prefix> for Ipv6Prefix {
fn as_ref(&self) -> &Prefix {
&self.0
}
}
impl fmt::Display for Ipv6Prefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}/{}", self.0.to_v6(), self.0.addr_len())
}
}
impl fmt::Debug for Ipv6Prefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self)
}
}
//------------ AsNumber ----------------------------------------------------
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct AsNumber(u32);
impl AsNumber {
@@ -372,12 +489,30 @@ impl FromStr for AsNumber {
}
}
impl fmt::Debug for AsNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self)
}
}
impl fmt::Display for AsNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Ord for AsNumber {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
impl PartialOrd for AsNumber {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
//------------ AuthorizationFmtError -------------------------------------
#[derive(Clone, Debug, Display, Eq, PartialEq)]
@@ -388,10 +523,10 @@ pub enum AuthorizationFmtError {
#[display(fmt = "Invalid asn in string: {}", _0)]
Asn(String),
#[display(fmt = "Invalid authorisation string: {}", _0)]
#[display(fmt = "Invalid authorization string: {}", _0)]
Auth(String),
#[display(fmt = "Invalid authorisation delta string: {}", _0)]
#[display(fmt = "Invalid authorization delta string: {}", _0)]
Delta(String),
}
@@ -419,6 +554,8 @@ impl AuthorizationFmtError {
mod tests {
use super::*;
use crate::test::definition;
#[test]
fn parse_delta() {
let delta = concat!(
@@ -432,11 +569,11 @@ mod tests {
let expected = {
let mut added = HashSet::new();
added.insert(RoaDefinition::from_str("192.168.0.0/16 => 64496").unwrap());
added.insert(RoaDefinition::from_str("192.168.1.0/24 => 64496").unwrap());
added.insert(definition("192.168.0.0/16 => 64496"));
added.insert(definition("192.168.1.0/24 => 64496"));
let mut removed = HashSet::new();
removed.insert(RoaDefinition::from_str("192.168.3.0/24 => 64496").unwrap());
removed.insert(definition("192.168.3.0/24 => 64496"));
RoaDefinitionUpdates::new(added, removed)
};
@@ -455,12 +592,12 @@ mod tests {
#[test]
fn normalize_roa_definition_json() {
let def = RoaDefinition::from_str("192.168.0.0/16 => 64496").unwrap();
let def = definition("192.168.0.0/16 => 64496");
let json = serde_json::to_string(&def).unwrap();
let expected = "{\"asn\":64496,\"prefix\":\"192.168.0.0/16\"}";
assert_eq!(json, expected);
let def = RoaDefinition::from_str("192.168.0.0/16-24 => 64496").unwrap();
let def = definition("192.168.0.0/16-24 => 64496");
let json = serde_json::to_string(&def).unwrap();
let expected = "{\"asn\":64496,\"prefix\":\"192.168.0.0/16\",\"max_length\":24}";
assert_eq!(json, expected);
@@ -469,7 +606,7 @@ mod tests {
#[test]
fn serde_roa_definition() {
fn parse_ser_de_print_definition(s: &str) {
let def = RoaDefinition::from_str(s).unwrap();
let def = definition(s);
let ser = serde_json::to_string(&def).unwrap();
let de = serde_json::from_str(&ser).unwrap();
assert_eq!(def, de);
@@ -508,4 +645,23 @@ mod tests {
invalid_max_length("2001:db8::/32-31 => 64496");
invalid_max_length("2001:db8::/32-129 => 64496");
}
#[test]
fn roa_includes() {
let covering = definition("192.168.0.0/16-20 => 64496");
let included_no_ml = definition("192.168.0.0/16 => 64496");
let included_more_specific = definition("192.168.0.0/20 => 64496");
let allowing_more_specific = definition("192.168.0.0/16-24 => 64496");
let more_specific = definition("192.168.3.0/24 => 64496");
let other_asn = definition("192.168.3.0/24 => 64497");
assert!(covering.includes(&included_no_ml));
assert!(covering.includes(&included_more_specific));
assert!(!covering.includes(&more_specific));
assert!(!covering.includes(&allowing_more_specific));
assert!(!covering.includes(&other_asn));
}
}
+272
View File
@@ -0,0 +1,272 @@
use std::env;
use std::sync::RwLock;
use chrono::Duration;
use rpki::x509::Time;
use crate::commons::api::{ResourceSet, RoaDefinition};
use crate::commons::bgp::{
make_roa_tree, make_validated_announcement_tree, Announcement, AnnouncementValidity,
Announcements, BgpAnalysisEntry, BgpAnalysisReport, IpRange, RisDumpError, RisDumpLoader,
ValidatedAnnouncement,
};
use crate::constants::{BGP_RIS_REFRESH_MINUTES, KRILL_ENV_TEST_ANN};
//------------ BgpAnalyser -------------------------------------------------
/// This type helps analyse ROAs vs BGP and vice versa.
pub struct BgpAnalyser {
dumploader: Option<RisDumpLoader>,
seen: RwLock<Announcements>,
}
impl BgpAnalyser {
pub fn new(ris_enabled: bool, ris_v4_uri: &str, ris_v6_uri: &str) -> Self {
if env::var(KRILL_ENV_TEST_ANN).is_ok() {
Self::with_test_announcements()
} else {
let dumploader = if ris_enabled {
Some(RisDumpLoader::new(ris_v4_uri, ris_v6_uri))
} else {
None
};
BgpAnalyser {
dumploader,
seen: RwLock::new(Announcements::default()),
}
}
}
pub async fn update(&self) -> Result<bool, BgpAnalyserError> {
if let Some(loader) = &self.dumploader {
let mut seen = self.seen.write().unwrap();
if let Some(last_time) = seen.last_updated() {
if (last_time + Duration::minutes(BGP_RIS_REFRESH_MINUTES)) > Time::now() {
debug!("Will not check BGP Ris Dumps until the refresh interval has passed");
return Ok(false); // no need to update yet
}
}
let announcements = loader.download_updates().await?;
if seen.equivalent(&announcements) {
info!("BGP Ris Dumps unchanged");
Ok(false)
} else {
info!(
"Found {} announcements based on BGP Ris Dumps",
announcements.len()
);
seen.update(announcements);
Ok(true)
}
} else {
Ok(false)
}
}
pub fn analyse(&self, roas: &[RoaDefinition], scope: &ResourceSet) -> BgpAnalysisReport {
let seen = self.seen.read().unwrap();
let mut entries = vec![];
if seen.last_updated().is_none() {
// nothing to analyse, just push all ROAs as 'no announcement info'
for roa in roas {
entries.push(BgpAnalysisEntry::roa_no_announcement_info(*roa));
}
} else {
let roa_tree = make_roa_tree(roas);
let (v4_scope, v6_scope) = IpRange::for_resource_set(&scope);
let mut scoped_announcements = vec![];
for block in v4_scope.into_iter() {
scoped_announcements.append(&mut seen.contained_by(block));
}
for block in v6_scope.into_iter() {
scoped_announcements.append(&mut seen.contained_by(block));
}
let validated: Vec<ValidatedAnnouncement> = scoped_announcements
.into_iter()
.map(|a| a.validate(&roa_tree))
.collect();
// Check all ROAs.. and report ROA state in relation to validated announcements
let validated_tree = make_validated_announcement_tree(validated.as_slice());
for roa in roas {
let covered = validated_tree.matching_or_more_specific(&roa.prefix());
if covered.is_empty() {
entries.push(BgpAnalysisEntry::roa_stale(*roa))
} else {
let allows: Vec<Announcement> = covered
.iter()
.filter(|va| {
// VALID announcements under THIS ROA
// Already covered so it's under this ROA's prefix
// ASN must match
// Prefix length must be allowed under this ROA (it could be allowed by another ROA and therefore valid)
va.validity() == AnnouncementValidity::Valid
&& va.announcement().prefix().addr_len()
<= roa.effective_max_length()
&& va.announcement().asn() == &roa.asn()
})
.map(|va| va.announcement())
.collect();
let disallows: Vec<Announcement> = covered
.iter()
.filter(|va| {
let validity = va.validity();
validity == AnnouncementValidity::InvalidLength
|| validity == AnnouncementValidity::InvalidAsn
})
.map(|va| va.announcement())
.collect();
entries.push(BgpAnalysisEntry::roa_seen(*roa, allows, disallows))
}
}
// Loop over all validated announcements and report
for v in validated.into_iter() {
let (announcement, validity, allowed_by, invalidating_roas) = v.unpack();
match validity {
AnnouncementValidity::Valid => {
entries.push(BgpAnalysisEntry::announcement_valid(
announcement,
allowed_by.unwrap(), // always set for valid announcements
))
}
AnnouncementValidity::InvalidLength => {
entries.push(BgpAnalysisEntry::announcement_invalid_length(
announcement,
invalidating_roas,
));
}
AnnouncementValidity::InvalidAsn => {
entries.push(BgpAnalysisEntry::announcement_invalid_asn(
announcement,
invalidating_roas,
));
}
AnnouncementValidity::NotFound => {
entries.push(BgpAnalysisEntry::announcement_not_found(announcement));
}
}
}
}
BgpAnalysisReport::new(entries)
}
fn test_announcements() -> Vec<Announcement> {
use crate::test::announcement;
let mut res = vec![];
res.push(announcement("10.0.0.0/22 => 64496"));
res.push(announcement("10.0.2.0/23 => 64496"));
res.push(announcement("10.0.0.0/24 => 64496"));
res.push(announcement("10.0.0.0/22 => 64497"));
res.push(announcement("10.0.0.0/21 => 64497"));
res.push(announcement("192.168.0.0/26 => 64497"));
res.push(announcement("192.168.0.0/26 => 64496"));
res.push(announcement("2001:DB8::/32 => 64498"));
res
}
fn with_test_announcements() -> Self {
let mut announcements = Announcements::default();
announcements.update(Self::test_announcements());
BgpAnalyser {
dumploader: None,
seen: RwLock::new(announcements),
}
}
}
//------------ Error --------------------------------------------------------
#[derive(Debug, Display)]
pub enum BgpAnalyserError {
#[display(fmt = "BGP RIS update error: {}", _0)]
RisDump(RisDumpError),
}
impl From<RisDumpError> for BgpAnalyserError {
fn from(e: RisDumpError) -> Self {
BgpAnalyserError::RisDump(e)
}
}
//------------ Tests --------------------------------------------------------
#[cfg(test)]
mod tests {
use crate::commons::bgp::BgpAnalysisState;
use crate::test::*;
use super::*;
#[tokio::test]
#[ignore]
async fn download_ris_dumps() {
let bgp_risdump_v4_uri = "http://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz";
let bgp_risdump_v6_uri = "http://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz";
let analyser = BgpAnalyser::new(true, bgp_risdump_v4_uri, bgp_risdump_v6_uri);
assert!(analyser.seen.read().unwrap().is_empty());
assert!(analyser.seen.read().unwrap().last_updated().is_none());
analyser.update().await.unwrap();
assert!(!analyser.seen.read().unwrap().is_empty());
assert!(analyser.seen.read().unwrap().last_updated().is_some());
}
#[test]
fn analyse_bgp() {
let roa_authorizing = definition("10.0.0.0/22-23 => 64496");
let roa_stale = definition("10.0.3.0/24 => 64497");
let roa_disallowing = definition("10.0.4.0/24 => 0");
let resources = ResourceSet::from_strs("", "10.0.0.0/16", "").unwrap();
let analyser = BgpAnalyser::with_test_announcements();
let report = analyser.analyse(&[roa_authorizing, roa_stale, roa_disallowing], &resources);
let expected: BgpAnalysisReport = serde_json::from_str(include_str!(
"../../../test-resources/bgp/expected_full_report.json"
))
.unwrap();
assert_eq!(report, expected);
}
#[test]
fn analyse_bgp_no_announcements() {
let roa1 = definition("10.0.0.0/23-24 => 64496");
let roa2 = definition("10.0.3.0/24 => 64497");
let roa3 = definition("10.0.4.0/24 => 0");
let resources = ResourceSet::from_strs("", "10.0.0.0/16", "").unwrap();
let analyser = BgpAnalyser::new(false, "", "");
let table = analyser.analyse(&[roa1, roa2, roa3], &resources);
let table_entries = table.entries();
assert_eq!(3, table_entries.len());
let roas_no_info: Vec<&RoaDefinition> = table_entries
.iter()
.filter(|e| e.state() == BgpAnalysisState::RoaNoAnnouncementInfo)
.map(|e| e.definition())
.collect();
assert_eq!(roas_no_info.as_slice(), &[&roa1, &roa2, &roa3]);
}
}
+328
View File
@@ -0,0 +1,328 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::fmt;
use std::iter::FromIterator;
use std::str::FromStr;
use rpki::x509::Time;
use crate::commons::api::{AsNumber, RoaDefinition, TypedPrefix};
use crate::commons::bgp::{IpRange, TypedPrefixTree, TypedPrefixTreeBuilder};
//------------ AnnouncementTree ----------------------------------------------
pub type AnnouncementTree = TypedPrefixTree<Announcement>;
//------------ RoaTree -------------------------------------------------------
pub type RoaTree = TypedPrefixTree<RoaDefinition>;
pub fn make_roa_tree(roas: &[RoaDefinition]) -> RoaTree {
make_tree(roas)
}
pub type ValidatedAnnouncementTree = TypedPrefixTree<ValidatedAnnouncement>;
pub fn make_validated_announcement_tree(
validated: &[ValidatedAnnouncement],
) -> ValidatedAnnouncementTree {
make_tree(validated)
}
fn make_tree<V>(els: &[V]) -> TypedPrefixTree<V>
where
V: AsRef<TypedPrefix> + Clone,
{
let mut builder = TypedPrefixTreeBuilder::default();
for el in els {
builder.add(el.clone());
}
builder.build()
}
//------------ Announcement --------------------------------------------------
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Announcement {
asn: AsNumber,
prefix: TypedPrefix,
}
impl Announcement {
pub fn new(asn: AsNumber, prefix: TypedPrefix) -> Self {
Announcement { asn, prefix }
}
pub fn asn(&self) -> &AsNumber {
&self.asn
}
pub fn prefix(&self) -> &TypedPrefix {
&self.prefix
}
pub fn validate(&self, roas: &RoaTree) -> ValidatedAnnouncement {
let covering = roas.matching_or_less_specific(&self.prefix);
if covering.is_empty() {
ValidatedAnnouncement {
announcement: *self,
validity: AnnouncementValidity::NotFound,
authorizing: None,
disallowing: vec![],
}
} else {
let mut invalidating = vec![];
let mut same_asn_found = false;
for roa in covering {
if roa.asn() == self.asn {
if roa.prefix().matching_or_less_specific(&self.prefix)
&& roa.effective_max_length() >= self.prefix.addr_len()
{
return ValidatedAnnouncement {
announcement: *self,
validity: AnnouncementValidity::Valid,
authorizing: Some(*roa),
disallowing: vec![],
};
} else {
same_asn_found = true;
}
}
invalidating.push(*roa);
}
let validity = if same_asn_found {
AnnouncementValidity::InvalidLength
} else {
AnnouncementValidity::InvalidAsn
};
ValidatedAnnouncement {
announcement: *self,
validity,
authorizing: None,
disallowing: invalidating,
}
}
}
}
impl FromStr for Announcement {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let as_roa =
RoaDefinition::from_str(s).map_err(|e| format!("Can't parse: {}, Error: {}", s, e))?;
if as_roa.max_length().is_some() {
Err(format!(
"Cannot parse announcement (max length not allowed): {}",
s
))
} else {
Ok(as_roa.into())
}
}
}
impl fmt::Display for Announcement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} => {}", self.prefix, self.asn)
}
}
impl Ord for Announcement {
fn cmp(&self, other: &Self) -> Ordering {
let mut ordering = self.prefix.cmp(&other.prefix());
if ordering == Ordering::Equal {
ordering = self.asn.cmp(&other.asn);
}
ordering
}
}
impl PartialOrd for Announcement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl From<Announcement> for RoaDefinition {
fn from(a: Announcement) -> Self {
RoaDefinition::new(a.asn, a.prefix, None)
}
}
impl From<RoaDefinition> for Announcement {
fn from(d: RoaDefinition) -> Self {
Announcement {
asn: d.asn(),
prefix: d.prefix(),
}
}
}
impl AsRef<TypedPrefix> for Announcement {
fn as_ref(&self) -> &TypedPrefix {
&self.prefix
}
}
//------------ Announcements -------------------------------------------------
pub struct Announcements {
seen: TypedPrefixTree<Announcement>,
last_updated: Option<Time>,
}
impl Announcements {
pub fn update(&mut self, announcements: Vec<Announcement>) {
let mut builder = TypedPrefixTreeBuilder::default();
for a in announcements {
builder.add(a);
}
let tree = builder.build();
self.seen = tree;
self.last_updated = Some(Time::now());
}
pub fn equivalent(&self, announcements: &[Announcement]) -> bool {
let current_set: HashSet<&Announcement> = HashSet::from_iter(self.seen.all().into_iter());
let new_set: HashSet<&Announcement> = HashSet::from_iter(announcements.iter());
current_set == new_set
}
pub fn all(&self) -> Vec<&Announcement> {
self.seen.all()
}
pub fn contained_by(&self, range: impl Into<IpRange>) -> Vec<&Announcement> {
self.seen.matching_or_more_specific(range)
}
pub fn size(&self) -> usize {
self.seen.size()
}
pub fn is_empty(&self) -> bool {
self.size() == 0
}
pub fn last_updated(&self) -> Option<Time> {
self.last_updated
}
}
impl Default for Announcements {
fn default() -> Self {
Announcements {
seen: TypedPrefixTreeBuilder::default().build(),
last_updated: None,
}
}
}
//------------ ValidatedAnnouncement -----------------------------------------
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ValidatedAnnouncement {
announcement: Announcement,
validity: AnnouncementValidity,
authorizing: Option<RoaDefinition>,
disallowing: Vec<RoaDefinition>,
}
impl ValidatedAnnouncement {
pub fn validity(&self) -> AnnouncementValidity {
self.validity
}
pub fn announcement(&self) -> Announcement {
self.announcement
}
pub fn unpack(
self,
) -> (
Announcement,
AnnouncementValidity,
Option<RoaDefinition>,
Vec<RoaDefinition>,
) {
(
self.announcement,
self.validity,
self.authorizing,
self.disallowing,
)
}
}
impl AsRef<TypedPrefix> for ValidatedAnnouncement {
fn as_ref(&self) -> &TypedPrefix {
&self.announcement.prefix
}
}
//------------ AnnouncementValidity -------------------------------------------
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum AnnouncementValidity {
Valid,
InvalidLength,
InvalidAsn,
NotFound,
}
//------------ Tests --------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::test::*;
#[test]
fn find_contained() {
let ann_v4 = Announcement::from_str("1.0.0.0/24 => 13335").unwrap();
let ann_v6 = Announcement::from_str("2001:4:112::/48 => 112").unwrap();
let mut announcements = Announcements::default();
announcements.update(vec![ann_v4.clone(), ann_v6.clone()]);
let matches = announcements.contained_by(ann_v4.prefix());
assert_eq!(1, matches.len());
assert!(matches.contains(&&ann_v4));
let matches = announcements.contained_by(ann_v6.prefix());
assert_eq!(1, matches.len());
assert!(matches.contains(&&ann_v6));
}
#[test]
fn validate_announcement() {
let roa_authorizing_1 = definition("10.0.0.0/23-24 => 64496");
let roa_authorizing_2 = definition("10.0.0.0/23 => 64498");
let roa_irrelevant = definition("10.1.0.0/23-24 => 64496");
let ann_v1 = announcement("10.0.0.0/24 => 64496");
let ann_v2 = announcement("10.0.1.0/24 => 64496");
let ann_ia = announcement("10.0.0.0/24 => 64497");
let ann_il = announcement("10.0.1.0/24 => 64498");
let ann_nf = announcement("10.2.0.0/24 => 64497");
let mut roas_builder = TypedPrefixTreeBuilder::default();
roas_builder.add(roa_authorizing_1);
roas_builder.add(roa_authorizing_2);
roas_builder.add(roa_irrelevant);
let roas = roas_builder.build();
fn assert_state(ann: &Announcement, roas: &RoaTree, expected: AnnouncementValidity) {
assert_eq!(ann.validate(roas).validity, expected);
}
assert_state(&ann_v1, &roas, AnnouncementValidity::Valid);
assert_state(&ann_v2, &roas, AnnouncementValidity::Valid);
assert_state(&ann_ia, &roas, AnnouncementValidity::InvalidAsn);
assert_state(&ann_il, &roas, AnnouncementValidity::InvalidLength);
assert_state(&ann_nf, &roas, AnnouncementValidity::NotFound);
}
}
+217
View File
@@ -0,0 +1,217 @@
use std::collections::HashMap;
use std::ops::Range;
use intervaltree::IntervalTree;
use crate::commons::api::{ResourceSet, TypedPrefix};
//------------ IpRange -----------------------------------------------------
#[derive(Clone, Debug)]
pub struct IpRange(Range<u128>);
impl IpRange {
/// Returns the IPv4 (left) and IPv6 (right) ranges as a tuple.
pub fn for_resource_set(set: &ResourceSet) -> (Vec<IpRange>, Vec<IpRange>) {
let mut v4_ranges = vec![];
let mut v6_ranges = vec![];
if let Some(v4) = set.to_ip_resources_v4().as_blocks() {
for block in v4.iter() {
let min = block.min();
let max = block.max();
let start = min.to_v4().to_ipv6_mapped().into();
let end = max.to_v4().to_ipv6_mapped().into();
v4_ranges.push(IpRange(Range { start, end }))
}
}
if let Some(v6) = set.to_ip_resources_v6().as_blocks() {
for block in v6.iter() {
let min = block.min();
let max = block.max();
let start = min.to_v6().into();
let end = max.to_v6().into();
v6_ranges.push(IpRange(Range { start, end }))
}
}
(v4_ranges, v6_ranges)
}
pub fn contains(&self, other: &Range<u128>) -> bool {
self.0.start <= other.start && self.0.end >= other.end
}
pub fn is_contained_by(&self, other: &Range<u128>) -> bool {
other.start <= self.0.start && other.end >= self.0.end
}
}
impl From<&TypedPrefix> for IpRange {
fn from(tp: &TypedPrefix) -> Self {
match tp {
TypedPrefix::V4(pfx) => {
let (min, max) = pfx.as_ref().range();
let start = min.to_v4().to_ipv6_mapped().into();
let end = max.to_v4().to_ipv6_mapped().into();
IpRange(Range { start, end })
}
TypedPrefix::V6(pfx) => {
let (min, max) = pfx.as_ref().range();
let start = min.to_v6().into();
let end = max.to_v6().into();
IpRange(Range { start, end })
}
}
}
}
//------------ TypedPrefixTree ---------------------------------------------
pub struct TypedPrefixTree<V: AsRef<TypedPrefix>> {
tree: IntervalTree<u128, Vec<V>>,
}
impl<V: AsRef<TypedPrefix>> TypedPrefixTree<V> {
pub fn matching_or_more_specific(&self, range: impl Into<IpRange>) -> Vec<&V> {
let range: IpRange = range.into();
let mut res = vec![];
for el in self.tree.query(range.0.clone()) {
if range.contains(&el.range) {
for v in &el.value {
res.push(v)
}
}
}
res
}
pub fn matching_or_less_specific(&self, range: impl Into<IpRange>) -> Vec<&V> {
let range: IpRange = range.into();
let mut res = vec![];
for el in self.tree.query(range.0.clone()) {
if range.is_contained_by(&el.range) {
for v in &el.value {
res.push(v)
}
}
}
res
}
pub fn size(&self) -> usize {
self.tree.iter().count()
}
pub fn all(&self) -> Vec<&V> {
self.tree
.iter()
.flat_map(|el| el.value.as_slice())
.collect()
}
}
//------------ TypedPrefixTreeBuilder --------------------------------------
pub struct TypedPrefixTreeBuilder<V: AsRef<TypedPrefix>> {
values: HashMap<Range<u128>, Vec<V>>,
}
impl<V: AsRef<TypedPrefix>> TypedPrefixTreeBuilder<V> {
pub fn add(&mut self, value: V) {
let range = IpRange::from(value.as_ref()).0;
let entry = self.values.entry(range).or_insert_with(|| vec![]);
entry.push(value);
}
pub fn build(self) -> TypedPrefixTree<V> {
let tree = self.values.into_iter().collect();
TypedPrefixTree { tree }
}
}
impl<V: AsRef<TypedPrefix>> Default for TypedPrefixTreeBuilder<V> {
fn default() -> Self {
TypedPrefixTreeBuilder {
values: HashMap::new(),
}
}
}
//------------ Tests --------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::commons::api::ResourceSet;
use crate::commons::bgp::Announcement;
use std::str::FromStr;
fn ann(s: &str) -> Announcement {
Announcement::from_str(s).unwrap()
}
fn pfx(s: &str) -> TypedPrefix {
TypedPrefix::from_str(s).unwrap()
}
fn range_pfx(s: &str) -> IpRange {
IpRange::from(&pfx(s))
}
fn make_test_tree() -> TypedPrefixTree<Announcement> {
let mut builder = TypedPrefixTreeBuilder::default();
builder.add(ann("10.0.0.0/24 => 64496"));
builder.add(ann("10.0.1.0/24 => 64496"));
builder.add(ann("10.0.0.0/23 => 64496"));
builder.add(ann("10.0.0.0/20 => 64496"));
builder.add(ann("10.0.0.0/16 => 64496"));
builder.build()
}
#[test]
fn range_contains() {
let more_specific_1 = range_pfx("10.0.0.0/24");
let more_specific_2 = range_pfx("10.0.1.0/24");
let test_pfx = range_pfx("10.0.0.0/23");
assert!(test_pfx.contains(&more_specific_1.0));
assert!(test_pfx.contains(&more_specific_2.0));
}
#[test]
fn typed_prefix_tree_more_specific() {
let tree = make_test_tree();
let search = TypedPrefix::from_str("10.0.0.0/23").unwrap();
assert_eq!(3, tree.matching_or_more_specific(&search).len());
let search = TypedPrefix::from_str("10.0.2.0/24").unwrap();
assert_eq!(0, tree.matching_or_more_specific(&search).len());
}
#[test]
fn typed_prefix_tree_less_specific() {
let tree = make_test_tree();
let search = TypedPrefix::from_str("10.0.0.0/23").unwrap();
assert_eq!(3, tree.matching_or_less_specific(&search).len());
let search = TypedPrefix::from_str("10.0.0.0/24").unwrap();
assert_eq!(4, tree.matching_or_less_specific(&search).len());
let search = TypedPrefix::from_str("10.0.0.0/16").unwrap();
assert_eq!(1, tree.matching_or_less_specific(&search).len());
let search = TypedPrefix::from_str("10.0.0.0/15").unwrap();
assert_eq!(0, tree.matching_or_less_specific(&search).len());
}
#[test]
fn set_to_ranges() {
let asns = "AS65000-AS65003, AS65005";
let ipv4s = "10.0.0.0/8, 192.168.0.0";
let ipv6s = "::1, 2001:db8::/32";
let set = ResourceSet::from_strs(asns, ipv4s, ipv6s).unwrap();
let (v4_ranges, v6_ranges) = IpRange::for_resource_set(&set);
assert_eq!(2, v4_ranges.len());
assert_eq!(2, v6_ranges.len());
}
}
+14
View File
@@ -0,0 +1,14 @@
mod analyser;
pub use self::analyser::*;
mod announcements;
pub use self::announcements::*;
mod iptree;
pub use self::iptree::*;
mod risdumps;
pub use self::risdumps::*;
mod report;
pub use self::report::*;
+558
View File
@@ -0,0 +1,558 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use crate::commons::api::{BgpStats, RoaDefinition};
use crate::commons::bgp::Announcement;
//------------ BgpAnalysisReport -------------------------------------------
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct BgpAnalysisReport(Vec<BgpAnalysisEntry>);
impl BgpAnalysisReport {
pub fn new(mut roas: Vec<BgpAnalysisEntry>) -> Self {
roas.sort();
BgpAnalysisReport(roas)
}
pub fn entries(&self) -> &Vec<BgpAnalysisEntry> {
&self.0
}
pub fn matching_defs(&self, state: BgpAnalysisState) -> Vec<&RoaDefinition> {
self.matching_entries(state)
.into_iter()
.map(|e| &e.definition)
.collect()
}
pub fn matching_entries(&self, state: BgpAnalysisState) -> Vec<&BgpAnalysisEntry> {
self.0.iter().filter(|e| e.state == state).collect()
}
}
impl From<BgpAnalysisReport> for BgpStats {
fn from(r: BgpAnalysisReport) -> BgpStats {
let mut stats = BgpStats::default();
for e in r.0.iter() {
match e.state {
BgpAnalysisState::AnnouncementValid => stats.increment_valid(),
BgpAnalysisState::AnnouncementInvalidAsn => stats.increment_invalid_asn(),
BgpAnalysisState::AnnouncementInvalidLength => stats.increment_invalid_length(),
BgpAnalysisState::AnnouncementNotFound => stats.increment_not_found(),
BgpAnalysisState::RoaStale => stats.increment_stale(),
_ => {} // nothing to see, move along
}
}
stats
}
}
impl fmt::Display for BgpAnalysisReport {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let entries = self.entries();
let mut entry_map: HashMap<BgpAnalysisState, Vec<&BgpAnalysisEntry>> = HashMap::new();
for entry in entries.iter() {
let state = entry.state();
entry_map.entry(state).or_insert_with(|| vec![]);
entry_map.get_mut(&state).unwrap().push(entry);
}
if entry_map.contains_key(&BgpAnalysisState::RoaNoAnnouncementInfo) {
write!(f, "no BGP announcements known")
} else {
if let Some(authorizing) = entry_map.get(&BgpAnalysisState::RoaSeen) {
writeln!(f, "Authorizations covering announcements seen:")?;
for roa in authorizing {
writeln!(f)?;
writeln!(f, "\tDefinition: {}", roa.definition)?;
writeln!(f)?;
writeln!(f, "\t\tAuthorizes:")?;
for ann in roa.authorizes.iter() {
writeln!(f, "\t\t{}", ann)?;
}
if !roa.disallows.is_empty() {
writeln!(f)?;
writeln!(f, "\t\tDisallows:")?;
for ann in roa.disallows.iter() {
writeln!(f, "\t\t{}", ann)?;
}
}
}
writeln!(f)?;
}
if let Some(stales) = entry_map.get(&BgpAnalysisState::RoaStale) {
writeln!(
f,
"Authorizations for which no announcements are seen (possibly stale):"
)?;
writeln!(f)?;
for roa in stales {
writeln!(f, "\tDefinition: {}", roa.definition)?;
}
writeln!(f)?;
}
if let Some(valids) = entry_map.get(&BgpAnalysisState::AnnouncementValid) {
writeln!(f, "Announcements which are valid:")?;
writeln!(f)?;
for ann in valids {
writeln!(f, "\tAnnouncement: {}", ann.definition)?;
}
writeln!(f)?;
}
if let Some(invalid_asn) = entry_map.get(&BgpAnalysisState::AnnouncementInvalidAsn) {
writeln!(f, "Announcements from an unauthorized ASN:")?;
for ann in invalid_asn {
writeln!(f)?;
writeln!(f, "\tAnnouncement: {}", ann.definition)?;
writeln!(f)?;
writeln!(f, "\t\tDisallowed by authorization(s):")?;
for roa in ann.disallowed_by.iter() {
writeln!(f, "\t\t{}", roa)?;
}
}
writeln!(f)?;
}
if let Some(invalid_length) =
entry_map.get(&BgpAnalysisState::AnnouncementInvalidLength)
{
writeln!(f, "Announcements from an authorized ASN, which are too specific (not allowed by max length):")?;
for ann in invalid_length {
writeln!(f)?;
writeln!(f, "\tAnnouncement: {}", ann.definition)?;
writeln!(f)?;
writeln!(f, "\t\tDisallowed by authorization(s):")?;
for roa in ann.disallowed_by.iter() {
writeln!(f, "\t\t{}", roa)?;
}
}
writeln!(f)?;
}
if let Some(not_found) = entry_map.get(&BgpAnalysisState::AnnouncementNotFound) {
writeln!(f, "Announcements which are 'not found' (not covered by any of your authorizations):")?;
writeln!(f)?;
for ann in not_found {
writeln!(f, "\tAnnouncement: {}", ann.definition)?;
}
writeln!(f)?;
}
Ok(())
}
}
}
//------------ BgpAnalysisEntry --------------------------------------------
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct BgpAnalysisEntry {
#[serde(flatten)]
definition: RoaDefinition,
state: BgpAnalysisState,
#[serde(skip_serializing_if = "Option::is_none")]
allowed_by: Option<RoaDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
disallowed_by: Vec<RoaDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
authorizes: Vec<Announcement>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
disallows: Vec<Announcement>,
}
impl BgpAnalysisEntry {
pub fn definition(&self) -> &RoaDefinition {
&self.definition
}
pub fn state(&self) -> BgpAnalysisState {
self.state
}
pub fn allowed_by(&self) -> Option<&RoaDefinition> {
self.allowed_by.as_ref()
}
pub fn disallowed_by(&self) -> &Vec<RoaDefinition> {
&self.disallowed_by
}
pub fn authorizes(&self) -> &Vec<Announcement> {
&self.authorizes
}
pub fn disallows(&self) -> &Vec<Announcement> {
&self.disallows
}
pub fn roa_seen(
definition: RoaDefinition,
mut authorizes: Vec<Announcement>,
mut disallows: Vec<Announcement>,
) -> Self {
authorizes.sort();
disallows.sort();
BgpAnalysisEntry {
definition,
state: BgpAnalysisState::RoaSeen,
allowed_by: None,
disallowed_by: vec![],
authorizes,
disallows,
}
}
pub fn roa_stale(definition: RoaDefinition) -> Self {
BgpAnalysisEntry {
definition,
state: BgpAnalysisState::RoaStale,
allowed_by: None,
disallowed_by: vec![],
authorizes: vec![],
disallows: vec![],
}
}
pub fn roa_no_announcement_info(definition: RoaDefinition) -> Self {
BgpAnalysisEntry {
definition,
state: BgpAnalysisState::RoaNoAnnouncementInfo,
allowed_by: None,
disallowed_by: vec![],
authorizes: vec![],
disallows: vec![],
}
}
pub fn announcement_valid(announcement: Announcement, allowed_by: RoaDefinition) -> Self {
BgpAnalysisEntry {
definition: RoaDefinition::from(announcement),
state: BgpAnalysisState::AnnouncementValid,
allowed_by: Some(allowed_by),
disallowed_by: vec![],
authorizes: vec![],
disallows: vec![],
}
}
pub fn announcement_invalid_asn(
announcement: Announcement,
mut disallowed_by: Vec<RoaDefinition>,
) -> Self {
disallowed_by.sort();
BgpAnalysisEntry {
definition: RoaDefinition::from(announcement),
state: BgpAnalysisState::AnnouncementInvalidAsn,
allowed_by: None,
disallowed_by,
authorizes: vec![],
disallows: vec![],
}
}
pub fn announcement_invalid_length(
announcement: Announcement,
mut disallowed_by: Vec<RoaDefinition>,
) -> Self {
disallowed_by.sort();
BgpAnalysisEntry {
definition: RoaDefinition::from(announcement),
state: BgpAnalysisState::AnnouncementInvalidLength,
allowed_by: None,
disallowed_by,
authorizes: vec![],
disallows: vec![],
}
}
pub fn announcement_not_found(announcement: Announcement) -> Self {
BgpAnalysisEntry {
definition: RoaDefinition::from(announcement),
state: BgpAnalysisState::AnnouncementNotFound,
allowed_by: None,
disallowed_by: vec![],
authorizes: vec![],
disallows: vec![],
}
}
}
impl Ord for BgpAnalysisEntry {
fn cmp(&self, other: &Self) -> Ordering {
let mut ordering = self.state.cmp(&other.state);
if ordering == Ordering::Equal {
ordering = self.definition.cmp(&other.definition);
}
ordering
}
}
impl PartialOrd for BgpAnalysisEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
//------------ BgpAnalysisState --------------------------------------------
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialOrd, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BgpAnalysisState {
RoaSeen,
RoaStale,
AnnouncementValid,
AnnouncementInvalidLength,
AnnouncementInvalidAsn,
AnnouncementNotFound,
RoaNoAnnouncementInfo,
}
//------------ AnnouncementReport ------------------------------------------
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct AnnouncementReport(Vec<AnnouncementReportEntry>);
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct AnnouncementReportEntry {
definition: RoaDefinition,
state: AnnouncementReportState,
}
impl fmt::Display for AnnouncementReportEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let state_str = match self.state {
AnnouncementReportState::Valid => "announcement 'valid'",
AnnouncementReportState::InvalidAsn => "announcement 'invalid': unauthorized asn",
AnnouncementReportState::InvalidLength => {
"announcement 'invalid': more specific than allowed"
}
AnnouncementReportState::NotFound => {
"announcement 'not found': not covered by your ROAs"
}
AnnouncementReportState::Stale => {
"ROA does not cover any known announcement (stale or backup?)"
}
AnnouncementReportState::NoInfo => "ROA exists, but no bgp info currently available",
};
write!(f, "{}\t{}", self.definition, state_str)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AnnouncementReportState {
Valid,
InvalidAsn,
InvalidLength,
NotFound,
Stale,
NoInfo,
}
impl From<BgpAnalysisReport> for AnnouncementReport {
fn from(table: BgpAnalysisReport) -> Self {
let mut entries: Vec<AnnouncementReportEntry> = vec![];
for def in table.matching_defs(BgpAnalysisState::AnnouncementValid) {
entries.push(AnnouncementReportEntry {
definition: *def,
state: AnnouncementReportState::Valid,
})
}
for def in table.matching_defs(BgpAnalysisState::AnnouncementInvalidAsn) {
entries.push(AnnouncementReportEntry {
definition: *def,
state: AnnouncementReportState::InvalidAsn,
})
}
for def in table.matching_defs(BgpAnalysisState::AnnouncementInvalidLength) {
entries.push(AnnouncementReportEntry {
definition: *def,
state: AnnouncementReportState::InvalidLength,
})
}
for def in table.matching_defs(BgpAnalysisState::AnnouncementNotFound) {
entries.push(AnnouncementReportEntry {
definition: *def,
state: AnnouncementReportState::NotFound,
})
}
for def in table.matching_defs(BgpAnalysisState::RoaStale) {
entries.push(AnnouncementReportEntry {
definition: *def,
state: AnnouncementReportState::Stale,
})
}
for def in table.matching_defs(BgpAnalysisState::RoaNoAnnouncementInfo) {
entries.push(AnnouncementReportEntry {
definition: *def,
state: AnnouncementReportState::NoInfo,
})
}
AnnouncementReport(entries)
}
}
impl fmt::Display for AnnouncementReport {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for e in self.0.iter() {
writeln!(f, "{}", e)?;
}
Ok(())
}
}
//------------ RoaReport ---------------------------------------------------
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RoaReport(Vec<RoaReportEntry>);
impl fmt::Display for RoaReport {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for e in self.0.iter() {
writeln!(f, "{}", e)?;
}
Ok(())
}
}
impl From<BgpAnalysisReport> for RoaReport {
fn from(table: BgpAnalysisReport) -> Self {
let mut entries: Vec<RoaReportEntry> = vec![];
// for def in table.matching_defs(BgpAnalysisState::RoaStale) {
// entries.push(AnnouncementReportEntry {
// definition: def.clone(),
// state: AnnouncementReportState::Stale,
// })
// }
for entry in table.0 {
match &entry.state {
BgpAnalysisState::RoaSeen => entries.push(RoaReportEntry {
definition: entry.definition,
state: RoaReportEntryState::Covering,
authorizes: entry.authorizes,
disallows: entry.disallows,
}),
BgpAnalysisState::RoaStale => entries.push(RoaReportEntry {
definition: entry.definition,
state: RoaReportEntryState::Stale,
authorizes: entry.authorizes,
disallows: entry.disallows,
}),
BgpAnalysisState::RoaNoAnnouncementInfo => entries.push(RoaReportEntry {
definition: entry.definition,
state: RoaReportEntryState::NoInfo,
authorizes: entry.authorizes,
disallows: entry.disallows,
}),
BgpAnalysisState::AnnouncementNotFound => entries.push(RoaReportEntry {
definition: entry.definition,
state: RoaReportEntryState::NotFound,
authorizes: entry.authorizes,
disallows: entry.disallows,
}),
_ => {}
}
}
RoaReport(entries)
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RoaReportEntry {
definition: RoaDefinition,
state: RoaReportEntryState,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
authorizes: Vec<Announcement>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
disallows: Vec<Announcement>,
}
impl fmt::Display for RoaReportEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let state_str = match self.state {
RoaReportEntryState::Covering | RoaReportEntryState::Stale => format!(
"roa authorizes {}, disallows {} announcements",
self.authorizes.len(),
self.disallows.len()
),
RoaReportEntryState::NotFound => {
"announcement 'not found': not covered by your ROAs".to_string()
}
RoaReportEntryState::NoInfo => {
"ROA exists, but no bgp info currently available".to_string()
}
};
write!(f, "{}\t{}", self.definition, state_str)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RoaReportEntryState {
Covering,
Stale,
NotFound,
NoInfo,
}
//------------ Tests --------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_bgp_report_full() {
let json = include_str!("../../../test-resources/bgp/expected_full_report.json");
let report: BgpAnalysisReport = serde_json::from_str(json).unwrap();
let expected = include_str!("../../../test-resources/bgp/expected_full_report.txt");
assert_eq!(report.to_string(), expected);
}
#[test]
fn print_bgp_report_announcements() {
let json = include_str!("../../../test-resources/bgp/expected_full_report.json");
let report: BgpAnalysisReport = serde_json::from_str(json).unwrap();
let report: AnnouncementReport = report.into();
let expected_json =
include_str!("../../../test-resources/bgp/expected_announcement_report.json");
let expected: AnnouncementReport = serde_json::from_str(expected_json).unwrap();
assert_eq!(report, expected);
let expected_text =
include_str!("../../../test-resources/bgp/expected_announcement_report.txt");
assert_eq!(report.to_string(), expected_text);
}
#[test]
fn print_bgp_report_roas() {
let json = include_str!("../../../test-resources/bgp/expected_full_report.json");
let report: BgpAnalysisReport = serde_json::from_str(json).unwrap();
let report: RoaReport = report.into();
let expected_json = include_str!("../../../test-resources/bgp/expected_roa_report.json");
let expected: RoaReport = serde_json::from_str(expected_json).unwrap();
assert_eq!(report, expected);
let expected_text = include_str!("../../../test-resources/bgp/expected_roa_report.txt");
assert_eq!(report.to_string(), expected_text);
}
}
+146
View File
@@ -0,0 +1,146 @@
//! Support parsing announcements in RIS Dumps
//!
//! http://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz
use std::fmt;
use std::io::{BufRead, Read};
use std::num::ParseIntError;
use std::str::FromStr;
use bytes::Bytes;
use libflate::gzip::Decoder;
use crate::commons::api::{AsNumber, AuthorizationFmtError, TypedPrefix};
use crate::commons::bgp::Announcement;
pub struct RisDumpLoader {
bgp_risdumps_v4_uri: String,
bgp_risdumps_v6_uri: String,
}
impl RisDumpLoader {
pub fn new(bgp_risdumps_v4_uri: &str, bgp_risdumps_v6_uri: &str) -> Self {
RisDumpLoader {
bgp_risdumps_v4_uri: bgp_risdumps_v4_uri.to_string(),
bgp_risdumps_v6_uri: bgp_risdumps_v6_uri.to_string(),
}
}
pub async fn download_updates(&self) -> Result<Vec<Announcement>, RisDumpError> {
let v4_bytes: Bytes = reqwest::get(&self.bgp_risdumps_v4_uri)
.await?
.bytes()
.await?;
let v4_bytes = Self::gunzip(v4_bytes)?;
let mut res = Self::parse_dump(v4_bytes.as_slice())?;
let v6_bytes: Bytes = reqwest::get(&self.bgp_risdumps_v6_uri)
.await?
.bytes()
.await?;
let v6_bytes = Self::gunzip(v6_bytes)?;
res.append(&mut Self::parse_dump(v6_bytes.as_slice())?);
Ok(res)
}
fn gunzip(bytes: Bytes) -> Result<Vec<u8>, RisDumpError> {
let mut gunzipped: Vec<u8> = vec![];
let mut decoder = Decoder::new(bytes.as_ref()).unwrap();
decoder.read_to_end(&mut gunzipped).unwrap();
Ok(gunzipped)
}
fn parse_dump(bytes: &[u8]) -> Result<Vec<Announcement>, RisDumpError> {
let mut res = vec![];
for lres in bytes.lines() {
let line = lres.map_err(RisDumpError::parse_error)?;
if line.is_empty() || line.starts_with('%') {
continue;
}
let mut values = line.split_whitespace();
let asn_str = values.next().ok_or(RisDumpError::MissingColumn)?;
let prefix_str = values.next().ok_or(RisDumpError::MissingColumn)?;
let peers = values.next().ok_or(RisDumpError::MissingColumn)?;
if u32::from_str(peers)? <= 5 {
continue;
}
if asn_str.contains('{') {
continue; // assets not supported (not important here either)
}
let asn = AsNumber::from_str(asn_str)?;
let prefix = TypedPrefix::from_str(prefix_str)?;
let ann = Announcement::new(asn, prefix);
res.push(ann);
}
Ok(res)
}
}
//------------ Error --------------------------------------------------------
#[derive(Debug, Display)]
pub enum RisDumpError {
#[display(fmt = "Cannot get uri: {}", _0)]
ReqwestError(reqwest::Error),
#[display(fmt = "Missing column in announcements input")]
MissingColumn,
#[display(fmt = "Error parsing announcements: {}", _0)]
ParseError(String),
}
impl RisDumpError {
fn parse_error(e: impl fmt::Display) -> Self {
RisDumpError::ParseError(format!("{}", e))
}
}
impl From<AuthorizationFmtError> for RisDumpError {
fn from(e: AuthorizationFmtError) -> Self {
Self::parse_error(e)
}
}
impl From<ParseIntError> for RisDumpError {
fn from(e: ParseIntError) -> Self {
RisDumpError::parse_error(e)
}
}
impl From<reqwest::Error> for RisDumpError {
fn from(e: reqwest::Error) -> RisDumpError {
RisDumpError::ReqwestError(e)
}
}
//------------ Tests --------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore]
async fn download_bgp_ris_dumps() {
let bgp_risdump_v4_uri = "http://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz";
let bgp_risdump_v6_uri = "http://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz";
let loader = RisDumpLoader::new(bgp_risdump_v4_uri, bgp_risdump_v6_uri);
let announcements = loader.download_updates().await.unwrap();
assert!(!announcements.is_empty())
}
}
+118 -112
View File
@@ -179,7 +179,7 @@ pub enum Error {
CaChildExtraResources(Handle, ChildHandle),
#[display(fmt = "CA '{}' does not know id certificate for child '{}'", _0, _1)]
CaChildUnauthorised(Handle, ChildHandle),
CaChildUnauthorized(Handle, ChildHandle),
#[display(
fmt = "You can only update one aspect for child '{}' of CA '{}' at a time - i.e. either resources or ID cert",
@@ -189,17 +189,26 @@ pub enum Error {
CaChildUpdateOneThing(Handle, ChildHandle),
// RouteAuthorizations - ROAs
#[display(fmt = "Cannot remove unknown ROA '{}' from CA '{}'", _0, _1)]
CaAuthorisationUnknown(Handle, RouteAuthorization),
#[display(fmt = "Cannot remove unknown ROA '{}'", _1)]
CaAuthorizationUnknown(Handle, RouteAuthorization),
#[display(fmt = "Duplicate ROA '{}' for CA '{}'", _1, _0)]
CaAuthorisationDuplicate(Handle, RouteAuthorization),
#[display(fmt = "ROA '{}' already present", _1)]
CaAuthorizationDuplicate(Handle, RouteAuthorization),
#[display(fmt = "Invalid max length in ROA: '{}' for CA '{}", _1, _0)]
CaAuthorisationInvalidMaxlength(Handle, RouteAuthorization),
#[display(fmt = "ROA '{}' was not added because it is redundant", _1)]
CaAuthorizationRedundant(Handle, RouteAuthorization),
#[display(fmt = "Prefix in ROA '{}' not held by CA '{}'.", _1, _0)]
CaAuthorisationNotEntitled(Handle, RouteAuthorization),
#[display(
fmt = "ROA '{}' was not added because it would make existing ROAs redundant",
_1
)]
CaAuthorizationIncludes(Handle, RouteAuthorization),
#[display(fmt = "Invalid max length in ROA: '{}'", _1)]
CaAuthorizationInvalidMaxlength(Handle, RouteAuthorization),
#[display(fmt = "Prefix in ROA '{}' not held by you", _1)]
CaAuthorizationNotEntitled(Handle, RouteAuthorization),
//-----------------------------------------------------------------
// Key Usage Issues
@@ -496,8 +505,8 @@ impl Error {
.with_ca(ca)
.with_child(child)
}
Error::CaChildUnauthorised(ca, child) => {
ErrorResponse::new("ca-child-unauthorised", &self)
Error::CaChildUnauthorized(ca, child) => {
ErrorResponse::new("ca-child-unauthorized", &self)
.with_ca(ca)
.with_child(child)
}
@@ -509,23 +518,35 @@ impl Error {
}
// RouteAuthorizations
Error::CaAuthorisationUnknown(ca, auth) => ErrorResponse::new("ca-roa-unknown", &self)
Error::CaAuthorizationUnknown(ca, auth) => ErrorResponse::new("ca-roa-unknown", &self)
.with_ca(ca)
.with_auth(auth),
Error::CaAuthorisationDuplicate(ca, auth) => {
Error::CaAuthorizationDuplicate(ca, auth) => {
ErrorResponse::new("ca-roa-duplicate", &self)
.with_ca(ca)
.with_auth(auth)
}
Error::CaAuthorisationInvalidMaxlength(ca, auth) => {
Error::CaAuthorizationRedundant(ca, auth) => {
ErrorResponse::new("ca-roa-redundant", &self)
.with_ca(ca)
.with_auth(auth)
}
Error::CaAuthorizationIncludes(ca, auth) => {
ErrorResponse::new("ca-roa-includes", &self)
.with_ca(ca)
.with_auth(auth)
}
Error::CaAuthorizationInvalidMaxlength(ca, auth) => {
ErrorResponse::new("ca-roa-invalid-max-length", &self)
.with_ca(ca)
.with_auth(auth)
}
Error::CaAuthorisationNotEntitled(ca, auth) => {
Error::CaAuthorizationNotEntitled(ca, auth) => {
ErrorResponse::new("ca-roa-not-entitled", &self)
.with_ca(ca)
.with_auth(auth)
@@ -611,7 +632,7 @@ mod tests {
fn verify(expected_json: &str, e: Error) {
let actual = e.to_error_response();
let expected: ErrorResponse = serde_json::from_str(expected_json).unwrap();
assert_eq!(expected, actual);
assert_eq!(actual, expected);
// check that serde works too
let serialized = serde_json::to_string(&actual).unwrap();
@@ -625,24 +646,24 @@ mod tests {
let io_err = io::Error::new(io::ErrorKind::Other, "can't read file");
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/sys-io.json"),
include_str!("../../test-resources/api/regressions/errors/sys-io.json"),
Error::IoError(io_err),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/sys-store.json"),
include_str!("../../test-resources/api/regressions/errors/sys-store.json"),
Error::AggregateStoreError(AggregateStoreError::InitError),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/sys-signer.json"),
include_str!("../../test-resources/api/regressions/errors/sys-signer.json"),
Error::SignerError("signer issue".to_string()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/sys-https.json"),
include_str!("../../test-resources/api/regressions/errors/sys-https.json"),
Error::HttpsSetup("can't find pem file".to_string()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/sys-http-client.json"),
include_str!("../../test-resources/api/regressions/errors/sys-http-client.json"),
Error::HttpClientError(httpclient::Error::Forbidden),
);
@@ -654,19 +675,15 @@ mod tests {
.err()
.unwrap();
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/api-json.json"),
include_str!("../../test-resources/api/regressions/errors/api-json.json"),
Error::JsonError(json_err),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/api-unknown-method.json"
),
include_str!("../../test-resources/api/regressions/errors/api-unknown-method.json"),
Error::ApiUnknownMethod,
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/api-unknown-resource.json"
),
include_str!("../../test-resources/api/regressions/errors/api-unknown-resource.json"),
Error::ApiUnknownResource,
);
@@ -674,7 +691,7 @@ mod tests {
// Repository Issues
//-----------------------------------------------------------------
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/repo-not-set.json"),
include_str!("../../test-resources/api/regressions/errors/repo-not-set.json"),
Error::RepoNotSet,
);
@@ -682,32 +699,26 @@ mod tests {
// Publisher Issues
//-----------------------------------------------------------------
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/pub-unknown.json"),
include_str!("../../test-resources/api/regressions/errors/pub-unknown.json"),
Error::PublisherUnknown(publisher.clone()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/pub-duplicate.json"),
include_str!("../../test-resources/api/regressions/errors/pub-duplicate.json"),
Error::PublisherDuplicate(publisher),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/pub-outside-jail.json"
),
include_str!("../../test-resources/api/regressions/errors/pub-outside-jail.json"),
Error::PublisherUriOutsideBase(
"rsync://somehost/module/folder".to_string(),
"rsync://otherhost/module/folder".to_string(),
),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/pub-uri-no-slash.json"
),
include_str!("../../test-resources/api/regressions/errors/pub-uri-no-slash.json"),
Error::PublisherBaseUriNoSlash("rsync://host/module/folder".to_string()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/pub-no-embedded-repo.json"
),
include_str!("../../test-resources/api/regressions/errors/pub-no-embedded-repo.json"),
Error::PublisherNoEmbeddedRepo,
);
@@ -715,23 +726,21 @@ mod tests {
// RFC 8181
//-----------------------------------------------------------------
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/rfc8181-validation.json"
),
include_str!("../../test-resources/api/regressions/errors/rfc8181-validation.json"),
Error::Rfc8181Validation(ValidationError),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/rfc8181-decode.json"),
include_str!("../../test-resources/api/regressions/errors/rfc8181-decode.json"),
Error::Rfc8181Decode("could not parse CMS".to_string()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/rfc8181-protocol-message.json"
"../../test-resources/api/regressions/errors/rfc8181-protocol-message.json"
),
Error::Rfc8181MessageError(rfc8181::MessageError::InvalidVersion),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/rfc8181-delta.json"),
include_str!("../../test-resources/api/regressions/errors/rfc8181-delta.json"),
Error::Rfc8181Delta(PublicationDeltaError::ObjectAlreadyPresent(
uri::Rsync::from_str("rsync://host/module/file.cer").unwrap(),
)),
@@ -741,187 +750,184 @@ mod tests {
// CA Issues (label: ca-*)
//-----------------------------------------------------------------
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-duplicate.json"),
include_str!("../../test-resources/api/regressions/errors/ca-duplicate.json"),
Error::CaDuplicate(ca.clone()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-unknown.json"),
include_str!("../../test-resources/api/regressions/errors/ca-unknown.json"),
Error::CaUnknown(ca.clone()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-repo-same.json"),
include_str!("../../test-resources/api/regressions/errors/ca-repo-same.json"),
Error::CaRepoInUse(ca.clone()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-repo-issue.json"),
include_str!("../../test-resources/api/regressions/errors/ca-repo-issue.json"),
Error::CaRepoIssue(ca.clone(), "cannot connect".to_string()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-repo-response-invalid-xml.json"),
include_str!(
"../../test-resources/api/regressions/errors/ca-repo-response-invalid-xml.json"
),
Error::CaRepoResponseInvalidXml(ca.clone(), "expected some tag".to_string()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-repo-response-wrong-xml.json"),
include_str!(
"../../test-resources/api/regressions/errors/ca-repo-response-wrong-xml.json"
),
Error::CaRepoResponseWrongXml(ca.clone()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-parent-duplicate.json"
),
include_str!("../../test-resources/api/regressions/errors/ca-parent-duplicate.json"),
Error::CaParentDuplicate(ca.clone(), parent.clone()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-parent-unknown.json"
),
include_str!("../../test-resources/api/regressions/errors/ca-parent-unknown.json"),
Error::CaParentUnknown(ca.clone(), parent.clone()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-parent-issue.json"),
include_str!("../../test-resources/api/regressions/errors/ca-parent-issue.json"),
Error::CaParentIssue(ca.clone(), parent, "connection refused".to_string()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-parent-response-invalid-xml.json"),
include_str!(
"../../test-resources/api/regressions/errors/ca-parent-response-invalid-xml.json"
),
Error::CaParentResponseInvalidXml(ca.clone(), "expected something".to_string()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-parent-response-wrong-xml.json"),
include_str!(
"../../test-resources/api/regressions/errors/ca-parent-response-wrong-xml.json"
),
Error::CaParentResponseWrongXml(ca.clone()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/rfc6492-protocol.json"
),
include_str!("../../test-resources/api/regressions/errors/rfc6492-protocol.json"),
Error::Rfc6492(rfc6492::Error::InvalidVersion),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/rfc6492-invalid-csr.json"
),
include_str!("../../test-resources/api/regressions/errors/rfc6492-invalid-csr.json"),
Error::Rfc6492InvalidCsrSent("invalid signature".to_string()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/rfc6492-invalid-signature.json"
"../../test-resources/api/regressions/errors/rfc6492-invalid-signature.json"
),
Error::Rfc6492SignatureInvalid,
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-child-duplicate.json"
),
include_str!("../../test-resources/api/regressions/errors/ca-child-duplicate.json"),
Error::CaChildDuplicate(ca.clone(), child.clone()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-child-unknown.json"
),
include_str!("../../test-resources/api/regressions/errors/ca-child-unknown.json"),
Error::CaChildUnknown(ca.clone(), child.clone()),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-child-resources-required.json"),
include_str!(
"../../test-resources/api/regressions/errors/ca-child-resources-required.json"
),
Error::CaChildMustHaveResources(ca.clone(), child.clone()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-child-resources-extra.json"
"../../test-resources/api/regressions/errors/ca-child-resources-extra.json"
),
Error::CaChildExtraResources(ca.clone(), child.clone()),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-child-unauthorised.json"
),
Error::CaChildUnauthorised(ca.clone(), child),
include_str!("../../test-resources/api/regressions/errors/ca-child-unauthorized.json"),
Error::CaChildUnauthorized(ca.clone(), child),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ca-roa-unknown.json"),
Error::CaAuthorisationUnknown(ca.clone(), auth),
include_str!("../../test-resources/api/regressions/errors/ca-roa-unknown.json"),
Error::CaAuthorizationUnknown(ca.clone(), auth),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-roa-duplicate.json"
),
Error::CaAuthorisationDuplicate(ca.clone(), auth),
include_str!("../../test-resources/api/regressions/errors/ca-roa-duplicate.json"),
Error::CaAuthorizationDuplicate(ca.clone(), auth),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-roa-invalid-max-length.json"
),
Error::CaAuthorisationInvalidMaxlength(ca.clone(), auth),
include_str!("../../test-resources/api/regressions/errors/ca-roa-redundant.json"),
Error::CaAuthorizationRedundant(ca.clone(), auth),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ca-roa-not-entitled.json"
),
Error::CaAuthorisationNotEntitled(ca, auth),
include_str!("../../test-resources/api/regressions/errors/ca-roa-includes.json"),
Error::CaAuthorizationIncludes(ca.clone(), auth),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/key-re-use.json"),
include_str!(
"../../test-resources/api/regressions/errors/ca-roa-invalid-max-length.json"
),
Error::CaAuthorizationInvalidMaxlength(ca.clone(), auth),
);
verify(
include_str!("../../test-resources/api/regressions/errors/ca-roa-not-entitled.json"),
Error::CaAuthorizationNotEntitled(ca, auth),
);
verify(
include_str!("../../test-resources/api/regressions/errors/key-re-use.json"),
Error::KeyUseAttemptReuse,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/key-no-new.json"),
include_str!("../../test-resources/api/regressions/errors/key-no-new.json"),
Error::KeyUseNoNewKey,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/key-no-current.json"),
include_str!("../../test-resources/api/regressions/errors/key-no-current.json"),
Error::KeyUseNoCurrentKey,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/key-no-old.json"),
include_str!("../../test-resources/api/regressions/errors/key-no-old.json"),
Error::KeyUseNoOldKey,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/key-no-cert.json"),
include_str!("../../test-resources/api/regressions/errors/key-no-cert.json"),
Error::KeyUseNoIssuedCert,
);
let ki = test_id_certificate()
.subject_public_key_info()
.key_identifier();
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/key-no-match.json"),
include_str!("../../test-resources/api/regressions/errors/key-no-match.json"),
Error::KeyUseNoMatch(ki),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/rc-unknown.json"),
include_str!("../../test-resources/api/regressions/errors/rc-unknown.json"),
Error::ResourceClassUnknown(ResourceClassName::from("RC0")),
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/rc-resources.json"),
include_str!("../../test-resources/api/regressions/errors/rc-resources.json"),
Error::ResourceSetError(ResourceSetError::Mix),
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/rc-missing-resources.json"
),
include_str!("../../test-resources/api/regressions/errors/rc-missing-resources.json"),
Error::MissingResources,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ta-not-allowed.json"),
include_str!("../../test-resources/api/regressions/errors/ta-not-allowed.json"),
Error::TaNotAllowed,
);
verify(
include_str!(
"../../test-resources/api/regressions/v0_6_0/errors/ta-name-reserved.json"
),
include_str!("../../test-resources/api/regressions/errors/ta-name-reserved.json"),
Error::TaNameReserved,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/ta-initialised.json"),
include_str!("../../test-resources/api/regressions/errors/ta-initialised.json"),
Error::TaAlreadyInitialised,
);
verify(
include_str!("../../test-resources/api/regressions/v0_6_0/errors/general-error.json"),
include_str!("../../test-resources/api/regressions/errors/general-error.json"),
Error::custom("some unlikely corner case"),
);
@@ -929,7 +935,7 @@ mod tests {
// for e in errs {
// let error_response = e.to_error_response();
//
// let path = format!("test-resources/api/regressions/v0_6_0/errors/{}.json", error_response.label());
// let path = format!("test-resources/api/regressions/errors/{}.json", error_response.label());
// let path = PathBuf::from(&path);
//
// file::save_json(&error_response, &path).unwrap();
+2 -2
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
@@ -11,7 +12,6 @@ use crate::commons::eventsourcing::{
Aggregate, CommandKey, DiskKeyStore, Event, EventListener, KeyStore, KeyStoreError,
KeyStoreVersion, StoredCommand,
};
use std::io;
const SNAPSHOT_FREQ: u64 = 5;
@@ -116,7 +116,7 @@ impl<A: Aggregate> DiskAggregateStore<A> {
if store.aggregates().is_empty() {
store
.set_version(&KeyStoreVersion::V0_6)
.set_version(&KeyStoreVersion::V0_7)
.map_err(AggregateStoreError::KeyStoreError)?;
}
+1
View File
@@ -47,6 +47,7 @@ impl Default for StoredValueInfo {
pub enum KeyStoreVersion {
Pre0_6,
V0_6,
V0_7,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+1
View File
@@ -1,5 +1,6 @@
//! Common types used by the various Krill components.
pub mod api;
pub mod bgp;
pub mod error;
pub mod eventsourcing;
pub mod remote;
+1 -1
View File
@@ -810,7 +810,7 @@ mod tests {
}
fn example_rrdp_uri() -> uri::Https {
test::https("https://rpki.example/rrdp/notify.xml")
test::https("https://rpki.example/rrdp/notification.xml")
}
fn example_sia_base() -> uri::Rsync {
+10 -1
View File
@@ -1,9 +1,16 @@
pub const KRILL_VERSION: &str = "0.6.3";
pub const KRILL_VERSION: &str = "0.7.0";
pub const KRILL_SERVER_APP: &str = "Krill";
pub const KRILL_CLIENT_APP: &str = "Krill Client";
pub const KRILL_DEFAULT_CONFIG_FILE: &str = "./defaults/krill.conf";
pub const KRILL_ENV_TEST: &str = "KRILL_TEST";
pub const KRILL_ENV_TEST_ANN: &str = "KRILL_TEST_ANN";
pub const KRILL_ENV_REPO_ENABLED: &str = "KRILL_REPO_ENABLED";
pub const KRILL_ENV_USE_TA: &str = "KRILL_USE_TA";
pub const KRILL_ENV_LOG_LEVEL: &str = "KRILL_LOG_LEVEL";
pub const KRILL_ENV_AUTH_TOKEN: &str = "KRILL_AUTH_TOKEN";
pub const CASERVER_DIR: &str = "cas";
pub const PUBSERVER_DFLT: &str = "0";
@@ -39,4 +46,6 @@ pub const ROA_CERTIFICATE_VALIDITY_YEARS: i32 = 1;
pub const ROA_CERTIFICATE_REISSUE_WEEKS: i64 = 4;
pub const ID_CERTIFICATE_VALIDITY_YEARS: i32 = 15;
pub const BGP_RIS_REFRESH_MINUTES: i64 = 60;
pub const HTTTP_CLIENT_TIMEOUT_SECS: u64 = 120;
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::commons::api::Token;
//------------ Authorizer ----------------------------------------------------
/// This type is responsible for checking authorisations when the API is
/// This type is responsible for checking authorizations when the API is
/// accessed.
#[derive(Clone, Debug)]
pub struct Authorizer {
+16 -10
View File
@@ -29,7 +29,9 @@ use crate::commons::remote::rfc6492;
use crate::commons::remote::rfc8183;
use crate::commons::remote::sigmsg::SignedMessage;
use crate::commons::KrillResult;
use crate::constants::{CHILD_CERTIFICATE_REISSUE_WEEKS, CHILD_CERTIFICATE_VALIDITY_YEARS};
use crate::constants::{
CHILD_CERTIFICATE_REISSUE_WEEKS, CHILD_CERTIFICATE_VALIDITY_YEARS, KRILL_ENV_TEST,
};
use crate::daemon::ca::events::ChildCertificateUpdates;
use crate::daemon::ca::rc::PublishMode;
use crate::daemon::ca::signing::CsrInfo;
@@ -538,7 +540,7 @@ impl<S: Signer> CertAuth<S> {
let child_cert = child
.id_cert()
.ok_or_else(|| Error::CaChildUnauthorised(self.handle.clone(), child_handle.clone()))?;
.ok_or_else(|| Error::CaChildUnauthorized(self.handle.clone(), child_handle.clone()))?;
msg.validate(child_cert)
.map_err(|_| Error::Rfc6492SignatureInvalid)?;
@@ -687,7 +689,7 @@ impl<S: Signer> CertAuth<S> {
/// Certifies a child, unless:
/// = the child is unknown,
/// = the child is not authorised,
/// = the child is not authorized,
/// = the csr is invalid,
/// = the limit exceeds the child allocation,
/// = the signer throws up..
@@ -703,7 +705,7 @@ impl<S: Signer> CertAuth<S> {
let (rcn, limit, csr) = request.unpack();
let csr_info = CsrInfo::try_from(&csr)?;
if csr_info.contains_localhost() && env::var("KRILL_TEST").is_err() {
if csr_info.contains_localhost() && env::var(KRILL_ENV_TEST).is_err() {
return Err(Error::invalid_csr(
"Cannot use localhost in certificate requests unless server uses TEST mode.",
));
@@ -1480,7 +1482,7 @@ impl<S: Signer> CertAuth<S> {
///
impl<S: Signer> CertAuth<S> {
/// Updates the route authorizations for this CA, and update ROAs. Will return
/// an error in case authorisations are added for which this CA does not hold
/// an error in case authorizations are added for which this CA does not hold
/// the prefix.
fn route_authorizations_update(
&self,
@@ -1502,17 +1504,21 @@ impl<S: Signer> CertAuth<S> {
for auth in added {
if !auth.max_length_valid() {
return Err(Error::CaAuthorisationInvalidMaxlength(
return Err(Error::CaAuthorizationInvalidMaxlength(
self.handle.clone(),
auth,
));
}
if current_auths.contains(&auth) {
return Err(Error::CaAuthorisationDuplicate(self.handle.clone(), auth));
return Err(Error::CaAuthorizationDuplicate(self.handle.clone(), auth));
} else if !all_resources.contains(&auth.prefix().into()) {
return Err(Error::CaAuthorisationNotEntitled(self.handle.clone(), auth));
return Err(Error::CaAuthorizationNotEntitled(self.handle.clone(), auth));
} else if current_auths.iter().any(|a| a.includes(auth.as_ref())) {
return Err(Error::CaAuthorizationRedundant(self.handle.clone(), auth));
} else if current_auths.iter().any(|a| auth.includes(a.as_ref())) {
return Err(Error::CaAuthorizationIncludes(self.handle.clone(), auth));
} else {
current_auths.insert(auth);
current_auths.insert(auth.explicit_length());
res.push(StoredEvent::new(
self.handle(),
version,
@@ -1532,7 +1538,7 @@ impl<S: Signer> CertAuth<S> {
));
version += 1;
} else {
return Err(Error::CaAuthorisationUnknown(self.handle.clone(), auth));
return Err(Error::CaAuthorizationUnknown(self.handle.clone(), auth));
}
}
+11 -1
View File
@@ -29,6 +29,10 @@ impl RouteAuthorization {
pub fn new(definition: RoaDefinition) -> Self {
RouteAuthorization(definition)
}
pub fn explicit_length(self) -> Self {
RouteAuthorization(self.0.explicit_max_length())
}
}
impl AsRef<RoaDefinition> for RouteAuthorization {
@@ -300,7 +304,13 @@ impl Roas {
let signing_key = certified_key.key_id();
let mut roa_builder = RoaBuilder::new(auth.asn().into());
roa_builder.push_addr(prefix.ip_addr(), prefix.addr_len(), auth.max_length());
if auth.effective_max_length() > auth.prefix().prefix().addr_len() {
roa_builder.push_addr(prefix.ip_addr(), prefix.addr_len(), auth.max_length());
} else {
roa_builder.push_addr(prefix.ip_addr(), prefix.addr_len(), None);
}
let mut object_builder = SignedObjectBuilder::new(
Serial::random(signer).map_err(ca::Error::signer)?,
SignSupport::sign_validity_years(ROA_CERTIFICATE_VALIDITY_YEARS),
+44 -15
View File
@@ -33,13 +33,13 @@ impl ConfigDefaults {
3000
}
fn test_mode() -> bool {
env::var("KRILL_TEST").is_ok()
env::var(KRILL_ENV_TEST).is_ok()
}
fn repo_enabled() -> bool {
env::var("KRILL_REPO_ENABLED").is_ok()
env::var(KRILL_ENV_REPO_ENABLED).is_ok()
}
fn use_ta() -> bool {
env::var("KRILL_USE_TA").is_ok()
env::var(KRILL_ENV_USE_TA).is_ok()
}
fn https_mode() -> HttpsMode {
HttpsMode::Generate
@@ -54,7 +54,7 @@ impl ConfigDefaults {
"https://localhost:3000/".to_string()
}
fn log_level() -> LevelFilter {
match env::var("KRILL_LOG_LEVEL") {
match env::var(KRILL_ENV_LOG_LEVEL) {
Ok(level) => LevelFilter::from_str(&level).unwrap(),
_ => LevelFilter::Info,
}
@@ -70,7 +70,7 @@ impl ConfigDefaults {
}
fn auth_token() -> Token {
match env::var("KRILL_AUTH_TOKEN") {
match env::var(KRILL_ENV_AUTH_TOKEN) {
Ok(token) => Token::from(token),
Err(_) => {
eprintln!("You MUST provide a value for the master API key, either by setting \"auth_token\" in the config file, or by setting the KRILL_AUTH_TOKEN environment variable.");
@@ -93,6 +93,18 @@ impl ConfigDefaults {
fn post_limit_rfc6492() -> u64 {
1024 * 1024 // 1MB (for ref. the NIC br cert is about 200kB)
}
fn bgp_risdumps_enabled() -> bool {
true
}
fn bgp_risdumps_v4_uri() -> String {
"http://www.ris.ripe.net/dumps/riswhoisdump.IPv4.gz".to_string()
}
fn bgp_risdumps_v6_uri() -> String {
"http://www.ris.ripe.net/dumps/riswhoisdump.IPv6.gz".to_string()
}
}
//------------ Config --------------------------------------------------------
@@ -166,6 +178,14 @@ pub struct Config {
#[serde(default = "ConfigDefaults::post_limit_rfc6492")]
pub post_limit_rfc6492: u64,
pub rfc6492_log_dir: Option<PathBuf>,
// RIS BGP
#[serde(default = "ConfigDefaults::bgp_risdumps_enabled")]
pub bgp_risdumps_enabled: bool,
#[serde(default = "ConfigDefaults::bgp_risdumps_v4_uri")]
pub bgp_risdumps_v4_uri: String,
#[serde(default = "ConfigDefaults::bgp_risdumps_v6_uri")]
pub bgp_risdumps_v6_uri: String,
}
/// # Accessors
@@ -258,6 +278,10 @@ impl Config {
Some(dir)
};
let bgp_risdumps_enabled = false;
let bgp_risdumps_v4_uri = ConfigDefaults::bgp_risdumps_v4_uri();
let bgp_risdumps_v6_uri = ConfigDefaults::bgp_risdumps_v6_uri();
Config {
ip,
port,
@@ -281,6 +305,9 @@ impl Config {
rfc8181_log_dir,
post_limit_rfc6492,
rfc6492_log_dir,
bgp_risdumps_enabled,
bgp_risdumps_v4_uri,
bgp_risdumps_v6_uri,
}
}
@@ -308,13 +335,7 @@ impl Config {
.short("c")
.long("config")
.value_name("FILE")
.help(
"Specify non-default config file. If no file is \
specified './daemon/defaults/krill.conf' will be used to \
determine default values for all settings. Note that you \
can use any of the following options to override any of \
these values..",
)
.help("Override the path to the config file (default: './defaults/krill.conf')")
.required(false),
)
.get_matches();
@@ -370,7 +391,7 @@ impl Config {
// Set KRILL_TEST env var so that it can easily be accessed without the need to pass
// this setting down all over the application. Used by CertAuth in particular to allow
// the use of 'localhost' in Certificate Sign Requests in test mode only.
env::set_var("KRILL_TEST", "1");
env::set_var(KRILL_ENV_TEST, "1");
}
if !self.test_mode
@@ -405,6 +426,14 @@ impl Config {
if !self.service_uri.ends_with('/') {
return Err(ConfigError::other("service URI must end with '/'"));
} else {
uri::Https::from_str(&self.service_uri).map_err(|_| {
ConfigError::Other(format!("Invalid service uri: {}", self.service_uri))
})?;
if self.service_uri.as_str().matches('/').count() != 3 {
return Err(ConfigError::other("Service URI MUST specify a host name only, e.g. https://rpki.example.com:3000/"));
}
}
if !self.rrdp_service_uri().to_string().ends_with('/') {
@@ -650,8 +679,8 @@ mod tests {
// Config for auth token is required! If there is nothing in the conf
// file, then an environment variable must be set.
use std::env;
env::set_var("KRILL_AUTH_TOKEN", "secret");
env::set_var("KRILL_TEST", "1");
env::set_var(KRILL_ENV_AUTH_TOKEN, "secret");
env::set_var(KRILL_ENV_TEST, "1");
let c = Config::read_config("./defaults/krill.conf").unwrap();
let expected_socket_addr: SocketAddr = ([127, 0, 0, 1], 3000).into();
+24 -7
View File
@@ -70,6 +70,7 @@ impl AsRef<str> for ContentType {
struct Response {
status: StatusCode,
content_type: ContentType,
max_age: Option<usize>,
body: Vec<u8>,
}
@@ -78,18 +79,22 @@ impl Response {
Response {
status,
content_type: ContentType::Text,
max_age: None,
body: Vec::new(),
}
}
fn finalize(self) -> HttpResponse {
HttpResponse(
hyper::Response::builder()
.status(self.status)
.header("Content-Type", self.content_type.as_ref())
.body(self.body.into())
.unwrap(),
)
let mut builder = hyper::Response::builder()
.status(self.status)
.header("Content-Type", self.content_type.as_ref());
if let Some(max_age) = self.max_age {
builder = builder.header("Cache-Control", &format!("max-age={}", max_age));
}
let response = builder.body(self.body.into()).unwrap();
HttpResponse(response)
}
}
@@ -118,6 +123,7 @@ impl HttpResponse {
Response {
status: StatusCode::OK,
content_type,
max_age: None,
body,
}
.finalize()
@@ -142,6 +148,16 @@ impl HttpResponse {
Self::ok_response(ContentType::Xml, body)
}
pub fn xml_with_cache(body: Vec<u8>, seconds: usize) -> Self {
Response {
status: StatusCode::OK,
content_type: ContentType::Xml,
max_age: Some(seconds),
body,
}
.finalize()
}
pub fn rfc8181(body: Vec<u8>) -> Self {
Self::ok_response(ContentType::Rfc8181, body)
}
@@ -190,6 +206,7 @@ impl HttpResponse {
Response {
status,
content_type: ContentType::Json,
max_age: None,
body: body.into_bytes(),
}
.finalize()
+122 -11
View File
@@ -1,5 +1,6 @@
//! Hyper based HTTP server for Krill.
//!
use std::collections::HashMap;
use std::convert::Infallible;
use std::fs::File;
use std::path::PathBuf;
@@ -19,8 +20,8 @@ use hyper::service::{make_service_fn, service_fn};
use hyper::Method;
use crate::commons::api::{
ChildHandle, CommandHistoryCriteria, Handle, ParentCaContact, ParentCaReq, ParentHandle,
PublisherList, RepositoryUpdate,
BgpStats, ChildHandle, CommandHistoryCriteria, Handle, ParentCaContact, ParentCaReq,
ParentHandle, PublisherList, RepositoryUpdate,
};
use crate::commons::error::Error;
use crate::commons::remote::rfc8183;
@@ -28,7 +29,7 @@ use crate::daemon::config::Config;
use crate::daemon::http::statics::statics;
use crate::daemon::http::{tls, tls_keys, HttpResponse, Request, RequestPath, RoutingResult};
use crate::daemon::krillserver::KrillServer;
use crate::upgrades::upgrade;
use crate::upgrades::{post_start_upgrade, pre_start_upgrade};
//------------ State -----------------------------------------------------
@@ -36,13 +37,16 @@ pub type State = Arc<RwLock<KrillServer>>;
pub async fn start(config: Config) -> Result<(), Error> {
// Call upgrade, this will only do actual work if needed.
upgrade(&config.data_dir).map_err(|_| Error::custom("Could not upgrade Krill, check logs!"))?;
pre_start_upgrade(&config.data_dir)
.map_err(|_| Error::custom("Could not upgrade Krill, check logs!"))?;
// Create the server, this will create the necessary data sub-directories if needed
let state = {
let krill = KrillServer::build(&config)?;
Arc::new(RwLock::new(krill))
};
let krill = KrillServer::build(&config)?;
post_start_upgrade(&config.data_dir, &krill)
.map_err(|_| Error::custom("Could not upgrade Krill, check logs!"))?;
let state = Arc::new(RwLock::new(krill));
let service = make_service_fn(move |_| {
let state = state.clone();
@@ -172,6 +176,28 @@ pub async fn metrics(req: Request) -> RoutingResult {
let server = req.state();
let server = server.read().await;
struct AllBgpStats {
announcements_valid: HashMap<Handle, usize>,
announcements_invalid_asn: HashMap<Handle, usize>,
announcements_invalid_length: HashMap<Handle, usize>,
announcements_not_found: HashMap<Handle, usize>,
roas_stale: HashMap<Handle, usize>,
}
impl AllBgpStats {
fn add_ca(&mut self, ca: &Handle, stats: &BgpStats) {
self.announcements_valid
.insert(ca.clone(), stats.announcements_valid);
self.announcements_invalid_asn
.insert(ca.clone(), stats.announcements_invalid_asn);
self.announcements_invalid_length
.insert(ca.clone(), stats.announcements_invalid_length);
self.announcements_not_found
.insert(ca.clone(), stats.announcements_not_found);
self.roas_stale.insert(ca.clone(), stats.roas_stale);
}
}
let mut res = String::new();
let info = server.server_info();
@@ -274,6 +300,76 @@ pub async fn metrics(req: Request) -> RoutingResult {
));
}
// Aggregate ROA vs BGP stats per status
let mut all_bgp_stats = AllBgpStats {
announcements_valid: HashMap::new(),
announcements_invalid_asn: HashMap::new(),
announcements_invalid_length: HashMap::new(),
announcements_not_found: HashMap::new(),
roas_stale: HashMap::new(),
};
for (ca, status) in cas_status.iter() {
all_bgp_stats.add_ca(ca, status.bgp_stats());
}
res.push_str("\n");
res.push_str("# HELP krill_cas_bgp_announcements_valid number of announcements seen for CA resources with RPKI state VALID\n");
res.push_str("# TYPE krill_cas_bgp_announcements_valid gauge\n");
for (ca, nr) in all_bgp_stats.announcements_valid.iter() {
res.push_str(&format!(
"krill_cas_bgp_announcements_valid{{ca=\"{}\"}} {}\n",
ca, nr
));
}
res.push_str("\n");
res.push_str(
"# HELP krill_cas_bgp_announcements_invalid_asn number of announcements seen for CA resources with RPKI state INVALID (ASN mismatch)\n",
);
res.push_str("# TYPE krill_cas_bgp_announcements_invalid_asn gauge\n");
for (ca, nr) in all_bgp_stats.announcements_invalid_asn.iter() {
res.push_str(&format!(
"krill_cas_bgp_announcements_invalid_asn{{ca=\"{}\"}} {}\n",
ca, nr
));
}
res.push_str("\n");
res.push_str(
"# HELP krill_cas_bgp_announcements_invalid_length number of announcements seen for CA resources with RPKI state INVALID (prefix exceeds max length)\n",
);
res.push_str("# TYPE krill_cas_bgp_announcements_invalid_length gauge\n");
for (ca, nr) in all_bgp_stats.announcements_invalid_length.iter() {
res.push_str(&format!(
"krill_cas_bgp_announcements_invalid_length{{ca=\"{}\"}} {}\n",
ca, nr
));
}
res.push_str("\n");
res.push_str(
"# HELP krill_cas_bgp_announcements_not_found number of announcements seen for CA resources with RPKI state NOT FOUND (none of the CA's ROAs cover this)\n",
);
res.push_str("# TYPE krill_cas_bgp_announcements_not_found gauge\n");
for (ca, nr) in all_bgp_stats.announcements_not_found.iter() {
res.push_str(&format!(
"krill_cas_bgp_announcements_not_found{{ca=\"{}\"}} {}\n",
ca, nr
));
}
res.push_str("\n");
res.push_str(
"# HELP krill_cas_bgp_roas_stale number of ROAs for this CA for which no announcements are seen (0 may also indicate that no BGP info is available)\n",
);
res.push_str("# TYPE krill_cas_bgp_roas_stale gauge\n");
for (ca, nr) in all_bgp_stats.roas_stale.iter() {
res.push_str(&format!(
"krill_cas_bgp_roas_stale{{ca=\"{}\"}} {}\n",
ca, nr
));
}
Ok(HttpResponse::text(res.into_bytes()))
} else {
Err(req)
@@ -490,6 +586,10 @@ async fn api_ca_routes(req: Request, path: &mut RequestPath, ca: Handle) -> Rout
Method::POST => ca_routes_update(req, ca).await,
_ => render_unknown_method(),
},
Some("analysis") => match *req.method() {
Method::GET => ca_routes_analysis(req, path, ca).await,
_ => render_unknown_method(),
},
_ => render_unknown_method(),
}
}
@@ -975,8 +1075,6 @@ async fn ca_kr_activate(req: Request, handle: Handle) -> RoutingResult {
render_empty_res(req.state().read().await.ca_keyroll_activate(handle))
}
//------------ Admin: Force republish ----------------------------------------
/// Update the route authorizations for this CA
async fn ca_routes_update(req: Request, handle: Handle) -> RoutingResult {
let state = req.state().clone();
@@ -995,6 +1093,14 @@ async fn ca_routes_show(req: Request, handle: Handle) -> RoutingResult {
}
}
/// Show the state of ROAs vs BGP for this CA
async fn ca_routes_analysis(req: Request, path: &mut RequestPath, handle: Handle) -> RoutingResult {
match path.next() {
Some("full") => render_json_res(req.state().read().await.ca_routes_bgp_analysis(&handle)),
_ => render_unknown_method(),
}
}
//------------ Admin: Force republish ----------------------------------------
async fn republish_all(req: Request) -> RoutingResult {
@@ -1027,6 +1133,11 @@ async fn rrdp(req: Request) -> RoutingResult {
} else {
let mut full_path: PathBuf = req.state.read().await.rrdp_base_path();
let (_, path) = req.path.remaining().split_at(1);
let cache_seconds = if path.ends_with("notification.xml") {
60
} else {
86400
};
full_path.push(path);
match File::open(full_path) {
@@ -1035,7 +1146,7 @@ async fn rrdp(req: Request) -> RoutingResult {
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).unwrap();
Ok(HttpResponse::xml(buffer))
Ok(HttpResponse::xml_with_cache(buffer, cache_seconds))
}
_ => Ok(HttpResponse::not_found()),
}
+29 -3
View File
@@ -18,6 +18,7 @@ use crate::commons::api::{
RepositoryContact, RepositoryUpdate, RoaDefinition, RoaDefinitionUpdates, ServerInfo,
TaCertDetails, UpdateChildRequest,
};
use crate::commons::bgp::{BgpAnalyser, BgpAnalysisReport};
use crate::commons::error::Error;
use crate::commons::eventsourcing::CommandKey;
use crate::commons::remote::rfc8183;
@@ -43,7 +44,7 @@ pub struct KrillServer {
// The base working directory, used for various storage
work_dir: PathBuf,
// Component responsible for API authorisation checks
// Component responsible for API authorization checks
authorizer: Authorizer,
// Publication server, with configured publishers
@@ -52,6 +53,9 @@ pub struct KrillServer {
// Handles the internal TA and/or CAs
caserver: Arc<ca::CaServer<OpenSslSigner>>,
// Handles the internal TA and/or CAs
bgp_analyser: Arc<BgpAnalyser>,
// Responsible for background tasks, e.g. re-publishing
#[allow(dead_code)] // just need to keep this in scope
scheduler: Scheduler,
@@ -173,10 +177,17 @@ impl KrillServer {
}
}
let bgp_analyser = Arc::new(BgpAnalyser::new(
config.bgp_risdumps_enabled,
&config.bgp_risdumps_v4_uri,
&config.bgp_risdumps_v6_uri,
));
let scheduler = Scheduler::build(
event_queue,
caserver.clone(),
pubserver.clone(),
bgp_analyser.clone(),
ca_refresh_rate,
);
@@ -192,6 +203,7 @@ impl KrillServer {
authorizer,
pubserver,
caserver,
bgp_analyser,
scheduler,
started: Time::now(),
post_limits,
@@ -426,12 +438,17 @@ impl KrillServer {
for ca in self.caserver.ca_list().cas() {
// can't fail really, but to be sure
if let Ok(ca) = self.caserver.get_ca(ca.handle()) {
let roa_count = ca.roa_definitions().len();
let roas = ca.roa_definitions();
let roa_count = roas.len();
let child_count = ca.children().count();
let bgp_report = self
.bgp_analyser
.analyse(roas.as_slice(), &ca.all_resources());
res.insert(
ca.handle().clone(),
CertAuthStats::new(roa_count, child_count),
CertAuthStats::new(roa_count, child_count, bgp_report.into()),
);
}
}
@@ -667,6 +684,15 @@ impl KrillServer {
let ca = self.caserver.get_ca(handle)?;
Ok(ca.roa_definitions())
}
pub fn ca_routes_bgp_analysis(&self, handle: &Handle) -> KrillResult<BgpAnalysisReport> {
let ca = self.caserver.get_ca(handle)?;
let definitions = ca.roa_definitions();
let resources = ca.all_resources();
Ok(self
.bgp_analyser
.analyse(definitions.as_slice(), &resources))
}
}
/// # Handle publication requests
+21
View File
@@ -10,6 +10,7 @@ use tokio::runtime::Runtime;
use rpki::x509::Time;
use crate::commons::api::Handle;
use crate::commons::bgp::BgpAnalyser;
use crate::commons::util::softsigner::OpenSslSigner;
use crate::daemon::ca::CaServer;
use crate::daemon::mq::{EventQueueListener, QueueEvent};
@@ -31,6 +32,10 @@ pub struct Scheduler {
/// they are not renewed within the configured grace period.
#[allow(dead_code)] // just need to keep this in scope
ca_refresh_sh: ScheduleHandle,
/// Responsible for refreshing announcement information
#[allow(dead_code)] // just need to keep this in scope
announcements_refresh_sh: ScheduleHandle,
}
impl Scheduler {
@@ -38,16 +43,19 @@ impl Scheduler {
event_queue: Arc<EventQueueListener>,
caserver: Arc<CaServer<OpenSslSigner>>,
pubserver: Option<Arc<PubServer>>,
bgp_analyser: Arc<BgpAnalyser>,
ca_refresh_rate: u32,
) -> Self {
let event_sh = make_event_sh(event_queue, caserver.clone(), pubserver);
let republish_sh = make_republish_sh(caserver.clone());
let ca_refresh_sh = make_ca_refresh_sh(caserver, ca_refresh_rate);
let announcements_refresh_sh = make_announcements_refresh_sh(bgp_analyser);
Scheduler {
event_sh,
republish_sh,
ca_refresh_sh,
announcements_refresh_sh,
}
}
}
@@ -207,3 +215,16 @@ fn make_ca_refresh_sh(caserver: Arc<CaServer<OpenSslSigner>>, refresh_rate: u32)
});
scheduler.watch_thread(Duration::from_millis(100))
}
fn make_announcements_refresh_sh(bgp_analyser: Arc<BgpAnalyser>) -> ScheduleHandle {
let mut scheduler = clokwerk::Scheduler::new();
scheduler.every(1.seconds()).run(move || {
let mut rt = Runtime::new().unwrap();
rt.block_on(async {
if let Err(e) = bgp_analyser.update().await {
error!("Failed to update BGP announcements: {}", e)
}
})
});
scheduler.watch_thread(Duration::from_millis(100))
}
+2
View File
@@ -11,6 +11,8 @@ extern crate futures;
extern crate futures_util;
extern crate hex;
extern crate hyper;
extern crate intervaltree;
extern crate libflate;
#[macro_use]
extern crate log;
extern crate openssl;
+14 -2
View File
@@ -22,9 +22,10 @@ use crate::cli::{Error, KrillClient};
use crate::commons::api::{
AddChildRequest, CertAuthInfo, CertAuthInit, CertifiedKeyInfo, ChildAuthRequest, ChildHandle,
Handle, ParentCaContact, ParentCaReq, ParentHandle, Publish, PublisherDetails, PublisherHandle,
RepositoryUpdate, ResourceClassKeysInfo, ResourceClassName, ResourceSet, RoaDefinitionUpdates,
UpdateChildRequest,
RepositoryUpdate, ResourceClassKeysInfo, ResourceClassName, ResourceSet, RoaDefinition,
RoaDefinitionUpdates, UpdateChildRequest,
};
use crate::commons::bgp::Announcement;
use crate::commons::remote::rfc8183;
use crate::commons::remote::rfc8183::ChildRequest;
use crate::commons::util::httpclient;
@@ -515,3 +516,14 @@ pub fn save_file(base_dir: &PathBuf, file_name: &str, content: &[u8]) {
let mut f = File::create(full_name).unwrap();
f.write_all(content).unwrap();
}
// Support testing announcements and ROAs etc
pub fn announcement(s: &str) -> Announcement {
let def = definition(s);
Announcement::from(def)
}
pub fn definition(s: &str) -> RoaDefinition {
RoaDefinition::from_str(s).unwrap()
}
+34 -7
View File
@@ -6,10 +6,13 @@ use std::path::PathBuf;
use std::{fmt, fs, io};
use crate::commons::api::Handle;
use crate::commons::eventsourcing::{DiskKeyStore, KeyStoreError};
use crate::commons::eventsourcing::{DiskKeyStore, KeyStore, KeyStoreError, KeyStoreVersion};
use crate::commons::util::file;
use crate::daemon::krillserver::KrillServer;
use crate::upgrades::roa_cleanup_0_7_0::RoaCleanupError;
pub mod pre_0_6_0;
pub mod roa_cleanup_0_7_0;
//------------ UpgradeError --------------------------------------------------
@@ -27,6 +30,9 @@ pub enum UpgradeError {
#[display(fmt = "Cannot load: {}", _0)]
CannotLoadAggregate(Handle),
#[display(fmt = "Cannot clean up redundant roas: {}", _0)]
RoaCleanup(RoaCleanupError),
#[display(fmt = "{}", _0)]
Custom(String),
}
@@ -59,6 +65,12 @@ impl From<io::Error> for UpgradeError {
}
}
impl From<RoaCleanupError> for UpgradeError {
fn from(e: RoaCleanupError) -> Self {
UpgradeError::RoaCleanup(e)
}
}
//------------ UpgradeStore --------------------------------------------------
/// Implement this for automatic upgrades to key stores
@@ -67,12 +79,27 @@ pub trait UpgradeStore {
fn migrate(&self, store: &DiskKeyStore) -> Result<(), UpgradeError>;
}
/// Should be called when Krill starts
pub fn upgrade(work_dir: &PathBuf) -> Result<(), UpgradeError> {
/// Should be called when Krill starts, before the KrillServer is initiated
pub fn pre_start_upgrade(work_dir: &PathBuf) -> Result<(), UpgradeError> {
upgrade_pre_0_6_0_cas_commands(work_dir)?;
upgrade_pre_0_6_0_pubd_commands(work_dir)
}
/// Should be called right after the KrillServer is initiated
pub fn post_start_upgrade(work_dir: &PathBuf, server: &KrillServer) -> Result<(), UpgradeError> {
let version_0_7 = KeyStoreVersion::V0_7;
let ca_store = DiskKeyStore::new(work_dir, "cas");
let pubd_store = DiskKeyStore::new(work_dir, "pubd");
if ca_store.get_version()? != version_0_7 {
info!("Will clean up redundant ROAs for all CAs and update version of storage dirs");
roa_cleanup_0_7_0::roa_cleanup(server)?;
ca_store.set_version(&version_0_7)?;
pubd_store.set_version(&version_0_7)?;
}
Ok(())
}
fn upgrade_pre_0_6_0_cas_commands(work_dir: &PathBuf) -> Result<(), UpgradeError> {
let pre_0_6_0_ca_commands = pre_0_6_0::UpgradeCas;
@@ -115,10 +142,10 @@ fn upgrade_pre_0_6_0_pubd_commands(work_dir: &PathBuf) -> Result<(), UpgradeErro
// Prepare to do the work on the real "cas" directory
let mut pubd_dir = work_dir.clone();
pubd_dir.push("pubd");
let ca_store = DiskKeyStore::new(work_dir, "pubd");
let pubd_store = DiskKeyStore::new(work_dir, "pubd");
// bail out if there is nothing to do
if !pre_0_6_0_pubd_commands.needs_migrate(&ca_store)? {
if !pre_0_6_0_pubd_commands.needs_migrate(&pubd_store)? {
return Ok(());
}
@@ -128,7 +155,7 @@ fn upgrade_pre_0_6_0_pubd_commands(work_dir: &PathBuf) -> Result<(), UpgradeErro
backup_dir.push("pubd_bk");
file::backup_dir(&pubd_dir, &backup_dir)?;
if let Err(e) = pre_0_6_0_pubd_commands.migrate(&ca_store) {
if let Err(e) = pre_0_6_0_pubd_commands.migrate(&pubd_store) {
// If the upgrade failed, then rename the now broken directory for inspection,
// and restore the backup directory by renaming it.
let mut failed = work_dir.clone();
@@ -170,7 +197,7 @@ mod tests {
pubd_test.push("pubd");
file::backup_dir(&pubd_source, &pubd_test).unwrap();
upgrade(&tmp).unwrap();
pre_start_upgrade(&tmp).unwrap();
})
}
}
+6 -2
View File
@@ -29,8 +29,8 @@ impl UpgradeStore for UpgradeCas {
fn needs_migrate(&self, store: &DiskKeyStore) -> Result<bool, UpgradeError> {
match store.get_version() {
Ok(version) => match version {
KeyStoreVersion::V0_6 => Ok(false),
KeyStoreVersion::Pre0_6 => Ok(true),
_ => Ok(false),
},
Err(e) => match e {
KeyStoreError::NotInitialised => Ok(false),
@@ -128,8 +128,8 @@ impl UpgradeStore for UpgradePubd {
} else {
match store.get_version() {
Ok(version) => match version {
KeyStoreVersion::V0_6 => Ok(false),
KeyStoreVersion::Pre0_6 => Ok(true),
_ => Ok(false),
},
Err(e) => match e {
KeyStoreError::NotInitialised => Ok(false),
@@ -164,6 +164,10 @@ impl UpgradeStore for UpgradePubd {
last_command = seq;
seq += 1;
}
if seq % 100 == 0 {
info!(".. {} done", seq)
}
}
info!("Done migrating commands for Repository server");
+123
View File
@@ -0,0 +1,123 @@
use std::collections::HashSet;
use crate::commons::api::{RoaDefinition, RoaDefinitionUpdates};
use crate::commons::bgp::make_roa_tree;
use crate::daemon::krillserver::KrillServer;
pub fn roa_cleanup(server: &KrillServer) -> Result<(), RoaCleanupError> {
for ca in server.cas().cas() {
let roas = server.ca_routes_show(ca.handle())?;
if let Some(updates) = clean(roas) {
server.ca_routes_update(ca.handle().clone(), updates)?;
}
}
Ok(())
}
fn clean(roas: Vec<RoaDefinition>) -> Option<RoaDefinitionUpdates> {
let tree = make_roa_tree(roas.as_slice());
let mut added = HashSet::new();
let mut removed = HashSet::new();
for roa in roas.into_iter() {
// if this ROA is covered by any other ROA
let prefix = roa.prefix();
let asn = roa.asn();
let mut should_remove = false;
for covering in tree.matching_or_less_specific(&prefix) {
if covering == &roa || covering.asn() != asn {
continue;
}
if covering.prefix() == prefix && roa.max_length().is_none() {
should_remove = true;
break;
}
if covering.effective_max_length() > roa.effective_max_length() {
// covering prefix is bigger and allows this
should_remove = true;
break;
}
}
if should_remove {
removed.insert(roa);
} else if roa.max_length().is_none() {
// If this does not have a max length then remove
// this one and add the equivalent with max length.
// Note: if that equivalent would have existed this
// roa would have been marked for removal
let with_length = RoaDefinition::new(asn, prefix, Some(roa.effective_max_length()));
removed.insert(roa);
added.insert(with_length);
}
}
if !added.is_empty() || !removed.is_empty() {
Some(RoaDefinitionUpdates::new(added, removed))
} else {
None
}
}
#[derive(Debug, Display)]
#[display(fmt = "{}", _0)]
pub struct RoaCleanupError(String);
impl From<crate::commons::error::Error> for RoaCleanupError {
fn from(e: crate::commons::error::Error) -> Self {
RoaCleanupError(e.to_string())
}
}
//------------ Tests ---------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::test::definition;
use std::collections::HashSet;
#[test]
fn upgrade_roa_cleanup() {
let roas = vec![
definition("192.0.0.0/8-8 => 64496"), // keep
definition("192.168.0.0/16 => 64496"), // remove there is a longer ml
definition("192.168.0.0/16-16 => 64496"), // remove there is a longer ml
definition("192.168.0.0/16-20 => 64496"), // keep
definition("192.168.0.0/16-18 => 64496"), // remove there is a longer ml
definition("192.168.0.0/18-20 => 64496"), // remove covering has longer ml
definition("192.168.0.0/18-24 => 64496"), // keep, this is more permissive for specific bit
definition("192.168.127.0/24-24 => 64496"), // keep, this is more specific
definition("192.168.0.0/16-20 => 64497"), // different asn -> keep
definition("10.0.0.0/8 => 64496"), // replace with one with max length
definition("10.0.1.0/24 => 64498"), // remove, there is one with explicit ml
definition("10.0.1.0/24-24 => 64498"), // keep
];
let update = clean(roas).unwrap();
let mut expected_added = HashSet::new();
expected_added.insert(definition("10.0.0.0/8-8 => 64496"));
let mut expected_removed = HashSet::new();
expected_removed.insert(definition("10.0.0.0/8 => 64496"));
expected_removed.insert(definition("10.0.1.0/24 => 64498"));
expected_removed.insert(definition("192.168.0.0/16 => 64496"));
expected_removed.insert(definition("192.168.0.0/16-16 => 64496"));
expected_removed.insert(definition("192.168.0.0/16-18 => 64496"));
expected_removed.insert(definition("192.168.0.0/18-20 => 64496"));
let expected = RoaDefinitionUpdates::new(expected_added, expected_removed);
assert_eq!(update, expected);
}
}
@@ -0,0 +1 @@
{"label":"ca-child-unauthorized","msg":"CA 'ca' does not know id certificate for child 'child'","args":{"ca":"ca","child":"child"}}
@@ -0,0 +1 @@
{"label":"ca-roa-duplicate","msg":"ROA '192.168.0.0/16-24 => 64496' already present", "args":{"asn":"64496","ca":"ca","prefix":"192.168.0.0/16","max_length":"24"}}
@@ -0,0 +1 @@
{"label":"ca-roa-includes","msg":"ROA '192.168.0.0/16-24 => 64496' was not added because it would make existing ROAs redundant", "args":{"asn":"64496","ca":"ca","prefix":"192.168.0.0/16","max_length":"24"}}
@@ -0,0 +1 @@
{"label":"ca-roa-invalid-max-length","msg":"Invalid max length in ROA: '192.168.0.0/16-24 => 64496'","args":{"ca":"ca","prefix":"192.168.0.0/16","max_length":"24","asn":"64496"}}
@@ -0,0 +1 @@
{"label":"ca-roa-not-entitled","msg":"Prefix in ROA '192.168.0.0/16-24 => 64496' not held by you", "args":{"asn":"64496","ca":"ca","prefix":"192.168.0.0/16","max_length":"24"}}
@@ -0,0 +1 @@
{"label":"ca-roa-redundant","msg":"ROA '192.168.0.0/16-24 => 64496' was not added because it is redundant", "args":{"asn":"64496","ca":"ca","prefix":"192.168.0.0/16","max_length":"24"}}
@@ -0,0 +1 @@
{"label":"ca-roa-unknown","msg":"Cannot remove unknown ROA '192.168.0.0/16-24 => 64496'","args":{"ca":"ca","max_length":"24","asn":"64496","prefix":"192.168.0.0/16"}}
@@ -1 +0,0 @@
{"label":"ca-child-unauthorised","msg":"CA 'ca' does not know id certificate for child 'child'","args":{"ca":"ca","child":"child"}}
@@ -1 +0,0 @@
{"label":"ca-roa-duplicate","msg":"Duplicate ROA '192.168.0.0/16-24 => 64496' for CA 'ca'","args":{"asn":"64496","ca":"ca","prefix":"192.168.0.0/16","max_length":"24"}}
@@ -1 +0,0 @@
{"label":"ca-roa-invalid-max-length","msg":"Invalid max length in ROA: '192.168.0.0/16-24 => 64496' for CA 'ca","args":{"ca":"ca","prefix":"192.168.0.0/16","max_length":"24","asn":"64496"}}
@@ -1 +0,0 @@
{"label":"ca-roa-not-entitled","msg":"Prefix in ROA '192.168.0.0/16-24 => 64496' not held by CA 'ca'.","args":{"asn":"64496","ca":"ca","prefix":"192.168.0.0/16","max_length":"24"}}

Some files were not shown because too many files have changed in this diff Show More