Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
"crates/connetto-ca",
"crates/connetto-app-attest",
"crates/connetto-peer",
"crates/connetto-peer-android",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# The documents and the build-time translation every browser demo shares.
# A member rather than an exclusion, so the root gate lints it with the rest.
"examples/deployment",
Expand Down
6 changes: 6 additions & 0 deletions crates/connetto-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ tokio = { version = "1", default-features = false, features = [
"macros",
] }
thiserror.workspace = true
# The hotspot machine's answer, waited on by the API methods (R76).
parking_lot = "0.12"
# Binary error handling (connetto-client bin).
anyhow.workspace = true
# Structured logging (R12). The facade only: a library emits and installs no
Expand Down Expand Up @@ -252,6 +254,10 @@ android-native-keyring-store = { version = "1.0.0", optional = true }
jni = { version = "0.21", optional = true }
# The API level, since the gated store needs Android 11.
android_system_properties = { version = "0.1.6", optional = true }
# The dial socket the joined network binds, through the hotspot plugin (R76).
socket2 = "0.6"
# The joined network's subnet, the targets its bind admits (R76).
ipnet = "2.12"
libsqlite3-sys = { version = ">=0.38, <0.39", features = [
"bundled-sqlcipher-vendored-openssl",
] }
Expand Down
157 changes: 157 additions & 0 deletions crates/connetto-client/src/builder/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ use crate::teardown::content_dir;
use crate::teardown::{ForgetError, PurgeError, forget_device, wipe_replica};
use crate::{ClientError, ConnettoClient, Custody};
#[cfg(feature = "peer")]
use crate::{ClientEvent, HotspotError, HotspotOffer, JoinError};
#[cfg(feature = "peer")]
use connetto_core::device_cert::AttestationLevel;
#[cfg(feature = "device-identity")]
use connetto_core::device_cert::{DeviceCertificate, DeviceDescriptor, KeyHome};
Expand Down Expand Up @@ -696,6 +698,10 @@ where
///
/// [`ClientError`] on a sign-in, key, dial, database, or handshake
/// failure.
#[expect(
clippy::too_many_lines,
reason = "the device-identity, native-auth and peer seams each add their cfg-forked block"
)]
pub async fn connect_with_pump(
self,
) -> Result<(NativeClient<T, C::Handle>, CorePump), ClientError> {
Expand Down Expand Up @@ -738,6 +744,8 @@ where
#[cfg(feature = "peer")]
let (peer_events, peer_events_rx) = mpsc::unbounded_channel();
#[cfg(feature = "peer")]
let (hotspot_tx, hotspot_rx) = mpsc::unbounded_channel();
#[cfg(feature = "peer")]
let node = connetto_peer::Node::new(
connetto_peer::Trust {
roots: device.roots.clone(),
Expand All @@ -752,6 +760,8 @@ where
#[cfg(feature = "peer")]
let (discovery_events, discovery_events_rx) = mpsc::unbounded_channel();
#[cfg(feature = "peer")]
let peer_node = node.clone();
#[cfg(feature = "peer")]
let peer = Peer {
node: node.clone(),
discovery: connetto_peer::Discovery::new(
Expand All @@ -775,8 +785,86 @@ where
lists,
#[cfg(feature = "peer")]
peer,
#[cfg(feature = "peer")]
hotspot_tx,
);
let enrolment = core.client().enrolment(enroller);
#[cfg(feature = "peer")]
let hotspot_runner = {
let backend: Arc<dyn crate::hotspot::HotspotBackend> = {
#[cfg(target_os = "android")]
{
match device.java.clone() {
Some(java) => {
Arc::new(crate::hotspot::AndroidHotspotBackend::new(java))
}
None => Arc::new(crate::hotspot::UnsupportedBackend),
}
}
#[cfg(not(target_os = "android"))]
{
Arc::new(crate::hotspot::UnsupportedBackend)
}
};
let port_node = peer_node.clone();
#[cfg(target_os = "android")]
let bind_node = peer_node.clone();
let dial_handle = handle.clone();
let event_sender = core.client().event_sender();
#[cfg(target_os = "android")]
let bind = {
let java = device.java.clone();
Arc::new(
move |subnet: Option<(std::net::Ipv4Addr, u8)>| match subnet {
Some(subnet) => {
if let Some(java) = &java {
bind_node.set_socket_prep(Some(Arc::new(
crate::hotspot::JoinedBind::new(java.clone(), subnet),
)));
}
}
None => bind_node.set_socket_prep(None),
},
)
};
#[cfg(not(target_os = "android"))]
let bind = Arc::new(|_| {});
let machine = crate::hotspot::Machine::new(
backend,
device.peer_autolink,
Arc::new(move || port_node.local_addr().map(|addr| addr.port())),
bind,
Arc::new(
move |addr: SocketAddr, answer: tokio::sync::oneshot::Sender<bool>| {
let handle = dial_handle.clone();
tokio::spawn(async move {
match handle.link_peer(addr).await {
Ok(_) => {
let _ = answer.send(true);
}
Err(err) => {
tracing::debug!(
%addr,
?err,
"the dial to the joined hotspot's gateway failed"
);
let _ = answer.send(false);
}
}
});
},
),
Arc::new(move |event: ClientEvent| {
let _ = event_sender.send(event);
}),
);
crate::hotspot::run(machine, hotspot_rx)
};
#[cfg(feature = "peer")]
let pump: CorePump = Box::pin(async move {
tokio::join!(pump, enrolment, hotspot_runner);
});
#[cfg(not(feature = "peer"))]
let pump: CorePump = Box::pin(async move {
tokio::join!(pump, enrolment);
});
Expand Down Expand Up @@ -989,6 +1077,75 @@ where
None => Err(PeerError::NoIdentity),
}
}

/// Host this device's hotspot, answering its details or the reason it
/// will not, within the machine's bound (R76).
///
/// # Errors
///
/// [`HotspotError::Unsupported`] on a system without a hotspot or for a
/// build without a device identity, [`HotspotError::MissingPermission`]
/// with a permission the device has not granted, the mapped failure from
/// the device's Wi-Fi manager, and [`HotspotError::TimedOut`] when the
/// hotspot does not start within its bound.
#[cfg(feature = "peer")]
pub async fn host_hotspot(&self) -> Result<HotspotOffer, HotspotError> {
match &self.device {
Some(device) => device.host_hotspot().await,
None => Err(HotspotError::Unsupported),
}
}

/// Stop hosting, or cancel the pending request (R76).
///
/// A no-op on a system without a hotspot or for a build without a device
/// identity.
#[cfg(feature = "peer")]
#[expect(
clippy::unused_async,
clippy::unused_async_trait_impl,
reason = "uniform awaited hotspot API, the command send is synchronous"
)]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
pub async fn stop_hotspot(&self) {
if let Some(device) = &self.device {
device.stop_hotspot();
}
}

/// Join `offer`'s network, answering its gateway or the reason it will
/// not, within the machine's bound (R76).
///
/// # Errors
///
/// [`JoinError::Unsupported`] on a system without a hotspot or for a
/// build without a device identity, [`JoinError::MissingPermission`]
/// with a permission the device has not granted,
/// [`JoinError::Declined`] when the network is not available, and
/// [`JoinError::TimedOut`] when the network does not answer within its
/// bound.
#[cfg(feature = "peer")]
pub async fn join_hotspot(&self, offer: &HotspotOffer) -> Result<std::net::IpAddr, JoinError> {
match &self.device {
Some(device) => device.join_hotspot(offer).await,
None => Err(JoinError::Unsupported),
}
}

/// Leave the joined network, or cancel the pending request (R76).
///
/// A no-op on a system without a hotspot or for a build without a device
/// identity.
#[cfg(feature = "peer")]
#[expect(
clippy::unused_async,
clippy::unused_async_trait_impl,
reason = "uniform awaited hotspot API, the command send is synchronous"
)]
pub async fn leave_hotspot(&self) {
if let Some(device) = &self.device {
device.leave_hotspot();
}
}
}

#[cfg(feature = "native-auth")]
Expand Down
59 changes: 58 additions & 1 deletion crates/connetto-client/src/enrolment/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ use super::{Answer, Held, KeptList, Standing, TOLERANCE, half_life};
#[cfg(feature = "peer")]
use crate::PeerError;
use crate::device_key::{ChipError, ChipKeys, KeyRecords, OpenedKey};
#[cfg(feature = "peer")]
use crate::hotspot::{HOST_BOUND, HotspotError, HotspotOffer, JOIN_BOUND, JoinError, MARGIN};
#[cfg(all(feature = "peer", target_os = "android"))]
use crate::multicast::MulticastLock;
use crate::{ClientError, ClientEvent};
Expand Down Expand Up @@ -278,6 +280,7 @@ pub(crate) struct Enroller {
}

/// The application's half, held by the native client.
#[derive(Clone)]
pub(crate) struct EnrolHandle {
keys: Arc<dyn DeviceKeys>,
commands: mpsc::UnboundedSender<Command>,
Expand All @@ -289,6 +292,9 @@ pub(crate) struct EnrolHandle {
clock_off: Arc<AtomicBool>,
#[cfg(feature = "peer")]
peer: connetto_peer::Node,
#[cfg(feature = "peer")]
/// The hotspot machine's commands, until it ends with the client (R76).
hotspot: mpsc::UnboundedSender<crate::hotspot::Command>,
}

impl Enroller {
Expand All @@ -301,7 +307,7 @@ impl Enroller {
feature = "peer",
expect(
clippy::too_many_arguments,
reason = "the peer node joins the seven enrolment inputs and a config struct would hide the same arity behind another type"
reason = "the peer node and its hotspot channel join the seven enrolment inputs and a config struct would hide the same arity behind another type"
)
)]
pub(crate) fn new(
Expand All @@ -313,6 +319,7 @@ impl Enroller {
kept: Vec<KeptList>,
lists: super::ListInbox,
#[cfg(feature = "peer")] peer: Peer,
#[cfg(feature = "peer")] hotspot: mpsc::UnboundedSender<crate::hotspot::Command>,
) -> (Self, EnrolHandle) {
let (sender, commands) = mpsc::unbounded_channel();
let (published, observed) = watch::channel(held.as_ref().map(|held| held.leaf.clone()));
Expand Down Expand Up @@ -345,6 +352,8 @@ impl Enroller {
clock_off,
#[cfg(feature = "peer")]
peer: peer_node,
#[cfg(feature = "peer")]
hotspot,
},
)
}
Expand Down Expand Up @@ -426,6 +435,54 @@ impl EnrolHandle {
},
}
}

/// Host this device's hotspot, answered with its details or the reason
/// it will not, within the machine's bound (R76).
#[cfg(feature = "peer")]
pub(crate) async fn host_hotspot(&self) -> Result<HotspotOffer, HotspotError> {
let (reply, answer) = oneshot::channel();
self.hotspot
.send(crate::hotspot::Command::Host(reply))
.map_err(|_| HotspotError::Failed)?;
match tokio::time::timeout(HOST_BOUND + MARGIN, answer).await {
Ok(Ok(offer)) => offer,
Ok(Err(_)) => Err(HotspotError::Failed),
Err(_) => Err(HotspotError::TimedOut),
}
}

/// Stop hosting, or cancel the pending request (R76).
#[cfg(feature = "peer")]
pub(crate) fn stop_hotspot(&self) {
let _ = self.hotspot.send(crate::hotspot::Command::StopHost);
}

/// Join `offer`'s network, answered with its gateway, within the
/// machine's bound (R76).
#[cfg(feature = "peer")]
pub(crate) async fn join_hotspot(
&self,
offer: &HotspotOffer,
) -> Result<std::net::IpAddr, JoinError> {
let (reply, answer) = oneshot::channel();
self.hotspot
.send(crate::hotspot::Command::Join {
offer: offer.clone(),
reply,
})
.map_err(|_| JoinError::Failed)?;
match tokio::time::timeout(JOIN_BOUND + MARGIN, answer).await {
Ok(Ok(gateway)) => gateway,
Ok(Err(_)) => Err(JoinError::Failed),
Err(_) => Err(JoinError::TimedOut),
}
}

/// Leave the joined network, or cancel the pending request (R76).
#[cfg(feature = "peer")]
pub(crate) fn leave_hotspot(&self) {
let _ = self.hotspot.send(crate::hotspot::Command::Leave);
}
}

/// How long to sleep before looking at `held` again.
Expand Down
Loading
Loading