diff --git a/elixir/apps/api/test/api/client/channel_test.exs b/elixir/apps/api/test/api/client/channel_test.exs index e30e66179..deaaa776c 100644 --- a/elixir/apps/api/test/api/client/channel_test.exs +++ b/elixir/apps/api/test/api/client/channel_test.exs @@ -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", %{ diff --git a/elixir/apps/domain/lib/domain/crypto.ex b/elixir/apps/domain/lib/domain/crypto.ex index a609c86c6..912184755 100644 --- a/elixir/apps/domain/lib/domain/crypto.ex +++ b/elixir/apps/domain/lib/domain/crypto.ex @@ -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 diff --git a/elixir/apps/domain/test/domain/crypto_test.exs b/elixir/apps/domain/test/domain/crypto_test.exs index 2ffb3d069..e532673ba 100644 --- a/elixir/apps/domain/test/domain/crypto_test.exs +++ b/elixir/apps/domain/test/domain/crypto_test.exs @@ -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 diff --git a/rust/connlib/tunnel/src/gateway.rs b/rust/connlib/tunnel/src/gateway.rs index 3a94d3b81..9cfc1ba44 100644 --- a/rust/connlib/tunnel/src/gateway.rs +++ b/rust/connlib/tunnel/src/gateway.rs @@ -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, buffered_events: VecDeque, + buffered_transmits: VecDeque>, } #[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>, + 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, + 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> { - self.node.poll_transmit() + self.buffered_transmits + .pop_front() + .or_else(|| self.node.poll_transmit()) } pub(crate) fn poll_event(&mut self) -> Option { @@ -351,6 +437,80 @@ impl GatewayState { } } +fn handle_p2p_control_packet( + fz_p2p_control: FzP2pControlSlice, + peer: &ClientOnGateway, + buffered_events: &mut VecDeque, +) -> Option { + 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, + buffer: &'a mut EncryptBuffer, + now: Instant, +) -> Option> { + 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, +} + +impl ResolveDnsRequest { + pub fn domain(&self) -> &DomainName { + &self.domain + } +} + fn is_client(dst: IpAddr) -> bool { match dst { IpAddr::V4(v4) => IPV4_PEERS.contains(v4), diff --git a/rust/connlib/tunnel/src/lib.rs b/rust/connlib/tunnel/src/lib.rs index 0e11c318a..7c7069c7f 100644 --- a/rust/connlib/tunnel/src/lib.rs +++ b/rust/connlib/tunnel/src/lib.rs @@ -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; pub type ClientTunnel = Tunnel; 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, to_add: Vec) { - self.role_state - .update_relays(to_remove, turn(&to_add), Instant::now()) - } - pub fn poll_next_event(&mut self, cx: &mut Context<'_>) -> Poll> { 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, } -#[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(routes: &BTreeSet, f: &mut fmt::Formatter) -> fmt::Result diff --git a/rust/connlib/tunnel/src/messages.rs b/rust/connlib/tunnel/src/messages.rs index 1856dcdec..eca9e9b04 100644 --- a/rust/connlib/tunnel/src/messages.rs +++ b/rust/connlib/tunnel/src/messages.rs @@ -143,7 +143,6 @@ pub struct DomainResponse { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct ConnectionAccepted { pub ice_parameters: Answer, - pub domain_response: Option, } #[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 { diff --git a/rust/connlib/tunnel/src/messages/gateway.rs b/rust/connlib/tunnel/src/messages/gateway.rs index 0e015223b..ad1a7aac2 100644 --- a/rust/connlib/tunnel/src/messages/gateway.rs +++ b/rust/connlib/tunnel/src/messages/gateway.rs @@ -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>, } /// 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::(json).unwrap(); + + assert!(matches!(message, IngressMessages::AuthorizeFlow(_))); + } } diff --git a/rust/connlib/tunnel/src/peer.rs b/rust/connlib/tunnel/src/peer.rs index 1bc0e6d2e..ae72c4cda 100644 --- a/rust/connlib/tunnel/src/peer.rs +++ b/rust/connlib/tunnel/src/peer.rs @@ -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, 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>, ) { + 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()], diff --git a/rust/connlib/tunnel/src/tests/sut.rs b/rust/connlib/tunnel/src/tests/sut.rs index caacd82f5..d0449fc90 100644 --- a/rust/connlib/tunnel/src/tests/sut.rs +++ b/rust/connlib/tunnel/src/tests/sut.rs @@ -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, + gateway: &mut Host, + global_dns_records: &BTreeMap>, 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() + }) + } } } diff --git a/rust/gateway/src/eventloop.rs b/rust/gateway/src/eventloop.rs index 619de1d26..fee544999 100644 --- a/rust/gateway/src/eventloop.rs +++ b/rust/gateway/src/eventloop.rs @@ -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) { 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, }), }), ); diff --git a/website/src/components/Changelog/Gateway.tsx b/website/src/components/Changelog/Gateway.tsx index 7d320c053..7c191b31f 100644 --- a/website/src/components/Changelog/Gateway.tsx +++ b/website/src/components/Changelog/Gateway.tsx @@ -15,6 +15,10 @@ export default function Gateway() { Separates CIDR and DNS resources filters, preventing filters from one applying to the other. + + Implements support for the new control protocol; delivering faster + and more robust connection establishment. +