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
81 changes: 73 additions & 8 deletions src/dialdbg/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@
/// URI to dial. Must be provided.
#[arg(short, long, required(true), display_order(0))]
uri: Option<String>,

/// Force ICE transport policy to relay-only (only TURN candidates). Implies WebRTC.
#[arg(long, action, conflicts_with("nowebrtc"), conflicts_with("force_p2p"))]
force_relay: bool,

/// Strip TURN servers so only host/srflx candidates are used. Implies WebRTC.
#[arg(
long,
action,
conflicts_with("nowebrtc"),
conflicts_with("force_relay")
)]
force_p2p: bool,

/// Filter the signaling server's TURN list to only the server whose parsed URI
/// matches. Example: "turn:turn.viam.com:443". Implies WebRTC.
#[arg(long, conflicts_with("nowebrtc"))]
turn_uri: Option<String>,

/// Override the signaling server address used for WebRTC negotiation.
#[arg(long, conflicts_with("nowebrtc"))]
signaling_server: Option<String>,

/// Disable mDNS discovery. Useful when the robot is on the same local network and
/// you want to test cloud relay without mDNS bypassing it.
#[arg(long, action)]
disable_mdns: bool,
}

async fn dial_grpc(
Expand Down Expand Up @@ -106,33 +133,66 @@
}
}

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.94.0/index.html#too_many_arguments = note: `#[warn(clippy::too_many_arguments)]` on by default
let dial_result = match credential {
"" => {
dial::DialOptions::builder()
let mut b = dial::DialOptions::builder()
.uri(uri)
.without_credentials()
.allow_downgrade()
.connect()
.await
.allow_downgrade();
if force_relay {
b = b.force_relay();
}
if force_p2p {
b = b.force_p2p();
}
if let Some(u) = turn_uri {
b = b.turn_uri(u);
}
if let Some(server) = signaling_server {
b = b.signaling_server(server);
}
if disable_mdns {
b = b.disable_mdns();
}
b.connect().await
}
_ => {
let creds = dial::RPCCredentials::new(
entity,
credential_type.to_string(),
credential.to_string(),
);
dial::DialOptions::builder()
let mut b = dial::DialOptions::builder()
.uri(uri)
.with_credentials(creds)
.allow_downgrade()
.connect()
.await
.allow_downgrade();
if force_relay {
b = b.force_relay();
}
if force_p2p {
b = b.force_p2p();
}
if let Some(u) = turn_uri {
b = b.turn_uri(u);
}
if let Some(server) = signaling_server {
b = b.signaling_server(server);
}
if disable_mdns {
b = b.disable_mdns();
}
b.connect().await
}
};

Expand All @@ -149,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.94.0/index.html#len_zero
writeln!(out, "\nno mDNS addresses discovered on current subnet")?;
return Ok(());
}
Expand Down Expand Up @@ -255,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, consider using `is_ok()`

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

Expand Down Expand Up @@ -287,6 +347,11 @@
credential.as_str(),
credential_type.as_str(),
args.entity.clone(),
args.force_relay,
args.force_p2p,
args.turn_uri.clone(),
args.signaling_server.clone(),
args.disable_mdns,
)
.await;
let wrtc_res = parse::parse_webrtc_logs(log_path.clone(), &mut out)?;
Expand Down Expand Up @@ -323,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, consider using `is_ok()`

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

Expand Down
49 changes: 41 additions & 8 deletions src/dialdbg/stats.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::fmt;
use tokio::time::Instant;
use webrtc::stats;

pub(crate) struct StatsReport(pub(crate) stats::StatsReport);
Expand All @@ -8,9 +7,46 @@
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// NOTE(benjirewis): StatsReport contains 13 types of stat reports; there may be more relevant stats
// to print here, but for now I have stuck with only printing the candidates.
writeln!(f, "\nnominated ICE candidates:\n")?;
let now = Instant::now();
writeln!(f, "\nnominated ICE candidate pair:\n")?;

// Find the nominated candidate pair first, then look up its local/remote
// candidates by ID so we only print the pair that was actually used.
let nominated_pair = self.0.reports.values().find_map(|v| {
if let stats::StatsReportType::CandidatePair(ref pair) = v {
if pair.nominated {
return Some(pair);
}
}
None
});

if let Some(pair) = nominated_pair {
for (id, label) in [
(&pair.local_candidate_id, "local"),
(&pair.remote_candidate_id, "remote"),
] {
let cand = self.0.reports.get(id).and_then(|v| match v {
stats::StatsReportType::LocalCandidate(c)
| stats::StatsReportType::RemoteCandidate(c) => Some(c),
_ => None,
});
let Some(cand) = cand else {
continue;
};
writeln!(f, "\t{label} ICE candidate:")?;
writeln!(f, "\t\tIP address: {}", cand.ip)?;
writeln!(f, "\t\tport: {}", cand.port)?;
writeln!(f, "\t\tcandidate type: {}", cand.candidate_type)?;
writeln!(f, "\t\tnominated {:#?} ago", cand.timestamp.elapsed())?;
writeln!(f, "\t\trelay protocol: {}", cand.relay_protocol)?;
writeln!(f, "\t\tnetwork type: {}", cand.network_type)?;
}
} else {
writeln!(f, "\t(no nominated pair found)")?;
}

writeln!(f, "\nall ICE candidates:\n")?;
for (_, value) in &self.0.reports {

Check warning on line 49 in src/dialdbg/stats.rs

View workflow job for this annotation

GitHub Actions / clippy

you seem to want to iterate on a map's values

warning: you seem to want to iterate on a map's values --> src/dialdbg/stats.rs:49:27 | 49 | for (_, value) in &self.0.reports { | ^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#for_kv_map = note: `#[warn(clippy::for_kv_map)]` on by default help: use the corresponding method | 49 - for (_, value) in &self.0.reports { 49 + for value in self.0.reports.values() { |
match value {
stats::StatsReportType::LocalCandidate(ref cand)
| stats::StatsReportType::RemoteCandidate(ref cand) => {
Expand All @@ -22,11 +58,8 @@
writeln!(f, "\t{} ICE candidate:", remote_or_local)?;
writeln!(f, "\t\tIP address: {}", cand.ip)?;
writeln!(f, "\t\tport: {}", cand.port)?;
writeln!(
f,
"\t\tnominated {:#?} ago",
now.duration_since(cand.timestamp)
)?;
writeln!(f, "\t\tcandidate type: {}", cand.candidate_type)?;
writeln!(f, "\t\tnominated {:#?} ago", cand.timestamp.elapsed())?;
writeln!(f, "\t\trelay protocol: {}", cand.relay_protocol)?;
writeln!(f, "\t\tnetwork type: {}", cand.network_type)?;
}
Expand Down
92 changes: 92 additions & 0 deletions src/dialdbg/test-relay-and-p2p-dial-opts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail

cargo build --features dialdbg --bin viam-dialdbg 2>&1 | tail -1
BINARY=./target/debug/viam-dialdbg

HOST="<machine-fqdn>"
ENTITY="<api-key-id>"
APIKEY="<api-key>"
# TURN_URI should be the URI of a TURN server returned by the signaling server.
# Example: "turn:turn.viam.com:443"
TURN_URI="<turn-uri>"
COMMON=(-u "$HOST" -e "$ENTITY" -t api-key -c "$APIKEY" --nogrpc --nortt)

PASS=0
FAIL=0

# Extract the local ICE candidate type from dialdbg output.
# Stats print "local ICE candidate:" followed by fields including "candidate type: <type>".
# We grab the type value on the line immediately after the local candidate header.
local_candidate_type() {
awk '/\tlocal ICE candidate:/{found=1} found && /candidate type:/{print $NF; found=0}' <<< "$1" | head -1
}

assert() {
local name="$1" output="$2" want_type="$3" # want_type: "relay", "!relay", or "none"
printf '\n=== %s ===\n' "$name"
printf '%s\n' "$output"

local actual
actual=$(local_candidate_type "$output")

if [[ "$want_type" == "none" ]]; then
# Expect connection failure — no candidates should be nominated.
if [[ -z "$actual" ]]; then
printf 'PASS: no candidates nominated (expected failure)\n'
((++PASS))
else
printf 'FAIL: expected no candidates, got local candidate type: %s\n' "$actual"
((++FAIL))
fi
elif [[ "$want_type" == "!relay" ]]; then
# Expect a successful non-relay connection.
if [[ -n "$actual" && "$actual" != "relay" ]]; then
printf 'PASS: local candidate type: %s (not relay)\n' "$actual"
((++PASS))
elif [[ -z "$actual" ]]; then
printf 'FAIL: connection failed (no candidates nominated)\n'
((++FAIL))
else
printf 'FAIL: expected non-relay candidate, got: %s\n' "$actual"
((++FAIL))
fi
else
# Expect a specific candidate type (e.g. "relay").
if [[ "$actual" == "$want_type" ]]; then
printf 'PASS: local candidate type: %s\n' "$actual"
((++PASS))
elif [[ -z "$actual" ]]; then
printf 'FAIL: connection failed (no candidates nominated)\n'
((++FAIL))
else
printf 'FAIL: expected candidate type %s, got: %s\n' "$want_type" "$actual"
((++FAIL))
fi
fi
}

run() {
"$BINARY" "${COMMON[@]}" "$@" 2>/dev/null || true
}

# 1. Baseline — expect non-relay (host or srflx)
assert "baseline (no flags)" "$(run)" "!relay"

# 2. ForceRelay — expect relay
assert "ForceRelay" "$(run --force-relay)" "relay"

# 3. ForceRelay + TurnUri matching a valid TURN server — expect relay
assert "ForceRelay + TurnUri" "$(run --force-relay --turn-uri "$TURN_URI")" "relay"

# 4. ForceP2P — expect non-relay (host or srflx)
assert "ForceP2P" "$(run --force-p2p)" "!relay"

# 5. TurnUri alone — filters TURN options but ICE still picks host/srflx; expect non-relay
assert "TurnUri (no ForceRelay)" "$(run --turn-uri "$TURN_URI")" "!relay"

# 6. ForceRelay + non-matching TurnUri — all TURN filtered out, no relay candidates, expect failure
assert "ForceRelay + TurnUri=notexist (expect: fail)" "$(run --force-relay --turn-uri "turn:notexist.example.com:3478")" "none"

printf '\n%d passed, %d failed.\n' "$PASS" "$FAIL"
[[ $FAIL -eq 0 ]]
Loading
Loading