A new crate, bgp-policy, runs functions of a script on routes: on their
way in, after the general checks of a peer's import and before the
checks that can change (RPKI, next hop reachability); and on their way
out to a peer. A function is given the route and the peer, may refuse
the route, and may change its communities, large communities,
LOCAL_PREF and MED, and on the way out prepend our AS:
fn from_peer(route, peer) {
if route.has_community("65535:666") { return false; }
route.add_community(LEARNED_FROM[peer.role]);
route.local_pref = PREFERENCE[peer.role];
true
}
Configuration: policy { path | source, import_function, export_function }
and, for a peer, import { function } and export { function }. The script
is compiled at startup; `bgp --check` reads it and says which function
is whose. A function that fails on a route has refused it. Scripts have
no files, network or clock, and are stopped after 200,000 steps.
Route selection now goes by a LOCAL_PREF on a route from an external
peer: the decoder never lets a peer's own through, so one that is there
was set by policy.
What it costs was measured before it was built on (benches/policy.rs):
a call that tests a few communities and changes two takes 1 to 3
microseconds, some two hundred times what Rust would. So a function is
asked once for the routes of an UPDATE, which share their attributes,
unless it asks for the prefix. One peer sending 1.23 M routes over a
session: 0.37 s without a function, 0.80 s with one that marks and
ranks every route, 3.4 s if it also looks at the prefix.
Export applies to what is announced, which today is what the daemon
originates; learned routes are not passed on yet.
Tested by unit tests of the crate, sessions end to end, a fuzz target on
the example script (testdata/policy.rhai), and a lab phase in which a
script changes which of FRR's and BIRD's route is chosen and what each
of them is told.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
662 lines
21 KiB
Rust
662 lines
21 KiB
Rust
//! Policies as an operator would write them, run on routes.
|
|
|
|
use std::net::Ipv4Addr;
|
|
use std::sync::Arc;
|
|
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LargeCommunitiesPathAttribute,
|
|
LargeCommunity, LocalPrefPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
|
|
OriginPathAttribute, PathAttribute,
|
|
};
|
|
use bgp_policy::{Direction, Peer, Policy, PolicyError, Subject, Verdict};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
|
|
fn prefix(s: &str) -> IpPrefix {
|
|
IpPrefix::try_from(s).unwrap()
|
|
}
|
|
|
|
fn peer(role: &str) -> Peer {
|
|
Peer::new(
|
|
"transit-a",
|
|
Ipv4Addr::new(192, 0, 2, 2).into(),
|
|
64602,
|
|
64601,
|
|
role,
|
|
)
|
|
}
|
|
|
|
/// A route by way of AS 64602 and 3356 from 15169, with communities of
|
|
/// 3356.
|
|
fn attributes() -> Vec<PathAttribute> {
|
|
vec![
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![64602, 3356, 15169]),
|
|
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 2))),
|
|
PathAttribute::Communities(CommunitiesPathAttribute(vec![(3356, 3), (3356, 2012)])),
|
|
]
|
|
}
|
|
|
|
fn run_on(
|
|
script: &str,
|
|
function: &str,
|
|
peer: &Peer,
|
|
route: &str,
|
|
attributes: &[PathAttribute],
|
|
direction: Direction,
|
|
) -> Result<Verdict, PolicyError> {
|
|
let policy = Policy::compile(script)?;
|
|
policy.hook(function)?.run(
|
|
peer,
|
|
&Subject {
|
|
prefix: &prefix(route),
|
|
attributes,
|
|
next_hop: None,
|
|
direction,
|
|
},
|
|
)
|
|
}
|
|
|
|
fn import(script: &str, route: &str, attributes: &[PathAttribute]) -> Verdict {
|
|
run_on(
|
|
script,
|
|
"policy",
|
|
&peer("provider"),
|
|
route,
|
|
attributes,
|
|
Direction::Import,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn communities(attributes: &[PathAttribute]) -> Vec<(u16, u16)> {
|
|
attributes
|
|
.iter()
|
|
.find_map(|attribute| match attribute {
|
|
PathAttribute::Communities(communities) => Some(communities.0.clone()),
|
|
_ => None,
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// The policy this was built for: mark routes by where they were learned,
|
|
/// and prefer some to others.
|
|
#[test]
|
|
fn routes_are_marked_and_preferred_by_who_sent_them() {
|
|
let script = r#"
|
|
const LEARNED_FROM = #{ customer: "64601:1000", peer: "64601:2000", provider: "64601:3000" };
|
|
const PREFERENCE = #{ customer: 300, peer: 200, provider: 100 };
|
|
|
|
fn policy(route, peer) {
|
|
// What others marked their routes with means nothing here.
|
|
route.remove_community("64601:*");
|
|
route.add_community(LEARNED_FROM[peer.role]);
|
|
route.local_pref = PREFERENCE[peer.role];
|
|
true
|
|
}
|
|
"#;
|
|
let mut given = attributes();
|
|
given[3] = PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
|
(3356, 3),
|
|
(64601, 1000),
|
|
(64601, 7),
|
|
]));
|
|
for (role, mark, preference) in [
|
|
("customer", 1000, 300),
|
|
("peer", 2000, 200),
|
|
("provider", 3000, 100),
|
|
] {
|
|
let verdict = run_on(
|
|
script,
|
|
"policy",
|
|
&peer(role),
|
|
"203.0.113.0/24",
|
|
&given,
|
|
Direction::Import,
|
|
)
|
|
.unwrap();
|
|
assert!(verdict.accept);
|
|
assert!(!verdict.saw_prefix, "the answer is good for any prefix");
|
|
let changed = verdict.attributes.expect("the route was changed");
|
|
assert_eq!(communities(&changed), [(3356, 3), (64601, mark)], "{role}");
|
|
assert!(
|
|
changed.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(
|
|
preference
|
|
))),
|
|
"{role}: {changed:?}"
|
|
);
|
|
// In the order of their type codes: LOCAL_PREF (5) before
|
|
// COMMUNITIES (8).
|
|
let codes: Vec<u8> = changed.iter().map(PathAttribute::type_code).collect();
|
|
assert_eq!(codes, [1, 2, 3, 5, 8]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_policy_refuses_by_what_a_route_carries() {
|
|
let script = r#"
|
|
const BLACKHOLE = "65535:666";
|
|
fn policy(route, peer) {
|
|
if route.has_community(BLACKHOLE) { return false; }
|
|
if route.has_community("3356:*") && route.path_has(174) { return false; }
|
|
if route.as_path_len > 5 || route.origin_as == 64512 { return false; }
|
|
if route.origin != "igp" { return false; }
|
|
true
|
|
}
|
|
"#;
|
|
let verdict = import(script, "203.0.113.0/24", &attributes());
|
|
assert_eq!(
|
|
verdict,
|
|
Verdict {
|
|
accept: true,
|
|
attributes: None,
|
|
saw_prefix: false
|
|
},
|
|
"nothing to object to, and nothing changed"
|
|
);
|
|
|
|
let mut blackholed = attributes();
|
|
blackholed[3] = PathAttribute::Communities(CommunitiesPathAttribute(vec![(65535, 666)]));
|
|
assert!(!import(script, "203.0.113.0/24", &blackholed).accept);
|
|
|
|
let mut by_cogent = attributes();
|
|
by_cogent[1] = AsPathAttribute::from_asns(vec![64602, 174, 15169]);
|
|
assert!(!import(script, "203.0.113.0/24", &by_cogent).accept);
|
|
|
|
let mut long = attributes();
|
|
long[1] = AsPathAttribute::from_asns(vec![64602; 6]);
|
|
assert!(!import(script, "203.0.113.0/24", &long).accept);
|
|
|
|
let mut incomplete = attributes();
|
|
incomplete[0] = PathAttribute::Origin(OriginPathAttribute::INCOMPLETE);
|
|
assert!(!import(script, "203.0.113.0/24", &incomplete).accept);
|
|
}
|
|
|
|
/// A function that asks for the prefix is asked about every prefix; the
|
|
/// verdict says that it did.
|
|
#[test]
|
|
fn a_policy_that_looks_at_the_prefix_says_so() {
|
|
let script = r#"
|
|
fn policy(route, peer) {
|
|
if route.family == "ipv6" { return route.prefix_len <= 48; }
|
|
if route.within("203.0.112.0/22") { route.local_pref = 50; }
|
|
route.prefix != "198.51.100.0/24"
|
|
}
|
|
"#;
|
|
let inside = import(script, "203.0.113.0/24", &attributes());
|
|
assert!(inside.accept && inside.saw_prefix);
|
|
assert!(
|
|
inside
|
|
.attributes
|
|
.unwrap()
|
|
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(50)))
|
|
);
|
|
let outside = import(script, "192.0.2.0/24", &attributes());
|
|
assert_eq!(
|
|
outside,
|
|
Verdict {
|
|
accept: true,
|
|
attributes: None,
|
|
saw_prefix: true
|
|
}
|
|
);
|
|
assert!(!import(script, "198.51.100.0/24", &attributes()).accept);
|
|
assert!(import(script, "2001:db8::/48", &attributes()).accept);
|
|
assert!(!import(script, "2001:db8::/56", &attributes()).accept);
|
|
}
|
|
|
|
#[test]
|
|
fn everything_about_a_route_and_a_peer_can_be_read() {
|
|
let script = r#"
|
|
fn policy(route, peer) {
|
|
if route.as_path != [64602, 3356, 15169] { throw "as_path"; }
|
|
if route.as_path_len != 3 { throw "as_path_len"; }
|
|
if route.neighbor_as != 64602 || route.origin_as != 15169 { throw "ends"; }
|
|
if route.next_hop != "192.0.2.2" { throw "next_hop " + route.next_hop; }
|
|
if route.communities != ["3356:3", "3356:2012"] { throw "communities"; }
|
|
if !route.has_community(3356, 3) || route.has_community(3356, 4) { throw "has"; }
|
|
if route.large_communities != ["64601:1:2"] { throw "large"; }
|
|
if !route.has_large_community("64601:*:2") { throw "has large"; }
|
|
if route.med != 70 || route.local_pref != () { throw "numbers"; }
|
|
if peer.name != "transit-a" || peer.asn != 64602 || peer.local_asn != 64601 {
|
|
throw "peer";
|
|
}
|
|
if peer.address != "192.0.2.2" || peer.role != "provider" || peer.ibgp {
|
|
throw "peer";
|
|
}
|
|
}
|
|
"#;
|
|
let mut given = attributes();
|
|
given.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(70)));
|
|
given.push(PathAttribute::LargeCommunities(
|
|
LargeCommunitiesPathAttribute {
|
|
communities: vec![LargeCommunity {
|
|
global_admin: 64601,
|
|
data_1: 1,
|
|
data_2: 2,
|
|
}],
|
|
},
|
|
));
|
|
// It ends without returning anything: no objection.
|
|
let verdict = import(script, "203.0.113.0/24", &given);
|
|
assert!(verdict.accept && verdict.attributes.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn everything_that_can_be_changed_is_changed_only_if_it_differs() {
|
|
let script = r#"
|
|
fn same(route, peer) {
|
|
route.add_community("3356:3");
|
|
route.remove_community("1:1");
|
|
route.remove_large_community("1:*:*");
|
|
route.local_pref = ();
|
|
route.med = ();
|
|
}
|
|
fn other(route, peer) {
|
|
route.remove_community("*:*");
|
|
route.add_large_community("64601:1:2");
|
|
route.add_large_community("64601:1:2");
|
|
route.med = 5;
|
|
route.local_pref = 150;
|
|
route.local_pref = ();
|
|
}
|
|
"#;
|
|
let policy = Policy::compile(script).unwrap();
|
|
let ask = |function: &str| {
|
|
policy
|
|
.hook(function)
|
|
.unwrap()
|
|
.run(
|
|
&peer(""),
|
|
&Subject {
|
|
prefix: &prefix("203.0.113.0/24"),
|
|
attributes: &attributes(),
|
|
next_hop: None,
|
|
direction: Direction::Import,
|
|
},
|
|
)
|
|
.unwrap()
|
|
};
|
|
assert_eq!(ask("same").attributes, None);
|
|
let changed = ask("other").attributes.unwrap();
|
|
assert_eq!(
|
|
changed,
|
|
[
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![64602, 3356, 15169]),
|
|
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 2))),
|
|
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(5)),
|
|
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
|
|
communities: vec![LargeCommunity {
|
|
global_admin: 64601,
|
|
data_1: 1,
|
|
data_2: 2
|
|
}]
|
|
}),
|
|
],
|
|
"no communities left, a MED and a large community added, LOCAL_PREF as it was"
|
|
);
|
|
}
|
|
|
|
/// On the way out a policy says what a peer is told, and how.
|
|
#[test]
|
|
fn a_route_is_announced_with_what_the_policy_gives_it() {
|
|
let script = r#"
|
|
fn to_peer(route, peer) {
|
|
if route.has_community("64601:3000") { return false; } // learned from a provider
|
|
if peer.name == "transit-a" { route.prepend(2); }
|
|
route.remove_community("64601:*");
|
|
true
|
|
}
|
|
"#;
|
|
let ours = |communities: Vec<(u16, u16)>| {
|
|
vec![
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![64601]),
|
|
PathAttribute::Communities(CommunitiesPathAttribute(communities)),
|
|
]
|
|
};
|
|
let export = |attributes: &[PathAttribute]| {
|
|
run_on(
|
|
script,
|
|
"to_peer",
|
|
&peer("peer"),
|
|
"193.0.0.0/21",
|
|
attributes,
|
|
Direction::Export,
|
|
)
|
|
.unwrap()
|
|
};
|
|
assert!(!export(&ours(vec![(64601, 3000)])).accept);
|
|
let verdict = export(&ours(vec![(64601, 1000), (64601, 9)]));
|
|
assert_eq!(
|
|
verdict.attributes.unwrap(),
|
|
[
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![64601, 64601, 64601]),
|
|
]
|
|
);
|
|
|
|
// Prepending to a path that begins with a set, or is empty.
|
|
let set_first = vec![PathAttribute::AsPath(AsPathAttribute {
|
|
segments: vec![AsPathSegment {
|
|
ordered: false,
|
|
path: vec![1, 2],
|
|
}],
|
|
})];
|
|
let prepended = run_on(
|
|
"fn f(route, peer) { route.prepend(1); }",
|
|
"f",
|
|
&peer(""),
|
|
"193.0.0.0/21",
|
|
&set_first,
|
|
Direction::Export,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
prepended.attributes.unwrap(),
|
|
[PathAttribute::AsPath(AsPathAttribute {
|
|
segments: vec![
|
|
AsPathSegment {
|
|
ordered: true,
|
|
path: vec![64601]
|
|
},
|
|
AsPathSegment {
|
|
ordered: false,
|
|
path: vec![1, 2]
|
|
}
|
|
]
|
|
})]
|
|
);
|
|
}
|
|
|
|
/// A function that fails has refused the route: the caller is told what
|
|
/// went wrong, and has nothing to take.
|
|
#[test]
|
|
fn a_policy_that_fails_gives_no_verdict() {
|
|
let failure = |script: &str| {
|
|
run_on(
|
|
script,
|
|
"policy",
|
|
&peer("provider"),
|
|
"203.0.113.0/24",
|
|
&attributes(),
|
|
Direction::Import,
|
|
)
|
|
.unwrap_err()
|
|
.to_string()
|
|
};
|
|
let cases = [
|
|
(
|
|
"fn policy(route, peer) { route.add_community(\"3356\"); }",
|
|
"is not a community",
|
|
),
|
|
(
|
|
"fn policy(route, peer) { route.add_community(\"3356:*\"); }",
|
|
"stands for more than one",
|
|
),
|
|
(
|
|
"fn policy(route, peer) { route.add_community(70000, 1); }",
|
|
"70000 is not half a community",
|
|
),
|
|
(
|
|
"fn policy(route, peer) { route.local_pref = -1; }",
|
|
"-1 is not a LOCAL_PREF",
|
|
),
|
|
(
|
|
"fn policy(route, peer) { route.within(\"nowhere\") }",
|
|
"is not a prefix",
|
|
),
|
|
(
|
|
"fn policy(route, peer) { route.prepend(1); }",
|
|
"prepend is for routes on their way out",
|
|
),
|
|
("fn policy(route, peer) { route.nonsense }", "nonsense"),
|
|
("fn policy(route, peer) { throw \"no\"; }", "no"),
|
|
(
|
|
"fn policy(route, peer) { 7 }",
|
|
"returned 7; a policy returns true or false",
|
|
),
|
|
// One that never ends is stopped.
|
|
("fn policy(route, peer) { loop { } }", "operations"),
|
|
(
|
|
"fn policy(route, peer) { policy(route, peer) }",
|
|
"Stack overflow",
|
|
),
|
|
];
|
|
for (script, complaint) in cases {
|
|
let said = failure(script);
|
|
assert!(said.contains(complaint), "{script}: {said}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_script_that_is_wrong_is_found_when_it_is_loaded() {
|
|
let load = |script: &str, function: &str| -> PolicyError {
|
|
match Policy::compile(script) {
|
|
Err(e) => e,
|
|
Ok(policy) => policy.hook(function).unwrap_err(),
|
|
}
|
|
};
|
|
assert!(matches!(
|
|
load("fn policy(route, peer) {", "policy"),
|
|
PolicyError::Parse(_)
|
|
));
|
|
assert!(matches!(
|
|
load("let x = 1 / 0;", "policy"),
|
|
PolicyError::Load(_)
|
|
));
|
|
assert_eq!(
|
|
load(
|
|
"fn from_a(r, p) { true } fn from_b(r, p) { true }",
|
|
"from_c"
|
|
)
|
|
.to_string(),
|
|
"the script has no function \"from_c\"; it has: from_a, from_b"
|
|
);
|
|
assert_eq!(
|
|
load("fn policy(route) { true }", "policy").to_string(),
|
|
"the function \"policy\" takes 1 arguments; a policy takes two, the route and the peer"
|
|
);
|
|
// Nothing of the machine is within reach of a script.
|
|
for out_of_reach in [
|
|
"import \"other\" as other;",
|
|
"fn policy(route, peer) { open_file(\"/etc/passwd\") }",
|
|
"fn policy(route, peer) { let t = timestamp(); true }",
|
|
] {
|
|
let policy = Policy::compile(out_of_reach).and_then(|policy| {
|
|
policy.hook("policy").and_then(|hook| {
|
|
hook.run(
|
|
&peer(""),
|
|
&Subject {
|
|
prefix: &prefix("203.0.113.0/24"),
|
|
attributes: &attributes(),
|
|
next_hop: None,
|
|
direction: Direction::Import,
|
|
},
|
|
)
|
|
})
|
|
});
|
|
assert!(policy.is_err(), "{out_of_reach}");
|
|
}
|
|
}
|
|
|
|
/// Constants are for every function of the script, also one that another
|
|
/// calls; and many sessions use one script at once.
|
|
#[test]
|
|
fn a_script_is_shared_by_every_session() {
|
|
let script = r#"
|
|
const TAGS = ["64601:1", "64601:2", "64601:3"];
|
|
fn tag(route, n) { route.add_community(TAGS[n]); }
|
|
fn policy(route, peer) { tag(route, peer.asn % 3); true }
|
|
"#;
|
|
let policy = Policy::compile(script).unwrap();
|
|
let hook = policy.hook("policy").unwrap();
|
|
assert_eq!(policy.functions(), ["policy", "tag"]);
|
|
let threads: Vec<_> = (0..8u32)
|
|
.map(|n| {
|
|
let hook = hook.clone();
|
|
std::thread::spawn(move || {
|
|
let peer = Peer::new(
|
|
"p",
|
|
Ipv4Addr::new(192, 0, 2, 9).into(),
|
|
64700 + n,
|
|
64601,
|
|
"",
|
|
);
|
|
for _ in 0..2000 {
|
|
let verdict = hook
|
|
.run(
|
|
&peer,
|
|
&Subject {
|
|
prefix: &prefix("203.0.113.0/24"),
|
|
attributes: &attributes(),
|
|
next_hop: None,
|
|
direction: Direction::Import,
|
|
},
|
|
)
|
|
.unwrap();
|
|
let expected = (64601, 1 + ((64700 + n) % 3) as u16);
|
|
assert_eq!(
|
|
communities(&verdict.attributes.unwrap()).last(),
|
|
Some(&expected)
|
|
);
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
for thread in threads {
|
|
thread.join().unwrap();
|
|
}
|
|
assert_eq!(Arc::strong_count(&policy), 2);
|
|
}
|
|
|
|
/// The example operators start from does what its comments say.
|
|
#[test]
|
|
fn the_example_script_does_what_it_says() {
|
|
let script = include_str!("../../../testdata/policy.rhai");
|
|
let policy = Policy::compile(script).unwrap();
|
|
let from_peer = policy.hook("from_peer").unwrap();
|
|
let to_peer = policy.hook("to_peer").unwrap();
|
|
let them = |name: &str, role: &str| {
|
|
Peer::new(name, Ipv4Addr::new(192, 0, 2, 2).into(), 64602, 65000, role)
|
|
};
|
|
let with = |communities: Vec<(u16, u16)>| {
|
|
vec![
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![64602]),
|
|
PathAttribute::Communities(CommunitiesPathAttribute(communities)),
|
|
]
|
|
};
|
|
let ask = |hook: &bgp_policy::Hook, peer: &Peer, attributes: &[PathAttribute], direction| {
|
|
hook.run(
|
|
peer,
|
|
&Subject {
|
|
prefix: &prefix("203.0.113.0/24"),
|
|
attributes,
|
|
next_hop: None,
|
|
direction,
|
|
},
|
|
)
|
|
.unwrap()
|
|
};
|
|
let local_pref = |attributes: &[PathAttribute]| {
|
|
attributes.iter().find_map(|attribute| match attribute {
|
|
PathAttribute::LocalPref(local_pref) => Some(local_pref.0),
|
|
_ => None,
|
|
})
|
|
};
|
|
|
|
// In: marked and ranked by who it is from, whatever it was marked with.
|
|
for (role, mark, preference) in [
|
|
("customer", 1000, 300),
|
|
("peer", 2000, 200),
|
|
("provider", 3000, 100),
|
|
] {
|
|
let verdict = ask(
|
|
&from_peer,
|
|
&them("r", role),
|
|
&with(vec![(65000, 1000), (3356, 3)]),
|
|
Direction::Import,
|
|
);
|
|
let changed = verdict.attributes.unwrap();
|
|
assert_eq!(communities(&changed), [(3356, 3), (65000, mark)], "{role}");
|
|
assert_eq!(local_pref(&changed), Some(preference), "{role}");
|
|
}
|
|
// A peer with no role is taken, with our marks removed and no more.
|
|
let verdict = ask(
|
|
&from_peer,
|
|
&them("r", ""),
|
|
&with(vec![(65000, 7)]),
|
|
Direction::Import,
|
|
);
|
|
assert!(verdict.accept);
|
|
assert_eq!(communities(&verdict.attributes.unwrap()), []);
|
|
// The peer's own "last resort" mark, and a blackhole request.
|
|
let verdict = ask(
|
|
&from_peer,
|
|
&them("r", "peer"),
|
|
&with(vec![(64602, 80)]),
|
|
Direction::Import,
|
|
);
|
|
assert_eq!(local_pref(&verdict.attributes.unwrap()), Some(80));
|
|
assert!(
|
|
!ask(
|
|
&from_peer,
|
|
&them("r", "peer"),
|
|
&with(vec![(65535, 666)]),
|
|
Direction::Import
|
|
)
|
|
.accept
|
|
);
|
|
|
|
// Out: what came from above goes to customers only, without our marks.
|
|
let from_provider = with(vec![(65000, 3000), (3356, 3)]);
|
|
assert!(
|
|
!ask(
|
|
&to_peer,
|
|
&them("r", "peer"),
|
|
&from_provider,
|
|
Direction::Export
|
|
)
|
|
.accept
|
|
);
|
|
assert!(
|
|
!ask(
|
|
&to_peer,
|
|
&them("r", "provider"),
|
|
&from_provider,
|
|
Direction::Export
|
|
)
|
|
.accept
|
|
);
|
|
let verdict = ask(
|
|
&to_peer,
|
|
&them("r", "customer"),
|
|
&from_provider,
|
|
Direction::Export,
|
|
);
|
|
assert_eq!(communities(&verdict.attributes.unwrap()), [(3356, 3)]);
|
|
assert!(
|
|
!ask(
|
|
&to_peer,
|
|
&them("r", "customer"),
|
|
&with(vec![(65535, 65281)]),
|
|
Direction::Export
|
|
)
|
|
.accept
|
|
);
|
|
// And the path is longer to the one it should be.
|
|
let verdict = ask(
|
|
&to_peer,
|
|
&them("backup-transit", "provider"),
|
|
&with(vec![]),
|
|
Direction::Export,
|
|
);
|
|
assert!(
|
|
verdict
|
|
.attributes
|
|
.unwrap()
|
|
.contains(&AsPathAttribute::from_asns(vec![65000, 65000, 64602]))
|
|
);
|
|
}
|