Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 230 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,234 @@
# bgpsim

BGP path propagation inference
BGP path propagation inference. Given an AS-level topology with annotated
business relationships, bgpsim computes all AS-paths tied for best towards a
set of prefix origins, following the Gao-Rexford routing policy model.

## Dependencies

- Python 3.10+
- [NetworkX](https://networkx.org/)

## Quick start

```python
from bgpsim import ASGraph, Announcement, Relationship

# Build a small topology:
# 1
# / \
# 2 3
# \ /
# 4
graph = ASGraph()
graph.add_peering(1, 2, Relationship.P2C)
graph.add_peering(1, 3, Relationship.P2C)
graph.add_peering(2, 4, Relationship.P2C)
graph.add_peering(3, 4, Relationship.P2C)

# AS 1 announces a prefix to all neighbors.
announce = Announcement.make_anycast_announcement(graph, [1])
graph.infer_paths(announce)

# AS 4 learns two equally-preferred paths through its providers:
print(graph.g.nodes[4]["best-paths"]) # [(2, 1), (3, 1)]
print(graph.g.nodes[4]["path-pref"]) # PathPref.PROVIDER (1)
```

## API reference

### Types

```python
ASPath = tuple[int, ...]
ImportFilter = Callable[[int, list[ASPath], Any], list[ASPath]]
```

`ASPath` is a tuple of AS numbers representing a route. The first element is
the next-hop AS and the last element is the origin. `ImportFilter` is the
signature for custom import filter functions (see
[`ASGraph.set_import_filter`](#asgraphset_import_filter)).

### `Relationship`

An `IntEnum` encoding the business relationship on a directed edge. The value
on the edge `(A, B)` describes A's role relative to B:

| Member | Value | Meaning |
|--------|-------|---------------------------------------------------------|
| `P2C` | -1 | A is a **provider** of B (A provides transit to B) |
| `P2P` | 0 | A and B are **peers** (settlement-free interconnection) |
| `C2P` | 1 | A is a **customer** of B (A purchases transit from B) |

`Relationship.reversed()` returns the relationship as seen from the other
end of the edge (e.g. `P2C.reversed() == C2P`).

### `PathPref`

An `IntEnum` modelling route preference at the importing AS. Higher values
mean higher preference, matching the Gao-Rexford "prefer customer" rule:

| Member | Value | When used |
|------------|-------|-------------------------------|
| `CUSTOMER` | 3 | Route learned from a customer |
| `PEER` | 2 | Route learned from a peer |
| `PROVIDER` | 1 | Route learned from a provider |
| `UNKNOWN` | 0 | No route learned yet |

`PathPref.from_relationship(graph, exporter, importer)` derives the
preference the importer would assign to a route received from the exporter,
based on the edge relationship in the graph.

### `ASGraph`

The central class. Wraps a NetworkX `DiGraph` and provides methods to build
the topology, configure policies, and run the inference algorithm.

#### `ASGraph.add_peering(source, sink, relationship)`

Add a bidirectional peering link. `relationship` is interpreted from
`source`'s perspective (e.g. `Relationship.P2C` means `source` is a
provider of `sink`). The reverse edge is added automatically. Duplicate
edges with the same relationship are silently ignored; duplicate edges with
different relationships raise `ValueError`.

```python
graph = ASGraph()
graph.add_peering(1, 2, Relationship.P2C) # 1 is provider of 2
graph.add_peering(3, 4, Relationship.P2P) # 3 and 4 are peers
graph.add_peering(5, 6, Relationship.C2P) # 5 is customer of 6
```

#### `ASGraph.set_import_filter(asn, func, data=None)`

Attach a custom import filter to an AS. The filter is called whenever the AS
is about to import routes from a neighbor. It receives the exporter's ASN,
the candidate paths (each already prepended with the exporter), and the
optional `data` argument. It must return the subset of paths to accept.

```python
def only_accept_origin(exporter, paths, allowed_origin):
return [p for p in paths if p[-1] == allowed_origin]

graph.set_import_filter(42, only_accept_origin, 100)
```

This can be used to implement mechanisms like peer-locking or selective
route filtering.

#### `ASGraph.infer_paths(announce)`

Run the path inference algorithm for the given `Announcement`. This performs
a modified breadth-first search that processes edges in decreasing order of
relationship preference (customer > peer > provider), and within the same
preference, in increasing order of path length. The result is that every AS
in the graph ends up with all AS-paths tied for best.

After calling this method, per-node results are available on the underlying
NetworkX graph:

| Node attribute | Type | Description |
|----------------|----------------|---------------------------------------------------------|
| `"best-paths"` | `list[ASPath]` | All AS-paths tied for best at this node |
| `"path-pref"` | `PathPref` | Preference of the best paths (`UNKNOWN` if unreachable) |
| `"path-len"` | `int` | Length of the best paths |

```python
announce = Announcement.make_anycast_announcement(graph, [origin_asn])
graph.infer_paths(announce)

for node in graph.g.nodes:
paths = graph.g.nodes[node]["best-paths"]
pref = graph.g.nodes[node]["path-pref"]
print(f"AS{node}: {len(paths)} path(s), pref={pref.name}")
```

**Important:** `infer_paths` can only be called once per `ASGraph` instance
because it mutates node metadata. Use `ASGraph.clone()` to run multiple
inferences on the same topology.

#### `ASGraph.clone()`

Return a deep copy of the graph (topology, node attributes, tier-1 and IXP
sets). The clone has no announcement state, so `infer_paths` can be called
on it. Import filters and callbacks are **not** copied.

```python
base = ASGraph()
# ... build topology ...
g1 = base.clone()
g1.infer_paths(announce_a)

g2 = base.clone()
g2.infer_paths(announce_b)
```

#### `ASGraph.set_callback(when, func)`

Register a callback for observing the inference algorithm. See
[`InferenceCallback`](#inferencecallback) for available hooks.

#### `ASGraph.read_caida_asrel_graph(filepath)`

Static method. Load a [CAIDA AS-relationship][caida-asrel] dataset (bz2
compressed). Returns a fully constructed `ASGraph` with `tier1s` and `ixps`
populated from the file's metadata comments.

```python
graph = ASGraph.read_caida_asrel_graph("20200101.as-rel.txt.bz2")
print(f"{len(graph.g.nodes)} ASes, {len(graph.g.edges)//2} peerings")
print(f"Tier-1 ASes: {graph.tier1s}")
```

### `Announcement`

Specifies which ASes originate a prefix and what AS-path they announce to
each neighbor. The core data structure is `source2neighbor2path`: a nested
dict mapping each source AS to a dict of neighbor AS to the AS-path
prepended to the announcement towards that neighbor.

#### `Announcement.make_anycast_announcement(asgraph, sources)`

Convenience constructor. Creates an announcement where each source
advertises to all its neighbors with an empty AS-path (no prepending).
`sources` can be a `list[int]` (no prepending) or a `dict[int, int]`
mapping each source to a prepend count.

```python
# Single origin, no prepending:
announce = Announcement.make_anycast_announcement(graph, [100])

# Anycast from two origins:
announce = Announcement.make_anycast_announcement(graph, [100, 200])

# AS 100 prepends once (appears twice in the path):
announce = Announcement.make_anycast_announcement(graph, {100: 1})
```

#### Custom announcements

For fine-grained control (per-neighbor prepending, poisoning), construct an
`Announcement` directly:

```python
announce = Announcement(source2neighbor2path={
100: {
200: (), # normal announcement to AS 200
300: (100,), # prepend once towards AS 300
400: (100, 100), # prepend twice towards AS 400
}
})
```

### `InferenceCallback`

An enum of hooks that can be registered via `ASGraph.set_callback`:

| Member | Called when | Signature |
|----------------------------|--------------------------------------------------------|--------------------------------------------------------------------------|
| `START_RELATIONSHIP_PHASE` | Algorithm starts processing a relationship type | `(pref: Relationship) -> None` |
| `NEIGHBOR_ANNOUNCE` | Origin announces to a neighbor at the start of a phase | `(origin: int, neighbor: int, pref: Relationship, path: ASPath) -> None` |
| `VISIT_EDGE` | Algorithm processes an edge during BFS | `(exporter: int, importer: int, pref: Relationship) -> None` |

## Running the tests

Expand All @@ -23,7 +251,7 @@ $ python3 -O tests/bench_bgpsim.py

## References

You man want to check these papers on an [introduction to BGP routing policies][bgp-policies], and on [how policies can be inferred in the wild][caida-asrel].
You may want to check these papers on an [introduction to BGP routing policies][bgp-policies], and on [how policies can be inferred in the wild][caida-asrel].

## TO-DO

Expand Down