mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
test(connlib): generate all resources ahead of time (#5916)
Currently, `tunnel_test` is broken as a result of #5871. In particular, adding a resource requires that the resource is assigned to a gateway which can only be done after it is being added. As a result, no resources are ever added in the test. With this patch, we align the test even closer with how Firezone works in production: We generate all resources ahead of time and selectively activate / deactivate them on the client. Unfortunately, this requires quite a few changes but overall, is a net-positive change. Replaces: #5914.
This commit is contained in:
@@ -80,6 +80,8 @@ pub enum Client {}
|
||||
/// We favor these generic parameters over having our own IDs to avoid mapping back and forth in upper layers.
|
||||
pub struct Node<T, TId, RId> {
|
||||
private_key: StaticSecret,
|
||||
public_key: PublicKey,
|
||||
|
||||
index: IndexLfsr,
|
||||
rate_limiter: Arc<RateLimiter>,
|
||||
host_candidates: HashSet<Candidate>,
|
||||
@@ -126,6 +128,7 @@ where
|
||||
let public_key = &(&private_key).into();
|
||||
Self {
|
||||
private_key,
|
||||
public_key: *public_key,
|
||||
marker: Default::default(),
|
||||
index: IndexLfsr::default(),
|
||||
rate_limiter: Arc::new(RateLimiter::new(public_key, HANDSHAKE_RATE_LIMIT)),
|
||||
@@ -175,7 +178,7 @@ where
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
(&self.private_key).into()
|
||||
self.public_key
|
||||
}
|
||||
|
||||
pub fn connection_id(&self, key: PublicKey) -> Option<TId> {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -979,6 +979,8 @@ impl ClientState {
|
||||
self.update_site_status_by_gateway(&gateway_id, Status::Unknown);
|
||||
// TODO: should we have a Node::remove_connection?
|
||||
}
|
||||
|
||||
self.resources_gateways.remove(&id);
|
||||
}
|
||||
|
||||
fn update_dns_mapping(&mut self) -> bool {
|
||||
|
||||
@@ -24,5 +24,5 @@ proptest_state_machine::prop_state_machine! {
|
||||
})]
|
||||
|
||||
#[test]
|
||||
fn run_tunnel_test(sequential 1..20 => TunnelTest);
|
||||
fn run_tunnel_test(sequential 1..10 => TunnelTest);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ use super::{
|
||||
composite_strategy::CompositeStrategy, sim_client::*, sim_gateway::*, sim_net::*, sim_relay::*,
|
||||
strategies::*, stub_portal::StubPortal, transition::*,
|
||||
};
|
||||
use crate::dns::is_subdomain;
|
||||
use chrono::{DateTime, Utc};
|
||||
use connlib_shared::{
|
||||
messages::{client, GatewayId, RelayId},
|
||||
messages::{
|
||||
client::{self, ResourceDescription},
|
||||
GatewayId, RelayId,
|
||||
},
|
||||
proptest::*,
|
||||
DomainName, StaticSecret,
|
||||
};
|
||||
@@ -68,7 +72,7 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
)
|
||||
.prop_filter_map(
|
||||
"network IPs must be unique",
|
||||
|(c, (gateways, portal), relays, global_dns, now, utc_now)| {
|
||||
|(c, (gateways, portal, records), relays, mut global_dns, now, utc_now)| {
|
||||
let mut routing_table = RoutingTable::default();
|
||||
|
||||
if !routing_table.add_host(c.inner().id, &c) {
|
||||
@@ -86,6 +90,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
};
|
||||
}
|
||||
|
||||
// Merge all DNS records into `global_dns`.
|
||||
global_dns.extend(records);
|
||||
|
||||
Some((
|
||||
c,
|
||||
gateways,
|
||||
@@ -143,19 +150,15 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
upstream_dns_servers()
|
||||
.prop_map(|servers| Transition::UpdateUpstreamDnsServers { servers }),
|
||||
)
|
||||
.with(
|
||||
1,
|
||||
add_cidr_resource(sample::select(state.portal.all_sites()).prop_map(|s| vec![s])),
|
||||
.with_if_not_empty(
|
||||
5,
|
||||
state.all_resources_not_known_to_client(),
|
||||
|resource_ids| sample::select(resource_ids).prop_map(Transition::ActivateResource),
|
||||
)
|
||||
.with_if_not_empty(1, state.client.inner().all_resource_ids(), |resource_ids| {
|
||||
sample::select(resource_ids).prop_map(Transition::DeactivateResource)
|
||||
})
|
||||
.with(1, roam_client())
|
||||
.with(
|
||||
1,
|
||||
prop_oneof![
|
||||
non_wildcard_dns_resource(sample::select(state.portal.all_sites())),
|
||||
star_wildcard_dns_resource(sample::select(state.portal.all_sites())),
|
||||
question_mark_wildcard_dns_resource(sample::select(state.portal.all_sites())),
|
||||
],
|
||||
)
|
||||
.with(1, Just(Transition::ReconnectPortal))
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
@@ -200,7 +203,7 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
5,
|
||||
(
|
||||
state.all_domains(state.client.inner()),
|
||||
state.client.inner().v4_dns_servers(),
|
||||
@@ -211,7 +214,7 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
5,
|
||||
(
|
||||
state.all_domains(state.client.inner()),
|
||||
state.client.inner().v6_dns_servers(),
|
||||
@@ -247,9 +250,6 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(1, state.client.inner().all_resource_ids(), |resources| {
|
||||
sample::select(resources).prop_map(Transition::RemoveResource)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -258,39 +258,35 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
/// Here is where we implement the "expected" logic.
|
||||
fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
|
||||
match transition {
|
||||
Transition::AddCidrResource { resource } => {
|
||||
state.client.exec_mut(|client| {
|
||||
client
|
||||
.cidr_resources
|
||||
.insert(resource.address, resource.clone());
|
||||
});
|
||||
state
|
||||
.portal
|
||||
.add_resource(client::ResourceDescription::Cidr(resource.clone()));
|
||||
}
|
||||
Transition::RemoveResource(id) => {
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.cidr_resources.retain(|_, r| &r.id != id));
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.connected_cidr_resources.remove(id));
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.dns_resources.remove(id));
|
||||
state.portal.remove_resource(*id);
|
||||
}
|
||||
Transition::AddDnsResource { resource, records } => {
|
||||
state.client.exec_mut(|client| {
|
||||
client.dns_resources.insert(resource.id, resource.clone());
|
||||
});
|
||||
state
|
||||
.portal
|
||||
.add_resource(client::ResourceDescription::Dns(resource.clone()));
|
||||
Transition::ActivateResource(resource) => {
|
||||
state.client.exec_mut(|client| match resource {
|
||||
client::ResourceDescription::Dns(r) => {
|
||||
client.dns_resources.insert(r.id, r.clone());
|
||||
|
||||
// For the client, there is no difference between a DNS resource and a truly global DNS name.
|
||||
// We store all records in the same map to follow the same model.
|
||||
state.global_dns_records.extend(records.clone());
|
||||
// TODO: PRODUCTION CODE CANNOT DO THIS.
|
||||
// Remove all prior DNS records.
|
||||
client.dns_records.retain(|domain, _| {
|
||||
if is_subdomain(domain, &r.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
}
|
||||
client::ResourceDescription::Cidr(r) => {
|
||||
client.cidr_resources.insert(r.address, r.clone());
|
||||
}
|
||||
client::ResourceDescription::Internet(_) => todo!("Unsupported"),
|
||||
});
|
||||
}
|
||||
Transition::DeactivateResource(id) => {
|
||||
state.client.exec_mut(|client| {
|
||||
client.cidr_resources.retain(|_, r| &r.id != id);
|
||||
client.dns_resources.remove(id);
|
||||
|
||||
client.connected_cidr_resources.remove(id);
|
||||
client.connected_dns_resources.retain(|(r, _)| r != id);
|
||||
});
|
||||
}
|
||||
Transition::SendDnsQuery {
|
||||
domain,
|
||||
@@ -335,7 +331,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
..
|
||||
} => {
|
||||
state.client.exec_mut(|client| {
|
||||
client.on_icmp_packet_to_cidr(*src, *dst, *seq, *identifier)
|
||||
client.on_icmp_packet_to_cidr(*src, *dst, *seq, *identifier, |r| {
|
||||
state.portal.gateway_for_resource(r).copied()
|
||||
})
|
||||
});
|
||||
}
|
||||
Transition::SendICMPPacketToDnsResource {
|
||||
@@ -345,7 +343,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
identifier,
|
||||
..
|
||||
} => state.client.exec_mut(|client| {
|
||||
client.on_icmp_packet_to_dns(*src, dst.clone(), *seq, *identifier)
|
||||
client.on_icmp_packet_to_dns(*src, dst.clone(), *seq, *identifier, |r| {
|
||||
state.portal.gateway_for_resource(r).copied()
|
||||
})
|
||||
}),
|
||||
Transition::UpdateSystemDnsServers { servers } => {
|
||||
state
|
||||
@@ -384,86 +384,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
/// Any additional checks on whether a particular [`Transition`] can be applied to a certain state.
|
||||
fn preconditions(state: &Self::State, transition: &Self::Transition) -> bool {
|
||||
match transition {
|
||||
Transition::AddCidrResource { resource } => {
|
||||
// Resource IDs must be unique.
|
||||
if state
|
||||
.client
|
||||
.inner()
|
||||
.all_resource_ids()
|
||||
.contains(&resource.id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(gid) = state.portal.gateway_for_resource(resource.id) else {
|
||||
return false;
|
||||
};
|
||||
let Some(gateway) = state.gateways.get(gid) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS!
|
||||
if resource.address.is_ipv6() && gateway.ip6.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if resource.address.is_ipv4() && gateway.ip4.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS!
|
||||
for dns_resolved_ip in state.global_dns_records.values().flat_map(|ip| ip.iter()) {
|
||||
// If the CIDR resource overlaps with an IP that a DNS record resolved to, we have problems ...
|
||||
if resource.address.contains(*dns_resolved_ip) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
Transition::AddDnsResource { records, resource } => {
|
||||
// TODO: Should we allow adding a DNS resource if we don't have an DNS resolvers?
|
||||
|
||||
// TODO: For these tests, we assign the resolved IP of a DNS resource as part of this transition.
|
||||
// Connlib cannot know, when a DNS record expires, thus we currently don't allow to add DNS resources where the same domain resolves to different IPs
|
||||
|
||||
for (name, resolved_ips) in records {
|
||||
if state.global_dns_records.contains_key(name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS.
|
||||
let any_real_ip_overlaps_with_cidr_resource =
|
||||
resolved_ips.iter().any(|resolved_ip| {
|
||||
state
|
||||
.client
|
||||
.inner()
|
||||
.cidr_resource_by_ip(*resolved_ip)
|
||||
.is_some()
|
||||
});
|
||||
|
||||
if any_real_ip_overlaps_with_cidr_resource {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resource IDs must be unique.
|
||||
if state
|
||||
.client
|
||||
.inner()
|
||||
.all_resource_ids()
|
||||
.contains(&resource.id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resource addresses must be unique.
|
||||
if state
|
||||
.client
|
||||
.inner()
|
||||
.dns_resources
|
||||
.values()
|
||||
.any(|r| r.address == resource.address)
|
||||
{
|
||||
Transition::ActivateResource(resource) => {
|
||||
// Don't add resource we already have.
|
||||
if state.client.inner().has_resource(resource.id()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -493,11 +416,15 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
..
|
||||
} => {
|
||||
let ref_client = state.client.inner();
|
||||
let Some(resource) = ref_client.cidr_resource_by_ip(*dst) else {
|
||||
return false;
|
||||
};
|
||||
let Some(gateway) = state.portal.gateway_for_resource(resource.id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ref_client.is_valid_icmp_packet(seq, identifier)
|
||||
&& ref_client
|
||||
.gateway_by_cidr_resource_ip(*dst)
|
||||
.is_some_and(|g| state.gateways.contains_key(&g))
|
||||
&& state.gateways.contains_key(gateway)
|
||||
}
|
||||
Transition::SendICMPPacketToDnsResource {
|
||||
seq,
|
||||
@@ -507,15 +434,19 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
..
|
||||
} => {
|
||||
let ref_client = state.client.inner();
|
||||
let Some(resource) = ref_client.dns_resource_by_domain(dst) else {
|
||||
return false;
|
||||
};
|
||||
let Some(gateway) = state.portal.gateway_for_resource(resource) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ref_client.is_valid_icmp_packet(seq, identifier)
|
||||
&& ref_client.dns_records.get(dst).is_some_and(|r| match src {
|
||||
IpAddr::V4(_) => r.contains(&RecordType::A),
|
||||
IpAddr::V6(_) => r.contains(&RecordType::AAAA),
|
||||
})
|
||||
&& ref_client
|
||||
.gateway_by_domain_name(dst)
|
||||
.is_some_and(|g| state.gateways.contains_key(&g))
|
||||
&& state.gateways.contains_key(gateway)
|
||||
}
|
||||
Transition::UpdateSystemDnsServers { servers } => {
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS!
|
||||
@@ -544,14 +475,31 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
Transition::SendDnsQuery {
|
||||
domain, dns_server, ..
|
||||
} => {
|
||||
state.global_dns_records.contains_key(domain)
|
||||
&& state
|
||||
.client
|
||||
.inner()
|
||||
.expected_dns_servers()
|
||||
.contains(dns_server)
|
||||
let is_known_domain = state.global_dns_records.contains_key(domain);
|
||||
let has_dns_server = state
|
||||
.client
|
||||
.inner()
|
||||
.expected_dns_servers()
|
||||
.contains(dns_server);
|
||||
let gateway_is_present_in_case_dns_server_is_cidr_resource = match state
|
||||
.client
|
||||
.inner()
|
||||
.dns_query_via_cidr_resource(dns_server.ip(), domain)
|
||||
{
|
||||
Some(r) => {
|
||||
let Some(gateway) = state.portal.gateway_for_resource(r) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
state.gateways.contains_key(gateway)
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
is_known_domain
|
||||
&& has_dns_server
|
||||
&& gateway_is_present_in_case_dns_server_is_cidr_resource
|
||||
}
|
||||
Transition::RemoveResource(id) => state.client.inner().all_resource_ids().contains(id),
|
||||
Transition::RoamClient { ip4, ip6, port } => {
|
||||
// In production, we always rebind to a new port so we never roam to our old existing IP / port combination.
|
||||
|
||||
@@ -562,6 +510,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
!is_assigned_ip4 && !is_assigned_ip6 && !is_previous_port
|
||||
}
|
||||
Transition::ReconnectPortal => true,
|
||||
Transition::DeactivateResource(r) => {
|
||||
state.client.inner().all_resource_ids().contains(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,6 +531,13 @@ impl ReferenceState {
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_resources_not_known_to_client(&self) -> Vec<ResourceDescription> {
|
||||
let mut all_resources = self.portal.all_resources();
|
||||
all_resources.retain(|r| !self.client.inner().has_resource(r.id()));
|
||||
|
||||
all_resources
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn private_key() -> impl Strategy<Value = PrivateKey> {
|
||||
|
||||
@@ -251,8 +251,6 @@ pub struct RefClient {
|
||||
HashMap<GatewayId, VecDeque<(ResourceDst, IcmpSeq, IcmpIdentifier)>>,
|
||||
/// The expected DNS handshakes.
|
||||
pub(crate) expected_dns_handshakes: VecDeque<QueryId>,
|
||||
|
||||
pub(crate) gateways_by_resource: HashMap<ResourceId, GatewayId>,
|
||||
}
|
||||
|
||||
impl RefClient {
|
||||
@@ -292,17 +290,21 @@ impl RefClient {
|
||||
dst: IpAddr,
|
||||
seq: u16,
|
||||
identifier: u16,
|
||||
gateway_by_resource: impl Fn(ResourceId) -> Option<GatewayId>,
|
||||
) {
|
||||
tracing::Span::current().record("dst", tracing::field::display(dst));
|
||||
|
||||
// Second, if we are not yet connected, check if we have a resource for this IP.
|
||||
let Some((_, resource)) = self.cidr_resources.longest_match(dst) else {
|
||||
let Some(resource) = self.cidr_resource_by_ip(dst) else {
|
||||
tracing::debug!("No resource corresponds to IP");
|
||||
return;
|
||||
};
|
||||
tracing::Span::current().record("resource", tracing::field::display(resource.id));
|
||||
|
||||
let gateway = *self.gateways_by_resource.get(&resource.id).unwrap();
|
||||
let Some(gateway) = gateway_by_resource(resource.id) else {
|
||||
tracing::error!("No gateway for resource");
|
||||
return;
|
||||
};
|
||||
|
||||
if self.is_connected_to_cidr(resource.id) && self.is_tunnel_ip(src) {
|
||||
tracing::debug!("Connected to CIDR resource, expecting packet to be routed");
|
||||
@@ -325,6 +327,7 @@ impl RefClient {
|
||||
dst: DomainName,
|
||||
seq: u16,
|
||||
identifier: u16,
|
||||
gateway_by_resource: impl Fn(ResourceId) -> Option<GatewayId>,
|
||||
) {
|
||||
tracing::Span::current().record("dst", tracing::field::display(&dst));
|
||||
|
||||
@@ -335,7 +338,10 @@ impl RefClient {
|
||||
|
||||
tracing::Span::current().record("resource", tracing::field::display(resource));
|
||||
|
||||
let gateway = *self.gateways_by_resource.get(&resource).unwrap();
|
||||
let Some(gateway) = gateway_by_resource(resource) else {
|
||||
tracing::error!("No gateway for resource");
|
||||
return;
|
||||
};
|
||||
|
||||
if self
|
||||
.connected_dns_resources
|
||||
@@ -381,7 +387,7 @@ impl RefClient {
|
||||
self.known_hosts.contains_key(name)
|
||||
}
|
||||
|
||||
fn dns_resource_by_domain(&self, domain: &DomainName) -> Option<ResourceId> {
|
||||
pub(crate) fn dns_resource_by_domain(&self, domain: &DomainName) -> Option<ResourceId> {
|
||||
self.dns_resources
|
||||
.values()
|
||||
.filter(|r| is_subdomain(&domain.to_string(), &r.address))
|
||||
@@ -407,20 +413,6 @@ impl RefClient {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn gateway_by_cidr_resource_ip(&self, dst: IpAddr) -> Option<GatewayId> {
|
||||
let resource_id = self.cidr_resource_by_ip(dst)?;
|
||||
let gateway_id = self.gateways_by_resource.get(&resource_id)?;
|
||||
|
||||
Some(*gateway_id)
|
||||
}
|
||||
|
||||
pub(crate) fn gateway_by_domain_name(&self, dst: &DomainName) -> Option<GatewayId> {
|
||||
let resource_id = self.dns_resource_by_domain(dst)?;
|
||||
let gateway_id = self.gateways_by_resource.get(&resource_id)?;
|
||||
|
||||
Some(*gateway_id)
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_v4_domains(&self) -> Vec<DomainName> {
|
||||
self.resolved_domains()
|
||||
.filter_map(|(domain, records)| {
|
||||
@@ -482,8 +474,8 @@ impl RefClient {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn cidr_resource_by_ip(&self, ip: IpAddr) -> Option<ResourceId> {
|
||||
self.cidr_resources.longest_match(ip).map(|(_, r)| r.id)
|
||||
pub(crate) fn cidr_resource_by_ip(&self, ip: IpAddr) -> Option<&ResourceDescriptionCidr> {
|
||||
self.cidr_resources.longest_match(ip).map(|(_, r)| r)
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_ip4_for_non_resources(
|
||||
@@ -539,7 +531,7 @@ impl RefClient {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.cidr_resource_by_ip(dns_server)
|
||||
self.cidr_resource_by_ip(dns_server).map(|r| r.id)
|
||||
}
|
||||
|
||||
pub(crate) fn all_resource_ids(&self) -> Vec<ResourceId> {
|
||||
@@ -549,6 +541,14 @@ impl RefClient {
|
||||
Vec::from_iter(cidr_resources.chain(dns_resources))
|
||||
}
|
||||
|
||||
pub(crate) fn has_resource(&self, resource_id: ResourceId) -> bool {
|
||||
if self.dns_resources.contains_key(&resource_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.cidr_resources.iter().any(|(_, r)| r.id == resource_id)
|
||||
}
|
||||
|
||||
pub(crate) fn all_resources(&self) -> Vec<ResourceDescription> {
|
||||
let cidr_resources = self
|
||||
.cidr_resources
|
||||
@@ -659,7 +659,6 @@ fn ref_client(
|
||||
connected_dns_resources: Default::default(),
|
||||
expected_icmp_handshakes: Default::default(),
|
||||
expected_dns_handshakes: Default::default(),
|
||||
gateways_by_resource: Default::default(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::tests::strategies::documentation_ip6s;
|
||||
use connlib_shared::messages::{ClientId, GatewayId, RelayId};
|
||||
use firezone_relay::{AddressFamily, IpStack};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use ip_network::{IpNetwork, Ipv4Network};
|
||||
use ip_network_table::IpNetworkTable;
|
||||
use itertools::Itertools as _;
|
||||
use prop::sample;
|
||||
@@ -97,6 +98,13 @@ impl<T> Host<T> {
|
||||
self.allocate_port(port, AddressFamily::V6);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn can_route_to(&self, network: IpNetwork) -> bool {
|
||||
match network {
|
||||
IpNetwork::V4(_) => self.ip4.is_some(),
|
||||
IpNetwork::V6(_) => self.ip6.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Host<T>
|
||||
@@ -276,14 +284,9 @@ pub(crate) fn host_ip4s() -> impl Strategy<Value = Ipv4Addr> {
|
||||
|
||||
/// A [`Strategy`] of [`Ipv6Addr`]s used for routing packets between hosts within our test.
|
||||
///
|
||||
/// This uses the `2001:DB8::/32` address space reserved for documentation and examples in [RFC3849](https://datatracker.ietf.org/doc/html/rfc3849).
|
||||
/// This uses a subnet of the `2001:DB8::/32` address space reserved for documentation and examples in [RFC3849](https://datatracker.ietf.org/doc/html/rfc3849).
|
||||
pub(crate) fn host_ip6s() -> impl Strategy<Value = Ipv6Addr> {
|
||||
let ips = Ipv6Network::new(Ipv6Addr::new(0x2001, 0xDB80, 0, 0, 0, 0, 0, 0), 32)
|
||||
.unwrap()
|
||||
.subnets_with_prefix(128)
|
||||
.map(|n| n.network_address())
|
||||
.take(100)
|
||||
.collect_vec();
|
||||
const HOST_SUBNET: u16 = 0x1010;
|
||||
|
||||
sample::select(ips)
|
||||
documentation_ip6s(HOST_SUBNET, 100)
|
||||
}
|
||||
|
||||
@@ -3,40 +3,27 @@ use super::{
|
||||
sim_net::Host,
|
||||
stub_portal::StubPortal,
|
||||
};
|
||||
use crate::client::{IPV4_RESOURCES, IPV6_RESOURCES};
|
||||
use connlib_shared::{
|
||||
messages::{client::SiteId, DnsServer, GatewayId},
|
||||
proptest::{domain_name, gateway_id, site},
|
||||
messages::{
|
||||
client::{ResourceDescriptionCidr, ResourceDescriptionDns, Site, SiteId},
|
||||
DnsServer, GatewayId,
|
||||
},
|
||||
proptest::{
|
||||
any_ip_network, cidr_resource, dns_resource, domain_label, domain_name, gateway_id, site,
|
||||
},
|
||||
DomainName,
|
||||
};
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use itertools::Itertools as _;
|
||||
use prop::sample;
|
||||
use proptest::{collection, prelude::*};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
str::FromStr as _,
|
||||
};
|
||||
|
||||
pub(crate) fn resolved_ips() -> impl Strategy<Value = HashSet<IpAddr>> {
|
||||
collection::hash_set(any::<IpAddr>(), 1..6)
|
||||
}
|
||||
|
||||
/// A strategy for generating a set of DNS records all nested under the provided base domain.
|
||||
pub(crate) fn subdomain_records(
|
||||
base: String,
|
||||
subdomains: impl Strategy<Value = String>,
|
||||
) -> impl Strategy<Value = HashMap<DomainName, HashSet<IpAddr>>> {
|
||||
collection::hash_map(subdomains, resolved_ips(), 1..4).prop_map(move |subdomain_ips| {
|
||||
subdomain_ips
|
||||
.into_iter()
|
||||
.map(|(label, ips)| {
|
||||
let domain = format!("{label}.{base}");
|
||||
|
||||
(domain.parse().unwrap(), ips)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn upstream_dns_servers() -> impl Strategy<Value = Vec<DnsServer>> {
|
||||
let ip4_dns_servers = collection::vec(
|
||||
any::<Ipv4Addr>().prop_map(|ip| DnsServer::from((ip, 53))),
|
||||
@@ -68,7 +55,7 @@ pub(crate) fn global_dns_records() -> impl Strategy<Value = BTreeMap<DomainName,
|
||||
collection::btree_map(
|
||||
domain_name(2..4).prop_map(|d| d.parse().unwrap()),
|
||||
collection::hash_set(any::<IpAddr>(), 1..6),
|
||||
0..15,
|
||||
0..5,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,33 +94,226 @@ pub(crate) fn tunnel_ip6s() -> impl Iterator<Item = Ipv6Addr> {
|
||||
}
|
||||
|
||||
/// A [`Strategy`] for sampling a set of gateways and a corresponding [`StubPortal`] that has a set of [`Site`]s configured with those gateways.
|
||||
pub(crate) fn gateways_and_portal(
|
||||
) -> impl Strategy<Value = (HashMap<GatewayId, Host<RefGateway>>, StubPortal)> {
|
||||
///
|
||||
/// Similar as in production, the portal holds a list of DNS and CIDR resources (those are also sampled from the given sites).
|
||||
/// Via this site mapping, these resources are implicitly assigned to a gateway.
|
||||
///
|
||||
/// Lastly, we also sample a set of DNS records for the DNS resources that we created.
|
||||
pub(crate) fn gateways_and_portal() -> impl Strategy<
|
||||
Value = (
|
||||
HashMap<GatewayId, Host<RefGateway>>,
|
||||
StubPortal,
|
||||
HashMap<DomainName, HashSet<IpAddr>>,
|
||||
),
|
||||
> {
|
||||
collection::hash_set(site(), 1..=3)
|
||||
.prop_flat_map(|sites| {
|
||||
let gateway_site = sample::select(sites.iter().map(|s| s.id).collect::<Vec<_>>());
|
||||
let gateway_site = any_site(sites.clone()).prop_map(|s| s.id);
|
||||
let cidr_resources = collection::hash_set(
|
||||
cidr_resource_outside_reserved_ranges(any_site(sites.clone())),
|
||||
1..5,
|
||||
);
|
||||
let dns_resources = collection::hash_set(
|
||||
prop_oneof![
|
||||
non_wildcard_dns_resource(any_site(sites.clone())),
|
||||
star_wildcard_dns_resource(any_site(sites.clone())),
|
||||
question_mark_wildcard_dns_resource(any_site(sites)),
|
||||
],
|
||||
1..5,
|
||||
);
|
||||
|
||||
let gateways =
|
||||
collection::hash_map(gateway_id(), (ref_gateway_host(), gateway_site), 1..=3);
|
||||
let gateway_selector = any::<sample::Selector>();
|
||||
|
||||
(gateways, Just(sites), gateway_selector)
|
||||
(gateways, cidr_resources, dns_resources, gateway_selector)
|
||||
})
|
||||
.prop_map(|(gateways, sites, gateway_selector)| {
|
||||
let (gateways, gateways_by_site) = gateways.into_iter().fold(
|
||||
(
|
||||
HashMap::<GatewayId, _>::default(),
|
||||
HashMap::<SiteId, HashSet<GatewayId>>::default(),
|
||||
),
|
||||
|(mut gateways, mut sites), (gid, (gateway, site))| {
|
||||
sites.entry(site).or_default().insert(gid);
|
||||
gateways.insert(gid, gateway);
|
||||
.prop_flat_map(
|
||||
|(gateways, cidr_resources, dns_resources, gateway_selector)| {
|
||||
let (gateways, gateways_by_site) = gateways.into_iter().fold(
|
||||
(
|
||||
HashMap::<GatewayId, _>::default(),
|
||||
HashMap::<SiteId, HashSet<GatewayId>>::default(),
|
||||
),
|
||||
|(mut gateways, mut sites), (gid, (gateway, site))| {
|
||||
sites.entry(site).or_default().insert(gid);
|
||||
gateways.insert(gid, gateway);
|
||||
|
||||
(gateways, sites)
|
||||
},
|
||||
);
|
||||
let portal = StubPortal::new(gateways_by_site, sites, gateway_selector);
|
||||
(gateways, sites)
|
||||
},
|
||||
);
|
||||
|
||||
(gateways, portal)
|
||||
})
|
||||
// For each DNS resource, we need to generate a set of DNS records.
|
||||
let dns_resource_records = dns_resources
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|resource| {
|
||||
let address = resource.address;
|
||||
|
||||
match address.chars().next().unwrap() {
|
||||
'*' => subdomain_records(
|
||||
address.trim_start_matches("*.").to_owned(),
|
||||
domain_name(1..3),
|
||||
)
|
||||
.boxed(),
|
||||
'?' => subdomain_records(
|
||||
address.trim_start_matches("?.").to_owned(),
|
||||
domain_label(),
|
||||
)
|
||||
.boxed(),
|
||||
_ => resolved_ips()
|
||||
.prop_map(move |resolved_ips| {
|
||||
HashMap::from([(address.parse().unwrap(), resolved_ips)])
|
||||
})
|
||||
.boxed(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.prop_map(|records| {
|
||||
let mut map = HashMap::default();
|
||||
|
||||
for record in records {
|
||||
map.extend(record)
|
||||
}
|
||||
|
||||
map
|
||||
});
|
||||
|
||||
let portal = StubPortal::new(
|
||||
gateways_by_site,
|
||||
gateway_selector,
|
||||
cidr_resources,
|
||||
dns_resources,
|
||||
);
|
||||
|
||||
(Just(gateways), Just(portal), dns_resource_records)
|
||||
},
|
||||
)
|
||||
.prop_filter(
|
||||
"gateway must be able to access their assigned CIDR resources",
|
||||
|(gateways, portal, _)| {
|
||||
portal.cidr_resources().all(|(rid, r)| {
|
||||
portal
|
||||
.gateway_for_resource(*rid)
|
||||
.and_then(|g| gateways.get(g))
|
||||
.is_some_and(|g| {
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS!
|
||||
|
||||
g.can_route_to(r.address)
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn any_site(sites: HashSet<Site>) -> impl Strategy<Value = Site> {
|
||||
sample::select(Vec::from_iter(sites))
|
||||
}
|
||||
|
||||
fn cidr_resource_outside_reserved_ranges(
|
||||
sites: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = ResourceDescriptionCidr> {
|
||||
cidr_resource(any_ip_network(8), sites.prop_map(|s| vec![s]))
|
||||
.prop_filter(
|
||||
"tests doesn't support yet CIDR resources overlapping DNS resources",
|
||||
|r| {
|
||||
// This works because CIDR resources' host mask is always <8 while IP resource is 21
|
||||
let is_ip4_reserved = IpNetwork::from_str(IPV4_RESOURCES)
|
||||
.unwrap()
|
||||
.contains(r.address.network_address());
|
||||
let is_ip6_reserved = IpNetwork::from_str(IPV6_RESOURCES)
|
||||
.unwrap()
|
||||
.contains(r.address.network_address());
|
||||
|
||||
!is_ip4_reserved && !is_ip6_reserved
|
||||
},
|
||||
)
|
||||
.prop_filter("resource must not be in the documentation range because we use those for host addresses and DNS IPs", |r| !r.address.is_documentation())
|
||||
}
|
||||
|
||||
fn non_wildcard_dns_resource(
|
||||
site: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = ResourceDescriptionDns> {
|
||||
dns_resource(site.prop_map(|s| vec![s]))
|
||||
}
|
||||
|
||||
fn star_wildcard_dns_resource(
|
||||
site: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = ResourceDescriptionDns> {
|
||||
dns_resource(site.prop_map(|s| vec![s])).prop_map(|r| ResourceDescriptionDns {
|
||||
address: format!("*.{}", r.address),
|
||||
..r
|
||||
})
|
||||
}
|
||||
|
||||
fn question_mark_wildcard_dns_resource(
|
||||
site: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = ResourceDescriptionDns> {
|
||||
dns_resource(site.prop_map(|s| vec![s])).prop_map(|r| ResourceDescriptionDns {
|
||||
address: format!("?.{}", r.address),
|
||||
..r
|
||||
})
|
||||
}
|
||||
|
||||
fn resolved_ips() -> impl Strategy<Value = HashSet<IpAddr>> {
|
||||
collection::hash_set(
|
||||
prop_oneof![
|
||||
dns_resource_ip4s().prop_map_into(),
|
||||
dns_resource_ip6s().prop_map_into()
|
||||
],
|
||||
1..6,
|
||||
)
|
||||
}
|
||||
|
||||
/// A strategy for generating a set of DNS records all nested under the provided base domain.
|
||||
fn subdomain_records(
|
||||
base: String,
|
||||
subdomains: impl Strategy<Value = String>,
|
||||
) -> impl Strategy<Value = HashMap<DomainName, HashSet<IpAddr>>> {
|
||||
collection::hash_map(subdomains, resolved_ips(), 1..4).prop_map(move |subdomain_ips| {
|
||||
subdomain_ips
|
||||
.into_iter()
|
||||
.map(|(label, ips)| {
|
||||
let domain = format!("{label}.{base}");
|
||||
|
||||
(domain.parse().unwrap(), ips)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// A [`Strategy`] of [`Ipv4Addr`]s used for the "real" IPs of DNS resources.
|
||||
///
|
||||
/// This uses the `TEST-NET-2` (`198.51.100.0/24`) address space reserved for documentation and examples in [RFC5737](https://datatracker.ietf.org/doc/html/rfc5737).
|
||||
/// `TEST-NET-2` only contains 256 addresses which is small enough to generate overlapping IPs for our DNS resources (i.e. two different domains pointing to the same IP).
|
||||
fn dns_resource_ip4s() -> impl Strategy<Value = Ipv4Addr> {
|
||||
let ips = Ipv4Network::new(Ipv4Addr::new(198, 51, 100, 0), 24)
|
||||
.unwrap()
|
||||
.hosts()
|
||||
.collect_vec();
|
||||
|
||||
sample::select(ips)
|
||||
}
|
||||
|
||||
/// A [`Strategy`] of [`Ipv6Addr`]s used for the "real" IPs of DNS resources.
|
||||
///
|
||||
/// This uses a subnet of the `2001:DB8::/32` address space reserved for documentation and examples in [RFC3849](https://datatracker.ietf.org/doc/html/rfc3849).
|
||||
fn dns_resource_ip6s() -> impl Strategy<Value = Ipv6Addr> {
|
||||
const DNS_SUBNET: u16 = 0x2020;
|
||||
|
||||
documentation_ip6s(DNS_SUBNET, 256)
|
||||
}
|
||||
|
||||
pub(crate) fn documentation_ip6s(subnet: u16, num_ips: usize) -> impl Strategy<Value = Ipv6Addr> {
|
||||
let ips = Ipv6Network::new_truncate(
|
||||
Ipv6Addr::new(0x2001, 0xDB80, subnet, subnet, 0, 0, 0, 0),
|
||||
32,
|
||||
)
|
||||
.unwrap()
|
||||
.subnets_with_prefix(128)
|
||||
.map(|n| n.network_address())
|
||||
.take(num_ips)
|
||||
.collect_vec();
|
||||
|
||||
sample::select(ips)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use std::{
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StubPortal {
|
||||
gateways_by_site: HashMap<client::SiteId, HashSet<GatewayId>>,
|
||||
sites: HashMap<client::SiteId, client::Site>,
|
||||
|
||||
sites_by_resource: HashMap<ResourceId, client::SiteId>,
|
||||
cidr_resources: HashMap<ResourceId, client::ResourceDescriptionCidr>,
|
||||
@@ -22,45 +21,67 @@ pub(crate) struct StubPortal {
|
||||
impl StubPortal {
|
||||
pub(crate) fn new(
|
||||
gateways_by_site: HashMap<client::SiteId, HashSet<GatewayId>>,
|
||||
sites: HashSet<client::Site>,
|
||||
gateway_selector: Selector,
|
||||
cidr_resources: HashSet<client::ResourceDescriptionCidr>,
|
||||
dns_resources: HashSet<client::ResourceDescriptionDns>,
|
||||
) -> Self {
|
||||
let cidr_resources = cidr_resources
|
||||
.into_iter()
|
||||
.map(|r| (r.id, r))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let dns_resources = dns_resources
|
||||
.into_iter()
|
||||
.map(|r| (r.id, r))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let cidr_sites = cidr_resources.iter().map(|(id, r)| {
|
||||
(
|
||||
*id,
|
||||
r.sites
|
||||
.iter()
|
||||
.exactly_one()
|
||||
.expect("only single-site resources")
|
||||
.id,
|
||||
)
|
||||
});
|
||||
let dns_sites = dns_resources.iter().map(|(id, r)| {
|
||||
(
|
||||
*id,
|
||||
r.sites
|
||||
.iter()
|
||||
.exactly_one()
|
||||
.expect("only single-site resources")
|
||||
.id,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
gateways_by_site,
|
||||
sites: sites.into_iter().map(|s| (s.id, s)).collect(),
|
||||
gateway_selector,
|
||||
sites_by_resource: Default::default(),
|
||||
cidr_resources: Default::default(),
|
||||
dns_resources: Default::default(),
|
||||
sites_by_resource: HashMap::from_iter(cidr_sites.chain(dns_sites)),
|
||||
cidr_resources,
|
||||
dns_resources,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn all_sites(&self) -> Vec<client::Site> {
|
||||
self.sites.values().cloned().collect()
|
||||
pub(crate) fn all_resources(&self) -> Vec<client::ResourceDescription> {
|
||||
self.cidr_resources
|
||||
.values()
|
||||
.cloned()
|
||||
.map(client::ResourceDescription::Cidr)
|
||||
.chain(
|
||||
self.dns_resources
|
||||
.values()
|
||||
.cloned()
|
||||
.map(client::ResourceDescription::Dns),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn add_resource(&mut self, resource: client::ResourceDescription) {
|
||||
let site = resource
|
||||
.sites()
|
||||
.into_iter()
|
||||
.exactly_one()
|
||||
.expect("only single-site resources are supported");
|
||||
|
||||
self.sites_by_resource.insert(resource.id(), site.id);
|
||||
|
||||
match resource {
|
||||
client::ResourceDescription::Dns(dns) => {
|
||||
self.dns_resources.insert(dns.id, dns);
|
||||
}
|
||||
client::ResourceDescription::Cidr(cidr) => {
|
||||
self.cidr_resources.insert(cidr.id, cidr);
|
||||
}
|
||||
client::ResourceDescription::Internet(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_resource(&mut self, resource: ResourceId) {
|
||||
self.sites_by_resource.remove(&resource);
|
||||
pub(crate) fn cidr_resources(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&ResourceId, &client::ResourceDescriptionCidr)> + '_ {
|
||||
self.cidr_resources.iter()
|
||||
}
|
||||
|
||||
/// Picks, which gateway and site we should connect to for the given resource.
|
||||
|
||||
@@ -4,14 +4,15 @@ use super::sim_gateway::SimGateway;
|
||||
use super::sim_net::{Host, HostId, RoutingTable};
|
||||
use super::sim_relay::SimRelay;
|
||||
use super::stub_portal::StubPortal;
|
||||
use crate::dns::is_subdomain;
|
||||
use crate::tests::assertions::*;
|
||||
use crate::tests::sim_relay::map_explode;
|
||||
use crate::tests::transition::Transition;
|
||||
use crate::{dns::DnsQuery, ClientEvent, GatewayEvent, Request};
|
||||
use chrono::{DateTime, Utc};
|
||||
use connlib_shared::messages::{Interface, RelayId};
|
||||
use connlib_shared::messages::client::ResourceDescription;
|
||||
use connlib_shared::{
|
||||
messages::{client::ResourceDescription, ClientId, GatewayId},
|
||||
messages::{ClientId, GatewayId, Interface, RelayId},
|
||||
DomainName,
|
||||
};
|
||||
use firezone_relay::IpStack;
|
||||
@@ -153,15 +154,29 @@ impl StateMachineTest for TunnelTest {
|
||||
|
||||
// Act: Apply the transition
|
||||
match transition {
|
||||
Transition::AddCidrResource { resource } => {
|
||||
state
|
||||
.client
|
||||
.exec_mut(|c| c.sut.add_resource(ResourceDescription::Cidr(resource)));
|
||||
Transition::ActivateResource(resource) => {
|
||||
state.client.exec_mut(|c| {
|
||||
// Flush DNS.
|
||||
match &resource {
|
||||
ResourceDescription::Dns(r) => {
|
||||
c.dns_records.retain(|domain, _| {
|
||||
if is_subdomain(domain, &r.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
}
|
||||
ResourceDescription::Cidr(_) => {}
|
||||
ResourceDescription::Internet(_) => {}
|
||||
}
|
||||
|
||||
c.sut.add_resource(resource);
|
||||
});
|
||||
}
|
||||
Transition::DeactivateResource(id) => {
|
||||
state.client.exec_mut(|c| c.sut.remove_resource(id))
|
||||
}
|
||||
Transition::AddDnsResource { resource, .. } => state
|
||||
.client
|
||||
.exec_mut(|c| c.sut.add_resource(ResourceDescription::Dns(resource))),
|
||||
Transition::RemoveResource(id) => state.client.exec_mut(|c| c.sut.remove_resource(id)),
|
||||
Transition::SendICMPPacketToNonResourceIp {
|
||||
src,
|
||||
dst,
|
||||
@@ -173,6 +188,7 @@ impl StateMachineTest for TunnelTest {
|
||||
dst,
|
||||
seq,
|
||||
identifier,
|
||||
..
|
||||
} => {
|
||||
let packet = ip_packet::make::icmp_request_packet(src, dst, seq, identifier);
|
||||
|
||||
@@ -188,6 +204,7 @@ impl StateMachineTest for TunnelTest {
|
||||
seq,
|
||||
identifier,
|
||||
resolved_ip,
|
||||
..
|
||||
} => {
|
||||
let available_ips = state
|
||||
.client
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
use crate::client::{IPV4_RESOURCES, IPV6_RESOURCES};
|
||||
|
||||
use super::{
|
||||
sim_net::{any_ip_stack, any_port},
|
||||
strategies::*,
|
||||
};
|
||||
use super::sim_net::{any_ip_stack, any_port};
|
||||
use connlib_shared::{
|
||||
messages::{
|
||||
client::{ResourceDescriptionCidr, ResourceDescriptionDns, Site},
|
||||
DnsServer, ResourceId,
|
||||
},
|
||||
proptest::*,
|
||||
messages::{client::ResourceDescription, DnsServer, ResourceId},
|
||||
DomainName,
|
||||
};
|
||||
use hickory_proto::rr::RecordType;
|
||||
use ip_network::IpNetwork;
|
||||
use proptest::{prelude::*, sample};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
/// The possible transitions of the state machine.
|
||||
#[derive(Clone, derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Transition {
|
||||
/// Add a new CIDR resource to the client.
|
||||
AddCidrResource { resource: ResourceDescriptionCidr },
|
||||
/// Activate a resource on the client.
|
||||
ActivateResource(ResourceDescription),
|
||||
/// Deactivate a resource on the client.
|
||||
DeactivateResource(ResourceId),
|
||||
/// Send an ICMP packet to non-resource IP.
|
||||
SendICMPPacketToNonResourceIp {
|
||||
src: IpAddr,
|
||||
@@ -53,12 +41,6 @@ pub(crate) enum Transition {
|
||||
identifier: u16,
|
||||
},
|
||||
|
||||
/// Add a new DNS resource to the client.
|
||||
AddDnsResource {
|
||||
resource: ResourceDescriptionDns,
|
||||
/// The DNS records to add together with the resource.
|
||||
records: HashMap<DomainName, HashSet<IpAddr>>,
|
||||
},
|
||||
/// Send a DNS query.
|
||||
SendDnsQuery {
|
||||
domain: DomainName,
|
||||
@@ -74,9 +56,6 @@ pub(crate) enum Transition {
|
||||
/// The upstream DNS servers changed.
|
||||
UpdateUpstreamDnsServers { servers: Vec<DnsServer> },
|
||||
|
||||
/// Remove a resource from the client.
|
||||
RemoveResource(ResourceId),
|
||||
|
||||
/// Roam the client to a new pair of sockets.
|
||||
RoamClient {
|
||||
ip4: Option<Ipv4Addr>,
|
||||
@@ -168,7 +147,7 @@ where
|
||||
{
|
||||
(
|
||||
domain,
|
||||
dns_server.prop_map(Into::into),
|
||||
dns_server.prop_map_into(),
|
||||
prop_oneof![Just(RecordType::A), Just(RecordType::AAAA)],
|
||||
any::<u16>(),
|
||||
)
|
||||
@@ -182,71 +161,6 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
// Adds a non-overlapping CIDR resource with the gateway
|
||||
pub(crate) fn add_cidr_resource(
|
||||
sites: impl Strategy<Value = Vec<Site>>,
|
||||
) -> impl Strategy<Value = Transition> {
|
||||
cidr_resource(any_ip_network(8), sites)
|
||||
.prop_filter(
|
||||
"tests doesn't support yet CIDR resources overlapping DNS resources",
|
||||
|r| {
|
||||
// This works because CIDR resourc's host mask is always <8 while IP resource is 21
|
||||
!IpNetwork::from_str(IPV4_RESOURCES)
|
||||
.unwrap()
|
||||
.contains(r.address.network_address())
|
||||
&& !IpNetwork::from_str(IPV6_RESOURCES)
|
||||
.unwrap()
|
||||
.contains(r.address.network_address())
|
||||
},
|
||||
)
|
||||
.prop_map(|resource| Transition::AddCidrResource { resource })
|
||||
}
|
||||
|
||||
pub(crate) fn non_wildcard_dns_resource(
|
||||
site: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = Transition> {
|
||||
(dns_resource(site.prop_map(|s| vec![s])), resolved_ips()).prop_map(
|
||||
|(resource, resolved_ips)| Transition::AddDnsResource {
|
||||
records: HashMap::from([(resource.address.parse().unwrap(), resolved_ips)]),
|
||||
resource,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn star_wildcard_dns_resource(
|
||||
site: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = Transition> {
|
||||
dns_resource(site.prop_map(|s| vec![s])).prop_flat_map(move |r| {
|
||||
let wildcard_address = format!("*.{}", r.address);
|
||||
|
||||
let records = subdomain_records(r.address, domain_name(1..3));
|
||||
let resource = Just(ResourceDescriptionDns {
|
||||
address: wildcard_address,
|
||||
..r
|
||||
});
|
||||
|
||||
(resource, records)
|
||||
.prop_map(|(resource, records)| Transition::AddDnsResource { records, resource })
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn question_mark_wildcard_dns_resource(
|
||||
site: impl Strategy<Value = Site>,
|
||||
) -> impl Strategy<Value = Transition> {
|
||||
dns_resource(site.prop_map(|s| vec![s])).prop_flat_map(move |r| {
|
||||
let wildcard_address = format!("?.{}", r.address);
|
||||
|
||||
let records = subdomain_records(r.address, domain_label());
|
||||
let resource = Just(ResourceDescriptionDns {
|
||||
address: wildcard_address,
|
||||
..r
|
||||
});
|
||||
|
||||
(resource, records)
|
||||
.prop_map(|(resource, records)| Transition::AddDnsResource { records, resource })
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn roam_client() -> impl Strategy<Value = Transition> {
|
||||
(any_ip_stack(), any_port()).prop_map(move |(ip_stack, port)| Transition::RoamClient {
|
||||
ip4: ip_stack.as_v4().copied(),
|
||||
|
||||
Reference in New Issue
Block a user