feat(connlib): implement idempotent control protocol for gateway (#6941)

This PR implements the new idempotent control protocol for the gateway.
We retain backwards-compatibility with old clients to allow admins to
perform a disruption-free update to the latest version.

With this new control protocol, we are moving the responsibility of
exchanging the proxy IPs we assigned to DNS resources to a p2p protocol
between client and gateway. As a result, wildcard DNS resources only get
authorized on the first access. Accessing a new domain within the same
resource will thus no longer require a roundtrip to the portal.

Overall, users will see a greatly decreased connection setup latency. On
top of that, the new protocol will allow us to more easily implement
packet buffering which will be another UX boost for Firezone.
This commit is contained in:
Thomas Eizinger
2024-10-19 02:59:47 +11:00
committed by GitHub
parent 9de1119b69
commit ce1e59c9fe
11 changed files with 382 additions and 96 deletions

View File

@@ -1259,7 +1259,7 @@ defmodule API.Client.ChannelTest do
assert client_id == client.id
assert resource_id == resource.id
assert authorization_expires_at == socket.assigns.subject.expires_at
assert String.length(preshared_key) == 32
assert String.length(preshared_key) == 44
end
test "returns online gateway connected to an internet resource", %{
@@ -1314,7 +1314,7 @@ defmodule API.Client.ChannelTest do
assert client_id == client.id
assert resource_id == resource.id
assert authorization_expires_at == socket.assigns.subject.expires_at
assert String.length(preshared_key) == 32
assert String.length(preshared_key) == 44
end
test "broadcasts authorize_flow to the gateway and flow_created to the client", %{

View File

@@ -3,7 +3,6 @@ defmodule Domain.Crypto do
def psk do
random_token(@wg_psk_length, encoder: :base64)
|> String.slice(0, @wg_psk_length)
end
def random_token(length \\ 16, opts \\ []) do

View File

@@ -4,7 +4,7 @@ defmodule Domain.CryptoTest do
describe "psk/0" do
test "it returns a string of proper length" do
assert 32 == String.length(psk())
assert 44 == String.length(psk())
end
end

View File

@@ -1,16 +1,17 @@
use crate::messages::ResolveRequest;
use crate::messages::{gateway::ResourceDescription, Answer};
use crate::peer::ClientOnGateway;
use crate::peer_store::PeerStore;
use crate::messages::{
gateway::ResourceDescription, Answer, IceCredentials, ResolveRequest, SecretKey,
};
use crate::utils::earliest;
use crate::GatewayEvent;
use crate::{p2p_control, GatewayEvent};
use crate::{peer::ClientOnGateway, peer_store::PeerStore};
use anyhow::Context;
use boringtun::x25519::PublicKey;
use chrono::{DateTime, Utc};
use connlib_model::{ClientId, DomainName, RelayId, ResourceId};
use ip_network::{Ipv4Network, Ipv6Network};
use ip_packet::IpPacket;
use snownet::{EncryptBuffer, RelaySocket, ServerNode};
use ip_packet::{FzP2pControlSlice, IpPacket};
use secrecy::{ExposeSecret as _, Secret};
use snownet::{Credentials, EncryptBuffer, RelaySocket, ServerNode, Transmit};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::time::{Duration, Instant};
@@ -42,6 +43,7 @@ pub struct GatewayState {
next_expiry_resources_check: Option<Instant>,
buffered_events: VecDeque<GatewayEvent>,
buffered_transmits: VecDeque<Transmit<'static>>,
}
#[derive(Debug)]
@@ -68,6 +70,7 @@ impl GatewayState {
node: ServerNode::new(seed),
next_expiry_resources_check: Default::default(),
buffered_events: VecDeque::default(),
buffered_transmits: VecDeque::default(),
}
}
@@ -102,7 +105,7 @@ impl GatewayState {
let transmit = self
.node
.encapsulate(peer.id(), packet, now, buffer)
.encapsulate(cid, packet, now, buffer)
.inspect_err(|e| tracing::debug!(%cid, "Failed to encapsulate: {e}"))
.ok()??;
@@ -137,6 +140,18 @@ impl GatewayState {
return None;
};
if let Some(fz_p2p_control) = packet.as_fz_p2p_control() {
let response =
handle_p2p_control_packet(fz_p2p_control, peer, &mut self.buffered_events)?;
let mut buffer = EncryptBuffer::new();
let transmit = encrypt_packet(response, cid, &mut self.node, &mut buffer, now)?;
self.buffered_transmits.push_back(transmit.into_owned());
return None;
}
let packet = peer
.decapsulate(packet, now)
.inspect_err(|e| tracing::debug!(%cid, "Invalid packet: {e:#}"))
@@ -195,6 +210,40 @@ impl GatewayState {
}
}
#[tracing::instrument(level = "debug", skip_all, fields(%client_id))]
#[expect(clippy::too_many_arguments)]
pub fn authorize_flow(
&mut self,
client_id: ClientId,
client_key: PublicKey,
preshared_key: SecretKey,
client_ice: IceCredentials,
gateway_ice: IceCredentials,
ipv4: Ipv4Addr,
ipv6: Ipv6Addr,
expires_at: Option<DateTime<Utc>>,
resource: ResourceDescription,
now: Instant,
) {
self.node.upsert_connection(
client_id,
client_key,
Secret::new(preshared_key.expose_secret().0),
Credentials {
username: gateway_ice.username,
password: gateway_ice.password,
},
Credentials {
username: client_ice.username,
password: client_ice.password,
},
now,
);
self.allow_access(client_id, ipv4, ipv6, expires_at, resource, None, now)
.expect("Should never fail without a `DnsResourceNatEntry`");
}
pub fn refresh_translation(
&mut self,
client: ClientId,
@@ -212,7 +261,7 @@ impl GatewayState {
};
}
#[expect(clippy::too_many_arguments)] // It is a deprecated API, we don't care.
#[expect(clippy::too_many_arguments)]
pub fn allow_access(
&mut self,
client: ClientId,
@@ -229,23 +278,58 @@ impl GatewayState {
.or_insert_with(|| ClientOnGateway::new(client, ipv4, ipv6));
peer.add_resource(resource.clone(), expires_at);
if let Some(entry) = dns_resource_nat {
peer.setup_nat(
entry.domain,
resource.id(),
&entry.resolved_ips,
entry.proxy_ips,
now,
)?;
}
self.peers.add_ip(&client, &ipv4.into());
self.peers.add_ip(&client, &ipv6.into());
tracing::info!(%client, resource = %resource.id(), expires = ?expires_at.map(|e| e.to_rfc3339()), "Allowing access to resource");
Ok(())
}
if let Some(entry) = dns_resource_nat {
self.peers
.get_mut(&client)
.context("Unknown peer")?
.assign_translations(
entry.domain,
resource.id(),
&entry.resolved_ips,
entry.proxy_ips,
now,
)?;
}
pub fn handle_domain_resolved(
&mut self,
req: ResolveDnsRequest,
addresses: Vec<IpAddr>,
now: Instant,
) -> anyhow::Result<()> {
use p2p_control::dns_resource_nat;
let nat_status = self
.peers
.get_mut(&req.client)
.context("Unknown peer")?
.setup_nat(
req.domain.clone(),
req.resource,
&addresses,
req.proxy_ips,
now,
)
.map(|()| dns_resource_nat::NatStatus::Active)
.unwrap_or_else(|e| {
tracing::debug!("Failed to setup DNS resource NAT: {e:#}");
dns_resource_nat::NatStatus::Inactive
});
let packet = dns_resource_nat::domain_status(req.resource, req.domain, nat_status);
let mut buffer = EncryptBuffer::new();
let Some(transmit) = encrypt_packet(packet, req.client, &mut self.node, &mut buffer, now)
else {
return Ok(());
};
self.buffered_transmits.push_back(transmit.into_owned());
Ok(())
}
@@ -323,7 +407,9 @@ impl GatewayState {
}
pub(crate) fn poll_transmit(&mut self) -> Option<snownet::Transmit<'static>> {
self.node.poll_transmit()
self.buffered_transmits
.pop_front()
.or_else(|| self.node.poll_transmit())
}
pub(crate) fn poll_event(&mut self) -> Option<GatewayEvent> {
@@ -351,6 +437,80 @@ impl GatewayState {
}
}
fn handle_p2p_control_packet(
fz_p2p_control: FzP2pControlSlice,
peer: &ClientOnGateway,
buffered_events: &mut VecDeque<GatewayEvent>,
) -> Option<IpPacket> {
use p2p_control::dns_resource_nat;
match fz_p2p_control.event_type() {
p2p_control::ASSIGNED_IPS_EVENT => {
let Ok(req) = dns_resource_nat::decode_assigned_ips(fz_p2p_control)
.inspect_err(|e| tracing::debug!("{e:#}"))
else {
return None;
};
if !peer.is_allowed(req.resource) {
tracing::debug!(cid = %peer.id(), resource = %req.resource, "Received `AssignedIpsEvent` for resource that is not allowed");
let packet = dns_resource_nat::domain_status(
req.resource,
req.domain,
dns_resource_nat::NatStatus::Inactive,
);
return Some(packet);
}
// TODO: Should we throttle concurrent events for the same domain?
buffered_events.push_back(GatewayEvent::ResolveDns(ResolveDnsRequest {
domain: req.domain,
client: peer.id(),
resource: req.resource,
proxy_ips: req.proxy_ips,
}));
}
code => {
tracing::debug!(code = %code.into_u8(), "Unknown control protocol event");
}
}
None
}
fn encrypt_packet<'a>(
packet: IpPacket,
cid: ClientId,
node: &mut ServerNode<ClientId, RelayId>,
buffer: &'a mut EncryptBuffer,
now: Instant,
) -> Option<Transmit<'a>> {
let encrypted_packet = node
.encapsulate(cid, packet, now, buffer)
.inspect_err(|e| tracing::debug!(%cid, "Failed to encapsulate: {e}"))
.ok()??;
Some(encrypted_packet.to_transmit(buffer))
}
/// Opaque request struct for when a domain name needs to be resolved.
#[derive(Debug)]
pub struct ResolveDnsRequest {
domain: DomainName,
client: ClientId,
resource: ResourceId,
proxy_ips: Vec<IpAddr>,
}
impl ResolveDnsRequest {
pub fn domain(&self) -> &DomainName {
&self.domain
}
}
fn is_client(dst: IpAddr) -> bool {
match dst {
IpAddr::V4(v4) => IPV4_PEERS.contains(v4),

View File

@@ -3,12 +3,10 @@
//! This is both the wireguard and ICE implementation that should work in tandem.
//! [Tunnel] is the main entry-point for this crate.
use crate::messages::{Offer, Relay, ResolveRequest, SecretKey};
use crate::messages::{Offer, ResolveRequest, SecretKey};
use bimap::BiMap;
use chrono::Utc;
use connlib_model::{
ClientId, DomainName, GatewayId, PublicKey, RelayId, ResourceId, ResourceView,
};
use connlib_model::{ClientId, DomainName, GatewayId, PublicKey, ResourceId, ResourceView};
use io::Io;
use ip_network::{Ipv4Network, Ipv6Network};
use snownet::EncryptBuffer;
@@ -54,7 +52,7 @@ pub type GatewayTunnel = Tunnel<GatewayState>;
pub type ClientTunnel = Tunnel<ClientState>;
pub use client::ClientState;
pub use gateway::{DnsResourceNatEntry, GatewayState, IPV4_PEERS, IPV6_PEERS};
pub use gateway::{DnsResourceNatEntry, GatewayState, ResolveDnsRequest, IPV4_PEERS, IPV6_PEERS};
pub use utils::turn;
/// [`Tunnel`] glues together connlib's [`Io`] component and the respective (pure) state of a client or gateway.
@@ -217,11 +215,6 @@ impl GatewayTunnel {
self.role_state.public_key()
}
pub fn update_relays(&mut self, to_remove: BTreeSet<RelayId>, to_add: Vec<Relay>) {
self.role_state
.update_relays(to_remove, turn(&to_add), Instant::now())
}
pub fn poll_next_event(&mut self, cx: &mut Context<'_>) -> Poll<std::io::Result<GatewayEvent>> {
for _ in 0..MAX_EVENTLOOP_ITERS {
ready!(self.io.poll_has_sockets(cx)); // Suspend everything if we don't have any sockets.
@@ -358,7 +351,7 @@ pub struct TunConfig {
pub ipv6_routes: BTreeSet<Ipv6Network>,
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub enum GatewayEvent {
AddedIceCandidates {
conn_id: ClientId,
@@ -373,6 +366,7 @@ pub enum GatewayEvent {
conn_id: ClientId,
resource_id: ResourceId,
},
ResolveDns(ResolveDnsRequest),
}
fn fmt_routes<T>(routes: &BTreeSet<T>, f: &mut fmt::Formatter) -> fmt::Result

View File

@@ -143,7 +143,6 @@ pub struct DomainResponse {
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct ConnectionAccepted {
pub ice_parameters: Answer,
pub domain_response: Option<DomainResponse>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
@@ -157,6 +156,12 @@ pub enum GatewayResponse {
ResourceAccepted(ResourceAccepted),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
pub struct IceCredentials {
pub username: String,
pub password: String,
}
#[derive(Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(tag = "protocol", rename_all = "snake_case")]
pub enum DnsServer {

View File

@@ -1,7 +1,8 @@
//! Gateway related messages that are needed within connlib
use crate::messages::{
GatewayResponse, Interface, Offer, Peer, Relay, RelaysPresence, ResolveRequest,
GatewayResponse, IceCredentials, Interface, Key, Offer, Peer, Relay, RelaysPresence,
ResolveRequest, SecretKey,
};
use chrono::{serde::ts_seconds_option, DateTime, Utc};
use connlib_model::{ClientId, ResourceId};
@@ -126,7 +127,7 @@ pub struct Config {
}
#[derive(Debug, Deserialize, Clone)]
pub struct Client {
pub struct LegacyClient {
pub id: ClientId,
pub payload: ClientPayload,
pub peer: Peer,
@@ -135,7 +136,7 @@ pub struct Client {
#[derive(Debug, Deserialize, Clone)]
pub struct RequestConnection {
pub resource: ResourceDescription,
pub client: Client,
pub client: LegacyClient,
#[serde(rename = "ref")]
pub reference: String,
#[serde(with = "ts_seconds_option")]
@@ -173,14 +174,38 @@ pub struct RejectAccess {
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case", tag = "event", content = "payload")]
pub enum IngressMessages {
RequestConnection(RequestConnection),
AllowAccess(AllowAccess),
RequestConnection(RequestConnection), // Deprecated.
AllowAccess(AllowAccess), // Deprecated.
RejectAccess(RejectAccess),
IceCandidates(ClientIceCandidates),
InvalidateIceCandidates(ClientIceCandidates),
Init(InitGateway),
RelaysPresence(RelaysPresence),
ResourceUpdated(ResourceDescription),
AuthorizeFlow(AuthorizeFlow),
}
#[derive(Debug, Deserialize, Clone)]
pub struct Client {
pub id: ClientId,
pub public_key: Key,
pub preshared_key: SecretKey,
pub ipv4: Ipv4Addr,
pub ipv6: Ipv6Addr,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AuthorizeFlow {
#[serde(rename = "ref")]
pub reference: String,
pub resource: ResourceDescription,
pub gateway_ice_credentials: IceCredentials,
pub client: Client,
pub client_ice_credentials: IceCredentials,
#[serde(with = "ts_seconds_option")]
pub expires_at: Option<DateTime<Utc>>,
}
/// A client's ice candidate message.
@@ -206,9 +231,13 @@ pub struct ClientIceCandidates {
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "snake_case", tag = "event", content = "payload")]
pub enum EgressMessages {
ConnectionReady(ConnectionReady),
ConnectionReady(ConnectionReady), // Deprecated.
BroadcastIceCandidates(ClientsIceCandidates),
BroadcastInvalidatedIceCandidates(ClientsIceCandidates),
FlowAuthorized {
#[serde(rename = "ref")]
reference: String,
},
}
#[derive(Debug, Serialize, Clone)]
@@ -526,4 +555,13 @@ mod tests {
assert!(matches!(message, IngressMessages::RelaysPresence(_)));
}
#[test]
fn can_deserialize_authorize_flow() {
let json = r#"{"event":"authorize_flow","ref":null,"topic":"gateway","payload":{"client":{"id":"3abd725a-733b-4801-ac16-72f26cd98a24","ipv6":"fd00:2021:1111::f:853b","public_key":"fiAjSBWDgQfD1CFJkTwOf4zg+1QhH0eTT+oLaVIMpH8=","ipv4":"100.93.74.51","preshared_key":"BzPiNE9qszKczZcZzGsyieLYeJ2EQfkfdibls/l3beM="},"resource":{"id":"c7793628-8579-465b-83e3-1a5d4af4db3b","name":"MyCorp Network","type":"cidr","address":"172.20.0.0/16","filters":[]},"actor":{"id":"24eb631e-c529-4182-a746-d99ee66f7426"},"ref":"SFMyNTY.g2gDbQAAAkxnMmdHV0hjVllYQnBRR0Z3YVM1amJIVnpkR1Z5TG14dlkyRnNBQUFEWlFBQUFBQm5FYU9DYUFWWWR4VmhjR2xBWVhCcExtTnNkWE4wWlhJdWJHOWpZV3dBQUFOakFBQUFBR2NSbzRKM0owVnNhWGhwY2k1UWFHOWxibWw0TGxOdlkydGxkQzVXTVM1S1UwOU9VMlZ5YVdGc2FYcGxjbTBBQUFBR1kyeHBaVzUwWVFGaEFHMEFBQUFrWXpjM09UTTJNamd0T0RVM09TMDBOalZpTFRnelpUTXRNV0UxWkRSaFpqUmtZak5pYlFBQUFDQnRTWFZ3TldWUVYwUkRVa1Z3WTNNM2QwaE5VMWREZGxwYWNqQlpTalZCZEhRQUFBQUNkd1pqYkdsbGJuUjBBQUFBQW5jSWRYTmxjbTVoYldWdEFBQUFCR2huZDJoM0NIQmhjM04zYjNKa2JRQUFBQlpxTW1aeGRXWmhkRzQzZUd4eWNuWjJObVp6ZG1WaGR3ZG5ZWFJsZDJGNWRBQUFBQUozQ0hWelpYSnVZVzFsYlFBQUFBUmxhbkYwZHdod1lYTnpkMjl5WkcwQUFBQVdlbVpxY25KcVpHdGlZMmswTW5ReVlYaDVaRFExWVd3QUFBQUJhQUp0QUFBQUMzUnlZV05sY0dGeVpXNTBiUUFBQURjd01DMDFNRGRoTUdSbE9HWm1NekpsWmpVMU9EaGlZV1psWkRZMk1XWXpaVFZrTlMxa1ptTTVZMkl3Wm1NeE5tRTBNbUU1TFRBeGFnPT1uBgCeY-eckgFiAAFRgA.5-aLUjF4RiPoYASwWYfSmWuTEc4cT0u8J9cyBUiP9BY","expires_at":1729813989,"flow_id":"eeb66205-5f53-4f64-acbc-deed47293f04","client_ice_credentials":{"username":"hgwh","password":"j2fqufatn7xlrrvv6fsvea"},"gateway_ice_credentials":{"username":"ejqt","password":"zfjrrjdkbci42t2axyd45a"}}}"#;
let message = serde_json::from_str::<IngressMessages>(json).unwrap();
assert!(matches!(message, IngressMessages::AuthorizeFlow(_)));
}
}

View File

@@ -202,31 +202,37 @@ impl ClientOnGateway {
})
.collect_vec();
self.assign_translations(name, resource_id, &resolved_ips, proxy_ips, now)?;
self.setup_nat(name, resource_id, &resolved_ips, proxy_ips, now)?;
Ok(())
}
/// Setup the NAT for a particular domain within a wildcard DNS resource.
#[tracing::instrument(level = "debug", skip_all, fields(cid = %self.id))]
pub(crate) fn assign_translations(
pub(crate) fn setup_nat(
&mut self,
name: DomainName,
resource_id: ResourceId,
mapped_ips: &[IpAddr],
resolved_ips: &[IpAddr],
proxy_ips: Vec<IpAddr>,
now: Instant,
) -> Result<()> {
let Some(ResourceOnGateway::Dns {
let resource = self
.resources
.get_mut(&resource_id)
.context("Unknown resource")?;
let ResourceOnGateway::Dns {
address, domains, ..
}) = self.resources.get_mut(&resource_id)
} = resource
else {
bail!("Cannot assign translation for non-DNS resource")
bail!("Cannot setup NAT for non-DNS resource")
};
anyhow::ensure!(crate::dns::is_subdomain(&name, address));
let mapped_ipv4 = mapped_ipv4(mapped_ips);
let mapped_ipv6 = mapped_ipv6(mapped_ips);
let mapped_ipv4 = mapped_ipv4(resolved_ips);
let mapped_ipv6 = mapped_ipv6(resolved_ips);
let ipv4_maps = proxy_ips
.iter()
@@ -249,7 +255,9 @@ impl ClientOnGateway {
);
}
domains.insert(name, mapped_ips.to_vec());
tracing::debug!(domain = %name, ?resolved_ips, ?proxy_ips, "Set up DNS resource NAT");
domains.insert(name, resolved_ips.to_vec());
self.recalculate_filters();
Ok(())
@@ -330,6 +338,8 @@ impl ClientOnGateway {
resource: crate::messages::gateway::ResourceDescription,
expires_at: Option<DateTime<Utc>>,
) {
tracing::info!(client = %self.id, resource = %resource.id(), expires = ?expires_at.map(|e| e.to_rfc3339()), "Allowing access to resource");
match self.resources.entry(resource.id()) {
hash_map::Entry::Vacant(v) => {
v.insert(ResourceOnGateway::new(resource, expires_at));
@@ -450,6 +460,10 @@ impl ClientOnGateway {
Ok(Some(packet))
}
pub(crate) fn is_allowed(&self, resource: ResourceId) -> bool {
self.resources.contains_key(&resource)
}
fn ensure_allowed_src(&self, packet: &IpPacket) -> anyhow::Result<()> {
let src = packet.source();
@@ -1100,7 +1114,7 @@ mod tests {
let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr());
peer.add_resource(foo_dns_resource(), None);
peer.add_resource(bar_cidr_resource(), None);
peer.assign_translations(
peer.setup_nat(
foo_name().parse().unwrap(),
resource_id(),
&[foo_real_ip().into()],
@@ -1161,7 +1175,7 @@ mod tests {
let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr());
peer.add_resource(foo_dns_resource(), None);
peer.add_resource(internet_resource(), None);
peer.assign_translations(
peer.setup_nat(
foo_name().parse().unwrap(),
resource_id(),
&[foo_real_ip().into()],

View File

@@ -375,7 +375,14 @@ impl TunnelTest {
continue;
};
on_gateway_event(*id, event, &mut self.client, now);
on_gateway_event(
*id,
event,
&mut self.client,
gateway,
&ref_state.global_dns_records,
now,
);
continue 'outer;
}
if let Some(event) = self.client.exec_mut(|c| c.sut.poll_event()) {
@@ -764,35 +771,33 @@ 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 = 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,
},
self.client.inner().sut.public_key(),
},
self.client.inner().sut.public_key(),
now,
);
g.sut
.allow_access(
self.client.inner().id,
self.client.inner().sut.tunnel_ip4().unwrap(),
self.client.inner().sut.tunnel_ip6().unwrap(),
None,
resource.clone(),
maybe_entry,
now,
);
g.sut
.allow_access(
self.client.inner().id,
self.client.inner().sut.tunnel_ip4().unwrap(),
self.client.inner().sut.tunnel_ip6().unwrap(),
None,
resource.clone(),
maybe_entry,
now,
)
.unwrap();
)
.unwrap();
anyhow::Ok(answer)
})
.unwrap();
answer
});
self.client
.exec_mut(|c| {
@@ -885,6 +890,8 @@ fn on_gateway_event(
src: GatewayId,
event: GatewayEvent,
client: &mut Host<SimClient>,
gateway: &mut Host<SimGateway>,
global_dns_records: &BTreeMap<DomainName, BTreeSet<IpAddr>>,
now: Instant,
) {
match event {
@@ -899,5 +906,17 @@ fn on_gateway_event(
}
}),
GatewayEvent::RefreshDns { .. } => todo!(),
GatewayEvent::ResolveDns(r) => {
let resolved_ips = global_dns_records
.get(r.domain())
.cloned()
.unwrap_or_default();
gateway.exec_mut(|g| {
g.sut
.handle_domain_resolved(r, Vec::from_iter(resolved_ips), now)
.unwrap()
})
}
}
}

View File

@@ -4,14 +4,12 @@ use connlib_model::DomainName;
use connlib_model::{ClientId, ResourceId};
#[cfg(not(target_os = "windows"))]
use dns_lookup::{AddrInfoHints, AddrInfoIter, LookupError};
use firezone_tunnel::messages::{
gateway::{
AllowAccess, ClientIceCandidates, ClientsIceCandidates, ConnectionReady, EgressMessages,
IngressMessages, RejectAccess, RequestConnection,
},
ConnectionAccepted, GatewayResponse, Interface, RelaysPresence,
use firezone_tunnel::messages::gateway::{
AllowAccess, ClientIceCandidates, ClientsIceCandidates, ConnectionReady, EgressMessages,
IngressMessages, RejectAccess, RequestConnection,
};
use firezone_tunnel::{DnsResourceNatEntry, GatewayTunnel};
use firezone_tunnel::messages::{ConnectionAccepted, GatewayResponse, Interface, RelaysPresence};
use firezone_tunnel::{DnsResourceNatEntry, GatewayTunnel, ResolveDnsRequest};
use futures::channel::mpsc;
use futures_bounded::Timeout;
use phoenix_channel::{PhoenixChannel, PublicKeyParam};
@@ -33,11 +31,12 @@ static_assertions::const_assert!(
DNS_RESOLUTION_TIMEOUT.as_secs() < snownet::HANDSHAKE_TIMEOUT.as_secs()
);
#[derive(Debug, Clone)]
#[derive(Debug)]
enum ResolveTrigger {
RequestConnection(RequestConnection),
AllowAccess(AllowAccess),
Refresh(DomainName, ClientId, ResourceId),
RequestConnection(RequestConnection), // Deprecated
AllowAccess(AllowAccess), // Deprecated
Refresh(DomainName, ClientId, ResourceId), // TODO: Can we delete this perhaps?
SetupNat(ResolveDnsRequest),
}
pub struct Eventloop {
@@ -59,7 +58,7 @@ impl Eventloop {
Self {
tunnel,
portal,
resolve_tasks: futures_bounded::FuturesTupleSet::new(DNS_RESOLUTION_TIMEOUT, 100),
resolve_tasks: futures_bounded::FuturesTupleSet::new(DNS_RESOLUTION_TIMEOUT, 1000),
tun_device_channel,
}
}
@@ -96,6 +95,25 @@ impl Eventloop {
self.refresh_translation(result, conn_id, resource_id, name);
continue;
}
Poll::Ready((result, ResolveTrigger::SetupNat(request))) => {
let addresses = result
.inspect_err(|e| {
tracing::debug!(
"DNS resolution timed out as part of setup NAT request: {e}"
)
})
.unwrap_or_default();
if let Err(e) = self.tunnel.state_mut().handle_domain_resolved(
request,
addresses,
Instant::now(),
) {
tracing::warn!("Failed to set DNS resource NAT: {e:#}");
};
continue;
}
Poll::Pending => {}
}
@@ -153,11 +171,47 @@ impl Eventloop {
tracing::warn!("Too many dns resolution requests, dropping existing one");
};
}
firezone_tunnel::GatewayEvent::ResolveDns(setup_nat) => {
if self
.resolve_tasks
.try_push(
resolve(Some(setup_nat.domain().clone())),
ResolveTrigger::SetupNat(setup_nat),
)
.is_err()
{
tracing::warn!("Too many dns resolution requests, dropping existing one");
};
}
}
}
fn handle_portal_event(&mut self, event: phoenix_channel::Event<IngressMessages, ()>) {
match event {
phoenix_channel::Event::InboundMessage {
msg: IngressMessages::AuthorizeFlow(msg),
..
} => {
self.tunnel.state_mut().authorize_flow(
msg.client.id,
PublicKey::from(msg.client.public_key.0),
msg.client.preshared_key,
msg.client_ice_credentials,
msg.gateway_ice_credentials,
msg.client.ipv4,
msg.client.ipv6,
msg.expires_at,
msg.resource,
Instant::now(),
);
self.portal.send(
PHOENIX_TOPIC,
EgressMessages::FlowAuthorized {
reference: msg.reference,
},
);
}
phoenix_channel::Event::InboundMessage {
msg: IngressMessages::RequestConnection(req),
..
@@ -323,7 +377,6 @@ impl Eventloop {
reference: req.reference,
gateway_payload: GatewayResponse::ConnectionAccepted(ConnectionAccepted {
ice_parameters: answer,
domain_response: None,
}),
}),
);

View File

@@ -15,6 +15,10 @@ export default function Gateway() {
Separates CIDR and DNS resources filters, preventing filters
from one applying to the other.
</ChangeItem>
<ChangeItem pull="6941">
Implements support for the new control protocol; delivering faster
and more robust connection establishment.
</ChangeItem>
</Unreleased>
<Entry version="1.3.2" date={new Date("2024-10-02")}>
<ChangeItem pull="6733">