|
3 | 3 | # Standard Library |
4 | 4 | import typing as t |
5 | 5 |
|
| 6 | +# Third Party |
| 7 | +import httpx |
| 8 | + |
6 | 9 | # Project |
7 | 10 | from hyperglass.log import log |
8 | 11 | from hyperglass.state import use_state |
9 | 12 | from hyperglass.external._base import BaseExternal |
10 | 13 |
|
11 | 14 | if t.TYPE_CHECKING: |
12 | | - # Standard Library |
13 | 15 | from ipaddress import IPv4Address, IPv6Address |
14 | 16 |
|
15 | | -RPKI_STATE_MAP = {"Invalid": 0, "Valid": 1, "NotFound": 2, "DEFAULT": 3} |
16 | | -RPKI_NAME_MAP = {v: k for k, v in RPKI_STATE_MAP.items()} |
| 17 | +# Maps normalized (lower-case, separators stripped) backend state names to the |
| 18 | +# integer states hyperglass uses internally. |
| 19 | +RPKI_STATE_MAP = { |
| 20 | + "invalid": 0, |
| 21 | + "valid": 1, |
| 22 | + "notfound": 2, |
| 23 | + "unknown": 2, |
| 24 | + "default": 3, |
| 25 | +} |
| 26 | +# Canonical integer -> display name, kept stable for logging. |
| 27 | +RPKI_NAME_MAP = {0: "Invalid", 1: "Valid", 2: "NotFound", 3: "DEFAULT"} |
17 | 28 | CACHE_KEY = "hyperglass.external.rpki" |
18 | 29 |
|
19 | 30 |
|
20 | | -def rpki_state(prefix: t.Union["IPv4Address", "IPv6Address", str], asn: t.Union[int, str]) -> int: |
| 31 | +def _normalize_state(value: str) -> int: |
| 32 | + """Normalize a backend RPKI state string to an internal integer state.""" |
| 33 | + key = str(value).strip().lower().replace("-", "").replace("_", "") |
| 34 | + return RPKI_STATE_MAP.get(key, 3) |
| 35 | + |
| 36 | + |
| 37 | +def rpki_state( |
| 38 | + prefix: t.Union["IPv4Address", "IPv6Address", str], |
| 39 | + asn: t.Union[int, str], |
| 40 | + backend: str = "cloudflare", |
| 41 | + rpki_server_url: str = "", |
| 42 | +) -> int: |
21 | 43 | """Get RPKI state and map to expected integer.""" |
22 | 44 | _log = log.bind(prefix=prefix, asn=asn) |
23 | 45 | _log.debug("Validating RPKI State") |
24 | 46 |
|
25 | 47 | cache = use_state("cache") |
26 | | - |
27 | 48 | state = 3 |
28 | 49 | ro = f"{prefix!s}@{asn!s}" |
29 | 50 |
|
30 | 51 | cached = cache.get_map(CACHE_KEY, ro) |
31 | | - |
32 | 52 | if cached is not None: |
33 | 53 | state = cached |
34 | 54 | else: |
35 | | - ql = 'query GetValidation {{ validation(prefix: "{}", asn: {}) {{ state }} }}' |
36 | | - query = ql.format(prefix, asn) |
37 | | - _log.bind(query=query).debug("Cloudflare RPKI GraphQL Query") |
38 | 55 | try: |
39 | | - with BaseExternal(base_url="https://rpki.cloudflare.com") as client: |
40 | | - response = client._post("/api/graphql", data={"query": query}) |
41 | | - try: |
| 56 | + if backend == "cloudflare": |
| 57 | + ql = 'query GetValidation {{ validation(prefix: "{}", asn: {}) {{ state }} }}' |
| 58 | + query = ql.format(prefix, asn) |
| 59 | + _log.bind(query=query).debug("Cloudflare RPKI GraphQL Query") |
| 60 | + with BaseExternal(base_url="https://rpki.cloudflare.com") as client: |
| 61 | + response = client._post("/api/graphql", data={"query": query}) |
42 | 62 | validation_state = response["data"]["validation"]["state"] |
43 | | - except KeyError as missing: |
44 | | - _log.error("Response from Cloudflare missing key '{}': {!r}", missing, response) |
45 | | - validation_state = 3 |
| 63 | + elif backend == "routinator": |
| 64 | + url = f"{rpki_server_url.rstrip('/')}/validity" |
| 65 | + _log.bind(url=url).debug("Routinator RPKI HTTP Query") |
| 66 | + response = httpx.get( |
| 67 | + url, params={"asn": str(asn), "prefix": str(prefix)}, timeout=5 |
| 68 | + ) |
| 69 | + response.raise_for_status() |
| 70 | + data = response.json() |
| 71 | + validation_state = data["validated_route"]["validity"]["state"] |
| 72 | + else: |
| 73 | + raise ValueError(f"Unknown RPKI backend: {backend}") |
46 | 74 |
|
47 | | - state = RPKI_STATE_MAP[validation_state] |
| 75 | + state = _normalize_state(validation_state) |
48 | 76 | cache.set_map_item(CACHE_KEY, ro, state) |
49 | 77 | except Exception as err: |
50 | 78 | log.error(err) |
51 | | - # Don't cache the state when an error produced it. |
52 | 79 | state = 3 |
53 | 80 |
|
54 | 81 | msg = "RPKI Validation State for {} via AS{} is {}".format(prefix, asn, RPKI_NAME_MAP[state]) |
55 | 82 | if cached is not None: |
56 | 83 | msg += " [CACHED]" |
57 | | - |
58 | 84 | log.debug(msg) |
59 | 85 | return state |
0 commit comments