Skip to content
Draft
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
3 changes: 3 additions & 0 deletions src/dialdbg/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,17 @@
}
}

async fn dial_webrtc(
uri: &str,
credential: &str,
credential_type: &str,
entity: Option<String>,
force_relay: bool,
force_p2p: bool,
turn_uri: Option<String>,
signaling_server: Option<String>,
disable_mdns: bool,
) -> Option<ViamChannel> {

Check warning on line 146 in src/dialdbg/main.rs

View workflow job for this annotation

GitHub Actions / clippy

this function has too many arguments (9/7)

warning: this function has too many arguments (9/7) --> src/dialdbg/main.rs:136:1 | 136 | / async fn dial_webrtc( 137 | | uri: &str, 138 | | credential: &str, 139 | | credential_type: &str, ... | 145 | | disable_mdns: bool, 146 | | ) -> Option<ViamChannel> { | |________________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#too_many_arguments = note: `#[warn(clippy::too_many_arguments)]` on by default
let dial_result = match credential {
"" => {
let mut b = dial::DialOptions::builder()
Expand Down Expand Up @@ -209,7 +209,7 @@

async fn output_all_mdns_addresses(out: &mut Box<dyn io::Write>) -> Result<()> {
let responses = all_mdns_addresses().await?;
if responses.len() == 0 {

Check warning on line 212 in src/dialdbg/main.rs

View workflow job for this annotation

GitHub Actions / clippy

length comparison to zero

warning: length comparison to zero --> src/dialdbg/main.rs:212:8 | 212 | if responses.len() == 0 { | ^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `responses.is_empty()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#len_zero
writeln!(out, "\nno mDNS addresses discovered on current subnet")?;
return Ok(());
}
Expand Down Expand Up @@ -315,7 +315,7 @@
}

// Remove temp log file after parsing if it exists.
if let Ok(_) = log_path.try_exists() {

Check warning on line 318 in src/dialdbg/main.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant pattern matching

warning: redundant pattern matching --> src/dialdbg/main.rs:318:16 | 318 | if let Ok(_) = log_path.try_exists() { | ^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#redundant_pattern_matching = note: `#[warn(clippy::redundant_pattern_matching)]` on by default help: consider using `is_ok()` | 318 - if let Ok(_) = log_path.try_exists() { 318 + if log_path.try_exists().is_ok() { |
fs::remove_file(log_path)?;
}

Expand Down Expand Up @@ -388,7 +388,7 @@
}

// Remove temp log file after parsing if it exists.
if let Ok(_) = log_path.try_exists() {

Check warning on line 391 in src/dialdbg/main.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant pattern matching

warning: redundant pattern matching --> src/dialdbg/main.rs:391:16 | 391 | if let Ok(_) = log_path.try_exists() { | ^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#redundant_pattern_matching help: consider using `is_ok()` | 391 - if let Ok(_) = log_path.try_exists() { 391 + if log_path.try_exists().is_ok() { |
fs::remove_file(log_path)?;
}

Expand All @@ -400,5 +400,8 @@

#[tokio::main]
async fn main() -> Result<()> {
// dialdbg is a diagnostic tool; its dials are synthetic (and often use force_p2p/force_relay),
// so keep them out of the best-effort dial connection reports sent to app (see rpc::dial_report).
std::env::set_var("VIAM_DISABLE_DIAL_REPORTING", "1");
main_inner(Args::parse()).await
}
219 changes: 214 additions & 5 deletions src/rpc/dial.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{
client_channel::*,
dial_report::{self, StageTracker},
log_prefixes,
webrtc::{webrtc_action_with_timeout, Options},
};
Expand All @@ -13,7 +14,8 @@
OptionalWebRtcConfigRequest, OptionalWebRtcConfigResponse,
};
use crate::gen::proto::rpc::webrtc::v1::{
CallRequest, IceCandidate, Metadata, RequestHeaders, Strings,
CallRequest, ConnectionSignalingPath, DialStage, IceCandidate, Metadata,
ReportConnectionMetadataRequest, RequestHeaders, Strings,
};
use crate::rpc::webrtc;
use ::http::header::HeaderName;
Expand Down Expand Up @@ -57,7 +59,7 @@
const STATUS_CODE_UNKNOWN: i32 = 2;
const STATUS_CODE_RESOURCE_EXHAUSTED: i32 = 8;

pub const VIAM_MDNS_SERVICE_NAME: &'static str = "_rpc._tcp.local";

Check warning on line 62 in src/rpc/dial.rs

View workflow job for this annotation

GitHub Actions / clippy

constants have by default a `'static` lifetime

warning: constants have by default a `'static` lifetime --> src/rpc/dial.rs:62:36 | 62 | pub const VIAM_MDNS_SERVICE_NAME: &'static str = "_rpc._tcp.local"; | -^^^^^^^---- help: consider removing `'static`: `&str` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#redundant_static_lifetimes = note: `#[warn(clippy::redundant_static_lifetimes)]` on by default

type SecretType = String;

Expand Down Expand Up @@ -290,7 +292,7 @@
impl<T> fmt::Debug for DialBuilder<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Dial")
.field("State", &format_args!("{}", &std::any::type_name::<T>()))

Check warning on line 295 in src/rpc/dial.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant reference in `format_args!` argument

warning: redundant reference in `format_args!` argument --> src/rpc/dial.rs:295:49 | 295 | .field("State", &format_args!("{}", &std::any::type_name::<T>())) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: remove the redundant `&`: `std::any::type_name::<T>()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#useless_borrows_in_formatting
.field("Opt", &format_args!("{:?}", self.config))
.finish()
}
Expand Down Expand Up @@ -725,6 +727,9 @@
original_uri,
self.config.signaling_server_override.as_deref(),
);
// `uri` is the resolved signaling server; classify how this WebRTC dial is signaled so a
// connection report can be delivered (only cloud-signaled dials report).
let signaling_path = dial_report::classify_signaling_path(uri.host().unwrap_or_default());
let domain = uri2.authority().to_owned().unwrap().as_str();

let mdns_uri = mdns_uri.and_then(|p| Uri::from_parts(p).ok());
Expand Down Expand Up @@ -780,7 +785,23 @@
log::debug!("{}", log_prefixes::DIALED_GRPC);
Ok(ViamChannel::Direct(channel.clone()))
} else {
match maybe_connect_via_webrtc(uri, intercepted_channel.clone(), webrtc_options).await {
// A cloud-signaled dial reports over its own channel. A locally-signaled dial would
// report to prod app reusing credentials, but this (WithoutCredentials) dial has none,
// so it is dropped (matches Go's fixUpReportDialOpts).
let report_target = match signaling_path {
ConnectionSignalingPath::CloudSignaled => {
Some(ReportTarget::Cloud(intercepted_channel.clone()))
}
_ => None,
};
match maybe_connect_via_webrtc(
uri,
intercepted_channel.clone(),
webrtc_options,
report_target,
)
.await
{
Ok(webrtc_channel) => Ok(ViamChannel::WebRTC(webrtc_channel)),
Err(e) => {
log::error!("error connecting via webrtc: {e}. Attempting to connect directly");
Expand Down Expand Up @@ -916,6 +937,18 @@
original_uri.clone(),
self.config.signaling_server_override.as_deref(),
);
// `uri_for_auth` is the resolved signaling server; classify how this WebRTC dial is
// signaled so a connection report can be delivered.
let signaling_path =
dial_report::classify_signaling_path(uri_for_auth.host().unwrap_or_default());
// Capture credentials for the local-dial report case before they are consumed by auth
// below; a locally-signaled WebRTC dial reports to prod app reusing these (see
// ReportTarget::Local).
let report_creds = self
.config
.credentials
.as_ref()
.map(|c| (c.credentials.clone(), c.entity.clone()));

let mdns_uri = mdns_uri.and_then(|p| Uri::from_parts(p).ok());
let attempting_mdns = mdns_uri.is_some();
Expand Down Expand Up @@ -982,7 +1015,30 @@
log::debug!("Connected via gRPC");
Ok(ViamChannel::DirectPreAuthorized(channel))
} else {
match maybe_connect_via_webrtc(original_uri, channel.clone(), webrtc_options).await {
// A cloud-signaled dial reports over its own authenticated channel; a locally-signaled
// dial reconstructs an app connection reusing these credentials (Go's
// fixUpReportDialOpts).
let report_target = match signaling_path {
ConnectionSignalingPath::CloudSignaled => {
Some(ReportTarget::Cloud(channel.clone()))
}
ConnectionSignalingPath::Local => {
report_creds.map(|(creds, entity)| ReportTarget::Local {
creds,
entity: entity.unwrap_or_else(|| domain.clone()),
host: domain.clone(),
})
}
_ => None,
};
match maybe_connect_via_webrtc(
original_uri,
channel.clone(),
webrtc_options,
report_target,
)
.await
{
Ok(webrtc_channel) => Ok(ViamChannel::WebRTC(webrtc_channel)),
Err(e) => {
log::error!(
Expand Down Expand Up @@ -1136,24 +1192,158 @@

impl fmt::Display for CallerUpdateStats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let average_duration = &self.total_duration.as_millis() / &self.count;

Check warning on line 1195 in src/rpc/dial.rs

View workflow job for this annotation

GitHub Actions / clippy

needlessly taken reference of both operands

warning: needlessly taken reference of both operands --> src/rpc/dial.rs:1195:32 | 1195 | let average_duration = &self.total_duration.as_millis() / &self.count; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#op_ref = note: `#[warn(clippy::op_ref)]` on by default help: use the values directly | 1195 - let average_duration = &self.total_duration.as_millis() / &self.count; 1195 + let average_duration = self.total_duration.as_millis() / self.count; |
writeln!(
f,
"Caller update statistics: num_updates: {}, average_duration: {}ms, max_duration: {}ms",
&self.count,

Check warning on line 1199 in src/rpc/dial.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant reference in `writeln!` argument

warning: redundant reference in `writeln!` argument --> src/rpc/dial.rs:1199:13 | 1199 | &self.count, | ^^^^^^^^^^^ help: remove the redundant `&`: `self.count` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#useless_borrows_in_formatting
average_duration,
&self.max_duration.as_millis()

Check warning on line 1201 in src/rpc/dial.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant reference in `writeln!` argument

warning: redundant reference in `writeln!` argument --> src/rpc/dial.rs:1201:13 | 1201 | &self.max_duration.as_millis() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: remove the redundant `&`: `self.max_duration.as_millis()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#useless_borrows_in_formatting
)?;
Ok(())
}
}

/// The prod app signaling server, used to deliver reports for locally-signaled dials (Go's
/// fixUpReportDialOpts redirects such dials to prod app). Like Go, this is hard-coded to prod, so a
/// staging robot dialed by a local address does not route its report to app.viam.dev.
const APP_SIGNALING_ADDRESS_FOR_REPORT: &str = "app.viam.com:443";

/// How to deliver a WebRTC dial's connection report to the app signaling server (only app
/// implements ReportConnectionMetadata). Mirrors goutils#583's per-dial app-dial options.
enum ReportTarget {
/// The dial was cloud-signaled (routed through app); deliver over that same authenticated,
/// rpc-host-stamped channel.
Cloud(AddAuthorization<SetRequestHeader<Channel, HeaderValue>>),
/// The dial was signaled through a local signaling server; reconstruct an authenticated
/// connection to prod app, reusing the dial's credentials, and deliver there (Go's
/// fixUpReportDialOpts). Only a cloud-managed robot dialed by a local address can authenticate
/// to app; for anything else the report RPC fails harmlessly.
Local {
creds: Credentials,
entity: String,
host: String,
},
}

impl ReportTarget {
fn signaling_path(&self) -> ConnectionSignalingPath {
match self {
ReportTarget::Cloud(_) => ConnectionSignalingPath::CloudSignaled,
ReportTarget::Local { .. } => ConnectionSignalingPath::Local,
}
}
}

/// Reconstructs an authenticated connection to prod app to deliver a connection report for a
/// locally-signaled dial, reusing the dial's credentials (Go's fixUpReportDialOpts). The rpc-host
/// is the robot's host so app can attribute the report. app is always TLS.
async fn connect_app_signaling_for_report(
creds: Credentials,
entity: String,
host: String,
) -> Result<AddAuthorization<SetRequestHeader<Channel, HeaderValue>>> {
let uri = Uri::from_parts(uri_parts_with_defaults(APP_SIGNALING_ADDRESS_FOR_REPORT))?;
let mut channel = Channel::builder(uri).connect().await?;
let token = get_auth_token(&mut channel, creds, entity).await?;
Ok(ServiceBuilder::new()
.layer(AddAuthorizationLayer::bearer(&token))
.layer(SetRequestHeaderLayer::overriding(
HeaderName::from_static("rpc-host"),
HeaderValue::from_str(&host)?,
))
.service(channel))
}

/// Delivers a connection report to the target, reconstructing an app connection first for a
/// locally-signaled dial. Best-effort; failures are logged at debug.
async fn deliver_report(target: ReportTarget, request: ReportConnectionMetadataRequest) {
match target {
ReportTarget::Cloud(channel) => dial_report::send_dial_report(channel, request).await,
ReportTarget::Local {
creds,
entity,
host,
} => {
let reconstruct = connect_app_signaling_for_report(creds, entity, host);
match tokio::time::timeout(Duration::from_secs(5), reconstruct).await {
Ok(Ok(channel)) => dial_report::send_dial_report(channel, request).await,
Ok(Err(e)) => {
log::debug!("failed to connect to app to report local dial metadata: {e:#}")
}
Err(_) => log::debug!("timed out connecting to app to report local dial metadata"),
}
}
}
}

/// Attempts a WebRTC dial and, on completion (success or failure), best-effort reports the dial's
/// connection metadata to the app signaling server (a port of goutils#583). Reporting never adds
/// latency to or fails the dial. `report_target` is None when the dial should not be reported
/// (e.g. a locally-signaled dial with no credentials to authenticate to app with).
async fn maybe_connect_via_webrtc(
uri: Uri,
channel: AddAuthorization<SetRequestHeader<Channel, HeaderValue>>,
webrtc_options: Option<Options>,
report_target: Option<ReportTarget>,
) -> Result<Arc<WebRTCClientChannel>> {
let dial_start = Instant::now();
let stage_tracker = Arc::new(StageTracker::new());

let result =
maybe_connect_via_webrtc_inner(uri, channel, webrtc_options, stage_tracker.clone()).await;

// Reporting is disabled under cfg(test).
if let Some(report_target) = report_target.filter(|_| dial_report::dial_reporting_enabled()) {
let duration_ms = dial_start.elapsed().as_millis().min(u32::MAX as u128) as u32;
let reached_stage = stage_tracker.reached();
let dial_succeeded = result.is_ok();
let signaling_path = report_target.signaling_path();

if dial_report::should_deliver(reached_stage, dial_succeeded, signaling_path) {
// On success, classify the selected ICE candidate pair from the live peer connection;
// a failed dial has no peer connection to inspect.
let peer_connection = result
.as_ref()
.ok()
.map(|client_channel| client_channel.base_channel.peer_connection.clone());
let failure_code = result.as_ref().err().map_or(0, dial_report::failure_code);

// Detached so reporting (gathering stats, reconnecting to app, and the RPC) never adds
// latency to the dial, and a cancelled dial still reports.
tokio::spawn(async move {
let (local, remote) =
dial_report::classify_connection(peer_connection.as_deref()).await;
let request = ReportConnectionMetadataRequest {
local: Some(local),
remote: Some(remote),
reached_stage,
duration_ms,
signaling_path: signaling_path as i32,
failure_code,
};
deliver_report(report_target, request).await;
});
}
}

result
}

async fn maybe_connect_via_webrtc_inner(
uri: Uri,
channel: AddAuthorization<SetRequestHeader<Channel, HeaderValue>>,
webrtc_options: Option<Options>,
stage_tracker: Arc<StageTracker>,
) -> Result<Arc<WebRTCClientChannel>> {
let webrtc_options = webrtc_options.unwrap_or_else(|| Options::infer_from_uri(uri.clone()));

// The channel was eagerly connected by create_channel (it uses `.connect().await`) before this
// function is called, so the signaling connection is already established on entry. Matching Go,
// set SIGNALING_CONNECTED here so a config-RPC failure against a reachable server reports
// SIGNALING_CONNECTED rather than UNSPECIFIED.
stage_tracker.advance(DialStage::SignalingConnected);

let mut signaling_client = SignalingServiceClient::new(channel.clone());
let response = match signaling_client
.optional_web_rtc_config(OptionalWebRtcConfigRequest::default())
Expand All @@ -1170,6 +1360,7 @@
};

let optional_config = response.into_inner().config;
stage_tracker.advance(DialStage::ConfigFetched);

if webrtc_options.force_relay && webrtc_options.force_p2p {
log::warn!(
Expand Down Expand Up @@ -1210,8 +1401,12 @@
log::debug!("TURN filter options set: turn_uri={uri:?}");
}

let (peer_connection, data_channel) =
webrtc::new_peer_connection_for_client(config, webrtc_options.disable_trickle_ice).await?;
let (peer_connection, data_channel) = webrtc::new_peer_connection_for_client(
config,
webrtc_options.disable_trickle_ice,
stage_tracker.clone(),
)
.await?;

let sent_done_or_error = Arc::new(AtomicBool::new(false));
let uuid_lock = Arc::new(RwLock::new("".to_string()));
Expand Down Expand Up @@ -1246,10 +1441,17 @@

let caller_update_stats = caller_update_stats.clone();
let caller_update_stats2 = caller_update_stats.clone();
let stage_tracker_ice = stage_tracker.clone();
peer_connection.on_ice_connection_state_change(Box::new(
move |state: RTCIceConnectionState| {
let caller_update_stats = caller_update_stats.clone();
let stage_tracker_ice = stage_tracker_ice.clone();
Box::pin(async move {
if state == RTCIceConnectionState::Connected
|| state == RTCIceConnectionState::Completed
{
stage_tracker_ice.advance(DialStage::IceConnected);
}
if state == RTCIceConnectionState::Completed {
let caller_update_stats_inner = caller_update_stats.lock().unwrap();
log::debug!("{}", caller_update_stats_inner);
Expand Down Expand Up @@ -1390,9 +1592,12 @@
let client_channel_for_ice_gathering_thread = Arc::downgrade(&client_channel);
let mut signaling_client = SignalingServiceClient::new(channel.clone());
let mut call_client = signaling_client.call(call_request).await?.into_inner();
// The Call was accepted: the SDP offer has been sent to the signaling server.
stage_tracker.advance(DialStage::OfferSent);

let channel2 = channel.clone();
let sent_done_or_error2 = sent_done_or_error.clone();
let stage_tracker_task = stage_tracker.clone();
tokio::spawn(async move {
let uuid = uuid_for_ice_gathering_thread;
let client_channel = client_channel_for_ice_gathering_thread;
Expand Down Expand Up @@ -1476,6 +1681,8 @@
break;
}
}
// The answerer's SDP answer was received and applied.
stage_tracker_task.advance(DialStage::AnswerReceived);
let _ = remote_description_set_s.send_replace(Some(()));
if webrtc_options.disable_trickle_ice {
send_done_once(sent_done.clone(), &response.uuid, channel2.clone()).await;
Expand Down Expand Up @@ -1548,6 +1755,8 @@
}

exchange_done.store(true, Ordering::Release);
// The data channel is open: the connection is fully ready. This is success.
stage_tracker.advance(DialStage::Ready);
let uuid = uuid_lock.read().unwrap().to_string();
send_done_once(sent_done_or_error, &uuid, channel.clone()).await;
Ok(client_channel)
Expand Down
Loading
Loading