mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
fix(connlib): reconnect in case we lose all relays (#7164)
During normal operation, we should never lose connectivity to the set of assigned relays in a client or gateway. In the presence of odd network conditions and partitions however, it is possible that we disconnect from a relay that is in fact only temporarily unavailable. Without an explicit mechanism to retrieve new relays, this means that both clients and gateways can end up with no relays at all. For clients, this can be fixed by either roaming or signing out and in again. For gateways, this can only be fixed by a restart! Without connected relays, no connections can be established. With #7163, we will at least be able to still establish direct connections. Yet, that isn't good enough and we need a mechanism for restoring full connectivity in such a case. We creating a new connection, we already sample one of our relays and assign it to this particular connection. This ensures that we don't create an excessive amount of candidates for each individual connection. Currently, this selection is allowed to be silently fallible. With this PR, we make this a hard-error and bubble up the error that all the way to the client's and gateway's event-loop. There, we initiate a reconnect to the portal as a compensating action. Reconnecting to the portal means we will receive another `init` message that allows us to reconnect the relays. Due to the nature of this implementation, this fix may only apply with a certain delay from when we actually lost connectivity to the last relay. However, this design has the advantage that we don't have to introduce an additional state within `snownet`: Connections now simply fail to establish and the next one soon after _should_ succeed again because we will have received a new `init` message. Resolves: #7162.
This commit is contained in:
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@@ -1116,6 +1116,7 @@ dependencies = [
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snownet",
|
||||
"socket-factory",
|
||||
"thiserror",
|
||||
"time",
|
||||
|
||||
@@ -14,6 +14,7 @@ ip_network = { version = "0.4", default-features = false }
|
||||
phoenix-channel = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = ["std", "derive"] }
|
||||
snownet = { workspace = true }
|
||||
socket-factory = { workspace = true }
|
||||
thiserror = "1.0.63"
|
||||
time = { version = "0.3.36", features = ["formatting"] }
|
||||
|
||||
@@ -331,7 +331,16 @@ where
|
||||
site_id,
|
||||
Instant::now(),
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(snownet::NoTurnServers {})) => {
|
||||
tracing::debug!(
|
||||
"Failed to request new connection: No TURN servers available"
|
||||
);
|
||||
|
||||
// Re-connecting to the portal means we will receive another `init` and thus new TURN servers.
|
||||
self.portal
|
||||
.connect(PublicKeyParam(self.tunnel.public_key().to_bytes()));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = anyhow_dyn_err(&e),
|
||||
|
||||
@@ -14,7 +14,7 @@ pub use allocation::RelaySocket;
|
||||
#[allow(deprecated)] // Rust bug: `expect` doesn't seem to work on imports?
|
||||
pub use node::{Answer, Offer};
|
||||
pub use node::{
|
||||
Client, ClientNode, Credentials, EncryptBuffer, EncryptedPacket, Error, Event, Node, Server,
|
||||
ServerNode, Transmit, HANDSHAKE_TIMEOUT,
|
||||
Client, ClientNode, Credentials, EncryptBuffer, EncryptedPacket, Error, Event, NoTurnServers,
|
||||
Node, Server, ServerNode, Transmit, HANDSHAKE_TIMEOUT,
|
||||
};
|
||||
pub use stats::{ConnectionStats, NodeStats};
|
||||
|
||||
@@ -149,6 +149,10 @@ pub enum Error {
|
||||
BadLocalAddress(#[from] str0m::error::IceError),
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("No TURN servers available")]
|
||||
pub struct NoTurnServers {}
|
||||
|
||||
#[expect(private_bounds, reason = "We don't want `Mode` to be public API")]
|
||||
impl<T, TId, RId> Node<T, TId, RId>
|
||||
where
|
||||
@@ -235,13 +239,13 @@ where
|
||||
local_creds: Credentials,
|
||||
remote_creds: Credentials,
|
||||
now: Instant,
|
||||
) {
|
||||
) -> Result<(), NoTurnServers> {
|
||||
let local_creds = local_creds.into();
|
||||
let remote_creds = remote_creds.into();
|
||||
|
||||
if self.connections.initial.contains_key(&cid) {
|
||||
debug_assert!(false, "The new `upsert_connection` API is incompatible with the previous `new_connection` API");
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self
|
||||
@@ -250,10 +254,10 @@ where
|
||||
.is_some_and(|c| c.agent.local_credentials() == &local_creds)
|
||||
{
|
||||
tracing::debug!("Already got a connection");
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let selected_relay = self.sample_relay();
|
||||
let selected_relay = self.sample_relay()?;
|
||||
|
||||
let mut agent = new_agent();
|
||||
agent.set_controlling(self.mode.is_client());
|
||||
@@ -279,6 +283,8 @@ where
|
||||
} else {
|
||||
tracing::info!("Created new connection");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
@@ -353,13 +359,8 @@ where
|
||||
| CandidateKind::PeerReflexive => {}
|
||||
}
|
||||
|
||||
let Some(rid) = relay else {
|
||||
tracing::debug!("No relay selected for connection");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(allocation) = self.allocations.get_mut(&rid) else {
|
||||
tracing::debug!(%rid, "Unknown relay");
|
||||
let Some(allocation) = self.allocations.get_mut(&relay) else {
|
||||
tracing::debug!(rid = %relay, "Unknown relay");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -700,7 +701,7 @@ where
|
||||
mut agent: IceAgent,
|
||||
remote: PublicKey,
|
||||
key: [u8; 32],
|
||||
relay: Option<RId>,
|
||||
relay: RId,
|
||||
intent_sent_at: Instant,
|
||||
now: Instant,
|
||||
) -> Connection<RId> {
|
||||
@@ -925,12 +926,17 @@ where
|
||||
}
|
||||
|
||||
/// Sample a relay to use for a new connection.
|
||||
fn sample_relay(&mut self) -> Option<RId> {
|
||||
let rid = self.allocations.keys().copied().choose(&mut self.rng)?;
|
||||
fn sample_relay(&mut self) -> Result<RId, NoTurnServers> {
|
||||
let rid = self
|
||||
.allocations
|
||||
.keys()
|
||||
.copied()
|
||||
.choose(&mut self.rng)
|
||||
.ok_or(NoTurnServers {})?;
|
||||
|
||||
tracing::debug!(%rid, "Sampled relay");
|
||||
|
||||
Some(rid)
|
||||
Ok(rid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,7 +953,12 @@ where
|
||||
#[must_use]
|
||||
#[deprecated]
|
||||
#[expect(deprecated)]
|
||||
pub fn new_connection(&mut self, cid: TId, intent_sent_at: Instant, now: Instant) -> Offer {
|
||||
pub fn new_connection(
|
||||
&mut self,
|
||||
cid: TId,
|
||||
intent_sent_at: Instant,
|
||||
now: Instant,
|
||||
) -> Result<Offer, NoTurnServers> {
|
||||
if self.connections.initial.remove(&cid).is_some() {
|
||||
tracing::info!("Replacing existing initial connection");
|
||||
};
|
||||
@@ -975,7 +986,7 @@ where
|
||||
session_key,
|
||||
created_at: now,
|
||||
intent_sent_at,
|
||||
relay: self.sample_relay(),
|
||||
relay: self.sample_relay()?,
|
||||
is_failed: false,
|
||||
span: info_span!("connection", %cid),
|
||||
};
|
||||
@@ -986,7 +997,7 @@ where
|
||||
|
||||
tracing::info!(?duration_since_intent, "Establishing new connection");
|
||||
|
||||
params
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
/// Whether we have sent an [`Offer`] for this connection and are currently expecting an [`Answer`].
|
||||
@@ -1052,7 +1063,7 @@ where
|
||||
offer: Offer,
|
||||
remote: PublicKey,
|
||||
now: Instant,
|
||||
) -> Answer {
|
||||
) -> Result<Answer, NoTurnServers> {
|
||||
debug_assert!(
|
||||
!self.connections.initial.contains_key(&cid),
|
||||
"server to not use `initial_connections`"
|
||||
@@ -1076,7 +1087,7 @@ where
|
||||
},
|
||||
};
|
||||
|
||||
let selected_relay = self.sample_relay();
|
||||
let selected_relay = self.sample_relay()?;
|
||||
self.seed_agent_with_local_candidates(cid, selected_relay, &mut agent);
|
||||
|
||||
let connection = self.init_connection(
|
||||
@@ -1094,7 +1105,7 @@ where
|
||||
|
||||
tracing::info!("Created new connection");
|
||||
|
||||
answer
|
||||
Ok(answer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,18 +1117,13 @@ where
|
||||
fn seed_agent_with_local_candidates(
|
||||
&mut self,
|
||||
connection: TId,
|
||||
selected_relay: Option<RId>,
|
||||
selected_relay: RId,
|
||||
agent: &mut IceAgent,
|
||||
) {
|
||||
for candidate in self.shared_candidates.iter().cloned() {
|
||||
add_local_candidate(connection, agent, candidate, &mut self.pending_events);
|
||||
}
|
||||
|
||||
let Some(selected_relay) = selected_relay else {
|
||||
tracing::debug!("Skipping seeding of relay candidates: No relay selected");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(allocation) = self.allocations.get(&selected_relay) else {
|
||||
tracing::debug!(%selected_relay, "Cannot seed relay candidates: Unknown relay");
|
||||
return;
|
||||
@@ -1175,7 +1181,7 @@ where
|
||||
) {
|
||||
// For initial connections, we can just update the relay to be used.
|
||||
for (_, c) in self.iter_initial_mut() {
|
||||
if c.relay.is_some_and(|r| allocations.contains_key(&r)) {
|
||||
if allocations.contains_key(&c.relay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1186,7 +1192,7 @@ where
|
||||
};
|
||||
|
||||
tracing::info!(old_rid = ?c.relay, %new_rid, "Updating relay");
|
||||
c.relay = Some(new_rid);
|
||||
c.relay = new_rid;
|
||||
}
|
||||
|
||||
// For established connections, we check if we are currently using the relay.
|
||||
@@ -1197,19 +1203,12 @@ where
|
||||
let peer_socket = match &mut c.state {
|
||||
Connected { peer_socket, .. } | Idle { peer_socket } => peer_socket,
|
||||
Failed => continue,
|
||||
Connecting {
|
||||
relay: maybe_relay, ..
|
||||
} => {
|
||||
let Some(relay) = maybe_relay else {
|
||||
continue;
|
||||
};
|
||||
|
||||
Connecting { relay, .. } => {
|
||||
if allocations.contains_key(relay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::debug!("Selected relay disconnected during ICE; connection may fail");
|
||||
*maybe_relay = None;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -1239,7 +1238,7 @@ where
|
||||
maybe_initial_connection.or(maybe_established_connection)
|
||||
}
|
||||
|
||||
fn connecting_agent_mut(&mut self, id: TId) -> Option<(&mut IceAgent, Option<RId>)> {
|
||||
fn connecting_agent_mut(&mut self, id: TId) -> Option<(&mut IceAgent, RId)> {
|
||||
let maybe_initial_connection = self.initial.get_mut(&id).map(|i| (&mut i.agent, i.relay));
|
||||
let maybe_pending_connection = self.established.get_mut(&id).and_then(|c| match c.state {
|
||||
ConnectionState::Connecting { relay, .. } => Some((&mut c.agent, relay)),
|
||||
@@ -1256,15 +1255,15 @@ where
|
||||
id: RId,
|
||||
) -> impl Iterator<Item = (TId, &mut IceAgent, tracing::span::Entered<'_>)> + '_ {
|
||||
let initial_connections = self.initial.iter_mut().filter_map(move |(cid, i)| {
|
||||
(i.relay? == id).then_some((*cid, &mut i.agent, i.span.enter()))
|
||||
(i.relay == id).then_some((*cid, &mut i.agent, i.span.enter()))
|
||||
});
|
||||
let pending_connections = self.established.iter_mut().filter_map(move |(cid, c)| {
|
||||
use ConnectionState::*;
|
||||
|
||||
match c.state {
|
||||
Connecting {
|
||||
relay: Some(relay), ..
|
||||
} if relay == id => Some((*cid, &mut c.agent, c.span.enter())),
|
||||
Connecting { relay, .. } if relay == id => {
|
||||
Some((*cid, &mut c.agent, c.span.enter()))
|
||||
}
|
||||
Failed | Idle { .. } | Connecting { .. } | Connected { .. } => None,
|
||||
}
|
||||
});
|
||||
@@ -1560,9 +1559,7 @@ struct InitialConnection<RId> {
|
||||
session_key: Secret<[u8; 32]>,
|
||||
|
||||
/// The fallback relay we sampled for this potential connection.
|
||||
///
|
||||
/// `None` if we don't have any relays available.
|
||||
relay: Option<RId>,
|
||||
relay: RId,
|
||||
|
||||
created_at: Instant,
|
||||
intent_sent_at: Instant,
|
||||
@@ -1630,9 +1627,7 @@ enum ConnectionState<RId> {
|
||||
/// We are still running ICE to figure out, which socket to use to send data.
|
||||
Connecting {
|
||||
/// The relay we have selected for this connection.
|
||||
///
|
||||
/// `None` if we didn't have any relays available.
|
||||
relay: Option<RId>,
|
||||
relay: RId,
|
||||
|
||||
/// Packets emitted by wireguard whilst are still running ICE.
|
||||
///
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
use secrecy::Secret;
|
||||
use snownet::{ClientNode, Credentials, Event, ServerNode};
|
||||
use std::{
|
||||
iter,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use str0m::{net::Protocol, Candidate};
|
||||
|
||||
#[test]
|
||||
#[expect(deprecated, reason = "Will be deleted together with deprecated API")]
|
||||
fn connection_times_out_after_20_seconds() {
|
||||
let (mut alice, _) = alice_and_bob();
|
||||
|
||||
let created_at = Instant::now();
|
||||
|
||||
let _ = alice.new_connection(1, Instant::now(), created_at);
|
||||
alice.handle_timeout(created_at + Duration::from_secs(20));
|
||||
|
||||
assert_eq!(alice.poll_event().unwrap(), Event::ConnectionFailed(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_without_candidates_times_out_after_10_seconds() {
|
||||
let _guard = firezone_logging::test("trace");
|
||||
let start = Instant::now();
|
||||
|
||||
let (mut alice, mut bob) = alice_and_bob();
|
||||
handshake(&mut alice, &mut bob, start);
|
||||
|
||||
alice.handle_timeout(start + Duration::from_secs(10));
|
||||
|
||||
assert_eq!(alice.poll_event().unwrap(), Event::ConnectionFailed(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_with_candidates_does_not_time_out_after_10_seconds() {
|
||||
let _guard = firezone_logging::test("trace");
|
||||
let start = Instant::now();
|
||||
|
||||
let (mut alice, mut bob) = alice_and_bob();
|
||||
handshake(&mut alice, &mut bob, start);
|
||||
|
||||
alice.add_local_host_candidate(s("10.0.0.2:4444")).unwrap();
|
||||
alice.add_remote_candidate(1, host("10.0.0.1:4444"), start);
|
||||
|
||||
alice.handle_timeout(start + Duration::from_secs(10));
|
||||
|
||||
let any_failed =
|
||||
iter::from_fn(|| alice.poll_event()).any(|e| matches!(e, Event::ConnectionFailed(_)));
|
||||
|
||||
assert!(!any_failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(deprecated, reason = "Will be deleted together with deprecated API")]
|
||||
fn answer_after_stale_connection_does_not_panic() {
|
||||
let start = Instant::now();
|
||||
|
||||
let (mut alice, mut bob) = alice_and_bob();
|
||||
let answer = send_offer(&mut alice, &mut bob, start);
|
||||
|
||||
let now = start + Duration::from_secs(10);
|
||||
alice.handle_timeout(now);
|
||||
|
||||
alice.accept_answer(1, bob.public_key(), answer, now + Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(deprecated, reason = "Will be deleted together with deprecated API")]
|
||||
fn only_generate_candidate_event_after_answer() {
|
||||
let local_candidate = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), 10000);
|
||||
|
||||
let mut alice = ClientNode::<u64, u64>::new(rand::random());
|
||||
alice.add_local_host_candidate(local_candidate).unwrap();
|
||||
|
||||
let mut bob = ServerNode::<u64, u64>::new(rand::random());
|
||||
|
||||
let offer = alice.new_connection(1, Instant::now(), Instant::now());
|
||||
|
||||
assert_eq!(
|
||||
alice.poll_event(),
|
||||
None,
|
||||
"no event to be emitted before accepting the answer"
|
||||
);
|
||||
|
||||
let answer = bob.accept_connection(1, offer, alice.public_key(), Instant::now());
|
||||
|
||||
alice.accept_answer(1, bob.public_key(), answer, Instant::now());
|
||||
|
||||
assert!(iter::from_fn(|| alice.poll_event()).any(|ev| ev
|
||||
== Event::NewIceCandidate {
|
||||
connection: 1,
|
||||
candidate: Candidate::host(local_candidate, Protocol::Udp)
|
||||
.unwrap()
|
||||
.to_sdp_string()
|
||||
}));
|
||||
}
|
||||
|
||||
fn alice_and_bob() -> (ClientNode<u64, u64>, ServerNode<u64, u64>) {
|
||||
let alice = ClientNode::new(rand::random());
|
||||
let bob = ServerNode::new(rand::random());
|
||||
|
||||
(alice, bob)
|
||||
}
|
||||
|
||||
#[expect(deprecated, reason = "Will be deleted together with deprecated API")]
|
||||
fn send_offer(
|
||||
alice: &mut ClientNode<u64, u64>,
|
||||
bob: &mut ServerNode<u64, u64>,
|
||||
now: Instant,
|
||||
) -> snownet::Answer {
|
||||
let offer = alice.new_connection(1, Instant::now(), now);
|
||||
|
||||
bob.accept_connection(1, offer, alice.public_key(), now)
|
||||
}
|
||||
|
||||
fn handshake(alice: &mut ClientNode<u64, u64>, bob: &mut ServerNode<u64, u64>, now: Instant) {
|
||||
alice.upsert_connection(
|
||||
1,
|
||||
bob.public_key(),
|
||||
Secret::new([0u8; 32]),
|
||||
Credentials {
|
||||
username: "foo".to_owned(),
|
||||
password: "foo".to_owned(),
|
||||
},
|
||||
Credentials {
|
||||
username: "bar".to_owned(),
|
||||
password: "bar".to_owned(),
|
||||
},
|
||||
now,
|
||||
);
|
||||
bob.upsert_connection(
|
||||
1,
|
||||
alice.public_key(),
|
||||
Secret::new([0u8; 32]),
|
||||
Credentials {
|
||||
username: "bar".to_owned(),
|
||||
password: "bar".to_owned(),
|
||||
},
|
||||
Credentials {
|
||||
username: "foo".to_owned(),
|
||||
password: "foo".to_owned(),
|
||||
},
|
||||
now,
|
||||
);
|
||||
}
|
||||
|
||||
fn host(socket: &str) -> String {
|
||||
Candidate::host(s(socket), Protocol::Udp)
|
||||
.unwrap()
|
||||
.to_sdp_string()
|
||||
}
|
||||
|
||||
fn s(socket: &str) -> SocketAddr {
|
||||
socket.parse().unwrap()
|
||||
}
|
||||
@@ -26,7 +26,7 @@ use crate::ClientEvent;
|
||||
use domain::base::Message;
|
||||
use lru::LruCache;
|
||||
use secrecy::{ExposeSecret as _, Secret};
|
||||
use snownet::{ClientNode, EncryptBuffer, RelaySocket, Transmit};
|
||||
use snownet::{ClientNode, EncryptBuffer, NoTurnServers, RelaySocket, Transmit};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
@@ -535,7 +535,7 @@ impl ClientState {
|
||||
gateway_id: GatewayId,
|
||||
site_id: SiteId,
|
||||
now: Instant,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> anyhow::Result<Result<(), NoTurnServers>> {
|
||||
tracing::trace!("Updating resource routing table");
|
||||
|
||||
let desc = self
|
||||
@@ -544,7 +544,7 @@ impl ClientState {
|
||||
.context("Unknown resource")?;
|
||||
|
||||
if self.node.is_expecting_answer(gateway_id) {
|
||||
return Ok(());
|
||||
return Ok(Ok(()));
|
||||
}
|
||||
|
||||
let awaiting_connection_details = self
|
||||
@@ -571,7 +571,16 @@ impl ClientState {
|
||||
gateway_id,
|
||||
maybe_domain: awaiting_connection_details.domain,
|
||||
});
|
||||
return Ok(());
|
||||
return Ok(Ok(()));
|
||||
};
|
||||
|
||||
let offer = match self.node.new_connection(
|
||||
gateway_id,
|
||||
awaiting_connection_details.last_intent_sent_at,
|
||||
now,
|
||||
) {
|
||||
Ok(o) => o,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
|
||||
self.peers.insert(
|
||||
@@ -581,12 +590,6 @@ impl ClientState {
|
||||
self.peers
|
||||
.add_ips_with_resource(&gateway_id, ips.into_iter(), &resource_id);
|
||||
|
||||
let offer = self.node.new_connection(
|
||||
gateway_id,
|
||||
awaiting_connection_details.last_intent_sent_at,
|
||||
now,
|
||||
);
|
||||
|
||||
self.buffered_events
|
||||
.push_back(ClientEvent::RequestConnection {
|
||||
gateway_id,
|
||||
@@ -599,7 +602,7 @@ impl ClientState {
|
||||
maybe_domain: awaiting_connection_details.domain,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
||||
fn is_upstream_set_by_the_portal(&self) -> bool {
|
||||
|
||||
@@ -12,7 +12,7 @@ use firezone_logging::{anyhow_dyn_err, std_dyn_err};
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use ip_packet::{FzP2pControlSlice, IpPacket};
|
||||
use secrecy::{ExposeSecret as _, Secret};
|
||||
use snownet::{Credentials, EncryptBuffer, RelaySocket, ServerNode, Transmit};
|
||||
use snownet::{Credentials, EncryptBuffer, NoTurnServers, RelaySocket, ServerNode, Transmit};
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -206,13 +206,13 @@ impl GatewayState {
|
||||
offer: snownet::Offer,
|
||||
client: PublicKey,
|
||||
now: Instant,
|
||||
) -> Answer {
|
||||
let answer = self.node.accept_connection(client_id, offer, client, now);
|
||||
) -> Result<Answer, NoTurnServers> {
|
||||
let answer = self.node.accept_connection(client_id, offer, client, now)?;
|
||||
|
||||
Answer {
|
||||
Ok(Answer {
|
||||
username: answer.credentials.username,
|
||||
password: answer.credentials.password,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(%client_id))]
|
||||
@@ -229,7 +229,7 @@ impl GatewayState {
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
resource: ResourceDescription,
|
||||
now: Instant,
|
||||
) {
|
||||
) -> Result<(), NoTurnServers> {
|
||||
self.node.upsert_connection(
|
||||
client_id,
|
||||
client_key,
|
||||
@@ -243,10 +243,12 @@ impl GatewayState {
|
||||
password: client_ice.password,
|
||||
},
|
||||
now,
|
||||
);
|
||||
)?;
|
||||
|
||||
self.allow_access(client_id, ipv4, ipv6, expires_at, resource, None, now)
|
||||
.expect("Should never fail without a `DnsResourceNatEntry`");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn refresh_translation(
|
||||
|
||||
@@ -710,6 +710,7 @@ impl TunnelTest {
|
||||
|
||||
self.client
|
||||
.exec_mut(|c| c.sut.on_routing_details(resource, gateway, site, now))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -794,18 +795,21 @@ impl TunnelTest {
|
||||
let client_id = self.client.inner().id;
|
||||
|
||||
let answer = gateway.exec_mut(|g| {
|
||||
let answer = g.sut.accept(
|
||||
client_id,
|
||||
snownet::Offer {
|
||||
session_key: preshared_key.expose_secret().0.into(),
|
||||
credentials: snownet::Credentials {
|
||||
username: offer.username,
|
||||
password: offer.password,
|
||||
let answer = g
|
||||
.sut
|
||||
.accept(
|
||||
client_id,
|
||||
snownet::Offer {
|
||||
session_key: preshared_key.expose_secret().0.into(),
|
||||
credentials: snownet::Credentials {
|
||||
username: offer.username,
|
||||
password: offer.password,
|
||||
},
|
||||
},
|
||||
},
|
||||
self.client.inner().sut.public_key(),
|
||||
now,
|
||||
);
|
||||
self.client.inner().sut.public_key(),
|
||||
now,
|
||||
)
|
||||
.unwrap();
|
||||
g.sut
|
||||
.allow_access(
|
||||
self.client.inner().id,
|
||||
|
||||
@@ -197,7 +197,7 @@ impl Eventloop {
|
||||
msg: IngressMessages::AuthorizeFlow(msg),
|
||||
..
|
||||
} => {
|
||||
self.tunnel.state_mut().authorize_flow(
|
||||
if let Err(snownet::NoTurnServers {}) = self.tunnel.state_mut().authorize_flow(
|
||||
msg.client.id,
|
||||
PublicKey::from(msg.client.public_key.0),
|
||||
msg.client.preshared_key,
|
||||
@@ -208,7 +208,14 @@ impl Eventloop {
|
||||
msg.expires_at,
|
||||
msg.resource,
|
||||
Instant::now(),
|
||||
);
|
||||
) {
|
||||
tracing::debug!("Failed to authorise flow: No TURN servers available");
|
||||
|
||||
// Re-connecting to the portal means we will receive another `init` and thus new TURN servers.
|
||||
self.portal
|
||||
.connect(PublicKeyParam(self.tunnel.public_key().to_bytes()));
|
||||
return;
|
||||
};
|
||||
|
||||
self.portal.send(
|
||||
PHOENIX_TOPIC,
|
||||
@@ -347,7 +354,7 @@ impl Eventloop {
|
||||
.inspect_err(|e| tracing::debug!(error = std_dyn_err(e), client = %req.client.id, reference = %req.reference, "DNS resolution timed out as part of connection request"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let answer = self.tunnel.state_mut().accept(
|
||||
let answer = match self.tunnel.state_mut().accept(
|
||||
req.client.id,
|
||||
req.client
|
||||
.payload
|
||||
@@ -355,7 +362,18 @@ impl Eventloop {
|
||||
.into_snownet_offer(req.client.peer.preshared_key),
|
||||
PublicKey::from(req.client.peer.public_key.0),
|
||||
Instant::now(),
|
||||
);
|
||||
) {
|
||||
Ok(a) => a,
|
||||
Err(snownet::NoTurnServers {}) => {
|
||||
tracing::debug!("Failed to accept new connection: No TURN servers available");
|
||||
|
||||
// Re-connecting to the portal means we will receive another `init` and thus new TURN servers.
|
||||
self.portal
|
||||
.connect(PublicKeyParam(self.tunnel.public_key().to_bytes()));
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = self.tunnel.state_mut().allow_access(
|
||||
req.client.id,
|
||||
|
||||
@@ -324,11 +324,17 @@ where
|
||||
|
||||
/// Establishes a new connection, dropping the current one if any exists.
|
||||
pub fn connect(&mut self, params: TFinish) {
|
||||
let url = self.url_prototype.expose_secret().to_url(params);
|
||||
|
||||
if matches!(self.state, State::Connecting(_)) && Some(&url) == self.last_url.as_ref() {
|
||||
tracing::debug!("We are already connecting");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Reset the backoff.
|
||||
self.reconnect_backoff.reset();
|
||||
|
||||
// 2. Set state to `Connecting` without a timer.
|
||||
let url = self.url_prototype.expose_secret().to_url(params);
|
||||
let user_agent = self.user_agent.clone();
|
||||
self.state = State::connect(url.clone(), user_agent, self.socket_factory.clone());
|
||||
self.last_url = Some(url);
|
||||
|
||||
@@ -20,6 +20,10 @@ export default function Android() {
|
||||
Fixes an issue where notifications would sometimes not get delivered
|
||||
when Firezone was active.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="7164">
|
||||
Fixes an issue where Firezone would fail to establish connections to
|
||||
Gateways and the user had to sign-out and in again.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.5" date={new Date("2024-10-03")}>
|
||||
<ChangeItem pull="6831">
|
||||
|
||||
@@ -16,6 +16,10 @@ export default function Apple() {
|
||||
<ChangeItem pull="7152">
|
||||
Adds always-on error reporting using sentry.io.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="7164">
|
||||
Fixes an issue where Firezone would fail to establish connections to
|
||||
Gateways and the user had to sign-out and in again.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.6" date={new Date("2024-10-02")}>
|
||||
<ChangeItem pull="6831">
|
||||
|
||||
@@ -25,6 +25,10 @@ export default function GUI({ title }: { title: string }) {
|
||||
<ChangeItem pull="6996">
|
||||
Supports Ubuntu 24.04, no longer supports Ubuntu 20.04.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="7164">
|
||||
Fixes an issue where Firezone would fail to establish connections to
|
||||
Gateways and the user had to sign-out and in again.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.9" date={new Date("2024-10-09")}>
|
||||
<ChangeItem enable={title === "Linux GUI"} pull="6987">
|
||||
|
||||
@@ -23,6 +23,10 @@ export default function Gateway() {
|
||||
Adds on-by-default error reporting using sentry.io.
|
||||
Disable by setting `FIREZONE_NO_TELEMETRY=1`.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="7164">
|
||||
Fixes an issue where the Gateway would fail to accept connections and
|
||||
had to be restarted.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.2" date={new Date("2024-10-02")}>
|
||||
<ChangeItem pull="6733">
|
||||
|
||||
@@ -13,6 +13,10 @@ export default function Headless() {
|
||||
{/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This must not be done when the issue's PR merges. */}
|
||||
<Unreleased>
|
||||
<ChangeItem>Handles DNS queries over TCP correctly.</ChangeItem>
|
||||
<ChangeItem pull="7164">
|
||||
Fixes an issue where Firezone would fail to establish connections to
|
||||
Gateways and the client had to be restarted.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.4" date={new Date("2024-10-02")}>
|
||||
<ChangeItem pull="6831">
|
||||
|
||||
Reference in New Issue
Block a user