diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f1828d2d9..2a726be21 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2216,6 +2216,7 @@ dependencies = [ "secrecy", "serde", "serde_json", + "sha2", "snownet", "socket-factory", "socket2", diff --git a/rust/connlib/clients/shared/src/eventloop.rs b/rust/connlib/clients/shared/src/eventloop.rs index 9e4925174..a8a4497c1 100644 --- a/rust/connlib/clients/shared/src/eventloop.rs +++ b/rust/connlib/clients/shared/src/eventloop.rs @@ -1,13 +1,17 @@ use crate::{callbacks::Callbacks, PHOENIX_TOPIC}; use anyhow::Result; -use connlib_model::ResourceId; +use connlib_model::{PublicKey, ResourceId}; use firezone_logging::{anyhow_dyn_err, err_with_src, telemetry_event}; -use firezone_tunnel::messages::{client::*, *}; +use firezone_tunnel::messages::client::{ + EgressMessages, FailReason, FlowCreated, FlowCreationFailed, GatewayIceCandidates, + GatewaysIceCandidates, IngressMessages, InitClient, +}; +use firezone_tunnel::messages::RelaysPresence; use firezone_tunnel::ClientTunnel; use phoenix_channel::{ErrorReply, OutboundRequestId, PhoenixChannel, PublicKeyParam}; use std::time::Instant; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::BTreeSet, io, net::IpAddr, task::{Context, Poll}, @@ -18,10 +22,8 @@ pub struct Eventloop { tunnel: ClientTunnel, callbacks: C, - portal: PhoenixChannel<(), IngressMessages, ReplyMessages, PublicKeyParam>, + portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>, rx: tokio::sync::mpsc::UnboundedReceiver, - - connection_intents: SentConnectionIntents, } /// Commands that can be sent to the [`Eventloop`]. @@ -37,7 +39,7 @@ impl Eventloop { pub(crate) fn new( tunnel: ClientTunnel, callbacks: C, - mut portal: PhoenixChannel<(), IngressMessages, ReplyMessages, PublicKeyParam>, + mut portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>, rx: tokio::sync::mpsc::UnboundedReceiver, ) -> Self { portal.connect(PublicKeyParam(tunnel.public_key().to_bytes())); @@ -45,7 +47,6 @@ impl Eventloop { Self { tunnel, portal, - connection_intents: SentConnectionIntents::default(), rx, callbacks, } @@ -143,29 +144,13 @@ where firezone_tunnel::ClientEvent::ConnectionIntent { connected_gateway_ids, resource, - .. - } => { - let id = self.portal.send( - PHOENIX_TOPIC, - EgressMessages::PrepareConnection { - resource_id: resource, - connected_gateway_ids, - }, - ); - self.connection_intents.register_new_intent(id, resource); - } - firezone_tunnel::ClientEvent::RequestAccess { - resource_id, - gateway_id, - maybe_domain, } => { self.portal.send( PHOENIX_TOPIC, - EgressMessages::ReuseConnection(ReuseConnection { - resource_id, - gateway_id, - payload: maybe_domain, - }), + EgressMessages::CreateFlow { + resource_id: resource, + connected_gateway_ids, + }, ); } firezone_tunnel::ClientEvent::ResourcesChanged { resources } => { @@ -181,40 +166,15 @@ where Vec::from_iter(config.ipv6_routes), ); } - firezone_tunnel::ClientEvent::RequestConnection { - gateway_id, - offer, - preshared_key, - resource_id, - maybe_domain, - } => { - self.portal.send( - PHOENIX_TOPIC, - EgressMessages::RequestConnection(RequestConnection { - gateway_id, - resource_id, - client_preshared_key: preshared_key, - client_payload: ClientPayload { - ice_parameters: offer, - domain: maybe_domain, - }, - }), - ); - } } } - fn handle_portal_event( - &mut self, - event: phoenix_channel::Event, - ) { + fn handle_portal_event(&mut self, event: phoenix_channel::Event) { match event { phoenix_channel::Event::InboundMessage { msg, .. } => { self.handle_portal_inbound_message(msg); } - phoenix_channel::Event::SuccessResponse { res, req_id, .. } => { - self.handle_portal_success_reply(res, req_id); - } + phoenix_channel::Event::SuccessResponse { res: (), .. } => {} phoenix_channel::Event::ErrorResponse { res, req_id, topic } => { self.handle_portal_error_reply(res, topic, req_id); } @@ -283,52 +243,23 @@ where ) } } - } - } - - fn handle_portal_success_reply(&mut self, res: ReplyMessages, req_id: OutboundRequestId) { - match res { - ReplyMessages::Connect(Connect { - gateway_payload: - GatewayResponse::ConnectionAccepted(ConnectionAccepted { ice_parameters, .. }), - gateway_public_key, + IngressMessages::FlowCreated(FlowCreated { resource_id, - .. - }) => { - if let Err(e) = self.tunnel.state_mut().accept_answer( - ice_parameters, - resource_id, - gateway_public_key.0.into(), - Instant::now(), - ) { - tracing::warn!(error = anyhow_dyn_err(&e), "Failed to accept connection"); - } - } - ReplyMessages::Connect(Connect { - gateway_payload: GatewayResponse::ResourceAccepted(ResourceAccepted { .. }), - .. - }) => { - tracing::trace!("Connection response received, ignored as it's deprecated") - } - ReplyMessages::ConnectionDetails(ConnectionDetails { gateway_id, - resource_id, site_id, - .. + gateway_public_key, + preshared_key, + client_ice_credentials, + gateway_ice_credentials, }) => { - let should_accept = self - .connection_intents - .handle_connection_details_received(req_id, resource_id); - - if !should_accept { - tracing::debug!(%resource_id, "Ignoring stale connection details"); - return; - } - - match self.tunnel.state_mut().on_routing_details( + match self.tunnel.state_mut().handle_flow_created( resource_id, gateway_id, + PublicKey::from(gateway_public_key.0), site_id, + preshared_key, + client_ice_credentials, + gateway_ice_credentials, Instant::now(), ) { Ok(Ok(())) => {} @@ -349,6 +280,16 @@ where } }; } + IngressMessages::FlowCreationFailed(FlowCreationFailed { + resource_id, + reason: FailReason::Offline, + .. + }) => { + self.tunnel.state_mut().set_resource_offline(resource_id); + } + IngressMessages::FlowCreationFailed(FlowCreationFailed { reason, .. }) => { + tracing::warn!("Failed to create flow: {reason:?}") + } } } @@ -359,130 +300,15 @@ where req_id: OutboundRequestId, ) { match res { - ErrorReply::Offline => { - let Some(offline_resource) = self.connection_intents.handle_error(req_id) else { - return; - }; - - tracing::debug!(resource_id = %offline_resource, "Resource is offline"); - - self.tunnel - .state_mut() - .set_resource_offline(offline_resource); - } - ErrorReply::Disabled => { tracing::debug!(%req_id, "Functionality is disabled"); } ErrorReply::UnmatchedTopic => { self.portal.join(topic, ()); } - reason @ (ErrorReply::InvalidVersion | ErrorReply::NotFound | ErrorReply::Other) => { + reason @ (ErrorReply::InvalidVersion | ErrorReply::Other) => { tracing::debug!(%req_id, %reason, "Request failed"); } } } } - -#[derive(Default)] -struct SentConnectionIntents { - inner: BTreeMap, -} - -impl SentConnectionIntents { - fn register_new_intent(&mut self, id: OutboundRequestId, resource: ResourceId) { - self.inner.insert(id, resource); - } - - /// To be called when we receive the connection details for a particular resource. - /// - /// Returns whether we should accept them. - fn handle_connection_details_received( - &mut self, - reference: OutboundRequestId, - r: ResourceId, - ) -> bool { - let has_more_recent_intent = self - .inner - .iter() - .any(|(req, resource)| req > &reference && resource == &r); - - if has_more_recent_intent { - return false; - } - - let has_intent = self - .inner - .get(&reference) - .is_some_and(|resource| resource == &r); - - if !has_intent { - return false; - } - - self.inner.retain(|_, v| v != &r); - - true - } - - fn handle_error(&mut self, req: OutboundRequestId) -> Option { - self.inner.remove(&req) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn discards_old_connection_intent() { - let mut intents = SentConnectionIntents::default(); - - let resource = ResourceId::random(); - - intents.register_new_intent(OutboundRequestId::for_test(1), resource); - intents.register_new_intent(OutboundRequestId::for_test(2), resource); - - let should_accept = - intents.handle_connection_details_received(OutboundRequestId::for_test(1), resource); - - assert!(!should_accept); - } - - #[test] - fn allows_unrelated_intents() { - let mut intents = SentConnectionIntents::default(); - - let resource1 = ResourceId::random(); - let resource2 = ResourceId::random(); - - intents.register_new_intent(OutboundRequestId::for_test(1), resource1); - intents.register_new_intent(OutboundRequestId::for_test(2), resource2); - - let should_accept_1 = - intents.handle_connection_details_received(OutboundRequestId::for_test(1), resource1); - let should_accept_2 = - intents.handle_connection_details_received(OutboundRequestId::for_test(2), resource2); - - assert!(should_accept_1); - assert!(should_accept_2); - } - - #[test] - fn handles_out_of_order_responses() { - let mut intents = SentConnectionIntents::default(); - - let resource = ResourceId::random(); - - intents.register_new_intent(OutboundRequestId::for_test(1), resource); - intents.register_new_intent(OutboundRequestId::for_test(2), resource); - - let should_accept_2 = - intents.handle_connection_details_received(OutboundRequestId::for_test(2), resource); - let should_accept_1 = - intents.handle_connection_details_received(OutboundRequestId::for_test(1), resource); - - assert!(should_accept_2); - assert!(!should_accept_1); - } -} diff --git a/rust/connlib/clients/shared/src/lib.rs b/rust/connlib/clients/shared/src/lib.rs index f4068220d..d5973ca7d 100644 --- a/rust/connlib/clients/shared/src/lib.rs +++ b/rust/connlib/clients/shared/src/lib.rs @@ -3,9 +3,7 @@ pub use crate::serde_routelist::{V4RouteList, V6RouteList}; pub use callbacks::{Callbacks, DisconnectError}; pub use connlib_model::StaticSecret; pub use eventloop::Eventloop; -pub use firezone_tunnel::messages::client::{ - ResourceDescription, {IngressMessages, ReplyMessages}, -}; +pub use firezone_tunnel::messages::client::{IngressMessages, ResourceDescription}; use connlib_model::ResourceId; use eventloop::Command; @@ -41,7 +39,7 @@ impl Session { tcp_socket_factory: Arc>, udp_socket_factory: Arc>, callbacks: CB, - portal: PhoenixChannel<(), IngressMessages, ReplyMessages, PublicKeyParam>, + portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>, handle: tokio::runtime::Handle, ) -> Self { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); @@ -118,7 +116,7 @@ async fn connect( tcp_socket_factory: Arc>, udp_socket_factory: Arc>, callbacks: CB, - portal: PhoenixChannel<(), IngressMessages, ReplyMessages, PublicKeyParam>, + portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>, rx: UnboundedReceiver, ) -> Result<(), phoenix_channel::Error> where diff --git a/rust/connlib/snownet/src/node.rs b/rust/connlib/snownet/src/node.rs index 3e444c7c5..f65b6ee8e 100644 --- a/rust/connlib/snownet/src/node.rs +++ b/rust/connlib/snownet/src/node.rs @@ -1001,11 +1001,6 @@ where Ok(params) } - /// Whether we have sent an [`Offer`] for this connection and are currently expecting an [`Answer`]. - pub fn is_expecting_answer(&self, id: TId) -> bool { - self.connections.initial.contains_key(&id) - } - /// Accept an [`Answer`] from the remote for a connection previously created via [`Node::new_connection`]. #[tracing::instrument(level = "info", skip_all, fields(%cid))] #[deprecated] diff --git a/rust/connlib/tunnel/Cargo.toml b/rust/connlib/tunnel/Cargo.toml index eed0d5bbd..6a732fd0b 100644 --- a/rust/connlib/tunnel/Cargo.toml +++ b/rust/connlib/tunnel/Cargo.toml @@ -48,6 +48,7 @@ firezone-relay = { workspace = true, features = ["proptest"] } ip-packet = { workspace = true, features = ["proptest"] } proptest-state-machine = { workspace = true } rand = { workspace = true } +sha2 = { workspace = true } test-case = { workspace = true } test-strategy = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/rust/connlib/tunnel/proptest-regressions/tests.txt b/rust/connlib/tunnel/proptest-regressions/tests.txt index 6a5ab964c..348079c33 100644 --- a/rust/connlib/tunnel/proptest-regressions/tests.txt +++ b/rust/connlib/tunnel/proptest-regressions/tests.txt @@ -131,3 +131,7 @@ cc 13e9996493813f93b3a4806aa1ef9c7c6a1da9f88f53901de2cada0b9bccddb0 cc 04e8c43a4ca60b44b4ead8fef8a9254fac53707dfc7e1bd146de1dfb702d6e00 # shrinks to (ReferenceState { client: Host { inner: RefClient { id: d98fa678-95af-030e-701d-874b68359c70, key: PrivateKey("99061ff43532599ccf514e8485fdb80cd861904e511413d9dae11abd0fc9b3d3"), known_hosts: {"api.firez.one": [::ffff:125.192.69.18], "api.firezone.dev": [::ffff:64.207.89.140]}, tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, ipv4_routes: {}, ipv6_routes: {} }, ip4: Some(203.0.113.20), ip6: Some(2001:db80::57), default_port: 22063, latency: 94ms }, gateways: {34ed48e4-dc9c-29dc-3c40-ea9a883b2301: Host { inner: RefGateway { key: PrivateKey("abcf4ebb37ca5a1a01db095055fa5841bc4c041d46ba94ef68b3cf19fdb5dad2") }, ip4: Some(203.0.113.61), ip6: Some(2001:db80::20), default_port: 32711, latency: 59ms }, ef82a02c-5f45-9726-0f82-941eb5e0c3b4: Host { inner: RefGateway { key: PrivateKey("8ed469148037c5e1fd244e58eb1517289e773aeb4565f28d1b92829caf47c213") }, ip4: Some(203.0.113.32), ip6: Some(2001:db80::34), default_port: 2019, latency: 152ms }, fe13eb71-9b73-995a-eadf-91c49ee91aaa: Host { inner: RefGateway { key: PrivateKey("223f44cc132d0cd766e3205c337cfa124fd7360f44df4465f960d036b6aaecf1") }, ip4: Some(203.0.113.47), ip6: Some(2001:db80::10), default_port: 19774, latency: 113ms }}, relays: {5c4865ec-e0c2-919b-57d1-8479c245fd6c: Host { inner: 14160162637930317615, ip4: Some(203.0.113.2), ip6: Some(2001:db80::1c), default_port: 3478, latency: 37ms }, 9e03d224-2bcd-a7f5-a798-b84c8ac01f37: Host { inner: 12978646275473331028, ip4: Some(203.0.113.86), ip6: Some(2001:db80::4), default_port: 3478, latency: 42ms }}, portal: StubPortal { gateways_by_site: {046d10cc-7f54-1c88-6081-6830e0cd3220: {34ed48e4-dc9c-29dc-3c40-ea9a883b2301, ef82a02c-5f45-9726-0f82-941eb5e0c3b4, fe13eb71-9b73-995a-eadf-91c49ee91aaa}}, cidr_resources: {45a8c42c-8efc-bdb3-d9d0-7291cd937bab: CidrResource { id: 45a8c42c-8efc-bdb3-d9d0-7291cd937bab, address: V4(Ipv4Network { network_address: 7.39.221.236, netmask: 30 }), name: "khfbxt", address_description: Some("pmhuy"), sites: [Site { id: 046d10cc-7f54-1c88-6081-6830e0cd3220, name: "utpydunjwm" }] }, d42bae47-9f32-8460-43c8-184f83465685: CidrResource { id: d42bae47-9f32-8460-43c8-184f83465685, address: V6(Ipv6Network { network_address: ::ffff:117.132.170.72, netmask: 126 }), name: "tzof", address_description: None, sites: [Site { id: 046d10cc-7f54-1c88-6081-6830e0cd3220, name: "utpydunjwm" }] }}, dns_resources: {1210e93d-8e24-b4fb-e9b0-d2ac1c8453f9: DnsResource { id: 1210e93d-8e24-b4fb-e9b0-d2ac1c8453f9, address: "**.bwnb.bzbo", name: "ffjjsov", address_description: Some("tyxajlydb"), sites: [Site { id: 046d10cc-7f54-1c88-6081-6830e0cd3220, name: "utpydunjwm" }] }}, internet_resource: InternetResource { name: "Internet Resource", id: b701cea8-6cbf-a9a2-5656-db8b6169c3c7, sites: [Site { id: 046d10cc-7f54-1c88-6081-6830e0cd3220, name: "utpydunjwm" }] } }, drop_direct_client_traffic: true, global_dns_records: DnsRecords { inner: {Name(jstcu.bwnb.bzbo.): {AllRecordData::A(A { addr: 198.51.100.33 }), AllRecordData::A(A { addr: 198.51.100.142 }), AllRecordData::A(A { addr: 198.51.100.206 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::54 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::93 })}, Name(mebr.bwnb.bzbo.): {AllRecordData::A(A { addr: 198.51.100.153 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::79 })}, Name(ztpyt.bwnb.bzbo.): {AllRecordData::Aaaa(Aaaa { addr: 2001:db80::f3 })}, Name(izubr.rett.ckim.): {AllRecordData::A(A { addr: 210.115.246.143 }), AllRecordData::A(A { addr: 255.22.147.87 }), AllRecordData::Aaaa(Aaaa { addr: c5cd:ff7c:f1c9:4f7b:4f0d:ce63:18cb:2e81 })}, Name(hkd.ica.httitw.): {AllRecordData::Txt(Txt("kcqsqnurxmodzngnujazfvespmnkxtclbgewjfwaauqwgbmygmdjoismypgewnplugcpofccrdnbeirqevruygnocdlxppttzxxsbjywpsjgwdqrcrsoraozbbjojfynswwwyfkpbtrynfqvqecrzomnrexosvzjvafuuefxajxqbdayvkkylmvacoytciaebwlgbdsfkprvaefvvdhhywnpjxdrwdvdpwbyiucpdmbbvawrzrkxdghipmukhhx" "qdkkzsrfdofpssipliyxwxurmsumhhgoxsshvwoelxyiscysuomerbdzogbcwyghxktczezaglzhobukznddtgpmfgkvyjezmrtipgtizjjjkvoeqywlmpzvjxrkzjnsprfjmowilpuihkiflpxaqhhwemfmhdlakfcralpoymuyzzxcttjlnoxmrsuxpyeypcebfpmkwjydjkzvlejbqkyylsoepsrmiksrlyjzeqosqcxjgwwyqpvalsmqokw" "opsxqqrqdvvomvrqlktyikeffzjijwnqmqlalkuekuewwvlojwwbqfmstdhsnmpooawcumgltpecyqjxzssrsmorhsinfolkvmlrwywvfnvbcncmhhfrjutrrcflngeqrdqtnvzodyhcxqyfbjwdmkbbxnlscaqkgwhodexaqfsarwsacpiibsqkqvfzqvuzyapdoqhtalfihqmasuwdzrgummyrmhybmlmoawsowctifdeazoveoljebhyiuyg" "epvstztutebpnjafuitvhsysmisfqlkaccknvgtfzdxcwmazkksmzuamjmmanofyhrtqyjyymjadokqgyekoickaaijzgzwomhntpydnrjfqzbynjrvqpwicdlfardqrapdkvnnnpmwjelcvomxwtymhvwiblafqgezhmzvgxkdlfmhcznjjbrbeieyilousaxcjqvhythijzhcwcoxcskqsjxxfiajhxpibpebyjwubltihivjnwuqbjfwckld" "knodnlqabujwwlptqaqzxyoafydtvligjrvhyhuhtemvjfbxkkitcugxoxjshpwxjibxesvlrkgvomldqstkibufecvnnfyhudvvebjujxtwjsfuvrmvslixjkkjynifgpiddhinvyzfwmkxpssfcsplosjgoiccxsxrxjzwobajyqzwpdintbqgmqqakdjffmetefnevutpfwbecgotcnopcwqcwwzulfluvmklchjdoqgupmiapmxussezbee" "vnlxottifnafenhcldmbjagtidxiwgcoxabotpomctvozdagamruzwqkhoecczxfyspxgbihdpsguurkeglayjiowkuthscqfssdpistulntfnaizhwtgiiqdejenfhsmnqgaxhwxxlbcpgvzjbixtooubwsjknakznqxztgapwnlfdgpoxkqdrwdfhqkidavjzfqiwszuqonloumctpnpsrtaeqvodahkjshvzsiganibbwkjylzgdkfzyaqoy")), AllRecordData::Txt(Txt("xiratdovbibmaqerksyalhzxceytnqxfaybizfsnkwsehkohrpyjimstgpavbvhbgtbqlwmpssigygufqpriryhtxjcablaycbiqrjroqzrpxrbjipmkvvrnhpnaqcgoqedcnoeycgwiyhcltylnzwuouomlqdnauoegkxewaxsjdiioimkdfeorxrxpfrapgrterkheaeurspbcofsnyysoiehmcpyaybgcagpglyfxuwxxldidmobfcosblmc" "dgdcgjyhlfrtgismotwelzvoplgqzktvzppqfnfwyxyaixdiyeqgwouxebmamxwjnamdcjqizojjcbcghpfxijjlgjsmdgrrsmnpriverzdzqbztffthlhrccwgkkomljzghzntxuinkbwkxiuaxcoyotrrdmluxttwavohozhylbkljabpaghrlbzobcjkogfqimaygxalkkhbfsmsynhsawqullvvdhpjuzvwoxmqznowctvdaiqhmoizdnjh" "arousmfdzdplpllvggjkzftzcasqdvkqfkabtqfljiiukcxnvmmhwikolvpawoicwuctegrfwlllixjfhbbzlfjodudlmbtluhomujzndheaeopaljvnbqgflyobjiitkchhnthcxejtuazdqbfhfndkguvkyqezgcpdcztfokcfcecasubmlbvszlcaqkpanospbnrktsfukofyrcsajudjtsubwvwiutzmpcknppisrsibhwqobnxaeuiawuv" "hkvicplvzhoxeeizvfxyqkgcggaeduearplviqwpdxdeuizbcufsxayuasijyjsffvsvqrowyqfapikvkjlhwqzctdpqnjhnmlbkeftjxpfzklipywkczmnodfkybtdyjkulpbwpytgohyaffbwfciezyvwdecikjgujnksshiyovehdlpjjdoshuijorjqpswnkvtxjvovgfoisdwacdzadjxajdjwwphlcqeqlvzvatssssresevkhspnvvoy" "puczwvewihpulwgsvpynfyajythwmsyycnhxrtdnccnnalnrewxdaifsfhylgvbzgbkqexapogksmvjvrdzdpvrntisxzhuaylfggkdltrguiomusjnppatyxesezmlfvhajbpzgzwsgvtdsvpgybjzxqojpkzbmcgvobhkkmophacwkilnvaxiqtbbcyhndlnkoxxbptmnprbtrvqocyjoovmabtluarptazkihhysejdbmytrtflrfxvymjtm" "vqgcdbtyuhdaincimktclgypklrjjyofjifigkzmrhorcaypomtxxgfnhtyugvlanicggidhotaksvpleooxbtwbxetvqdootisznjuybohveetwaookzkpwfyilkiszlsocpxmzzsoszglgvppniscefaqtnugtvvwyljgxsidmclnwsjsxvmlsufwudjhvpdkcvpcjlegspujacftjntaljnmjskdksflygxuygualjxwztybcpysldilchpj" "hvsgoouxgoekrcagliudngfhelfisoipjqrhiruqdwecihtajtitgommisiisjwgkadqanmwknzgmrilxdcnirjzyxwktqhwdyttippzpjaxjzwsnompgqyltyutpkzcpfacbklrtdrvjejjiqtiaajsxcrbxxpdjwzsxzhzgcvwmfllqheznlfzxfpttbffgiuaubjjgykxotepgsmjzozejjvmpzrmgvhujtjlhvclgchxrtjeyyffoglwkev"))}, Name(yqfrx.uyct.jeka.): {AllRecordData::Aaaa(Aaaa { addr: ::ffff:92.135.140.191 }), AllRecordData::Aaaa(Aaaa { addr: cb97:f4ea:bb16:681c:7352:ec3c:2ca3:48c7 })}, Name(utvigg.yzw.rdfi.): {AllRecordData::Aaaa(Aaaa { addr: 8804:102b:ae1c:b63:898d:782a:1819:c8d2 })}} }, network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.2, netmask: 32 }), Relay(5c4865ec-e0c2-919b-57d1-8479c245fd6c)), (V4(Ipv4Network { network_address: 203.0.113.20, netmask: 32 }), Client(d98fa678-95af-030e-701d-874b68359c70)), (V4(Ipv4Network { network_address: 203.0.113.32, netmask: 32 }), Gateway(ef82a02c-5f45-9726-0f82-941eb5e0c3b4)), (V4(Ipv4Network { network_address: 203.0.113.47, netmask: 32 }), Gateway(fe13eb71-9b73-995a-eadf-91c49ee91aaa)), (V4(Ipv4Network { network_address: 203.0.113.61, netmask: 32 }), Gateway(34ed48e4-dc9c-29dc-3c40-ea9a883b2301)), (V4(Ipv4Network { network_address: 203.0.113.86, netmask: 32 }), Relay(9e03d224-2bcd-a7f5-a798-b84c8ac01f37)), (V6(Ipv6Network { network_address: 2001:db80::4, netmask: 128 }), Relay(9e03d224-2bcd-a7f5-a798-b84c8ac01f37)), (V6(Ipv6Network { network_address: 2001:db80::10, netmask: 128 }), Gateway(fe13eb71-9b73-995a-eadf-91c49ee91aaa)), (V6(Ipv6Network { network_address: 2001:db80::1c, netmask: 128 }), Relay(5c4865ec-e0c2-919b-57d1-8479c245fd6c)), (V6(Ipv6Network { network_address: 2001:db80::20, netmask: 128 }), Gateway(34ed48e4-dc9c-29dc-3c40-ea9a883b2301)), (V6(Ipv6Network { network_address: 2001:db80::34, netmask: 128 }), Gateway(ef82a02c-5f45-9726-0f82-941eb5e0c3b4)), (V6(Ipv6Network { network_address: 2001:db80::57, netmask: 128 }), Client(d98fa678-95af-030e-701d-874b68359c70))} } }, [ActivateResource(Internet(InternetResource { name: "Internet Resource", id: b701cea8-6cbf-a9a2-5656-db8b6169c3c7, sites: [Site { id: 046d10cc-7f54-1c88-6081-6830e0cd3220, name: "utpydunjwm" }] })), SendDnsQueries([DnsQuery { domain: Name(mebr.bwnb.bzbo.), r_type: Rtype::A, query_id: 55, dns_server: 108.147.189.249:53, transport: Tcp }]), SendDnsQueries([DnsQuery { domain: Name(hkd.ica.httitw.), r_type: Rtype::TXT, query_id: 16726, dns_server: [::ffff:127.0.0.1]:53, transport: Udp }])], None) cc c993cdb2e051b4c654e513ce5f0cfa07978b16e2e1c1634875c4e7bd1604e6e0 cc 71d10f85bd4b49d380d6289461a6512f8273ee19d38122c664cb2eed6bda670e # shrinks to (ReferenceState { client: Host { inner: RefClient { id: c56de62c-e968-add6-b542-8ea9f7277003, key: PrivateKey("3cb4964251b03e18c0ff39ab565390b898fbb1617f4afc92baacadec1b46b549"), known_hosts: {"api.firezone.dev": [::ffff:71.48.84.167]}, tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, ipv4_routes: {}, ipv6_routes: {} }, ip4: None, ip6: Some(2001:db80::37), default_port: 61570, latency: 262ms }, gateways: {495ccc0b-860d-6f55-f65c-ccbb4cf00d8a: Host { inner: RefGateway { key: PrivateKey("0ed3228d14568d569833155194a09d3e7217fffad6c34bc36041c4dbbd9c4ac6") }, ip4: Some(203.0.113.10), ip6: Some(2001:db80::4b), default_port: 34733, latency: 11ms }}, relays: {53315a14-8c3e-1891-cb7f-ea7a5eaccca5: Host { inner: 11854331133322978157, ip4: Some(203.0.113.46), ip6: Some(2001:db80::49), default_port: 3478, latency: 46ms }, 72beb197-1f3f-db97-a98a-218fa6d759e1: Host { inner: 2086356341284431221, ip4: Some(203.0.113.100), ip6: Some(2001:db80::5d), default_port: 3478, latency: 14ms }}, portal: StubPortal { gateways_by_site: {cce06b37-6659-548e-20e8-dfcf4df7d294: {495ccc0b-860d-6f55-f65c-ccbb4cf00d8a}}, cidr_resources: {e4778fa5-597e-9ac0-2c07-80a18dbcb2b1: CidrResource { id: e4778fa5-597e-9ac0-2c07-80a18dbcb2b1, address: V6(Ipv6Network { network_address: c4bc:643d:d5be:3df5:dbf9:c737:7e10:3840, netmask: 122 }), name: "euaxfjnje", address_description: Some("unlmk"), sites: [Site { id: cce06b37-6659-548e-20e8-dfcf4df7d294, name: "kibkdh" }] }}, dns_resources: {026c7444-461e-9d96-def0-d5e63c91bd5a: DnsResource { id: 026c7444-461e-9d96-def0-d5e63c91bd5a, address: "**.likyrd.eqxit", name: "iscj", address_description: None, sites: [Site { id: cce06b37-6659-548e-20e8-dfcf4df7d294, name: "kibkdh" }] }, 1cfb539c-969d-3352-ac16-63b896261128: DnsResource { id: 1cfb539c-969d-3352-ac16-63b896261128, address: "**.pqao.bxn", name: "kafatj", address_description: None, sites: [Site { id: cce06b37-6659-548e-20e8-dfcf4df7d294, name: "kibkdh" }] }, 54b10b9e-98f0-ab9a-e797-3d2c7c9871ae: DnsResource { id: 54b10b9e-98f0-ab9a-e797-3d2c7c9871ae, address: "vhndgw.bmabh", name: "eyjajonjmd", address_description: Some("wbog"), sites: [Site { id: cce06b37-6659-548e-20e8-dfcf4df7d294, name: "kibkdh" }] }}, internet_resource: InternetResource { name: "Internet Resource", id: cfcb1140-5a2a-323b-73a6-6432a2a7251d, sites: [Site { id: cce06b37-6659-548e-20e8-dfcf4df7d294, name: "kibkdh" }] } }, drop_direct_client_traffic: true, global_dns_records: DnsRecords { inner: {Name(vhndgw.bmabh.): {AllRecordData::Aaaa(Aaaa { addr: 2001:db80::58 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::93 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::d2 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::e6 })}, Name(vdwb.pqao.bxn.): {AllRecordData::A(A { addr: 198.51.100.151 }), AllRecordData::A(A { addr: 198.51.100.176 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::76 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::87 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::ea })}, Name(qkdjh.likyrd.eqxit.): {AllRecordData::Aaaa(Aaaa { addr: 2001:db80::47 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::bf }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::d1 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::f8 })}, Name(qsr.likyrd.eqxit.): {AllRecordData::A(A { addr: 198.51.100.174 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::1 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::17 }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::7b }), AllRecordData::Aaaa(Aaaa { addr: 2001:db80::ad })}, Name(ons.neg.): {AllRecordData::A(A { addr: 127.0.0.1 }), AllRecordData::Txt(Txt("pctnrlymyfembfmggzufrrhvmoadndipdsfpolsqrtpyltkomqdikkaxvcjhiltglvpflgkfyczubshbxcwxwxupetnznqliofpwigttocsyxxprirydsgnncphwtskhlrcvtnebpnwzqkpcclkdfsgddevzsdsbptgtljbtnykovpygpelhsiwexdvcrhttdvkshxhemygwvbumdcpqcivqsirhnkwbketwfgeurbekqkfkmwfurjtqhkomzpr" "ydyqtctfegfheglivcjaurwllpouylnhaivawiykyrxhjrccpxktngitvkioqhahnvcucnlslfnprbwmwwfawdbexsnbflxvmmcuohmgewhkjhgupugytaldyzohsddlstzhcpagdrexkftfkgcnljtkhrhlhnhkcvkwecrlkpmpjjuzrqayppvsjwyivcqyozczgsgqkzyhwzyqfihyprapcpljufbzvaehlsyzftnvuaankrtsgcwzmkaqbgg" "djemdqavrdcmeohamepdqkvhupylzarxsooaddlptvxaodcwvcyruiyqvidvwqehgchocbyryyyeebmrfikhvkjjfkcycnjwfhqxpobpbqhdcnpynicojvycmktuvkjytgbcxjzvrcouvfmkbervzmtjeuddbgglfcugacrzcxvjpuvirbhixprmtnolmaltxvwabsvhsnokfsopgerhkbikatmsblytsfhfopxdfvzbupdgvxlvjudyavgpdmw" "jpczvouekbdarziwpbtczuqxdnsrlmskkodigxvsrkadwcguozzqyztacqfgogmacneqwlarokrsuogvyblkcwwtsgehqkfzebzevbitjeqavrrqgthhymnczhkueeahrrrbohmrpytvkobpuzuwfophrcwqvdpawdjawedchqyfyiychczvftyyaveialicpcdomawgkfnggcpdcnspqucwijzqqbhccfhmzhybymefqxrkxficlcpfjvyognn" "exsgbrtzxgrsdaantxhpuowkjxxcmbuiwszicndcijmcrhoagubkpfbmeuvvarsdbvlnkzgrssaniraokmpjibrmhtdrskxyavybdhbdfwrvcsprnkxpqpyhrxadxacmopebbtqzzlgqbxdokeovdiisqmjsdlnpbroaqrjovyqlfyytjycgoauhxgdnhcuuxulphxamtgawhysvmbttfwoekjrmejxojsmchwvpotvipbknwxpwxvyekwtovud" "gnubhqcezgksarbihcsjplkvbgivcaaxjlwsoyrdtauvwfzdcrvtgtmywokrehokmgnloautfmpzoauoznfytbvwkzqtqowauwihfzdcruuspoqnfuhaazjhdqsqtlmrwpqkeyoxudkfwhqvmdacuzztmcobkiptefhluavidswpfgguxkozwedtvvgsrkcbysaznihkhogxitjqytoyquskqxfhgyytbtrxwqckwxacqmkeaczbeqhnkukwnjx" "wukhmskhgueadpgvjyfhcurknuvbkvdlxxuvuimzjkneriprjmltdsutxoslrpiqxabhcknnwdgqrhmynjjgukwinbldpzlicvpdhtgxwjwhljpcamzdavnereetqokmdytqmjhluclmsohjvszcngeqqynrprijfacjschsqljioxopkdpejuouudundkjkqxlrnmgzjojellcrbbnbvjxdamuidjwqxncquokuzfzrberjcqprnczdykexoau" "ookracasppyyteqfqvuqclkcahwzyaghsufwktubrslbcjoilmtqbmgslmzuaohvvqmqjjqdtaczhvapxgihunujptahdywmkfuunxpafwnbizhbhgoykiyxcmcfueepmjmsfajxpdzyabkcvtquoryrjaslfdnhkcadpyfxhffplxvepsolxugsxucmbkfdjfsqrrtmuyybmddwismkamylffipmpsukvqniwtonyvcvigtjtkdnovddxqlwhv" "mhcbeaidhohuzulugdfqnydbqwbqwlraqdivdxopxivjzdwseltlzqkdrmwxxjkmhpnlfymntuummzgvaottfuienfcomafcnxdrpptdlfwytgvishcdbtaezejlcljaxyydlwakgosucntatgskfitdfqestrlhguvzlafqusucqslpdgsmkifpeghuwpkwljbkxtdhuxwlrixvqwmsflsshaqitcnwllvigsfjpcgskzklctobtstinrxrsas")), AllRecordData::Aaaa(Aaaa { addr: ::ffff:97.141.237.169 }), AllRecordData::Aaaa(Aaaa { addr: ::ffff:127.0.0.1 }), AllRecordData::Aaaa(Aaaa { addr: f732:f3e4:6b50:2ee9:2f47:a518:9438:1204 })}} }, unreachable_hosts: UnreachableHosts { inner: {2001:db80::17: Network, 2001:db80::58: Network, 2001:db80::87: Network, 2001:db80::93: Protocol, 2001:db80::e6: Network, 2001:db80::ea: Network} }, network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.10, netmask: 32 }), Gateway(495ccc0b-860d-6f55-f65c-ccbb4cf00d8a)), (V4(Ipv4Network { network_address: 203.0.113.46, netmask: 32 }), Relay(53315a14-8c3e-1891-cb7f-ea7a5eaccca5)), (V4(Ipv4Network { network_address: 203.0.113.100, netmask: 32 }), Relay(72beb197-1f3f-db97-a98a-218fa6d759e1)), (V6(Ipv6Network { network_address: 2001:db80::37, netmask: 128 }), Client(c56de62c-e968-add6-b542-8ea9f7277003)), (V6(Ipv6Network { network_address: 2001:db80::49, netmask: 128 }), Relay(53315a14-8c3e-1891-cb7f-ea7a5eaccca5)), (V6(Ipv6Network { network_address: 2001:db80::4b, netmask: 128 }), Gateway(495ccc0b-860d-6f55-f65c-ccbb4cf00d8a)), (V6(Ipv6Network { network_address: 2001:db80::5d, netmask: 128 }), Relay(72beb197-1f3f-db97-a98a-218fa6d759e1))} } }, [ActivateResource(Dns(DnsResource { id: 54b10b9e-98f0-ab9a-e797-3d2c7c9871ae, address: "vhndgw.bmabh", name: "eyjajonjmd", address_description: Some("wbog"), sites: [Site { id: cce06b37-6659-548e-20e8-dfcf4df7d294, name: "kibkdh" }] })), SendDnsQueries([DnsQuery { domain: Name(vhndgw.bmabh.), r_type: Rtype::AAAA, query_id: 12854, dns_server: [62fb:6ccd:149c:d214:fc65:f91:f4a4:526e]:53, transport: Udp }, DnsQuery { domain: Name(vdwb.pqao.bxn.), r_type: Rtype::AAAA, query_id: 13457, dns_server: [62fb:6ccd:149c:d214:fc65:f91:f4a4:526e]:53, transport: Tcp }]), SendIcmpPacket { src: fd00:2021:1111::, dst: DomainName { name: Name(vhndgw.bmabh.) }, seq: Seq(0), identifier: Identifier(0), payload: 0 }, SendTcpPayload { src: fd00:2021:1111::, dst: DomainName { name: Name(vhndgw.bmabh.) }, sport: SPort(0), dport: DPort(0), payload: 0 }], None) +cc 9ee5fca999d50cf96107b1b275a88af1b921f16bd487387a6389552dc1c5b5b4 +cc 1a01e04e8dbd861878590647c9d5a6b777a86dd36e299d4519a092f94a928dcd +cc d188ee5433064634b07dff90aeb3c26bd3698310bcc4d27f15f35a6296eb8687 +cc e60fe97280614a96052e1af1c8d3e4661ec2fead4d22106edf6fb5caf330b6a5 diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index f5284b03a..0dc009e38 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -5,14 +5,15 @@ pub(crate) use resource::{CidrResource, Resource}; pub(crate) use resource::{DnsResource, InternetResource}; use crate::dns::StubResolver; -use crate::messages::ResolveRequest; -use crate::messages::{DnsServer, Interface as InterfaceConfig, IpDnsServer, Key, Offer}; +use crate::messages::{DnsServer, Interface as InterfaceConfig, IpDnsServer}; +use crate::messages::{IceCredentials, SecretKey}; use crate::peer_store::PeerStore; -use crate::{dns, TunConfig}; +use crate::{dns, p2p_control, TunConfig}; use anyhow::Context; use bimap::BiMap; -use connlib_model::PublicKey; -use connlib_model::{GatewayId, RelayId, ResourceId, ResourceStatus, ResourceView}; +use connlib_model::{ + DomainName, GatewayId, PublicKey, RelayId, ResourceId, ResourceStatus, ResourceView, +}; use connlib_model::{Site, SiteId}; use firezone_logging::{ anyhow_dyn_err, err_with_src, telemetry_event, unwrap_or_debug, unwrap_or_warn, @@ -88,9 +89,14 @@ pub struct ClientState { node: ClientNode, /// All gateways we are connected to and the associated, connection-specific state. peers: PeerStore, - /// Which Resources we are trying to connect to. - awaiting_connection_details: HashMap, - + /// Tracks the flows to resources that we are currently trying to establish. + pending_flows: HashMap, + /// Tracks the domains for which we have set up a NAT per gateway. + /// + /// The IPs for DNS resources get assigned on the client. + /// In order to route them to the actual resource, the gateway needs to set up a NAT table. + /// Until the NAT is set up, packets sent to these resources are effectively black-holed. + dns_resource_nat_by_gateway: BTreeMap<(GatewayId, DomainName), DnsResourceNatState>, /// Tracks which gateway to use for a particular Resource. resources_gateways: HashMap, /// The site a gateway belongs to. @@ -144,10 +150,19 @@ pub struct ClientState { buffered_dns_queries: VecDeque, } -#[derive(Debug, Clone, PartialEq, Eq)] -struct AwaitingConnectionDetails { +enum DnsResourceNatState { + Pending { sent_at: Instant }, + Confirmed, +} + +impl DnsResourceNatState { + fn confirm(&mut self) { + *self = Self::Confirmed; + } +} + +struct PendingFlow { last_intent_sent_at: Instant, - domain: Option, } impl ClientState { @@ -157,7 +172,6 @@ impl ClientState { now: Instant, ) -> Self { Self { - awaiting_connection_details: Default::default(), resources_gateways: Default::default(), active_cidr_resources: IpNetworkTable::new(), resources_by_id: Default::default(), @@ -181,6 +195,8 @@ impl ClientState { tcp_dns_client: dns_over_tcp::Client::new(now, seed), tcp_dns_server: dns_over_tcp::Server::new(now), tcp_dns_sockets_by_upstream_and_query_id: Default::default(), + pending_flows: Default::default(), + dns_resource_nat_by_gateway: BTreeMap::new(), } } @@ -256,29 +272,113 @@ impl ClientState { self.node.public_key() } - fn request_access( - &mut self, - resource_ip: &IpAddr, - resource_id: ResourceId, - gateway_id: GatewayId, - ) { - let Some((fqdn, ips)) = self.stub_resolver.get_fqdn(resource_ip) else { - return; - }; - self.peers - .add_ips_with_resource(&gateway_id, ips.iter().copied(), &resource_id); - self.buffered_events.push_back(ClientEvent::RequestAccess { - resource_id, - gateway_id, - maybe_domain: Some(ResolveRequest { - name: fqdn.clone(), - proxy_ips: ips.clone(), - }), - }) + /// Updates the NAT for all domains resolved by the stub resolver on the corresponding gateway. + /// + /// In order to route traffic for DNS resources, the designated gateway needs to set up NAT from + /// the IPs assigned by the client's stub resolver and the actual IPs the domains resolve to. + /// + /// The corresponding control message containing the domain and IPs is sent over UDP through the tunnel. + /// UDP is unreliable, even through the WG tunnel, meaning we need our own way of making reliable. + /// The algorithm for that is simple: + /// 1. We track the timestamp when we've last sent the setup message. + /// 2. The message is designed to be idempotent on the gateway. + /// 3. If we don't receive a response within 2s and this function is called again, we send another message. + /// + /// The complexity of this function is O(N) with the number of resolved DNS resources. + fn update_dns_resource_nat(&mut self, now: Instant) { + use std::collections::btree_map::Entry; + + for (domain, rid, proxy_ips, gid) in + self.stub_resolver + .resolved_resources() + .map(|(domain, resource, proxy_ips)| { + let gateway = self.resources_gateways.get(resource); + + (domain, resource, proxy_ips, gateway) + }) + { + let Some(gid) = gid else { + tracing::trace!( + %domain, %rid, + "No gateway connected for resource, skipping DNS resource NAT setup" + ); + continue; + }; + + match self + .dns_resource_nat_by_gateway + .entry((*gid, domain.clone())) + { + Entry::Vacant(v) => { + self.peers + .add_ips_with_resource(gid, proxy_ips.iter().copied(), rid); + + v.insert(DnsResourceNatState::Pending { sent_at: now }); + } + Entry::Occupied(mut o) => match o.get_mut() { + DnsResourceNatState::Confirmed => continue, + DnsResourceNatState::Pending { sent_at } => { + let time_since_last_attempt = now.duration_since(*sent_at); + + if time_since_last_attempt < Duration::from_secs(2) { + continue; + } + + *sent_at = now; + } + }, + } + + let packet = match p2p_control::dns_resource_nat::assigned_ips( + *rid, + domain.clone(), + proxy_ips.clone(), + ) { + Ok(packet) => packet, + Err(e) => { + tracing::warn!( + error = anyhow_dyn_err(&e), + "Failed to create IP packet for `AssignedIp`s event" + ); + continue; + } + }; + + tracing::debug!(%gid, %domain, "Setting up DNS resource NAT"); + + let Some(transmit) = self + .node + .encapsulate(*gid, packet, now) + .inspect_err(|e| tracing::debug!(%gid, "Failed to encapsulate: {e}")) + .ok() + .flatten() + else { + continue; + }; + + self.buffered_transmits + .push_back(transmit.to_transmit().into_owned()); + } } - fn is_dns_resource(&self, resource: &ResourceId) -> bool { - matches!(self.resources_by_id.get(resource), Some(Resource::Dns(_))) + /// Clears the DNS resource NAT state for a given domain. + /// + /// Once cleared, this will trigger the client to submit another `AssignedIp`s event to the Gateway. + /// On the Gateway, such an event causes a new DNS resolution. + /// + /// We call this function every time a client issues a DNS query for a certain domain. + /// Coupling this behaviour together allows a client to refresh the DNS resolution of a DNS resource on the Gateway + /// through local DNS resolutions. + fn clear_dns_resource_nat_for_domain(&mut self, message: Message<&[u8]>) { + let Ok(question) = message.sole_question() else { + return; + }; + let domain = question.into_qname(); + + tracing::debug!(%domain, "Clearing DNS resource NAT"); + + self.dns_resource_nat_by_gateway + .retain(|(_, candidate), _| candidate != &domain); } fn is_cidr_resource_connected(&self, resource: &ResourceId) -> bool { @@ -334,6 +434,11 @@ impl ClientState { return None; } + if let Some(fz_p2p_control) = packet.as_fz_p2p_control() { + handle_p2p_control_packet(gid, fz_p2p_control, &mut self.dns_resource_nat_by_gateway); + return None; + } + let Some(peer) = self.peers.get_mut(&gid) else { tracing::error!(%gid, "Couldn't find connection by ID"); @@ -421,22 +526,26 @@ impl ClientState { return None; }; - // We read this here to prevent problems with the borrow checker - let is_dns_resource = self.is_dns_resource(&resource); - let Some(peer) = peer_by_resource_mut(&self.resources_gateways, &mut self.peers, resource) else { - self.on_not_connected_resource(resource, &dst, now); + self.on_not_connected_resource(resource, now); return None; }; - // Allowed IPs will track the IPs that we have sent to the gateway along with a list of ResourceIds - // for DNS resource we will send the IP one at a time. - if is_dns_resource && peer.allowed_ips.exact_match(dst).is_none() { - let gateway_id = peer.id(); - self.request_access(&dst, resource, gateway_id); - return None; - } + // TODO: Don't send packets unless we have a positive response for the DNS resource NAT. + + // TODO: Check DNS resource NAT state for the domain that the destination IP belongs to. + // Re-send if older than X. + + // if let Some((domain, _)) = self.stub_resolver.resolve_resource_by_ip(&dst) { + // if self + // .dns_resource_nat_by_gateway + // .get(&(peer.id(), domain.clone())) + // .is_some_and(|s| s.is_pending()) + // { + // self.update_dns_resource_nat(now); + // } + // } let gid = peer.id(); @@ -491,59 +600,29 @@ impl ClientState { self.drain_node_events(); } - #[tracing::instrument(level = "trace", skip_all, fields(%resource_id))] - #[expect(deprecated, reason = "Will be deleted together with deprecated API")] - pub fn accept_answer( - &mut self, - answer: impl Into, - resource_id: ResourceId, - gateway: PublicKey, - now: Instant, - ) -> anyhow::Result<()> { - let answer = answer.into(); - - debug_assert!(!self.awaiting_connection_details.contains_key(&resource_id)); - - let gateway_id = self - .gateway_by_resource(&resource_id) - .with_context(|| format!("No gateway associated with resource {resource_id}"))?; - - self.node.accept_answer(gateway_id, gateway, answer, now); - - Ok(()) - } - - /// Updates the "routing table". - /// - /// In a nutshell, this tells us which gateway in which site to use for the given resource. - #[tracing::instrument(level = "debug", skip_all, fields(%resource_id, %gateway_id))] - #[expect( - deprecated, - reason = "Will be refactored when deprecated control protocol is shipped" - )] - pub fn on_routing_details( + #[tracing::instrument(level = "debug", skip_all, fields(%gateway_id))] + #[expect(clippy::too_many_arguments)] + pub fn handle_flow_created( &mut self, resource_id: ResourceId, gateway_id: GatewayId, + gateway_key: PublicKey, site_id: SiteId, + preshared_key: SecretKey, + client_ice: IceCredentials, + gateway_ice: IceCredentials, now: Instant, ) -> anyhow::Result> { tracing::trace!("Updating resource routing table"); - let desc = self + let resource = self .resources_by_id .get(&resource_id) .context("Unknown resource")?; - if self.node.is_expecting_answer(gateway_id) { - return Ok(Ok(())); - } - - let awaiting_connection_details = self - .awaiting_connection_details + self.pending_flows .remove(&resource_id) - .context("No connection details found for resource")?; - let ips = get_addresses_for_awaiting_resource(desc, &awaiting_connection_details); + .context("No pending flow for resource")?; if let Some(old_gateway_id) = self.resources_gateways.insert(resource_id, gateway_id) { if self.peers.get(&old_gateway_id).is_some() { @@ -551,48 +630,39 @@ impl ClientState { } } + match self.node.upsert_connection( + gateway_id, + gateway_key, + Secret::new(preshared_key.expose_secret().0), + snownet::Credentials { + username: client_ice.username, + password: client_ice.password, + }, + snownet::Credentials { + username: gateway_ice.username, + password: gateway_ice.password, + }, + now, + ) { + Ok(()) => {} + Err(e) => return Ok(Err(e)), + }; + self.resources_gateways.insert(resource_id, gateway_id); self.gateways_site.insert(gateway_id, site_id); self.recently_connected_gateways.put(gateway_id, ()); - if self.peers.get(&gateway_id).is_some() { - self.peers - .add_ips_with_resource(&gateway_id, ips.into_iter(), &resource_id); - - self.buffered_events.push_back(ClientEvent::RequestAccess { - resource_id, - gateway_id, - maybe_domain: awaiting_connection_details.domain, - }); - return Ok(Ok(())); + if self.peers.get(&gateway_id).is_none() { + self.peers.insert(GatewayOnClient::new(gateway_id), &[]); }; - 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( - GatewayOnClient::new(gateway_id, &ips, HashSet::from([resource_id])), - &[], + // This only works for CIDR & Internet Resource. + self.peers.add_ips_with_resource( + &gateway_id, + resource.addresses().into_iter(), + &resource_id, ); - self.peers - .add_ips_with_resource(&gateway_id, ips.into_iter(), &resource_id); - self.buffered_events - .push_back(ClientEvent::RequestConnection { - gateway_id, - offer: Offer { - username: offer.credentials.username, - password: offer.credentials.password, - }, - preshared_key: Secret::new(Key(*offer.session_key.expose_secret())), - resource_id, - maybe_domain: awaiting_connection_details.domain, - }); + self.update_dns_resource_nat(now); Ok(Ok(())) } @@ -633,72 +703,53 @@ impl ClientState { } pub fn on_connection_failed(&mut self, resource: ResourceId) { - self.awaiting_connection_details.remove(&resource); - self.resources_gateways.remove(&resource); + self.pending_flows.remove(&resource); + let Some(disconnected_gateway) = self.resources_gateways.remove(&resource) else { + return; + }; + self.cleanup_connected_gateway(&disconnected_gateway); } #[tracing::instrument(level = "debug", skip_all, fields(%resource))] - fn on_not_connected_resource( - &mut self, - resource: ResourceId, - destination: &IpAddr, - now: Instant, - ) { + fn on_not_connected_resource(&mut self, resource: ResourceId, now: Instant) { debug_assert!(self.resources_by_id.contains_key(&resource)); - if self - .gateway_by_resource(&resource) - .is_some_and(|gateway_id| self.node.is_expecting_answer(gateway_id)) - { - tracing::debug!("Already connecting to gateway"); + match self.pending_flows.entry(resource) { + Entry::Vacant(v) => { + v.insert(PendingFlow { + last_intent_sent_at: now, + }); + } + Entry::Occupied(mut o) => { + let pending_flow = o.get_mut(); - return; - } - - match self.awaiting_connection_details.entry(resource) { - Entry::Occupied(mut occupied) => { - let time_since_last_intent = now.duration_since(occupied.get().last_intent_sent_at); + let time_since_last_intent = now.duration_since(pending_flow.last_intent_sent_at); if time_since_last_intent < Duration::from_secs(2) { tracing::trace!(?time_since_last_intent, "Skipping connection intent"); - return; } - occupied.get_mut().last_intent_sent_at = now; - } - Entry::Vacant(vacant) => { - vacant.insert(AwaitingConnectionDetails { - last_intent_sent_at: now, - // Note: in case of an overlapping CIDR resource this should be None instead of Some if the resource_id - // is for a CIDR resource. - // But this should never happen as DNS resources are always preferred, so we don't encode the logic here. - // Tests will prevent this from ever happening. - domain: self.stub_resolver.get_fqdn(destination).map(|(fqdn, ips)| { - ResolveRequest { - name: fqdn.clone(), - proxy_ips: ips.clone(), - } - }), - }); + pending_flow.last_intent_sent_at = now; } } tracing::debug!("Sending connection intent"); - // We tell the portal about all gateways we ever connected to, to encourage re-connecting us to the same ones during a session. - // The LRU cache visits them in MRU order, meaning a gateway that we recently connected to should still be preferred. - let connected_gateway_ids = self - .recently_connected_gateways - .iter() - .map(|(g, _)| *g) - .collect(); - self.buffered_events .push_back(ClientEvent::ConnectionIntent { resource, - connected_gateway_ids, - }); + connected_gateway_ids: self.connected_gateway_ids(), + }) + } + + // We tell the portal about all gateways we ever connected to, to encourage re-connecting us to the same ones during a session. + // The LRU cache visits them in MRU order, meaning a gateway that we recently connected to should still be preferred. + fn connected_gateway_ids(&self) -> BTreeSet { + self.recently_connected_gateways + .iter() + .map(|(g, _)| *g) + .collect() } pub fn gateway_by_resource(&self, resource: &ResourceId) -> Option { @@ -768,11 +819,14 @@ impl ClientState { self.dns_mapping.clone() } - #[tracing::instrument(level = "debug", skip_all, fields(gateway = %gateway_id))] - pub fn cleanup_connected_gateway(&mut self, gateway_id: &GatewayId) { - self.update_site_status_by_gateway(gateway_id, ResourceStatus::Unknown); - self.peers.remove(gateway_id); - self.resources_gateways.retain(|_, g| g != gateway_id); + #[tracing::instrument(level = "debug", skip_all, fields(gateway = %disconnected_gateway))] + fn cleanup_connected_gateway(&mut self, disconnected_gateway: &GatewayId) { + self.update_site_status_by_gateway(disconnected_gateway, ResourceStatus::Unknown); + self.peers.remove(disconnected_gateway); + self.resources_gateways + .retain(|_, g| g != disconnected_gateway); + self.dns_resource_nat_by_gateway + .retain(|(gateway, _), _| gateway != disconnected_gateway); } fn routes(&self) -> impl Iterator + '_ { @@ -802,6 +856,7 @@ impl ClientState { let maybe_dns_resource_id = self .stub_resolver .resolve_resource_by_ip(&destination) + .map(|(_, r)| *r) .filter(|resource| self.is_resource_enabled(resource)) .inspect( |resource| tracing::trace!(target: "tunnel_test_coverage", %destination, %resource, "Packet for DNS resource"), @@ -905,7 +960,7 @@ impl ClientState { // Check if have any pending TCP DNS queries. if let Some(query) = self.tcp_dns_server.poll_queries() { - self.handle_tcp_dns_query(query); + self.handle_tcp_dns_query(query, now); continue; } @@ -976,6 +1031,9 @@ impl ClientState { match self.stub_resolver.handle(message) { dns::ResolveStrategy::LocalResponse(response) => { + self.clear_dns_resource_nat_for_domain(response.for_slice_ref()); + self.update_dns_resource_nat(now); + unwrap_or_debug!( self.try_queue_udp_dns_response(upstream, source, &response), "Failed to queue UDP DNS response: {}" @@ -1007,7 +1065,7 @@ impl ClientState { ControlFlow::Break(()) } - fn handle_tcp_dns_query(&mut self, query: dns_over_tcp::Query) { + fn handle_tcp_dns_query(&mut self, query: dns_over_tcp::Query, now: Instant) { let message = query.message; let Some(upstream) = self.dns_mapping.get_by_left(&query.local.ip()) else { @@ -1018,6 +1076,9 @@ impl ClientState { match self.stub_resolver.handle(message.for_slice_ref()) { dns::ResolveStrategy::LocalResponse(response) => { + self.clear_dns_resource_nat_for_domain(response.for_slice_ref()); + self.update_dns_resource_nat(now); + unwrap_or_debug!( self.tcp_dns_server.send_message(query.socket, response), "Failed to send TCP DNS response: {}" @@ -1206,6 +1267,7 @@ impl ClientState { self.node.reset(); self.recently_connected_gateways.clear(); // Ensure we don't have sticky gateways when we roam. + self.dns_resource_nat_by_gateway.clear(); self.drain_node_events(); // Resetting the client will trigger a failed `QueryResult` for each one that is in-progress. @@ -1358,7 +1420,7 @@ impl ClientState { tracing::info!(%name, address, %sites, "Deactivating resource"); - self.awaiting_connection_details.remove(&id); + self.pending_flows.remove(&id); let Some(peer) = peer_by_resource_mut(&self.resources_gateways, &mut self.peers, id) else { return; @@ -1464,6 +1526,42 @@ fn parse_udp_dns_message<'b>(datagram: &UdpSlice<'b>) -> anyhow::Result, +) { + use p2p_control::dns_resource_nat; + + match fz_p2p_control.event_type() { + p2p_control::DOMAIN_STATUS_EVENT => { + let Ok(res) = dns_resource_nat::decode_domain_status(fz_p2p_control) + .inspect_err(|e| tracing::debug!("{e:#}")) + else { + return; + }; + + if res.status != dns_resource_nat::NatStatus::Active { + tracing::debug!(%gid, domain = %res.domain, "DNS resource NAT is not active"); + return; + } + + let Some(nat_state) = dns_resource_nat_by_gateway.get_mut(&(gid, res.domain.clone())) + else { + tracing::debug!(%gid, domain = %res.domain, "No DNS resource NAT state, ignoring response"); + return; + }; + + tracing::debug!(%gid, domain = %res.domain, "DNS resource NAT is active"); + + nat_state.confirm(); + } + code => { + tracing::debug!(code = %code.into_u8(), "Unknown control protocol"); + } + } +} + fn peer_by_resource_mut<'p>( resources_gateways: &HashMap, peers: &'p mut PeerStore, @@ -1475,28 +1573,6 @@ fn peer_by_resource_mut<'p>( Some(peer) } -fn get_addresses_for_awaiting_resource( - desc: &Resource, - awaiting_connection_details: &AwaitingConnectionDetails, -) -> Vec { - match desc { - Resource::Dns(_) => awaiting_connection_details - .domain - .as_ref() - .expect("for dns resources the awaiting connection should have an ip") - .proxy_ips - .iter() - .copied() - .map_into() - .collect_vec(), - Resource::Cidr(r) => vec![r.address], - Resource::Internet(_) => vec![ - Ipv4Network::DEFAULT_ROUTE.into(), - Ipv6Network::DEFAULT_ROUTE.into(), - ], - } -} - fn effective_dns_servers( upstream_dns: Vec, default_resolvers: Vec, @@ -1549,6 +1625,7 @@ fn sentinel_dns_mapping( }) .collect() } + /// Compares the given [`IpAddr`] against a static set of ignored IPs that are definitely not resources. fn is_definitely_not_a_resource(ip: IpAddr) -> bool { /// Source: https://en.wikipedia.org/wiki/Multicast_address#Notable_IPv4_multicast_addresses diff --git a/rust/connlib/tunnel/src/client/resource.rs b/rust/connlib/tunnel/src/client/resource.rs index ec679a713..bada70b9d 100644 --- a/rust/connlib/tunnel/src/client/resource.rs +++ b/rust/connlib/tunnel/src/client/resource.rs @@ -6,7 +6,7 @@ use connlib_model::{ CidrResourceView, DnsResourceView, InternetResourceView, ResourceId, ResourceStatus, ResourceView, Site, }; -use ip_network::IpNetwork; +use ip_network::{IpNetwork, Ipv4Network, Ipv6Network}; use itertools::Itertools as _; use crate::messages::client::{ @@ -131,6 +131,17 @@ impl Resource { } } + pub fn addresses(&self) -> Vec { + match self { + Resource::Dns(_) => vec![], + Resource::Cidr(c) => vec![c.address], + Resource::Internet(_) => vec![ + Ipv4Network::DEFAULT_ROUTE.into(), + Ipv6Network::DEFAULT_ROUTE.into(), + ], + } + } + pub fn with_status(self, status: ResourceStatus) -> ResourceView { match self { Resource::Dns(r) => ResourceView::Dns(r.with_status(status)), diff --git a/rust/connlib/tunnel/src/dns.rs b/rust/connlib/tunnel/src/dns.rs index 6995fce32..884482ff9 100644 --- a/rust/connlib/tunnel/src/dns.rs +++ b/rust/connlib/tunnel/src/dns.rs @@ -40,7 +40,7 @@ static DOH_CANARY_DOMAIN: LazyLock = LazyLock::new(|| { }); pub struct StubResolver { - fqdn_to_ips: HashMap>, + fqdn_to_ips: BTreeMap<(DomainName, ResourceId), Vec>, ips_to_fqdn: HashMap, ip_provider: IpProvider, /// All DNS resources we know about, indexed by the glob pattern they match against. @@ -166,22 +166,16 @@ impl StubResolver { /// /// Semantically, this is like a PTR query, i.e. we check whether we handed out this IP as part of answering a DNS query for one of our resources. /// This is in the hot-path of packet routing and must be fast! - pub(crate) fn resolve_resource_by_ip(&self, ip: &IpAddr) -> Option { - let (_, resource_id) = self.ips_to_fqdn.get(ip)?; - - Some(*resource_id) + pub(crate) fn resolve_resource_by_ip(&self, ip: &IpAddr) -> Option<&(DomainName, ResourceId)> { + self.ips_to_fqdn.get(ip) } - pub(crate) fn get_fqdn(&self, ip: &IpAddr) -> Option<(&DomainName, &Vec)> { - let (fqdn, _) = self.ips_to_fqdn.get(ip)?; - let ips = self.fqdn_to_ips.get(fqdn); - - debug_assert!( - ips.is_some(), - "fqdn_to_ips and ips_to_fqdn are inconsistent" - ); - - Some((fqdn, ips?)) + pub(crate) fn resolved_resources( + &self, + ) -> impl Iterator)> + '_ { + self.fqdn_to_ips + .iter() + .map(|((domain, resource), ips)| (domain, resource, ips)) } pub(crate) fn add_resource(&mut self, id: ResourceId, pattern: String) -> bool { @@ -221,7 +215,7 @@ impl StubResolver { fn get_or_assign_ips(&mut self, fqdn: DomainName, resource_id: ResourceId) -> Vec { let ips = self .fqdn_to_ips - .entry(fqdn.clone()) + .entry((fqdn.clone(), resource_id)) .or_insert_with(|| { let mut ips = self.ip_provider.get_n_ipv4(4); ips.extend_from_slice(&self.ip_provider.get_n_ipv6(4)); diff --git a/rust/connlib/tunnel/src/gateway.rs b/rust/connlib/tunnel/src/gateway.rs index 415bb3b87..aad43b5fd 100644 --- a/rust/connlib/tunnel/src/gateway.rs +++ b/rust/connlib/tunnel/src/gateway.rs @@ -1,6 +1,5 @@ -use crate::messages::{ - gateway::ResourceDescription, Answer, IceCredentials, ResolveRequest, SecretKey, -}; +use crate::messages::gateway::ResourceDescription; +use crate::messages::{Answer, IceCredentials, ResolveRequest, SecretKey}; use crate::utils::earliest; use crate::{p2p_control, GatewayEvent}; use crate::{peer::ClientOnGateway, peer_store::PeerStore}; @@ -8,7 +7,7 @@ use anyhow::{Context, Result}; use boringtun::x25519::PublicKey; use chrono::{DateTime, Utc}; use connlib_model::{ClientId, DomainName, RelayId, ResourceId}; -use firezone_logging::{anyhow_dyn_err, telemetry_span}; +use firezone_logging::anyhow_dyn_err; use ip_network::{Ipv4Network, Ipv6Network}; use ip_packet::{FzP2pControlSlice, IpPacket}; use secrecy::{ExposeSecret as _, Secret}; @@ -246,7 +245,7 @@ impl GatewayState { now, )?; - let result = self.allow_access(client_id, ipv4, ipv6, expires_at, resource, None, now); + let result = self.allow_access(client_id, ipv4, ipv6, expires_at, resource, None); debug_assert!( result.is_ok(), "`allow_access` should never fail without a `DnsResourceEntry`" @@ -255,26 +254,6 @@ impl GatewayState { Ok(()) } - pub fn refresh_translation( - &mut self, - client: ClientId, - resource_id: ResourceId, - name: DomainName, - resolved_ips: Vec, - now: Instant, - ) { - let _span = telemetry_span!("refresh_translation").entered(); - - let Some(peer) = self.peers.get_mut(&client) else { - return; - }; - - if let Err(e) = peer.refresh_translation(name.clone(), resource_id, resolved_ips, now) { - tracing::warn!(error = anyhow_dyn_err(&e), rid = %resource_id, %name, "Failed to refresh DNS resource IP translations"); - }; - } - - #[expect(clippy::too_many_arguments)] pub fn allow_access( &mut self, client: ClientId, @@ -283,7 +262,6 @@ impl GatewayState { expires_at: Option>, resource: ResourceDescription, dns_resource_nat: Option, - now: Instant, ) -> anyhow::Result<()> { let peer = self .peers @@ -296,9 +274,8 @@ impl GatewayState { peer.setup_nat( entry.domain, resource.id(), - &entry.resolved_ips, - entry.proxy_ips, - now, + BTreeSet::from_iter(entry.resolved_ips), + BTreeSet::from_iter(entry.proxy_ips), )?; } @@ -311,23 +288,25 @@ impl GatewayState { pub fn handle_domain_resolved( &mut self, req: ResolveDnsRequest, - addresses: Vec, + resolve_result: Result>, 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) + let nat_status = resolve_result + .and_then(|addresses| { + self.peers + .get_mut(&req.client) + .context("Unknown peer")? + .setup_nat( + req.domain.clone(), + req.resource, + BTreeSet::from_iter(addresses), + BTreeSet::from_iter(req.proxy_ips), + )?; + + Ok(dns_resource_nat::NatStatus::Active) + }) .unwrap_or_else(|e| { tracing::warn!( error = anyhow_dyn_err(&e), diff --git a/rust/connlib/tunnel/src/lib.rs b/rust/connlib/tunnel/src/lib.rs index 2a24321f1..86cf325d7 100644 --- a/rust/connlib/tunnel/src/lib.rs +++ b/rust/connlib/tunnel/src/lib.rs @@ -5,7 +5,6 @@ #![cfg_attr(test, allow(clippy::unwrap_used))] -use crate::messages::{Offer, ResolveRequest, SecretKey}; use bimap::BiMap; use chrono::Utc; use connlib_model::{ClientId, DomainName, GatewayId, PublicKey, ResourceId, ResourceView}; @@ -295,25 +294,6 @@ pub enum ClientEvent { resource: ResourceId, connected_gateway_ids: BTreeSet, }, - RequestAccess { - /// The resource we want to access. - resource_id: ResourceId, - /// The gateway we want to access the resource through. - gateway_id: GatewayId, - /// In the case of a DNS resource, its domain and the IPs we assigned to it. - maybe_domain: Option, - }, - RequestConnection { - /// The gateway we want to establish a connection to. - gateway_id: GatewayId, - /// The connection "offer". Contains our ICE credentials. - offer: Offer, - preshared_key: SecretKey, - /// The resource we want to access. - resource_id: ResourceId, - /// In the case of a DNS resource, its domain and the IPs we assigned to it. - maybe_domain: Option, - }, /// The list of resources has changed and UI clients may have to be updated. ResourcesChanged { resources: Vec, @@ -349,11 +329,6 @@ pub enum GatewayEvent { conn_id: ClientId, candidates: BTreeSet, }, - RefreshDns { - name: DomainName, - conn_id: ClientId, - resource_id: ResourceId, - }, ResolveDns(ResolveDnsRequest), } diff --git a/rust/connlib/tunnel/src/messages.rs b/rust/connlib/tunnel/src/messages.rs index eca9e9b04..3d847c0e0 100644 --- a/rust/connlib/tunnel/src/messages.rs +++ b/rust/connlib/tunnel/src/messages.rs @@ -2,9 +2,9 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use chrono::{serde::ts_seconds, DateTime, Utc}; -use connlib_model::{GatewayId, RelayId, ResourceId}; +use connlib_model::RelayId; use ip_network::IpNetwork; -use secrecy::{ExposeSecret, Secret}; +use secrecy::{ExposeSecret as _, Secret}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -46,56 +46,12 @@ impl PartialEq for Peer { } } -/// Represent a connection request from a client to a given resource. -/// -/// While this is a client-only message it's hosted in common since the tunnel -/// makes use of this message type. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct RequestConnection { - /// Gateway id for the connection - pub gateway_id: GatewayId, - /// Resource id the request is for. - pub resource_id: ResourceId, - /// The preshared key the client generated for the connection that it is trying to establish. - pub client_preshared_key: SecretKey, - pub client_payload: ClientPayload, -} - #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct ResolveRequest { pub name: DomainName, pub proxy_ips: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] -pub struct ClientPayload { - pub ice_parameters: Offer, - pub domain: Option, -} - -/// Represent a request to reuse an existing gateway connection from a client to a given resource. -/// -/// While this is a client-only message it's hosted in common since the tunnel -/// make use of this message type. -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct ReuseConnection { - /// Resource id the request is for. - pub resource_id: ResourceId, - /// Id of the gateway we want to reuse - pub gateway_id: GatewayId, - /// Payload that the gateway will receive - pub payload: Option, -} - -// Custom implementation of partial eq to ignore client_rtc_sdp -impl PartialEq for RequestConnection { - fn eq(&self, other: &Self) -> bool { - self.resource_id == other.resource_id - } -} - -impl Eq for RequestConnection {} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Answer { pub username: String, @@ -145,15 +101,9 @@ pub struct ConnectionAccepted { pub ice_parameters: Answer, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] -pub struct ResourceAccepted { - pub domain_response: DomainResponse, -} - #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub enum GatewayResponse { ConnectionAccepted(ConnectionAccepted), - ResourceAccepted(ResourceAccepted), } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)] diff --git a/rust/connlib/tunnel/src/messages/client.rs b/rust/connlib/tunnel/src/messages/client.rs index b94ef28bc..97293d6bf 100644 --- a/rust/connlib/tunnel/src/messages/client.rs +++ b/rust/connlib/tunnel/src/messages/client.rs @@ -1,12 +1,10 @@ //! Client related messages that are needed within connlib -use crate::messages::{ - GatewayResponse, Interface, Key, Relay, RelaysPresence, RequestConnection, ReuseConnection, -}; +use crate::messages::{IceCredentials, Interface, Key, Relay, RelaysPresence, SecretKey}; use connlib_model::{GatewayId, ResourceId, Site, SiteId}; use ip_network::IpNetwork; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeSet, net::IpAddr}; +use std::collections::BTreeSet; /// Description of a resource that maps to a DNS record. #[derive(Debug, Deserialize)] @@ -85,21 +83,46 @@ pub struct ConfigUpdate { pub interface: Interface, } -#[derive(Debug, Deserialize)] -pub struct ConnectionDetails { +#[derive(Debug, Deserialize, Clone)] +pub struct FlowCreated { pub resource_id: ResourceId, pub gateway_id: GatewayId, - pub gateway_remote_ip: IpAddr, + pub gateway_public_key: Key, #[serde(rename = "gateway_group_id")] pub site_id: SiteId, + pub preshared_key: SecretKey, + pub client_ice_credentials: IceCredentials, + pub gateway_ice_credentials: IceCredentials, } -#[derive(Debug, Deserialize)] -pub struct Connect { - pub gateway_payload: GatewayResponse, +#[derive(Debug, Deserialize, Clone)] +pub struct FlowCreationFailed { pub resource_id: ResourceId, - pub gateway_public_key: Key, - pub persistent_keepalive: u64, + pub reason: FailReason, + #[serde(default)] + pub violated_properties: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum FailReason { + NotFound, + Offline, + Forbidden, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ViolatedProperty { + RemoteIpLocationRegion, + RemoteIp, + ProviderId, + CurrentUtcDatetime, + ClientVerified, + #[serde(other)] + Unknown, } // These messages are the messages that can be received @@ -119,6 +142,9 @@ pub enum IngressMessages { ConfigChanged(ConfigUpdate), RelaysPresence(RelaysPresence), + + FlowCreated(FlowCreated), + FlowCreationFailed(FlowCreationFailed), } #[derive(Debug, Serialize)] @@ -137,25 +163,15 @@ pub struct GatewayIceCandidates { pub candidates: Vec, } -/// The replies that can arrive from the channel by a client -#[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum ReplyMessages { - ConnectionDetails(ConnectionDetails), - Connect(Connect), -} - // These messages can be sent from a client to a control pane #[derive(Debug, Serialize)] #[serde(rename_all = "snake_case", tag = "event", content = "payload")] // enum_variant_names: These are the names in the portal! pub enum EgressMessages { - PrepareConnection { + CreateFlow { resource_id: ResourceId, connected_gateway_ids: BTreeSet, }, - RequestConnection(RequestConnection), - ReuseConnection(ReuseConnection), /// Candidates that can be used by the addressed gateways. BroadcastIceCandidates(GatewaysIceCandidates), /// Candidates that should no longer be used by the addressed gateways. @@ -259,46 +275,12 @@ mod tests { } #[test] - fn can_deserialize_connect_reply() { - let json = r#"{ - "resource_id": "ea6570d1-47c7-49d2-9dc3-efff1c0c9e0b", - "gateway_public_key": "dvy0IwyxAi+txSbAdT7WKgf7K4TekhKzrnYwt5WfbSM=", - "gateway_payload": { - "ConnectionAccepted":{ - "domain_response":{ - "address":[ - "2607:f8b0:4008:804::200e", - "142.250.64.206" - ], - "domain":"google.com" - }, - "ice_parameters":{ - "username":"tGeqOjtGuPzPpuOx", - "password":"pMAxxTgHHSdpqHRzHGNvuNsZinLrMxwe" - } - } - }, - "persistent_keepalive": 25 - }"#; + fn can_deserialize_flow_created() { + let json = r#"{"event":"flow_created","ref":null,"topic":"client","payload":{"gateway_group_id":"ef42a07f-87d0-40da-baa7-e881e619ea1c","gateway_id":"d263d490-a0bb-452a-8990-01d27a1f1144","resource_id":"733e8d14-c18d-4931-af30-3639fa09c0c0","preshared_key":"anX2T9RH9mimT5Xd5+HqNGV0bfCodWDHQch1DLiFNls=","client_ice_credentials":{"username":"resc","password":"rqi3ibvfikfaxj3wgp7muh"},"gateway_ice_credentials":{"username":"jbi4","password":"a6oeevhlutevykcifd5r2a"},"gateway_public_key":"uMBCkAxTewfSgypIyxdQ18uCi84HLtKmQJy0wvQrYWY="}}"#; - let message = serde_json::from_str::(json).unwrap(); + let message = serde_json::from_str::(json).unwrap(); - assert!(matches!(message, ReplyMessages::Connect(_))) - } - - #[test] - fn can_deserialize_connection_details_reply() { - let json = r#" - { - "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" - }"#; - - let message = serde_json::from_str::(json).unwrap(); - - assert!(matches!(message, ReplyMessages::ConnectionDetails(_))); + assert!(matches!(message, IngressMessages::FlowCreated(_))); } #[test] @@ -415,12 +397,42 @@ mod tests { } #[test] - fn serialize_prepare_connection_message() { - let message = EgressMessages::PrepareConnection { + fn can_deserialize_unknown_flow_creation_failed_err() { + let json = r#"{"event":"flow_creation_failed","ref":null,"topic":"client","payload":{"resource_id":"f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3","reason":"foobar"}}"#; + + let message = serde_json::from_str::(json).unwrap(); + + assert!(matches!( + message, + IngressMessages::FlowCreationFailed(FlowCreationFailed { + reason: FailReason::Unknown, + .. + }) + )); + } + + #[test] + fn can_deserialize_known_flow_creation_failed_err() { + let json = r#"{"event":"flow_creation_failed","ref":null,"topic":"client","payload":{"resource_id":"f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3","reason":"offline"}}"#; + + let message = serde_json::from_str::(json).unwrap(); + + assert!(matches!( + message, + IngressMessages::FlowCreationFailed(FlowCreationFailed { + reason: FailReason::Offline, + .. + }) + )); + } + + #[test] + fn serialize_create_flow_message() { + let message = EgressMessages::CreateFlow { resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(), connected_gateway_ids: BTreeSet::new(), }; - let expected_json = r#"{"event":"prepare_connection","payload":{"resource_id":"f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3","connected_gateway_ids":[]}}"#; + let expected_json = r#"{"event":"create_flow","payload":{"resource_id":"f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3","connected_gateway_ids":[]}}"#; let actual_json = serde_json::to_string(&message).unwrap(); assert_eq!(actual_json, expected_json); diff --git a/rust/connlib/tunnel/src/messages/gateway.rs b/rust/connlib/tunnel/src/messages/gateway.rs index ad1a7aac2..000c3d08f 100644 --- a/rust/connlib/tunnel/src/messages/gateway.rs +++ b/rust/connlib/tunnel/src/messages/gateway.rs @@ -1,8 +1,8 @@ //! Gateway related messages that are needed within connlib use crate::messages::{ - GatewayResponse, IceCredentials, Interface, Key, Offer, Peer, Relay, RelaysPresence, - ResolveRequest, SecretKey, + GatewayResponse, IceCredentials, Interface, Key, Peer, Relay, RelaysPresence, ResolveRequest, + SecretKey, }; use chrono::{serde::ts_seconds_option, DateTime, Utc}; use connlib_model::{ClientId, ResourceId}; @@ -13,6 +13,8 @@ use std::{ net::{Ipv4Addr, Ipv6Addr}, }; +use super::Offer; + pub type Filters = Vec; /// Description of a resource that maps to a DNS record. diff --git a/rust/connlib/tunnel/src/p2p_control.rs b/rust/connlib/tunnel/src/p2p_control.rs index 224f3f33f..d9ebe2741 100644 --- a/rust/connlib/tunnel/src/p2p_control.rs +++ b/rust/connlib/tunnel/src/p2p_control.rs @@ -15,8 +15,6 @@ use ip_packet::FzP2pEventType; pub const ASSIGNED_IPS_EVENT: FzP2pEventType = FzP2pEventType::new(0); pub const DOMAIN_STATUS_EVENT: FzP2pEventType = FzP2pEventType::new(1); -/// The namespace for the DNS resource NAT protocol. -#[cfg_attr(not(test), expect(dead_code, reason = "Will be used soon."))] pub mod dns_resource_nat { use super::*; use anyhow::{Context as _, Result}; diff --git a/rust/connlib/tunnel/src/peer.rs b/rust/connlib/tunnel/src/peer.rs index 828f45822..436b77895 100644 --- a/rust/connlib/tunnel/src/peer.rs +++ b/rust/connlib/tunnel/src/peer.rs @@ -1,7 +1,7 @@ -use std::collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; +use std::collections::{hash_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use std::iter; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::time::{Duration, Instant}; +use std::time::Instant; use crate::client::{IPV4_RESOURCES, IPV6_RESOURCES}; use crate::messages::gateway::ResourceDescription; @@ -11,7 +11,6 @@ use connlib_model::{ClientId, DomainName, GatewayId, ResourceId}; use ip_network::{IpNetwork, Ipv4Network, Ipv6Network}; use ip_network_table::IpNetworkTable; use ip_packet::IpPacket; -use itertools::Itertools; use rangemap::RangeInclusiveSet; use crate::utils::network_contains_network; @@ -117,17 +116,11 @@ impl GatewayOnClient { } impl GatewayOnClient { - pub(crate) fn new( - id: GatewayId, - ips: &[IpNetwork], - resource: HashSet, - ) -> GatewayOnClient { - let mut allowed_ips = IpNetworkTable::new(); - for ip in ips { - allowed_ips.insert(*ip, resource.clone()); + pub(crate) fn new(id: GatewayId) -> GatewayOnClient { + GatewayOnClient { + id, + allowed_ips: IpNetworkTable::new(), } - - GatewayOnClient { id, allowed_ips } } } @@ -167,65 +160,14 @@ impl ClientOnGateway { [IpAddr::from(self.ipv4), IpAddr::from(self.ipv6)] } - pub(crate) fn refresh_translation( - &mut self, - name: DomainName, - resource_id: ResourceId, - resolved_ips: Vec, - now: Instant, - ) -> Result<()> { - let resource_on_gateway = self - .resources - .get_mut(&resource_id) - .context("Unknown resource")?; - - let domains = match resource_on_gateway { - ResourceOnGateway::Dns { domains, .. } => domains, - ResourceOnGateway::Cidr { .. } => { - bail!("Cannot refresh translation for CIDR resource") - } - ResourceOnGateway::Internet { .. } => { - bail!("Cannot refresh translation for Internet resource") - } - }; - - let old_ips: HashSet<&IpAddr> = - HashSet::from_iter(self.permanent_translations.values().filter_map(|state| { - (state.name == name && state.resource_id == resource_id) - .then_some(&state.resolved_ip) - })); - let new_ips: HashSet<&IpAddr> = HashSet::from_iter(resolved_ips.iter()); - if old_ips == new_ips { - return Ok(()); - } - - domains.insert( - name.clone(), - resolved_ips.iter().copied().map_into().collect_vec(), - ); - - let proxy_ips = self - .permanent_translations - .iter() - .filter_map(|(k, state)| { - (state.name == name && state.resource_id == resource_id).then_some(*k) - }) - .collect_vec(); - - 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 setup_nat( &mut self, name: DomainName, resource_id: ResourceId, - resolved_ips: &[IpAddr], - proxy_ips: Vec, - now: Instant, + resolved_ips: BTreeSet, + proxy_ips: BTreeSet, ) -> Result<()> { let resource = self .resources @@ -241,33 +183,31 @@ impl ClientOnGateway { anyhow::ensure!(crate::dns::is_subdomain(&name, address)); - let mapped_ipv4 = mapped_ipv4(resolved_ips); - let mapped_ipv6 = mapped_ipv6(resolved_ips); + let mapped_ipv4 = mapped_ipv4(&resolved_ips); + let mapped_ipv6 = mapped_ipv6(&resolved_ips); let ipv4_maps = proxy_ips .iter() .filter(|ip| ip.is_ipv4()) - .zip(mapped_ipv4.into_iter().cycle()); + .zip(mapped_ipv4.iter().cycle().copied()); let ipv6_maps = proxy_ips .iter() .filter(|ip| ip.is_ipv6()) - .zip(mapped_ipv6.into_iter().cycle()); + .zip(mapped_ipv6.iter().cycle().copied()); let ip_maps = ipv4_maps.chain(ipv6_maps); for (proxy_ip, real_ip) in ip_maps { tracing::debug!(%name, %proxy_ip, %real_ip); - self.permanent_translations.insert( - *proxy_ip, - TranslationState::new(resource_id, name.clone(), real_ip, now), - ); + self.permanent_translations + .insert(*proxy_ip, TranslationState::new(resource_id, real_ip)); } tracing::debug!(domain = %name, ?resolved_ips, ?proxy_ips, "Set up DNS resource NAT"); - domains.insert(name, resolved_ips.to_vec()); + domains.insert(name, resolved_ips); self.recalculate_filters(); Ok(()) @@ -302,39 +242,6 @@ impl ClientOnGateway { } pub(crate) fn handle_timeout(&mut self, now: Instant) { - let expired_translations = self - .permanent_translations - .iter() - .filter(|(_, state)| state.is_expired(now)); - - let mut for_refresh = HashSet::new(); - - for (proxy_ip, expired_state) in expired_translations { - let domain = &expired_state.name; - let resource_id = expired_state.resource_id; - let resolved_ip = expired_state.resolved_ip; - - // Only refresh DNS for a domain if all of the resolved IPs stop responding in order to not kill existing connections. - if self - .permanent_translations - .values() - .filter(|state| state.resource_id == resource_id && state.name == domain) - .all(|state| state.no_incoming_in_120s(now)) - { - tracing::debug!(%domain, conn_id = %self.id, %resource_id, %resolved_ip, %proxy_ip, "Refreshing DNS"); - - for_refresh.insert((expired_state.name.clone(), expired_state.resource_id)); - } - } - - for (name, resource_id) in for_refresh { - self.buffered_events.push_back(GatewayEvent::RefreshDns { - name, - conn_id: self.id, - resource_id, - }); - } - self.nat_table.handle_timeout(now); } @@ -433,8 +340,6 @@ impl ClientOnGateway { .context("Failed to translate packet")?; packet.update_checksum(); - state.on_outgoing_traffic(now); - Ok(packet) } @@ -471,12 +376,6 @@ impl ClientOnGateway { }; let mut packet = packet.translate_source(self.ipv4, self.ipv6, proto, ip)?; - - self.permanent_translations - .get_mut(&ip) - .context("No translation state for outgoing packet")? - .on_incoming_traffic(now); - packet.update_checksum(); Ok(packet) @@ -554,7 +453,7 @@ enum ResourceOnGateway { }, Dns { address: String, - domains: HashMap>, + domains: HashMap>, filters: Filters, expires_at: Option>, }, @@ -659,100 +558,28 @@ impl ResourceOnGateway { struct TranslationState { /// Which (DNS) resource we belong to. resource_id: ResourceId, - /// The concrete domain we have resolved (could be a sub-domain of a `*` or `?` resource). - name: DomainName, /// The IP we have resolved for the domain. resolved_ip: IpAddr, - - /// When we've last received a packet from the resolved IP. - last_incoming: Option, - /// When we've sent the first packet to the resolved IP. - first_outgoing: Option, - /// When we've last sent a packet to the resolved IP. - last_outgoing: Option, - /// When was this translation created - created_at: Instant, - /// When we first detected that we aren't getting any responses from this IP. - /// - /// This is set upon outgoing traffic if we haven't received inbound traffic for a while. - /// We don't want to immediately trigger a refresh in that case because protocols like TCP and ICMP have responses. - /// Thus, a DNS refresh is triggered after a grace-period of 1s after the packet that detected the missing responses. - ack_grace_period_started_at: Option, } impl TranslationState { - const USED_WINDOW: Duration = Duration::from_secs(10); - - fn new(resource_id: ResourceId, name: DomainName, resolved_ip: IpAddr, now: Instant) -> Self { + fn new(resource_id: ResourceId, resolved_ip: IpAddr) -> Self { Self { resource_id, - name, resolved_ip, - created_at: now, - last_incoming: None, - first_outgoing: None, - last_outgoing: None, - ack_grace_period_started_at: None, } } - - fn is_expired(&self, now: Instant) -> bool { - // Note: we don't need to check that it's used here because the ack grace period already implies it - self.ack_grace_period_expired(now) && self.no_incoming_in_120s(now) - } - - fn ack_grace_period_expired(&self, now: Instant) -> bool { - self.ack_grace_period_started_at - .is_some_and(|missing_responses_detected_at| { - now.duration_since(missing_responses_detected_at) >= Duration::from_secs(1) - }) - } - - fn no_incoming_in_120s(&self, now: Instant) -> bool { - const CONNTRACK_UDP_STREAM_TIMEOUT: Duration = Duration::from_secs(120); - - if let Some(last_incoming) = self.last_incoming { - now.duration_since(last_incoming) >= CONNTRACK_UDP_STREAM_TIMEOUT - } else { - now.duration_since(self.created_at) >= CONNTRACK_UDP_STREAM_TIMEOUT - } - } - - fn on_incoming_traffic(&mut self, now: Instant) { - self.last_incoming = Some(now); - self.ack_grace_period_started_at = None; - } - - fn on_outgoing_traffic(&mut self, now: Instant) { - // We need this because it means that if a packet arrives at some point less than 120s but more than 110s - // we still start the grace period so that the connection expires at some point after 120s - let with_this_packet_the_connection_will_be_considered_used_when_it_expires = - self.no_incoming_in_120s(now + Self::USED_WINDOW); - if self.ack_grace_period_started_at.is_none() - && with_this_packet_the_connection_will_be_considered_used_when_it_expires - { - self.ack_grace_period_started_at = Some(now); - } - - self.last_outgoing = Some(now); - - if self.first_outgoing.is_some() { - return; - } - - self.first_outgoing = Some(now); - } } -fn ipv4_addresses(ip: &[IpAddr]) -> Vec { - ip.iter().filter(|ip| ip.is_ipv4()).copied().collect_vec() +fn ipv4_addresses(ip: &BTreeSet) -> BTreeSet { + ip.iter().filter(|ip| ip.is_ipv4()).copied().collect() } -fn ipv6_addresses(ip: &[IpAddr]) -> Vec { - ip.iter().filter(|ip| ip.is_ipv6()).copied().collect_vec() +fn ipv6_addresses(ip: &BTreeSet) -> BTreeSet { + ip.iter().filter(|ip| ip.is_ipv6()).copied().collect() } -fn mapped_ipv4(ips: &[IpAddr]) -> Vec { +fn mapped_ipv4(ips: &BTreeSet) -> BTreeSet { if !ipv4_addresses(ips).is_empty() { ipv4_addresses(ips) } else { @@ -760,7 +587,7 @@ fn mapped_ipv4(ips: &[IpAddr]) -> Vec { } } -fn mapped_ipv6(ips: &[IpAddr]) -> Vec { +fn mapped_ipv6(ips: &BTreeSet) -> BTreeSet { if !ipv6_addresses(ips).is_empty() { ipv6_addresses(ips) } else { @@ -786,7 +613,8 @@ fn insert_filters<'a>( #[cfg(test)] mod tests { use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, + collections::BTreeSet, + net::{Ipv4Addr, Ipv6Addr}, time::{Duration, Instant}, }; @@ -797,7 +625,7 @@ mod tests { use connlib_model::{ClientId, ResourceId}; use ip_network::{IpNetwork, Ipv4Network}; - use super::{ClientOnGateway, TranslationState}; + use super::ClientOnGateway; #[test] fn gateway_filters_expire_individually() { @@ -864,273 +692,6 @@ mod tests { assert!(peer.ensure_allowed_dst(&udp_packet).is_err()); } - #[test] - fn initial_translation_state_is_not_expired() { - let now = Instant::now(); - let state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_is_not_used_but_expired_after_120s() { - let mut now = Instant::now(); - let state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(121); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_is_used_and_expired_after_120s_with_outgoing_packets() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - - now += Duration::from_secs(1); - - assert!(state.is_expired(now)); - } - - #[test] - fn translation_state_is_used_and_expired_after_121s_with_outgoing_packets() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(121); - state.on_outgoing_traffic(now); - - now += Duration::from_secs(1); - - assert!(state.is_expired(now)); - } - #[test] - fn translation_state_is_not_expired_with_incoming_packets() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_incoming_traffic(now); - - now += Duration::from_secs(1); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_doesnt_expire_with_incoming_and_outgoing_packets() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - now += Duration::from_millis(200); - state.on_incoming_traffic(now); - - now += Duration::from_secs(1); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_still_has_grace_period_after_incoming_traffic() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - now += Duration::from_millis(200); - state.on_incoming_traffic(now); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_still_expires_after_grace_period_after_incoming_traffic_resetted_it() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - now += Duration::from_millis(200); - state.on_incoming_traffic(now); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - now += Duration::from_secs(1); - - assert!(state.is_expired(now)); - } - - #[test] - fn translation_state_doesnt_expire_with_first_packet_after_silence() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_expires_after_silence_even_with_multiple_packets() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - now += Duration::from_secs(5); - state.on_outgoing_traffic(now); - - assert!(state.is_expired(now)); - } - - #[test] - fn translation_doesnt_expire_before_expected_period() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(110); - state.on_outgoing_traffic(now); - now += Duration::from_secs(5); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_expire_after_expected_period() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(110); - state.on_outgoing_traffic(now); - now += Duration::from_secs(5); - state.on_outgoing_traffic(now); - now += Duration::from_secs(5); - - assert!(state.is_expired(now)); - } - - #[test] - fn incoming_traffic_prevents_expiration() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_secs(120); - state.on_outgoing_traffic(now); - now += Duration::from_millis(500); - state.on_incoming_traffic(now); - now += Duration::from_secs(5); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_doesnt_expire_with_packet_that_didnt_had_time_to_be_responded() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_millis(119990); - state.on_outgoing_traffic(now); - now += Duration::from_millis(20); - - assert!(!state.is_expired(now)); - } - - #[test] - fn translation_state_expires_with_packet_that_had_time_to_be_responded() { - let mut now = Instant::now(); - let mut state = TranslationState::new( - ResourceId::random(), - "example.com".parse().unwrap(), - IpAddr::V4(Ipv4Addr::LOCALHOST), - now, - ); - - now += Duration::from_millis(119990); - state.on_outgoing_traffic(now); - now += Duration::from_secs(1); - - assert!(state.is_expired(now)); - } - #[test] fn dns_and_cidr_filters_dot_mix() { let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr()); @@ -1139,9 +700,8 @@ mod tests { peer.setup_nat( foo_name().parse().unwrap(), resource_id(), - &[foo_real_ip().into()], - vec![foo_proxy_ip().into()], - Instant::now(), + BTreeSet::from([foo_real_ip().into()]), + BTreeSet::from([foo_proxy_ip().into()]), ) .unwrap(); @@ -1200,9 +760,8 @@ mod tests { peer.setup_nat( foo_name().parse().unwrap(), resource_id(), - &[foo_real_ip().into()], - vec![foo_proxy_ip().into()], - Instant::now(), + BTreeSet::from([foo_real_ip().into()]), + BTreeSet::from([foo_proxy_ip().into()]), ) .unwrap(); @@ -1335,6 +894,7 @@ mod proptests { use crate::messages::gateway::{PortRange, ResourceDescription, ResourceDescriptionCidr}; use crate::proptest::*; use ip_packet::make::{icmp_request_packet, tcp_packet, udp_packet}; + use itertools::Itertools as _; use proptest::{ arbitrary::any, collection, prop_oneof, diff --git a/rust/connlib/tunnel/src/tests/reference.rs b/rust/connlib/tunnel/src/tests/reference.rs index f64a6c258..e34998cf5 100644 --- a/rust/connlib/tunnel/src/tests/reference.rs +++ b/rust/connlib/tunnel/src/tests/reference.rs @@ -6,7 +6,7 @@ use super::{ }; use crate::{client, DomainName}; use crate::{dns::is_subdomain, proptest::relay_id}; -use connlib_model::{GatewayId, RelayId, ResourceId, StaticSecret}; +use connlib_model::{GatewayId, RelayId, StaticSecret}; use domain::base::Rtype; use ip_network::{Ipv4Network, Ipv6Network}; use prop::sample::select; @@ -361,10 +361,7 @@ impl ReferenceState { } }), Transition::SendDnsQueries(queries) => { - let mut new_connections_via_gateways_udp_triggered = - BTreeMap::<_, BTreeSet>::new(); - let mut new_connections_via_gateways_tcp_triggered = - BTreeMap::<_, BTreeSet>::new(); + let mut new_connections = BTreeSet::new(); for query in queries { // Some queries get answered locally. @@ -400,29 +397,7 @@ impl ReferenceState { { tracing::debug!(%resource, %gateway, "Not connected yet, dropping packet"); - let connected_resources = match query.transport { - DnsTransport::Udp => &mut new_connections_via_gateways_udp_triggered, - DnsTransport::Tcp => &mut new_connections_via_gateways_tcp_triggered, - } - .entry(gateway) - .or_default(); - - if state.client.inner().is_connected_gateway(gateway) { - connected_resources.insert(resource); - } else { - match query.transport { - DnsTransport::Udp => { - // As part of batch-processing DNS queries, only the first resource per gateway will be connected / authorized. - if connected_resources.is_empty() { - connected_resources.insert(resource); - } - } - DnsTransport::Tcp => { - // TCP has retries, so those will always connect. - connected_resources.insert(resource); - } - } - } + new_connections.insert((resource, gateway)); continue; } @@ -430,15 +405,10 @@ impl ReferenceState { state.client.exec_mut(|client| client.on_dns_query(query)); } - for (gateway, resources) in new_connections_via_gateways_udp_triggered - .into_iter() - .chain(new_connections_via_gateways_tcp_triggered) - { - for resource in resources { - state.client.exec_mut(|client| { - client.connect_to_internet_or_cidr_resource(resource, gateway) - }); - } + for (resource, gateway) in new_connections.into_iter() { + state.client.exec_mut(|client| { + client.connect_to_internet_or_cidr_resource(resource, gateway) + }); } } Transition::SendIcmpPacket { diff --git a/rust/connlib/tunnel/src/tests/sim_client.rs b/rust/connlib/tunnel/src/tests/sim_client.rs index 857863e9e..c7328a22e 100644 --- a/rust/connlib/tunnel/src/tests/sim_client.rs +++ b/rust/connlib/tunnel/src/tests/sim_client.rs @@ -433,7 +433,7 @@ pub struct RefClient { /// The DNS resources the client is connected to. #[debug(skip)] - pub(crate) connected_dns_resources: HashSet<(ResourceId, DomainName)>, + pub(crate) connected_dns_resources: HashSet, #[debug(skip)] pub(crate) connected_gateways: BTreeSet, @@ -486,7 +486,7 @@ impl RefClient { self.ipv6_routes.remove(resource); self.connected_cidr_resources.remove(resource); - self.connected_dns_resources.retain(|(r, _)| r != resource); + self.connected_dns_resources.remove(resource); if self.internet_resource.is_some_and(|r| &r == resource) { self.connected_internet_resource = false; @@ -675,7 +675,7 @@ impl RefClient { return; }; - if self.is_connected_to_resource(resource, &dst) && self.is_tunnel_ip(src) { + if self.is_connected_to_resource(resource) && self.is_tunnel_ip(src) { tracing::debug!("Connected to resource, expecting packet to be routed"); map(self) .entry(gateway) @@ -692,6 +692,7 @@ impl RefClient { } tracing::debug!("Not connected to resource, expecting to trigger connection intent"); + self.connect_to_resource(resource, dst, gateway); } @@ -702,9 +703,9 @@ impl RefClient { gateway: GatewayId, ) { match destination { - Destination::DomainName { name, .. } => { + Destination::DomainName { .. } => { if !self.disabled_resources.contains(&resource) { - self.connected_dns_resources.insert((resource, name)); + self.connected_dns_resources.insert(resource); self.connected_gateways.insert(gateway); } } @@ -716,10 +717,6 @@ impl RefClient { self.is_connected_to_cidr(resource) || self.is_connected_to_internet(resource) } - pub(crate) fn is_connected_gateway(&self, gateway: GatewayId) -> bool { - self.connected_gateways.contains(&gateway) - } - pub(crate) fn connect_to_internet_or_cidr_resource( &mut self, resource: ResourceId, @@ -769,17 +766,12 @@ impl RefClient { .collect_vec() } - fn is_connected_to_resource(&self, resource: ResourceId, destination: &Destination) -> bool { + fn is_connected_to_resource(&self, resource: ResourceId) -> bool { if self.is_connected_to_internet_or_cidr(resource) { return true; } - let Destination::DomainName { name, .. } = destination else { - return false; - }; - - self.connected_dns_resources - .contains(&(resource, name.clone())) + self.connected_dns_resources.contains(&resource) } fn is_connected_to_internet(&self, id: ResourceId) -> bool { diff --git a/rust/connlib/tunnel/src/tests/sut.rs b/rust/connlib/tunnel/src/tests/sut.rs index 9a87693df..fd8667611 100644 --- a/rust/connlib/tunnel/src/tests/sut.rs +++ b/rust/connlib/tunnel/src/tests/sut.rs @@ -9,18 +9,20 @@ use super::stub_portal::StubPortal; use super::transition::{Destination, DnsQuery}; use super::unreachable_hosts::UnreachableHosts; use crate::client::Resource; -use crate::dns::{self, is_subdomain}; -use crate::gateway::DnsResourceNatEntry; +use crate::dns::is_subdomain; +use crate::messages::{IceCredentials, Key, SecretKey}; use crate::tests::assertions::*; use crate::tests::flux_capacitor::FluxCapacitor; use crate::tests::transition::Transition; use crate::utils::earliest; -use crate::{messages::Interface, ClientEvent, GatewayEvent}; -use connlib_model::{ClientId, GatewayId, RelayId}; +use crate::{dns, messages::Interface, ClientEvent, GatewayEvent}; +use connlib_model::{ClientId, GatewayId, PublicKey, RelayId}; use domain::base::iana::{Class, Rcode}; use domain::base::{Message, MessageBuilder, Record, RecordData, ToName as _, Ttl}; use firezone_logging::anyhow_dyn_err; -use secrecy::ExposeSecret as _; +use rand::distributions::DistString; +use rand::SeedableRng; +use sha2::Digest; use snownet::Transmit; use std::iter; use std::{ @@ -411,13 +413,9 @@ impl TunnelTest { ); continue 'outer; } + if let Some(event) = self.client.exec_mut(|c| c.sut.poll_event()) { - self.on_client_event( - self.client.inner().id, - event, - &ref_state.portal, - &ref_state.global_dns_records, - ); + self.on_client_event(self.client.inner().id, event, &ref_state.portal); continue; } if let Some(query) = self.client.exec_mut(|c| c.sut.poll_dns_queries()) { @@ -489,6 +487,7 @@ impl TunnelTest { buffered_transmits.push_from(transmit, &self.client, now); continue; } + self.client.exec_mut(|sim| { while let Some(packet) = sim.sut.poll_packets() { sim.on_received_packet(packet) @@ -675,13 +674,7 @@ impl TunnelTest { } } - fn on_client_event( - &mut self, - src: ClientId, - event: ClientEvent, - portal: &StubPortal, - global_dns_records: &DnsRecords, - ) { + fn on_client_event(&mut self, src: ClientId, event: ClientEvent, portal: &StubPortal) { let now = self.flux_capacitor.now(); match event { @@ -710,46 +703,51 @@ impl TunnelTest { }) } ClientEvent::ConnectionIntent { - resource, + resource: resource_id, connected_gateway_ids, } => { - let (gateway, site) = - portal.handle_connection_intent(resource, connected_gateway_ids); - - self.client - .exec_mut(|c| c.sut.on_routing_details(resource, gateway, site, now)) - .unwrap() - .unwrap(); - } - - ClientEvent::RequestAccess { - resource_id, - gateway_id, - maybe_domain, - } => { + let (gateway_id, site_id) = + portal.handle_connection_intent(resource_id, connected_gateway_ids); let gateway = self.gateways.get_mut(&gateway_id).expect("unknown gateway"); - let maybe_entry = maybe_domain.map(|r| { - let resolved_ips = global_dns_records.domain_ips_iter(&r.name).collect(); - - DnsResourceNatEntry::new(r, resolved_ips) - }); - let resource = portal.map_client_resource_to_gateway_resource(resource_id); - gateway.exec_mut(|g| { - g.sut - .allow_access( - self.client.inner().id, + let client_key = self.client.inner().sut.public_key(); + let gateway_key = gateway.inner().sut.public_key(); + let (preshared_key, client_ice, gateway_ice) = + make_preshared_key_and_ice(client_key, gateway_key); + + gateway + .exec_mut(|g| { + g.sut.authorize_flow( + src, + client_key, + preshared_key.clone(), + client_ice.clone(), + gateway_ice.clone(), self.client.inner().sut.tunnel_ip4().unwrap(), self.client.inner().sut.tunnel_ip6().unwrap(), None, - resource.clone(), - maybe_entry, + resource, now, ) - .unwrap(); - }); + }) + .unwrap(); + if let Err(e) = self.client.exec_mut(|c| { + c.sut.handle_flow_created( + resource_id, + gateway_id, + gateway_key, + site_id, + preshared_key, + client_ice, + gateway_ice, + now, + ) + }) { + tracing::error!("{e:#}") + }; } + ClientEvent::ResourcesChanged { .. } => { tracing::warn!("Unimplemented"); } @@ -780,75 +778,6 @@ impl TunnelTest { c.ipv6_routes = config.ipv6_routes; }); } - #[expect(deprecated, reason = "Will be deleted together with deprecated API")] - ClientEvent::RequestConnection { - gateway_id, - offer, - preshared_key, - resource_id, - maybe_domain, - } => { - let maybe_entry = maybe_domain.map(|r| { - let resolved_ips = global_dns_records.domain_ips_iter(&r.name).collect(); - - DnsResourceNatEntry::new(r, resolved_ips) - }); - let resource = portal.map_client_resource_to_gateway_resource(resource_id); - - let Some(gateway) = self.gateways.get_mut(&gateway_id) else { - tracing::error!("Unknown gateway"); - return; - }; - - 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, - }, - }, - self.client.inner().sut.public_key(), - now, - ) - .unwrap(); - 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(); - - answer - }); - - self.client - .exec_mut(|c| { - c.sut.accept_answer( - snownet::Answer { - credentials: snownet::Credentials { - username: answer.username, - password: answer.password, - }, - }, - resource_id, - gateway.inner().sut.public_key(), - now, - ) - }) - .unwrap(); - } } } @@ -929,6 +858,35 @@ fn address_from_destination(destination: &Destination, state: &TunnelTest, src: } } +fn make_preshared_key_and_ice( + client_key: PublicKey, + gateway_key: PublicKey, +) -> (SecretKey, IceCredentials, IceCredentials) { + let secret_key = SecretKey::new(Key(hkdf("SECRET_KEY_DOMAIN_SEP", client_key, gateway_key))); + let client_ice = ice_creds("CLIENT_ICE_DOMAIN_SEP", client_key, gateway_key); + let gateway_ice = ice_creds("GATEWAY_ICE_DOMAIN_SEP", client_key, gateway_key); + + (secret_key, client_ice, gateway_ice) +} + +fn ice_creds(domain: &str, client_key: PublicKey, gateway_key: PublicKey) -> IceCredentials { + let mut rng = rand::rngs::StdRng::from_seed(hkdf(domain, client_key, gateway_key)); + + IceCredentials { + username: rand::distributions::Alphanumeric.sample_string(&mut rng, 4), + password: rand::distributions::Alphanumeric.sample_string(&mut rng, 12), + } +} + +fn hkdf(domain: &str, client_key: PublicKey, gateway_key: PublicKey) -> [u8; 32] { + sha2::Sha256::default() + .chain_update(domain) + .chain_update(client_key.as_bytes()) + .chain_update(gateway_key.as_bytes()) + .finalize() + .into() +} + fn on_gateway_event( src: GatewayId, event: GatewayEvent, @@ -948,13 +906,14 @@ fn on_gateway_event( c.sut.remove_ice_candidate(src, candidate, now) } }), - event @ GatewayEvent::RefreshDns { .. } => { - tracing::warn!("Handling `{event:?}` is not yet implemented") - } GatewayEvent::ResolveDns(r) => { let resolved_ips = global_dns_records.domain_ips_iter(r.domain()).collect(); - gateway.exec_mut(|g| g.sut.handle_domain_resolved(r, resolved_ips, now).unwrap()) + gateway.exec_mut(|g| { + g.sut + .handle_domain_resolved(r, Ok(resolved_ips), now) + .unwrap() + }) } } } diff --git a/rust/gateway/src/eventloop.rs b/rust/gateway/src/eventloop.rs index 43af26e22..48d8243a8 100644 --- a/rust/gateway/src/eventloop.rs +++ b/rust/gateway/src/eventloop.rs @@ -1,7 +1,6 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use boringtun::x25519::PublicKey; use connlib_model::DomainName; -use connlib_model::{ClientId, ResourceId}; #[cfg(not(target_os = "windows"))] use dns_lookup::{AddrInfoHints, AddrInfoIter, LookupError}; use firezone_logging::{ @@ -14,7 +13,6 @@ use firezone_tunnel::messages::gateway::{ 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}; use std::collections::BTreeSet; use std::convert::Infallible; @@ -37,9 +35,8 @@ static_assertions::const_assert!( #[derive(Debug)] enum ResolveTrigger { - RequestConnection(RequestConnection), // Deprecated - AllowAccess(AllowAccess), // Deprecated - Refresh(DomainName, ClientId, ResourceId), // TODO: Can we delete this perhaps? + RequestConnection(RequestConnection), // Deprecated + AllowAccess(AllowAccess), // Deprecated SetupNat(ResolveDnsRequest), } @@ -48,7 +45,7 @@ pub struct Eventloop { portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>, tun_device_channel: mpsc::Sender, - resolve_tasks: futures_bounded::FuturesTupleSet, ResolveTrigger>, + resolve_tasks: futures_bounded::FuturesTupleSet>, ResolveTrigger>, } impl Eventloop { @@ -86,7 +83,14 @@ impl Eventloop { Poll::Pending => {} } - match self.resolve_tasks.poll_unpin(cx) { + match self.resolve_tasks.poll_unpin(cx).map(|(r, trigger)| { + ( + r.unwrap_or_else(|e| { + Err(anyhow::Error::new(e).context("DNS resolution timed out")) + }), + trigger, + ) + }) { Poll::Ready((result, ResolveTrigger::RequestConnection(req))) => { self.accept_connection(result, req); continue; @@ -95,23 +99,10 @@ impl Eventloop { self.allow_access(result, req); continue; } - Poll::Ready((result, ResolveTrigger::Refresh(name, conn_id, resource_id))) => { - self.refresh_translation(result, conn_id, resource_id, name); - continue; - } Poll::Ready((result, ResolveTrigger::SetupNat(request))) => { - let addresses = result - .inspect_err(|e| { - tracing::debug!( - error = std_dyn_err(e), - "DNS resolution timed out as part of setup NAT request" - ) - }) - .unwrap_or_default(); - if let Err(e) = self.tunnel.state_mut().handle_domain_resolved( request, - addresses, + result, Instant::now(), ) { tracing::warn!( @@ -163,22 +154,6 @@ impl Eventloop { }), ); } - firezone_tunnel::GatewayEvent::RefreshDns { - name, - conn_id, - resource_id, - } => { - if self - .resolve_tasks - .try_push( - resolve(Some(name.clone())), - ResolveTrigger::Refresh(name, conn_id, resource_id), - ) - .is_err() - { - tracing::warn!("Too many dns resolution requests, dropping existing one"); - }; - } firezone_tunnel::GatewayEvent::ResolveDns(setup_nat) => { if self .resolve_tasks @@ -348,14 +323,15 @@ impl Eventloop { } } - pub fn accept_connection( - &mut self, - result: Result, Timeout>, - req: RequestConnection, - ) { - let addresses = result - .inspect_err(|e| tracing::debug!(client = %req.client.id, reference = %req.reference, "DNS resolution timed out as part of connection request: {}", err_with_src(e))) - .unwrap_or_default(); + pub fn accept_connection(&mut self, result: Result>, req: RequestConnection) { + let addresses = match result { + Ok(addresses) => addresses, + Err(e) => { + tracing::debug!(client = %req.client.id, reference = %req.reference, "DNS resolution failed as part of connection request: {e:#}"); + + return; // Fail the connection so the client runs into a timeout. + } + }; let answer = match self.tunnel.state_mut().accept( req.client.id, @@ -388,7 +364,6 @@ impl Eventloop { .payload .domain .map(|r| DnsResourceNatEntry::new(r, addresses)), - Instant::now(), ) { let client = req.client.id; @@ -408,10 +383,17 @@ impl Eventloop { ); } - pub fn allow_access(&mut self, result: Result, Timeout>, req: AllowAccess) { - let addresses = result - .inspect_err(|e| tracing::debug!(client = %req.client_id, reference = %req.reference, "DNS resolution timed out as part of allow access request: {}", err_with_src(e))) - .unwrap_or_default(); + pub fn allow_access(&mut self, result: Result>, req: AllowAccess) { + // "allow access" doesn't have a response so we can't tell the client that things failed. + // It is legacy code so don't bother ... + let addresses = match result { + Ok(addresses) => addresses, + Err(e) => { + tracing::debug!(client = %req.client_id, reference = %req.reference, "DNS resolution failed as part of allow access request: {e:#}"); + + vec![] + } + }; if let Err(e) = self.tunnel.state_mut().allow_access( req.client_id, @@ -420,56 +402,26 @@ impl Eventloop { req.expires_at, req.resource, req.payload.map(|r| DnsResourceNatEntry::new(r, addresses)), - Instant::now(), ) { tracing::warn!(error = anyhow_dyn_err(&e), client = %req.client_id, "Allow access request failed"); }; } - - pub fn refresh_translation( - &mut self, - result: Result, Timeout>, - conn_id: ClientId, - resource_id: ResourceId, - name: DomainName, - ) { - let addresses = result - .inspect_err(|e| tracing::debug!(%conn_id, "DNS resolution timed out as part of allow access request: {}", err_with_src(e))) - .unwrap_or_default(); - - self.tunnel.state_mut().refresh_translation( - conn_id, - resource_id, - name, - addresses, - Instant::now(), - ); - } } -async fn resolve(domain: Option) -> Vec { +async fn resolve(domain: Option) -> Result> { let Some(domain) = domain.clone() else { - return vec![]; + return Ok(vec![]); }; let dname = domain.to_string(); - match tokio::task::spawn_blocking(move || resolve_addresses(&dname)) + let addresses = tokio::task::spawn_blocking(move || resolve_addresses(&dname)) .instrument(telemetry_span!("resolve_dns_resource")) .await - { - Ok(Ok(addresses)) => addresses, - Ok(Err(e)) => { - tracing::warn!(error = std_dyn_err(&e), %domain, "DNS resolution failed"); + .context("DNS resolution task failed")? + .context("DNS resolution failed")?; - vec![] - } - Err(e) => { - tracing::warn!(error = std_dyn_err(&e), %domain, "DNS resolution task failed"); - - vec![] - } - } + Ok(addresses) } #[cfg(target_os = "windows")] diff --git a/rust/phoenix-channel/src/lib.rs b/rust/phoenix-channel/src/lib.rs index a7f4e2e98..3d11cb31f 100644 --- a/rust/phoenix-channel/src/lib.rs +++ b/rust/phoenix-channel/src/lib.rs @@ -737,9 +737,7 @@ enum OkReply { pub enum ErrorReply { #[serde(rename = "unmatched topic")] UnmatchedTopic, - NotFound, InvalidVersion, - Offline, Disabled, #[serde(other)] Other, @@ -749,9 +747,7 @@ impl fmt::Display for ErrorReply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErrorReply::UnmatchedTopic => write!(f, "unmatched topic"), - ErrorReply::NotFound => write!(f, "not found"), ErrorReply::InvalidVersion => write!(f, "invalid version"), - ErrorReply::Offline => write!(f, "offline"), ErrorReply::Disabled => write!(f, "disabled"), ErrorReply::Other => write!(f, "other"), } @@ -935,28 +931,6 @@ mod tests { assert_eq!(actual_reply, expected_reply); } - #[test] - fn not_found() { - let actual_reply = r#" - { - "event": "phx_reply", - "ref": null, - "topic": "client", - "payload": { - "status": "error", - "response": { - "reason": "not_found" - } - } - } - "#; - let actual_reply: Payload<(), ()> = serde_json::from_str(actual_reply).unwrap(); - let expected_reply = Payload::<(), ()>::Reply(Reply::Error { - reason: ErrorReply::NotFound, - }); - assert_eq!(actual_reply, expected_reply); - } - #[test] fn unexpected_error_reply() { let actual_reply = r#" diff --git a/website/src/components/Changelog/Android.tsx b/website/src/components/Changelog/Android.tsx index ca058d078..d08db808b 100644 --- a/website/src/components/Changelog/Android.tsx +++ b/website/src/components/Changelog/Android.tsx @@ -20,6 +20,10 @@ export default function Android() { Adds support for GSO (Generic Segmentation Offload), delivering throughput improvements of up to 60%. + + Makes use of the new control protocol, delivering faster and more + robust connection establishment. + diff --git a/website/src/components/Changelog/Apple.tsx b/website/src/components/Changelog/Apple.tsx index 0cbe12d18..f88ce3176 100644 --- a/website/src/components/Changelog/Apple.tsx +++ b/website/src/components/Changelog/Apple.tsx @@ -20,6 +20,10 @@ export default function Apple() { Adds support for GSO (Generic Segmentation Offload), delivering throughput improvements of up to 60%. + + Makes use of the new control protocol, delivering faster and more + robust connection establishment. + diff --git a/website/src/components/Changelog/GUI.tsx b/website/src/components/Changelog/GUI.tsx index c1b2d3d5a..29a01af64 100644 --- a/website/src/components/Changelog/GUI.tsx +++ b/website/src/components/Changelog/GUI.tsx @@ -19,6 +19,10 @@ export default function GUI({ title }: { title: string }) { Adds support for GSO (Generic Segmentation Offload), delivering throughput improvements of up to 60%. + + Makes use of the new control protocol, delivering faster and more + robust connection establishment. + diff --git a/website/src/components/Changelog/Headless.tsx b/website/src/components/Changelog/Headless.tsx index 51cd39a65..715b0bb33 100644 --- a/website/src/components/Changelog/Headless.tsx +++ b/website/src/components/Changelog/Headless.tsx @@ -19,6 +19,10 @@ export default function Headless() { Adds support for GSO (Generic Segmentation Offload), delivering throughput improvements of up to 60%. + + Makes use of the new control protocol, delivering faster and more + robust connection establishment. +