Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
318f056
docs(routing): say what a fib writer's destroy panics on
daniel-noland Sep 6, 2026
f64fbe5
fix(spec): restore the scheme-stripping regex in duvet-summary
daniel-noland Sep 6, 2026
8111997
feat(config): Generate port-forwarding exposes for property tests
daniel-noland Aug 7, 2026
491b84d
feat(config): Generate masquerade exposes, and share the overlay arou…
daniel-noland Aug 7, 2026
fd64849
feat(config): Generate static NAT exposes, and pin the mapping is a b…
daniel-noland Aug 7, 2026
5cf5599
fix(config): Refuse a port-forwarding expose the dataplane cannot build
daniel-noland Aug 7, 2026
221d3a1
test(mgmt): Property-test the configuration chain, and unblock its ge…
daniel-noland Aug 7, 2026
102f192
test(mgmt): Drive the config builder with generated NAT peerings
daniel-noland Aug 7, 2026
2e7c5d8
test(k8s-intf): Generate configurations that are valid by construction
daniel-noland Aug 7, 2026
83d1559
test(k8s-intf): Generate peering ACLs
daniel-noland Aug 7, 2026
2b4b608
test(mgmt): Build every dataplane table a validated config implies
daniel-noland Aug 7, 2026
5a62ba1
test(config): Hunt validator permissiveness with near-miss configurat…
daniel-noland Aug 8, 2026
29ba829
test(mgmt): Let a fuzzing engine drive the near-miss property
daniel-noland Aug 8, 2026
f9f237e
test(k8s-intf): Draw prefixes from slots so exposes cannot overlap
daniel-noland Aug 8, 2026
d2ed353
fix(k8s-intf): Give every vpc its own slots, and assert the control v…
daniel-noland Aug 8, 2026
7244f02
test(mgmt): Check that a validated configuration has only one meaning
daniel-noland Aug 8, 2026
3a950b0
test(config): Assert the validator refuses what a mutation certainly …
daniel-noland Aug 8, 2026
164e14e
test(nat): Catch a static NAT table that holds one of two rules asked…
daniel-noland Aug 8, 2026
7ba9a4d
fix(k8s-intf): Put the generated gateway in its own gateway groups
daniel-noland Aug 8, 2026
492193a
test(mgmt): Check that every expose leaves a trace
daniel-noland Aug 8, 2026
97efac9
test(mgmt): Follow the genid out of MasqueradeConfig
daniel-noland Aug 17, 2026
560ae80
fix(mgmt): Refuse an IPv6 peering by name instead of two ways by acci…
daniel-noland Aug 27, 2026
af6dc1a
fix(mgmt): Route both build sites through the same declared-limitatio…
daniel-noland Aug 27, 2026
32254e5
fix(k8s-intf): Refuse a slot budget the scheme cannot separate
daniel-noland Aug 27, 2026
b9b0128
fix(k8s-intf): Give MAX_PREFIXES back its doc comment
daniel-noland Sep 3, 2026
79ee159
fix(config): Document what the contract overlay helpers can refuse
daniel-noland Sep 3, 2026
504d2ab
fix(k8s-intf): Size the interface mask floor by the count, not the ad…
daniel-noland Sep 3, 2026
ad6a616
fix(k8s-intf): Let a spec span vnis, group sizes and community values
daniel-noland Sep 3, 2026
300c96f
fix(k8s-intf): Permute what a peering contains, not which name holds it
daniel-noland Sep 3, 2026
a2cfd0b
test(mgmt): Only assert on a rate when there is a sample behind it
daniel-noland Sep 3, 2026
90cca05
fix(k8s-intf): Let an expose name any subset of its vpc's subnets
daniel-noland Sep 3, 2026
268c72e
test(mgmt): Put the gateway in the group its peering names
daniel-noland Sep 3, 2026
e27d4df
test(mgmt): Check that a peering's advertised prefixes reach the config
daniel-noland Sep 3, 2026
5706e27
test(mgmt): Check the peering half of the chain, and stop skipping si…
daniel-noland Sep 3, 2026
bdf1c27
test(mgmt): Check what a vrf is, not just which vni it carries
daniel-noland Sep 3, 2026
3b95bcf
test(nat): Check the ports and protocols a port-forward rule installs
daniel-noland Sep 3, 2026
b821070
test(mgmt): Build the acl and flow filters a validated config implies
daniel-noland Sep 3, 2026
c1adc6f
refactor(k8s-intf): Make the mutation list and the nat mode single facts
daniel-noland Sep 3, 2026
f06ea01
refactor(k8s-intf): Delete the generators the slot rewrite replaced
daniel-noland Sep 3, 2026
86e1675
fix(config): Let a caller fix the address family across a set of exposes
daniel-noland Sep 3, 2026
ba39d48
fix(k8s-intf): Refuse an unseparable slot budget in every build
daniel-noland Sep 3, 2026
1fc540e
fix(k8s-intf): Reach strum from a plain cargo test too
daniel-noland Sep 3, 2026
79fa295
test(mgmt): Say which half of the completeness property the mutations…
daniel-noland Sep 3, 2026
6374e2b
test(config): Guard the static-nat shape check on sample count
daniel-noland Sep 3, 2026
2f29ebe
test(k8s-intf): Give every expose its own checked slot
daniel-noland Sep 3, 2026
974f1ab
test(k8s-intf): Carve exclusions out of the prefix they exclude
daniel-noland Sep 3, 2026
747116e
test(k8s-intf): Refuse to build a sibling for a default route
daniel-noland Sep 3, 2026
464eda3
test(k8s-intf): Draw acl match entries that name subnets, and more th…
daniel-noland Sep 3, 2026
631ef14
test(mgmt): Compare the acl, and stop treating a failed build as a skip
daniel-noland Sep 3, 2026
6d542a3
docs(mgmt): Say what IMPORT_VRFS actually gates
daniel-noland Sep 3, 2026
e836094
fix(config): Refuse an IPv6 peering at validation, not at apply
daniel-noland Sep 3, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ license.workspace = true
publish.workspace = true
version.workspace = true

[features]
bolero = ["dep:bolero", "lpm/bolero"]

[dependencies]
# internal
common = { workspace = true }
Expand All @@ -16,6 +19,7 @@ net = { workspace = true }

# external
arc-swap = { workspace = true }
bolero = { workspace = true, optional = true, default-features = false, features = ["alloc"] }
chrono = { workspace = true, features = ["alloc", "std"] }
derive_builder = { workspace = true, features = [] }
ipnet = { workspace = true }
Expand Down
6 changes: 5 additions & 1 deletion config/src/converters/k8s/config/expose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ mod test {
"10.0.4.0/24".parse::<Prefix>().unwrap(),
),
]);
let expose_gen = k8s_intf::bolero::expose::LegalValueExposeGenerator::new(&subnets);
let expose_gen = k8s_intf::bolero::expose::AnyExposeGenerator::new(0, &subnets);
bolero::check!()
.with_generator(expose_gen)
.for_each(|k8s_expose| {
Expand Down Expand Up @@ -549,6 +549,7 @@ mod test {
})
.unwrap_or(vec![]);
k8s_nots.sort();
k8s_nots.dedup();
let k8s_subnets = k8s_expose
.ips
.as_ref()
Expand All @@ -566,6 +567,7 @@ mod test {
.unwrap_or(vec![]);
k8s_ips.extend(k8s_subnets);
k8s_ips.sort();
k8s_ips.dedup();

let k8s_as = k8s_expose.r#as.as_ref().map(|r#as| {
let mut ret = r#as
Expand All @@ -574,6 +576,7 @@ mod test {
.map(|r#as| r#as.cidr.as_ref().unwrap().clone())
.collect::<Vec<_>>();
ret.sort();
ret.dedup();
ret
});

Expand All @@ -584,6 +587,7 @@ mod test {
.map(|r#as| r#as.not.as_ref().unwrap().clone())
.collect::<Vec<_>>();
ret.sort();
ret.dedup();
ret
});

Expand Down
11 changes: 9 additions & 2 deletions config/src/converters/k8s/config/peering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,17 @@ mod test {
use k8s_intf::bolero::peering::{
LegalValuePeeringsGenerator, LegalValuePeeringsPeeringGenerator,
};
use k8s_intf::bolero::{AddressFamily, NatFlavour};
use lpm::prefix::Prefix;

use crate::converters::k8s::config::{SubnetMap, VpcSubnetMap};

#[test]
fn test_vpc_manifest_conversion() {
let subnets = SubnetMap::new(); // Let this be empty since we are test subnet conversion elsewhere
let generator = LegalValuePeeringsPeeringGenerator::new(&subnets);
let flavours = NatFlavour::all();
let generator =
LegalValuePeeringsPeeringGenerator::new(&subnets, &flavours, AddressFamily::V4, 3, 0);
bolero::check!()
.with_generator(generator)
.for_each(|peering| {
Expand Down Expand Up @@ -169,7 +172,11 @@ mod test {
]),
),
]);
let generator = LegalValuePeeringsGenerator::new(&subnets).unwrap();
let flavours = NatFlavour::all();
let families = AddressFamily::all();
let groups = vec!["gwgroup-0".to_string()];
let generator =
LegalValuePeeringsGenerator::new(&subnets, &flavours, &families, 3, &groups).unwrap();
bolero::check!()
.with_generator(generator)
.for_each(|peering| {
Expand Down
17 changes: 16 additions & 1 deletion config/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub enum ConfigError {
FailureApply(String),
#[error("Forbidden: {0}")]
Forbidden(&'static str),
#[error("Not supported yet: {0}")]
Unsupported(&'static str),
#[error("Bad VPC Id")]
BadVpcId(String),
#[error("Bad VTEP local address {0}: {1}")]
Expand Down Expand Up @@ -75,8 +77,21 @@ pub enum ConfigError {
#[error("Invalid ACL configuration: {0}")]
InvalidAcl(String),
// NAT-specific
#[error("Mismatched prefixes sizes for static NAT: {0:?} and {1:?}")]
#[error(
"Mismatched sizes for static NAT: the exposed prefixes and the range they translate to \
must cover the same number of address-port pairs (they cover {0:?} and {1:?})"
)]
MismatchedPrefixSizes(PrefixWithPortsSize, PrefixWithPortsSize),
#[error(
"Mismatched prefix lengths for port forwarding: /{private} exposed and /{public} \
translated to; a rule maps addresses one for one, so the two must be the same length"
)]
MismatchedPrefixLengths { private: u8, public: u8 },
#[error(
"Mismatched port range sizes for port forwarding: {private} ports exposed and {public} \
translated to; a rule maps ports one for one, so the two must be the same size"
)]
MismatchedPortRangeSizes { private: usize, public: usize },
#[error("Peering {0} has manifests using incompatible NAT modes")]
IncompatibleNatModes(String),
#[error("Vpc {0} has a peering with no exposes")]
Expand Down
68 changes: 68 additions & 0 deletions config/src/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,67 @@ impl ExternalConfig {
Ok(())
}

/// Refuse a peering *this* gateway would have to render and cannot.
///
/// The routing configuration built from a peering is IPv4-only: its prefix lists and
/// route-maps are `IpVer::V4` throughout and nothing in the builder has a v6
/// counterpart. `build_internal_config` used to be where that was discovered, which
/// made it a refusal at apply time: the configuration was accepted, reported good to
/// whoever submitted it, and then rejected as a whole when it was applied, taking its
/// IPv4 vpcs with it. Anything accepted here has to apply cleanly, so the refusal
/// belongs here.
///
/// The predicate has to be the builder's, though, and not "any IPv6 peering
/// anywhere". `build_routing_config` renders a peering only when this gateway is a
/// member of the group the peering names and a community exists for its rank, and
/// `build_internal_config` skips the overlay entirely when the underlay carries no
/// BGP. A peering outside all of that is never rendered, so it cannot fail to render,
/// and refusing it would reject a configuration over an IPv6 peering some *other*
/// gateway is responsible for -- taking this gateway's own IPv4 peerings down with
/// it, for a configuration that built perfectly well before.
///
/// It belongs here, and not on `Peering` or `Overlay`, for a related reason: the
/// limitation is the gateway's and not the overlay model's. The ACL filter builds
/// from a `ValidatedOverlay` and handles IPv6 perfectly well, and twelve of its tests
/// say so.
///
/// `Peering::validate` has already established that a peering's two manifests agree
/// on their IP version, so `is_v4` speaks for the whole peering.
///
/// # Errors
///
/// Returns [`ConfigError::Unsupported`] if a peering this gateway renders carries
/// IPv6 prefixes.
fn check_rendered_peerings_are_ipv4(
gwname: &str,
overlay: &ValidatedOverlay,
underlay: &Underlay,
gwgroups: &GwGroupTable,
communities: &PriorityCommunityTable,
) -> ConfigResult {
if underlay.vrf.bgp.is_none() {
// No underlay BGP means no overlay routing configuration is built at all.
return Ok(());
}
let renders = |peering: &ValidatedPeering| {
gwgroups
.get_group_member_rank(peering.gwgroup(), gwname)
.is_some_and(|rank| communities.get_community(rank).is_some())
};
if overlay
.vpc_table()
.peerings()
.filter(|peering| renders(peering))
.all(ValidatedPeering::is_v4)
{
return Ok(());
}
Err(ConfigError::Unsupported(
"IPv6 prefixes in a vpc peering: the routing configuration built from a peering is \
IPv4-only, so such a peering cannot be rendered",
))
}

/// Validate the external configuration.
/// This method consumes `ExternalConfig` and outputs a `ValidatedGwConfig` on success.
///
Expand All @@ -107,6 +168,13 @@ impl ExternalConfig {
let overlay = self.overlay.validate()?;
let peerings = overlay.vpc_table().peerings();
self.check_peering_gwgroups_exist(peerings)?;
Self::check_rendered_peerings_are_ipv4(
&self.gwname,
&overlay,
&underlay,
&self.gwgroups,
&self.communities,
)?;

// if there are vpcs configured, there MUST be a vtep configured
if !overlay.vpc_table().is_empty() && underlay.vtep.is_none() {
Expand Down
8 changes: 7 additions & 1 deletion config/src/external/overlay/validation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,13 @@ mod test {
.unwrap();
let result = expose.validate();
assert!(
matches!(result, Err(ConfigError::MismatchedPrefixSizes(_, _))),
matches!(
result,
Err(ConfigError::MismatchedPrefixLengths {
private: 24,
public: 25
})
),
"{result:?}",
);
}
Expand Down
Loading
Loading