mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
feat(connlib): report resource status to client (#4931)
This PR introduces site's `Status`. That's used to report to the client the status, either, unknown, online or offline, mostly as a hint to users as what's wrong with a connection. This are the criteria for an online or offline resource * If all sites related to a resource are offline the resource is considered offline, since there's no gateway that can respond to that resource's connection * If any site is online the resource is online, since that same peer can be used to reach that resource * Any other case is unknown Right now resources are single site so it doesn't matter too much but tracking online/offline per-site instead of per-gateway or resource seems like the better long-term solution. The way to "find out" the site's status is: * If a response to a connection details is offline, all sites related to that resource must be offline otherwise there would've been a gateway in the response * At the point we connect to a gateway, the site that corresponds to that gateway must be online * When a connection to a peer stops it's considered unknown again Fixes #4738
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
// ecosystem, so it's used here for consistency.
|
||||
|
||||
use connlib_client_shared::{
|
||||
file_logger, keypair, Callbacks, Cidrv4, Cidrv6, Error, LoginUrl, LoginUrlError,
|
||||
ResourceDescription, Session, Sockets,
|
||||
callbacks::ResourceDescription, file_logger, keypair, Callbacks, Cidrv4, Cidrv6, Error,
|
||||
LoginUrl, LoginUrlError, Session, Sockets,
|
||||
};
|
||||
use jni::{
|
||||
objects::{GlobalRef, JClass, JObject, JString, JValue},
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#![allow(clippy::unnecessary_cast, improper_ctypes, non_camel_case_types)]
|
||||
|
||||
use connlib_client_shared::{
|
||||
file_logger, keypair, Callbacks, Cidrv4, Cidrv6, Error, LoginUrl, ResourceDescription, Session,
|
||||
Sockets,
|
||||
callbacks::ResourceDescription, file_logger, keypair, Callbacks, Cidrv4, Cidrv6, Error,
|
||||
LoginUrl, Session, Sockets,
|
||||
};
|
||||
use secrecy::SecretString;
|
||||
use std::{
|
||||
|
||||
@@ -269,6 +269,7 @@ where
|
||||
gateway_id,
|
||||
resource_id,
|
||||
relays,
|
||||
site_id,
|
||||
..
|
||||
}) => {
|
||||
let should_accept = self
|
||||
@@ -280,10 +281,12 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
match self
|
||||
.tunnel
|
||||
.create_or_reuse_connection(resource_id, gateway_id, relays)
|
||||
{
|
||||
match self.tunnel.create_or_reuse_connection(
|
||||
resource_id,
|
||||
gateway_id,
|
||||
relays,
|
||||
site_id,
|
||||
) {
|
||||
Ok(firezone_tunnel::Request::NewConnection(connection_request)) => {
|
||||
// TODO: keep track for the response
|
||||
let _id = self.portal.send(
|
||||
@@ -321,7 +324,7 @@ where
|
||||
|
||||
tracing::debug!(resource_id = %offline_resource, "Resource is offline");
|
||||
|
||||
self.tunnel.cleanup_connection(offline_resource);
|
||||
self.tunnel.set_resource_offline(offline_resource);
|
||||
}
|
||||
|
||||
ErrorReply::Disabled => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Main connlib library for clients.
|
||||
pub use connlib_shared::messages::client::ResourceDescription;
|
||||
pub use connlib_shared::{
|
||||
keypair, Callbacks, Cidrv4, Cidrv6, Error, LoginUrl, LoginUrlError, StaticSecret,
|
||||
callbacks, keypair, Callbacks, Cidrv4, Cidrv6, Error, LoginUrl, LoginUrlError, StaticSecret,
|
||||
};
|
||||
pub use firezone_tunnel::Sockets;
|
||||
pub use tracing_appender::non_blocking::WorkerGuard;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use connlib_shared::messages::{
|
||||
client::ResourceDescription, GatewayId, GatewayResponse, Interface, Key, Relay, RelaysPresence,
|
||||
RequestConnection, ResourceId, ReuseConnection,
|
||||
client::{ResourceDescription, SiteId},
|
||||
GatewayId, GatewayResponse, Interface, Key, Relay, RelaysPresence, RequestConnection,
|
||||
ResourceId, ReuseConnection,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, net::IpAddr};
|
||||
@@ -25,6 +26,8 @@ pub struct ConnectionDetails {
|
||||
pub resource_id: ResourceId,
|
||||
pub gateway_id: GatewayId,
|
||||
pub gateway_remote_ip: IpAddr,
|
||||
#[serde(rename = "gateway_group_id")]
|
||||
pub site_id: SiteId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq)]
|
||||
@@ -101,8 +104,7 @@ mod test {
|
||||
use super::*;
|
||||
use chrono::DateTime;
|
||||
use connlib_shared::messages::{
|
||||
client::ResourceDescriptionCidr,
|
||||
client::{GatewayGroup, ResourceDescriptionDns},
|
||||
client::{ResourceDescriptionCidr, ResourceDescriptionDns, Site},
|
||||
DnsServer, IpDnsServer, Stun, Turn,
|
||||
};
|
||||
use phoenix_channel::{OutboundRequestId, PhoenixMessage};
|
||||
@@ -234,7 +236,7 @@ mod test {
|
||||
address: "172.172.0.0/16".parse().unwrap(),
|
||||
name: "172.172.0.0/16".to_string(),
|
||||
address_description: "cidr resource".to_string(),
|
||||
gateway_groups: vec![GatewayGroup {
|
||||
sites: vec![Site {
|
||||
name: "test".to_string(),
|
||||
id: "bf56f32d-7b2c-4f5d-a784-788977d014a4".parse().unwrap(),
|
||||
}],
|
||||
@@ -244,7 +246,7 @@ mod test {
|
||||
address: "gitlab.mycorp.com".to_string(),
|
||||
name: "gitlab.mycorp.com".to_string(),
|
||||
address_description: "dns resource".to_string(),
|
||||
gateway_groups: vec![GatewayGroup {
|
||||
sites: vec![Site {
|
||||
name: "test".to_string(),
|
||||
id: "bf56f32d-7b2c-4f5d-a784-788977d014a4".parse().unwrap(),
|
||||
}],
|
||||
@@ -307,7 +309,7 @@ mod test {
|
||||
address: "172.172.0.0/16".parse().unwrap(),
|
||||
name: "172.172.0.0/16".to_string(),
|
||||
address_description: "cidr resource".to_string(),
|
||||
gateway_groups: vec![GatewayGroup {
|
||||
sites: vec![Site {
|
||||
name: "test".to_string(),
|
||||
id: "bf56f32d-7b2c-4f5d-a784-788977d014a4".parse().unwrap(),
|
||||
}],
|
||||
@@ -317,7 +319,7 @@ mod test {
|
||||
address: "gitlab.mycorp.com".to_string(),
|
||||
name: "gitlab.mycorp.com".to_string(),
|
||||
address_description: "dns resource".to_string(),
|
||||
gateway_groups: vec![GatewayGroup {
|
||||
sites: vec![Site {
|
||||
name: "test".to_string(),
|
||||
id: "bf56f32d-7b2c-4f5d-a784-788977d014a4".parse().unwrap(),
|
||||
}],
|
||||
@@ -536,6 +538,7 @@ mod test {
|
||||
gateway_id: "73037362-715d-4a83-a749-f18eadd970e6".parse().unwrap(),
|
||||
gateway_remote_ip: "172.28.0.1".parse().unwrap(),
|
||||
resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(),
|
||||
site_id: "bf56f32d-7b2c-4f5d-a784-788977d014a4".parse().unwrap(),
|
||||
relays: vec![
|
||||
Relay::Stun(Stun {
|
||||
id: "c9cb8892-e355-41e6-a882-b6d6c38beb66".parse().unwrap(),
|
||||
@@ -573,6 +576,7 @@ mod test {
|
||||
"resource_id": "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3",
|
||||
"gateway_id": "73037362-715d-4a83-a749-f18eadd970e6",
|
||||
"gateway_remote_ip": "172.28.0.1",
|
||||
"gateway_group_id": "bf56f32d-7b2c-4f5d-a784-788977d014a4",
|
||||
"relays": [
|
||||
{
|
||||
"id": "c9cb8892-e355-41e6-a882-b6d6c38beb66",
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
mock = []
|
||||
proptest = ["dep:proptest", "dep:itertools"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
@@ -34,13 +35,14 @@ libc = "0.2"
|
||||
snownet = { workspace = true }
|
||||
phoenix-channel = { workspace = true }
|
||||
proptest = { version = "1.4.0", optional = true }
|
||||
itertools = { version = "0.12", optional = true }
|
||||
|
||||
# Needed for Android logging until tracing is working
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
itertools = "0.12"
|
||||
tempfile = "3.10.1"
|
||||
itertools = "0.12"
|
||||
mutants = "0.0.3" # Needed to mark functions as exempt from `cargo-mutants` testing
|
||||
tokio = { version = "1.36", features = ["macros", "rt"] }
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::messages::client::ResourceDescription;
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use serde::Serialize;
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use crate::messages::client::Site;
|
||||
use crate::messages::ResourceId;
|
||||
|
||||
// Avoids having to map types for Windows
|
||||
type RawFd = i32;
|
||||
|
||||
@@ -39,6 +42,123 @@ impl From<Ipv6Network> for Cidrv6 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Status {
|
||||
Unknown,
|
||||
Online,
|
||||
Offline,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ResourceDescription {
|
||||
Dns(ResourceDescriptionDns),
|
||||
Cidr(ResourceDescriptionCidr),
|
||||
}
|
||||
|
||||
impl ResourceDescription {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => &r.name,
|
||||
ResourceDescription::Cidr(r) => &r.name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => r.status,
|
||||
ResourceDescription::Cidr(r) => r.status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ResourceId {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => r.id,
|
||||
ResourceDescription::Cidr(r) => r.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the GUI clients should paste to the clipboard, e.g. `https://github.com/firezone`
|
||||
pub fn pastable(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => Cow::from(&r.address),
|
||||
ResourceDescription::Cidr(r) => Cow::from(r.address.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResourceDescription> for crate::messages::client::ResourceDescription {
|
||||
fn from(value: ResourceDescription) -> Self {
|
||||
match value {
|
||||
ResourceDescription::Dns(r) => {
|
||||
crate::messages::client::ResourceDescription::Dns(r.into())
|
||||
}
|
||||
ResourceDescription::Cidr(r) => {
|
||||
crate::messages::client::ResourceDescription::Cidr(r.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ResourceDescriptionDns {
|
||||
/// Resource's id.
|
||||
pub id: ResourceId,
|
||||
/// Internal resource's domain name.
|
||||
pub address: String,
|
||||
/// Name of the resource.
|
||||
///
|
||||
/// Used only for display.
|
||||
pub name: String,
|
||||
|
||||
pub address_description: String,
|
||||
pub sites: Vec<Site>,
|
||||
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
impl From<ResourceDescriptionDns> for crate::messages::client::ResourceDescriptionDns {
|
||||
fn from(r: ResourceDescriptionDns) -> Self {
|
||||
crate::messages::client::ResourceDescriptionDns {
|
||||
id: r.id,
|
||||
address: r.address,
|
||||
address_description: r.address_description,
|
||||
name: r.name,
|
||||
sites: r.sites,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of a resource that maps to a CIDR.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ResourceDescriptionCidr {
|
||||
/// Resource's id.
|
||||
pub id: ResourceId,
|
||||
/// CIDR that this resource points to.
|
||||
pub address: IpNetwork,
|
||||
/// Name of the resource.
|
||||
///
|
||||
/// Used only for display.
|
||||
pub name: String,
|
||||
|
||||
pub address_description: String,
|
||||
pub sites: Vec<Site>,
|
||||
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
impl From<ResourceDescriptionCidr> for crate::messages::client::ResourceDescriptionCidr {
|
||||
fn from(r: ResourceDescriptionCidr) -> Self {
|
||||
crate::messages::client::ResourceDescriptionCidr {
|
||||
id: r.id,
|
||||
address: r.address,
|
||||
address_description: r.address_description,
|
||||
name: r.name,
|
||||
sites: r.sites,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Traits that will be used by connlib to callback the client upper layers.
|
||||
pub trait Callbacks: Clone + Send + Sync {
|
||||
/// Called when the tunnel address is set.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! This includes types provided by external crates, i.e. [boringtun] to make sure that
|
||||
//! we are using the same version across our own crates.
|
||||
|
||||
mod callbacks;
|
||||
pub mod callbacks;
|
||||
pub mod error;
|
||||
pub mod messages;
|
||||
|
||||
|
||||
@@ -50,6 +50,13 @@ impl ClientId {
|
||||
}
|
||||
}
|
||||
|
||||
impl GatewayId {
|
||||
#[cfg(feature = "proptest")]
|
||||
pub(crate) fn from_u128(v: u128) -> Self {
|
||||
Self(Uuid::from_u128(v))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ClientId(Uuid);
|
||||
|
||||
@@ -315,7 +322,7 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
client::ResourceDescription,
|
||||
client::{GatewayGroup, ResourceDescriptionDns},
|
||||
client::{ResourceDescriptionDns, Site},
|
||||
ResourceId,
|
||||
};
|
||||
|
||||
@@ -325,7 +332,7 @@ mod tests {
|
||||
name: name.to_string(),
|
||||
address: "unused.example.com".to_string(),
|
||||
address_description: "test description".to_string(),
|
||||
gateway_groups: vec![GatewayGroup {
|
||||
sites: vec![Site {
|
||||
name: "test".to_string(),
|
||||
id: "99ba0c1e-5189-4cfc-a4db-fd6cb1c937fd".parse().unwrap(),
|
||||
}],
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! Client related messages that are needed within connlib
|
||||
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use ip_network::IpNetwork;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::callbacks::Status;
|
||||
|
||||
use super::ResourceId;
|
||||
|
||||
/// Description of a resource that maps to a DNS record.
|
||||
@@ -21,7 +23,21 @@ pub struct ResourceDescriptionDns {
|
||||
pub name: String,
|
||||
|
||||
pub address_description: String,
|
||||
pub gateway_groups: Vec<GatewayGroup>,
|
||||
#[serde(rename = "gateway_groups")]
|
||||
pub sites: Vec<Site>,
|
||||
}
|
||||
|
||||
impl ResourceDescriptionDns {
|
||||
fn with_status(self, status: Status) -> crate::callbacks::ResourceDescriptionDns {
|
||||
crate::callbacks::ResourceDescriptionDns {
|
||||
id: self.id,
|
||||
address: self.address,
|
||||
name: self.name,
|
||||
address_description: self.address_description,
|
||||
sites: self.sites,
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of a resource that maps to a CIDR.
|
||||
@@ -37,16 +53,30 @@ pub struct ResourceDescriptionCidr {
|
||||
pub name: String,
|
||||
|
||||
pub address_description: String,
|
||||
pub gateway_groups: Vec<GatewayGroup>,
|
||||
#[serde(rename = "gateway_groups")]
|
||||
pub sites: Vec<Site>,
|
||||
}
|
||||
|
||||
impl ResourceDescriptionCidr {
|
||||
fn with_status(self, status: Status) -> crate::callbacks::ResourceDescriptionCidr {
|
||||
crate::callbacks::ResourceDescriptionCidr {
|
||||
id: self.id,
|
||||
address: self.address,
|
||||
name: self.name,
|
||||
address_description: self.address_description,
|
||||
sites: self.sites,
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct GatewayGroup {
|
||||
pub struct Site {
|
||||
pub name: String,
|
||||
pub id: SiteId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct SiteId(Uuid);
|
||||
|
||||
impl FromStr for SiteId {
|
||||
@@ -79,6 +109,13 @@ impl ResourceDescription {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sites(&self) -> HashSet<&Site> {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => HashSet::from_iter(r.sites.iter()),
|
||||
ResourceDescription::Cidr(r) => HashSet::from_iter(r.sites.iter()),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the GUI clients should show as the user-friendly display name, e.g. `Firezone GitHub`
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
@@ -87,14 +124,6 @@ impl ResourceDescription {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the GUI clients should paste to the clipboard, e.g. `https://github.com/firezone`
|
||||
pub fn pastable(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => Cow::from(&r.address),
|
||||
ResourceDescription::Cidr(r) => Cow::from(r.address.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_different_address(&self, other: &ResourceDescription) -> bool {
|
||||
match (self, other) {
|
||||
(ResourceDescription::Dns(dns_a), ResourceDescription::Dns(dns_b)) => {
|
||||
@@ -106,6 +135,17 @@ impl ResourceDescription {
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(self, status: Status) -> crate::callbacks::ResourceDescription {
|
||||
match self {
|
||||
ResourceDescription::Dns(r) => {
|
||||
crate::callbacks::ResourceDescription::Dns(r.with_status(status))
|
||||
}
|
||||
ResourceDescription::Cidr(r) => {
|
||||
crate::callbacks::ResourceDescription::Cidr(r.with_status(status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ResourceDescription {
|
||||
|
||||
@@ -1,64 +1,110 @@
|
||||
use crate::messages::{
|
||||
client::ResourceDescriptionCidr,
|
||||
client::{GatewayGroup, ResourceDescriptionDns, SiteId},
|
||||
ClientId, ResourceId,
|
||||
client::{ResourceDescription, ResourceDescriptionDns, Site, SiteId},
|
||||
ClientId, GatewayId, ResourceId,
|
||||
};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use itertools::Itertools;
|
||||
use proptest::{
|
||||
arbitrary::{any, any_with},
|
||||
collection, sample,
|
||||
strategy::Strategy,
|
||||
strategy::{Just, Strategy},
|
||||
};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub fn dns_resource() -> impl Strategy<Value = ResourceDescriptionDns> {
|
||||
// Generate resources sharing 1 site
|
||||
pub fn resources_sharing_site() -> impl Strategy<Value = (Vec<ResourceDescription>, Site)> {
|
||||
(collection::vec(sites(), 1..=100), site()).prop_flat_map(|(sites, site)| {
|
||||
(
|
||||
sites
|
||||
.iter()
|
||||
.map(|sites| {
|
||||
let mut sites = sites.clone();
|
||||
sites.push(site.clone());
|
||||
resource(sites.clone())
|
||||
})
|
||||
.collect_vec(),
|
||||
Just(site),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Generate resources sharing all sites
|
||||
pub fn resources_sharing_all_sites() -> impl Strategy<Value = Vec<ResourceDescription>> {
|
||||
sites().prop_flat_map(|sites| collection::vec(resource(sites), 1..=100))
|
||||
}
|
||||
|
||||
pub fn resource(sites: Vec<Site>) -> impl Strategy<Value = ResourceDescription> {
|
||||
any::<bool>().prop_flat_map(move |is_dns| {
|
||||
if is_dns {
|
||||
dns_resource_with_sites(sites.clone())
|
||||
.prop_map(ResourceDescription::Dns)
|
||||
.boxed()
|
||||
} else {
|
||||
cidr_resource_with_sites(8, sites.clone())
|
||||
.prop_map(ResourceDescription::Cidr)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dns_resource_with_sites(sites: Vec<Site>) -> impl Strategy<Value = ResourceDescriptionDns> {
|
||||
(
|
||||
resource_id(),
|
||||
resource_name(),
|
||||
dns_resource_address(),
|
||||
gateway_groups(),
|
||||
address_description(),
|
||||
)
|
||||
.prop_map(|(id, name, address, gateway_groups, address_description)| {
|
||||
ResourceDescriptionDns {
|
||||
.prop_map(
|
||||
move |(id, name, address, address_description)| ResourceDescriptionDns {
|
||||
id,
|
||||
address,
|
||||
name,
|
||||
gateway_groups,
|
||||
sites: sites.clone(),
|
||||
address_description,
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn cidr_resource(host_mask_bits: usize) -> impl Strategy<Value = ResourceDescriptionCidr> {
|
||||
pub fn cidr_resource_with_sites(
|
||||
host_mask_bits: usize,
|
||||
sites: Vec<Site>,
|
||||
) -> impl Strategy<Value = ResourceDescriptionCidr> {
|
||||
(
|
||||
resource_id(),
|
||||
resource_name(),
|
||||
ip_network(host_mask_bits),
|
||||
gateway_groups(),
|
||||
address_description(),
|
||||
)
|
||||
.prop_map(|(id, name, address, gateway_groups, address_description)| {
|
||||
ResourceDescriptionCidr {
|
||||
.prop_map(
|
||||
move |(id, name, address, address_description)| ResourceDescriptionCidr {
|
||||
id,
|
||||
address,
|
||||
name,
|
||||
gateway_groups,
|
||||
sites: sites.clone(),
|
||||
address_description,
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dns_resource() -> impl Strategy<Value = ResourceDescriptionDns> {
|
||||
sites().prop_flat_map(dns_resource_with_sites)
|
||||
}
|
||||
|
||||
pub fn cidr_resource(host_mask_bits: usize) -> impl Strategy<Value = ResourceDescriptionCidr> {
|
||||
sites().prop_flat_map(move |sites| cidr_resource_with_sites(host_mask_bits, sites))
|
||||
}
|
||||
|
||||
pub fn address_description() -> impl Strategy<Value = String> {
|
||||
any_with::<String>("[a-z]{4,10}".into())
|
||||
}
|
||||
|
||||
pub fn gateway_groups() -> impl Strategy<Value = Vec<GatewayGroup>> {
|
||||
collection::vec(gateway_group(), 1..=10)
|
||||
pub fn sites() -> impl Strategy<Value = Vec<Site>> {
|
||||
collection::vec(site(), 1..=10)
|
||||
}
|
||||
|
||||
pub fn gateway_group() -> impl Strategy<Value = GatewayGroup> {
|
||||
(any_with::<String>("[a-z]{4,10}".into()), any::<u128>()).prop_map(|(name, id)| GatewayGroup {
|
||||
pub fn site() -> impl Strategy<Value = Site> {
|
||||
(any_with::<String>("[a-z]{4,10}".into()), any::<u128>()).prop_map(|(name, id)| Site {
|
||||
name,
|
||||
id: SiteId::from_u128(id),
|
||||
})
|
||||
@@ -68,6 +114,10 @@ pub fn resource_id() -> impl Strategy<Value = ResourceId> + Clone {
|
||||
any::<u128>().prop_map(ResourceId::from_u128)
|
||||
}
|
||||
|
||||
pub fn gateway_id() -> impl Strategy<Value = GatewayId> + Clone {
|
||||
any::<u128>().prop_map(GatewayId::from_u128)
|
||||
}
|
||||
|
||||
pub fn client_id() -> impl Strategy<Value = ClientId> {
|
||||
any::<u128>().prop_map(ClientId::from_u128)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::peer_store::PeerStore;
|
||||
use crate::{dns, dns::DnsQuery};
|
||||
use bimap::BiMap;
|
||||
use connlib_shared::callbacks::Status;
|
||||
use connlib_shared::error::{ConnlibError as Error, ConnlibError};
|
||||
use connlib_shared::messages::client::{Site, SiteId};
|
||||
use connlib_shared::messages::{
|
||||
client::ResourceDescription, client::ResourceDescriptionCidr, client::ResourceDescriptionDns,
|
||||
Answer, ClientPayload, DnsServer, DomainResponse, GatewayId, Interface as InterfaceConfig,
|
||||
IpDnsServer, Key, Offer, Relay, RelayId, RequestConnection, ResourceId, ReuseConnection,
|
||||
};
|
||||
use connlib_shared::{Callbacks, Dname, PublicKey, StaticSecret};
|
||||
use connlib_shared::{callbacks, Callbacks, Dname, PublicKey, StaticSecret};
|
||||
use domain::base::Rtype;
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use ip_network_table::IpNetworkTable;
|
||||
@@ -174,6 +176,15 @@ where
|
||||
self.role_state.on_connection_failed(id);
|
||||
}
|
||||
|
||||
pub fn set_resource_offline(&mut self, id: ResourceId) {
|
||||
self.role_state.set_resource_offline(id);
|
||||
|
||||
self.role_state.on_connection_failed(id);
|
||||
|
||||
self.callbacks
|
||||
.on_update_resources(self.role_state.resources());
|
||||
}
|
||||
|
||||
pub fn add_ice_candidate(&mut self, conn_id: GatewayId, ice_candidate: String) {
|
||||
self.role_state
|
||||
.node
|
||||
@@ -191,10 +202,12 @@ where
|
||||
resource_id: ResourceId,
|
||||
gateway_id: GatewayId,
|
||||
relays: Vec<Relay>,
|
||||
site_id: SiteId,
|
||||
) -> connlib_shared::Result<Request> {
|
||||
self.role_state.create_or_reuse_connection(
|
||||
resource_id,
|
||||
gateway_id,
|
||||
site_id,
|
||||
stun(&relays, |addr| self.io.sockets_ref().can_handle(addr)),
|
||||
turn(&relays),
|
||||
)
|
||||
@@ -277,6 +290,9 @@ pub struct ClientState {
|
||||
next_dns_refresh: Option<Instant>,
|
||||
|
||||
system_resolvers: Vec<IpAddr>,
|
||||
|
||||
gateways_site: HashMap<GatewayId, SiteId>,
|
||||
sites_status: HashMap<SiteId, Status>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -306,11 +322,51 @@ impl ClientState {
|
||||
next_dns_refresh: Default::default(),
|
||||
node: ClientNode::new(private_key),
|
||||
system_resolvers: Default::default(),
|
||||
sites_status: Default::default(),
|
||||
gateways_site: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resources(&self) -> Vec<ResourceDescription> {
|
||||
self.resource_ids.values().sorted().cloned().collect_vec()
|
||||
pub(crate) fn resources(&self) -> Vec<callbacks::ResourceDescription> {
|
||||
self.resource_ids
|
||||
.values()
|
||||
.sorted()
|
||||
.cloned()
|
||||
.map(|r| {
|
||||
let status = self.resource_status(&r);
|
||||
r.with_status(status)
|
||||
})
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
fn resource_status(&self, resource: &ResourceDescription) -> Status {
|
||||
if resource.sites().iter().any(|s| {
|
||||
self.sites_status
|
||||
.get(&s.id)
|
||||
.is_some_and(|s| *s == Status::Online)
|
||||
}) {
|
||||
return Status::Online;
|
||||
}
|
||||
|
||||
if resource.sites().iter().all(|s| {
|
||||
self.sites_status
|
||||
.get(&s.id)
|
||||
.is_some_and(|s| *s == Status::Offline)
|
||||
}) {
|
||||
return Status::Offline;
|
||||
}
|
||||
|
||||
Status::Unknown
|
||||
}
|
||||
|
||||
fn set_resource_offline(&mut self, id: ResourceId) {
|
||||
let Some(resource) = self.resource_ids.get(&id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for Site { id, .. } in resource.sites() {
|
||||
self.sites_status.insert(*id, Status::Offline);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encapsulate<'s>(
|
||||
@@ -434,11 +490,14 @@ impl ClientState {
|
||||
&mut self,
|
||||
resource_id: ResourceId,
|
||||
gateway_id: GatewayId,
|
||||
site_id: SiteId,
|
||||
allowed_stun_servers: HashSet<SocketAddr>,
|
||||
allowed_turn_servers: HashSet<(RelayId, RelaySocket, String, String, String)>,
|
||||
) -> connlib_shared::Result<Request> {
|
||||
tracing::trace!("create_or_reuse_connection");
|
||||
|
||||
self.gateways_site.insert(gateway_id, site_id);
|
||||
|
||||
let desc = self
|
||||
.resource_ids
|
||||
.get(&resource_id)
|
||||
@@ -746,6 +805,7 @@ impl ClientState {
|
||||
}
|
||||
|
||||
pub fn cleanup_connected_gateway(&mut self, gateway_id: &GatewayId) {
|
||||
self.update_site_status_by_gateway(gateway_id, Status::Unknown);
|
||||
self.peers.remove(gateway_id);
|
||||
self.dns_resources_internal_ips.retain(|resource, _| {
|
||||
!self
|
||||
@@ -853,11 +913,24 @@ impl ClientState {
|
||||
conn_id: connection,
|
||||
candidate,
|
||||
}),
|
||||
snownet::Event::ConnectionEstablished { .. } => {}
|
||||
snownet::Event::ConnectionEstablished(id) => {
|
||||
self.update_site_status_by_gateway(&id, Status::Online);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_site_status_by_gateway(&mut self, gateway_id: &GatewayId, status: Status) {
|
||||
// Note: we can do this because in theory we shouldn't have multiple gateways for the same site
|
||||
// connected at the same time.
|
||||
self.sites_status.insert(
|
||||
*self.gateways_site.get(gateway_id).expect(
|
||||
"if we're updating a site status there should be an associated site to a gateway",
|
||||
),
|
||||
status,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn poll_event(&mut self) -> Option<ClientEvent> {
|
||||
self.buffered_events.pop_front()
|
||||
}
|
||||
@@ -965,6 +1038,7 @@ impl ClientState {
|
||||
// If there's no allowed ip left we remove the whole peer because there's no point on keeping it around
|
||||
if peer.allowed_ips.is_empty() {
|
||||
self.peers.remove(&gateway_id);
|
||||
self.update_site_status_by_gateway(&gateway_id, Status::Unknown);
|
||||
// TODO: should we have a Node::remove_connection?
|
||||
}
|
||||
}
|
||||
@@ -1433,8 +1507,13 @@ mod proptests {
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
hashset(client_state.resources().iter()),
|
||||
hashset(&[
|
||||
hashset(
|
||||
client_state
|
||||
.resources()
|
||||
.into_iter()
|
||||
.map_into::<ResourceDescription>()
|
||||
),
|
||||
hashset([
|
||||
ResourceDescription::Cidr(resource1.clone()),
|
||||
ResourceDescription::Dns(resource2.clone())
|
||||
])
|
||||
@@ -1443,8 +1522,13 @@ mod proptests {
|
||||
client_state.add_resources(&[ResourceDescription::Cidr(resource3.clone())]);
|
||||
|
||||
assert_eq!(
|
||||
hashset(client_state.resources().iter()),
|
||||
hashset(&[
|
||||
hashset(
|
||||
client_state
|
||||
.resources()
|
||||
.into_iter()
|
||||
.map_into::<ResourceDescription>()
|
||||
),
|
||||
hashset([
|
||||
ResourceDescription::Cidr(resource1),
|
||||
ResourceDescription::Dns(resource2),
|
||||
ResourceDescription::Cidr(resource3)
|
||||
@@ -1468,8 +1552,13 @@ mod proptests {
|
||||
client_state.add_resources(&[ResourceDescription::Cidr(updated_resource.clone())]);
|
||||
|
||||
assert_eq!(
|
||||
hashset(client_state.resources().iter()),
|
||||
hashset(&[ResourceDescription::Cidr(updated_resource),])
|
||||
hashset(
|
||||
client_state
|
||||
.resources()
|
||||
.into_iter()
|
||||
.map_into::<ResourceDescription>()
|
||||
),
|
||||
hashset([ResourceDescription::Cidr(updated_resource),])
|
||||
);
|
||||
assert_eq!(
|
||||
hashset(client_state.routes()),
|
||||
@@ -1490,14 +1579,19 @@ mod proptests {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
address_description: resource.address_description,
|
||||
gateway_groups: resource.gateway_groups,
|
||||
sites: resource.sites,
|
||||
};
|
||||
|
||||
client_state.add_resources(&[ResourceDescription::Cidr(dns_as_cidr_resource.clone())]);
|
||||
|
||||
assert_eq!(
|
||||
hashset(client_state.resources().iter()),
|
||||
hashset(&[ResourceDescription::Cidr(dns_as_cidr_resource),])
|
||||
hashset(
|
||||
client_state
|
||||
.resources()
|
||||
.into_iter()
|
||||
.map_into::<ResourceDescription>()
|
||||
),
|
||||
hashset([ResourceDescription::Cidr(dns_as_cidr_resource),])
|
||||
);
|
||||
assert_eq!(
|
||||
hashset(client_state.routes()),
|
||||
@@ -1519,8 +1613,13 @@ mod proptests {
|
||||
client_state.remove_resources(&[dns_resource.id]);
|
||||
|
||||
assert_eq!(
|
||||
hashset(client_state.resources().iter()),
|
||||
hashset(&[ResourceDescription::Cidr(cidr_resource.clone())])
|
||||
hashset(
|
||||
client_state
|
||||
.resources()
|
||||
.into_iter()
|
||||
.map_into::<ResourceDescription>()
|
||||
),
|
||||
hashset([ResourceDescription::Cidr(cidr_resource.clone())])
|
||||
);
|
||||
assert_eq!(
|
||||
hashset(client_state.routes()),
|
||||
@@ -1552,8 +1651,13 @@ mod proptests {
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
hashset(client_state.resources().iter()),
|
||||
hashset(&[
|
||||
hashset(
|
||||
client_state
|
||||
.resources()
|
||||
.into_iter()
|
||||
.map_into::<ResourceDescription>()
|
||||
),
|
||||
hashset([
|
||||
ResourceDescription::Dns(dns_resource2),
|
||||
ResourceDescription::Cidr(cidr_resource2.clone()),
|
||||
])
|
||||
@@ -1563,4 +1667,100 @@ mod proptests {
|
||||
expected_routes(vec![cidr_resource2.address])
|
||||
);
|
||||
}
|
||||
|
||||
#[test_strategy::proptest]
|
||||
fn setting_gateway_online_sets_all_related_resources_online(
|
||||
#[strategy(resources_sharing_site())] resource_config_online: (
|
||||
Vec<ResourceDescription>,
|
||||
Site,
|
||||
),
|
||||
#[strategy(resources_sharing_site())] resource_config_unknown: (
|
||||
Vec<ResourceDescription>,
|
||||
Site,
|
||||
),
|
||||
#[strategy(gateway_id())] first_resource_gateway_id: GatewayId,
|
||||
) {
|
||||
let (resources_online, site) = resource_config_online;
|
||||
let (resources_unknown, _) = resource_config_unknown;
|
||||
let mut client_state = ClientState::for_test();
|
||||
client_state.add_resources(&resources_online);
|
||||
client_state.add_resources(&resources_unknown);
|
||||
client_state.resources_gateways.insert(
|
||||
resources_online.first().unwrap().id(),
|
||||
first_resource_gateway_id,
|
||||
);
|
||||
client_state
|
||||
.gateways_site
|
||||
.insert(first_resource_gateway_id, site.id);
|
||||
|
||||
client_state.update_site_status_by_gateway(&first_resource_gateway_id, Status::Online);
|
||||
|
||||
for resource in resources_online {
|
||||
assert_eq!(client_state.resource_status(&resource), Status::Online);
|
||||
}
|
||||
|
||||
for resource in resources_unknown {
|
||||
assert_eq!(client_state.resource_status(&resource), Status::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
#[test_strategy::proptest]
|
||||
fn disconnecting_gateway_sets_related_resources_unknown(
|
||||
#[strategy(resources_sharing_site())] resource_config: (Vec<ResourceDescription>, Site),
|
||||
#[strategy(gateway_id())] first_resource_gateway_id: GatewayId,
|
||||
) {
|
||||
let (resources, site) = resource_config;
|
||||
let mut client_state = ClientState::for_test();
|
||||
client_state.add_resources(&resources);
|
||||
client_state
|
||||
.resources_gateways
|
||||
.insert(resources.first().unwrap().id(), first_resource_gateway_id);
|
||||
client_state
|
||||
.gateways_site
|
||||
.insert(first_resource_gateway_id, site.id);
|
||||
|
||||
client_state.update_site_status_by_gateway(&first_resource_gateway_id, Status::Online);
|
||||
client_state.update_site_status_by_gateway(&first_resource_gateway_id, Status::Unknown);
|
||||
|
||||
for resource in resources {
|
||||
assert_eq!(client_state.resource_status(&resource), Status::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
#[test_strategy::proptest]
|
||||
fn setting_resource_offline_doesnt_set_all_related_resources_offline(
|
||||
#[strategy(resources_sharing_site())] resource_config_online: (
|
||||
Vec<ResourceDescription>,
|
||||
Site,
|
||||
),
|
||||
) {
|
||||
let (mut resources, _) = resource_config_online;
|
||||
let mut client_state = ClientState::for_test();
|
||||
client_state.add_resources(&resources);
|
||||
let resource_offline = resources.pop().unwrap();
|
||||
|
||||
client_state.set_resource_offline(resource_offline.id());
|
||||
|
||||
assert_eq!(
|
||||
client_state.resource_status(&resource_offline),
|
||||
Status::Offline
|
||||
);
|
||||
for resource in resources {
|
||||
assert_eq!(client_state.resource_status(&resource), Status::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
#[test_strategy::proptest]
|
||||
fn setting_resource_offline_set_all_resources_sharing_all_groups_offline(
|
||||
#[strategy(resources_sharing_all_sites())] resources: Vec<ResourceDescription>,
|
||||
) {
|
||||
let mut client_state = ClientState::for_test();
|
||||
client_state.add_resources(&resources);
|
||||
|
||||
client_state.set_resource_offline(resources.first().unwrap().id());
|
||||
|
||||
for resource in resources {
|
||||
assert_eq!(client_state.resource_status(&resource), Status::Offline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,8 @@ where
|
||||
)? {
|
||||
Poll::Ready(io::Input::Timeout(timeout)) => {
|
||||
self.role_state.handle_timeout(timeout);
|
||||
self.callbacks
|
||||
.on_update_resources(self.role_state.resources());
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(io::Input::Device(packet)) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::client::{
|
||||
Failure,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use connlib_client_shared::ResourceDescription;
|
||||
use connlib_client_shared::callbacks::ResourceDescription;
|
||||
use connlib_shared::messages::ResourceId;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! "Notification Area" is Microsoft's official name instead of "System tray":
|
||||
//! <https://learn.microsoft.com/en-us/windows/win32/shell/notification-area?redirectedfrom=MSDN#notifications-and-the-notification-area>
|
||||
|
||||
use connlib_client_shared::ResourceDescription;
|
||||
use connlib_client_shared::callbacks::ResourceDescription;
|
||||
use std::str::FromStr;
|
||||
use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu};
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use connlib_client_shared::{ResourceDescription, Sockets};
|
||||
use connlib_shared::{keypair, LoginUrl};
|
||||
use connlib_client_shared::Sockets;
|
||||
use connlib_shared::{callbacks::ResourceDescription, keypair, LoginUrl};
|
||||
use secrecy::SecretString;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use connlib_client_shared::{Callbacks, ResourceDescription};
|
||||
use connlib_client_shared::Callbacks;
|
||||
use connlib_shared::callbacks::ResourceDescription;
|
||||
use firezone_headless_client::{imp::sock_path, IpcClientMsg, IpcServerMsg};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
use super::{Cli, IpcClientMsg, IpcServerMsg, FIREZONE_GROUP, TOKEN_ENV_KEY};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use clap::Parser;
|
||||
use connlib_client_shared::{file_logger, Callbacks, ResourceDescription, Sockets};
|
||||
use connlib_client_shared::{file_logger, Callbacks, Sockets};
|
||||
use connlib_shared::{
|
||||
keypair,
|
||||
callbacks, keypair,
|
||||
linux::{etc_resolv_conf, get_dns_control_from_env, DnsControlMethod},
|
||||
LoginUrl,
|
||||
};
|
||||
@@ -249,8 +249,8 @@ impl Callbacks for CallbackHandlerIpc {
|
||||
None
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<ResourceDescription>) {
|
||||
tracing::info!(len = resources.len(), "New resource list");
|
||||
fn on_update_resources(&self, resources: Vec<callbacks::ResourceDescription>) {
|
||||
tracing::debug!(len = resources.len(), "New resource list");
|
||||
self.cb_tx
|
||||
.try_send(IpcServerMsg::OnUpdateResources(resources))
|
||||
.expect("Should be able to send OnUpdateResources");
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use connlib_client_shared::{
|
||||
file_logger, keypair, Callbacks, LoginUrl, ResourceDescription, Session, Sockets,
|
||||
};
|
||||
use connlib_client_shared::{file_logger, keypair, Callbacks, LoginUrl, Session, Sockets};
|
||||
use connlib_shared::callbacks;
|
||||
use firezone_cli_utils::setup_global_subscriber;
|
||||
use secrecy::SecretString;
|
||||
use std::{future, net::IpAddr, path::PathBuf, task::Poll};
|
||||
@@ -125,7 +124,7 @@ pub enum IpcClientMsg {
|
||||
pub enum IpcServerMsg {
|
||||
Ok,
|
||||
OnDisconnect,
|
||||
OnUpdateResources(Vec<ResourceDescription>),
|
||||
OnUpdateResources(Vec<callbacks::ResourceDescription>),
|
||||
TunnelReady,
|
||||
}
|
||||
|
||||
@@ -255,9 +254,8 @@ impl Callbacks for CallbackHandler {
|
||||
.expect("should be able to tell the main thread that we disconnected");
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resources: Vec<connlib_client_shared::ResourceDescription>) {
|
||||
fn on_update_resources(&self, resources: Vec<callbacks::ResourceDescription>) {
|
||||
// See easily with `export RUST_LOG=firezone_headless_client=debug`
|
||||
tracing::debug!(len = resources.len(), "Printing the resource list one time");
|
||||
for resource in &resources {
|
||||
tracing::debug!(?resource);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user