Add network-ping command for fast ICMP connectivity testing
@inureyes is already working on this.
Since Dec 16, 2025.
Assessment
This issue has not been assessed yet.
Description
Problem
Currently, the bssh ping command tests SSH connectivity by establishing a full SSH connection, authenticating, and executing a no-op command (true, see src/commands/ping.rs). While this is useful for verifying SSH service availability, it's relatively slow (2-5 seconds per host due to SSH handshake and authentication overhead).
Users often want to quickly check basic network connectivity across all cluster nodes without the SSH overhead, similar to the traditional ping command that uses ICMP packets and responds in milliseconds.
Proposed Solution
Add a new network-ping command that performs fast ICMP ping tests across all cluster nodes to verify basic network connectivity.
Implementation Details
CLI Changes
Add a new subcommand to the Commands enum in src/cli/bssh.rs (the old flat src/cli.rs was split into src/cli/{mod,bssh,pdsh}.rs in #105):
#[derive(Debug, Subcommand)]
pub enum Commands {
// ... existing commands ...
#[command(
visible_alias = "nping",
about = "Test network connectivity using ICMP ping (fast)",
long_about = "Sends ICMP echo requests to all target hosts in parallel.\nReports per-host packet loss and round-trip time statistics.\nDoes not open an SSH connection, so it does not verify SSH service availability or authentication.\n\nExit codes: 0 (all hosts answered), 1 (some hosts answered), 255 (no host answered, or bssh failed before sending)"
)]
NetworkPing {
#[arg(
short = 'c',
long = "count",
default_value = "4",
help = "Number of echo requests to send per host"
)]
count: u32,
#[arg(
short = 'W',
long = "timeout",
default_value = "2",
help = "Time to wait for a reply to each request, in seconds"
)]
timeout: u64,
#[arg(
long = "interval",
default_value = "0.2",
help = "Interval between requests, in seconds (minimum 0.2)"
)]
interval: f64,
},
}
Flag naming rationale (OpenSSH compatibility)
The original draft of this issue proposed -t for the timeout. That is rejected: the root Cli already binds -t to --tty (src/cli/bssh.rs:266), which is OpenSSH's "force pseudo-terminal allocation". Because no root argument is declared global = true, clap would not error, but -t would mean "force a TTY" before the subcommand and "timeout" after it. That is exactly the kind of positional ambiguity a drop-in SSH replacement must not introduce.
The rule adopted here: a subcommand short flag must not reuse a letter that the root Cli binds to a different meaning. Where the conventional ping short flag collides with an OpenSSH flag, the option becomes long-only.
Short flags already taken by the root Cli, nearly all with their OpenSSH meanings: -4 -6 -A -b -C -D -F -H -J -L -N -Q -R -S -T -f -i -k -l -o -p -q -t -v -x.
| Option | Choice | Reason |
|---|---|---|
| Packet count | -c, --count |
Matches ping -c. -c is free at the root (-C is --cluster, and clap is case sensitive). |
| Reply timeout | -W, --timeout |
Matches iputils ping -W ("time to wait for a response"). -W is free at the root. Avoids the -t/--tty collision. |
| Interval | --interval (long only) |
ping -i would shadow OpenSSH's -i (IdentityFile) at the root, so no short form is offered. |
| Address family | none; inherits root -4/-6 |
OpenSSH treats AddressFamily as a client-global setting, not a per-operation one. See the blocking dependency below. |
| Parallelism | none; inherits root --parallel |
Same as ping, upload, and download, which all read ctx.max_parallel (default 10). |
Two further constraints verified in the current code:
- Existing variants (
List,Ping,Upload,Download,Interactive,CacheStats) all use#[command(about = ..., long_about = ...)]prose rather than doc comments, so the new variant follows that style. - No subcommand in the codebase currently declares a clap alias, so
visible_alias = "nping"would be the first. This is accepted deliberately:npingis short enough to be worth it, and the alias does not shadow anything.
Core Implementation
Create src/commands/network_ping.rs and register it in src/commands/mod.rs:
- Use a Rust ICMP ping library (see options below)
- Perform parallel ping tests across all nodes
- Display results with latency statistics (min/avg/max/stddev)
- Show packet loss percentage
- Color-coded output (see thresholds below)
Reuse crate::ui::OutputFormatter::{format_command_header, format_summary} so the output matches ping, upload, and download.
Settled behavior
These points were unspecified in the original draft. They are now decided, following OpenSSH and iputils/BSD ping precedent.
Packet interval. Default 0.2 seconds, minimum 0.2 seconds. iputils ping permits intervals below 0.2s only for the superuser, so 0.2s is the safe unprivileged floor. With the default 4 packets this is roughly 0.6 seconds of wall clock per host, and hosts are probed in parallel.
Concurrency. No subcommand flag. The command reuses the global --parallel value (ctx.max_parallel, default 10), consistent with every other multi-node command.
Output modes. Normal output only. No TUI, no --stream, no --output-dir. This mirrors ping, whose implementation states "Use normal execution (no TUI, no streaming) for ping". A fixed-size result table gains nothing from streaming, and the run is short enough that progress monitoring is unnecessary.
pdsh compatibility mode. Not exposed. pdsh has no ICMP subcommand, and src/cli/pdsh.rs maps pdsh options onto bssh behavior rather than exposing bssh subcommands. network-ping stays bssh-only.
Latency color thresholds. There is no OpenSSH precedent here, so these are set as a starting point and may be tuned: green under 10 ms, yellow from 10 ms to 100 ms, red above 100 ms. Any packet loss forces red regardless of latency.
Statistics fields. min/avg/max/stddev, matching BSD ping's summary line. The original sample output omitted stddev; it is included below. (iputils prints mdev instead; stddev is chosen because it is the more widely understood label and the issue's own text already said stddev.)
Exit codes. 0 when every targeted host answered at least one echo request. 1 when at least one host answered and at least one did not. 255 when no host answered, or when bssh failed before it could send anything (ICMP socket creation denied, no hosts resolved, bad arguments). The 255 case follows OpenSSH's convention that 255 means "ssh itself failed" rather than "the remote operation failed". This must stay consistent with #245, which settles the same question for the sibling ping command.
Suggested Libraries
Option 1: surge-ping (Recommended)
- Pure Rust, async-friendly with tokio
- Cross-platform (Linux, macOS, Windows)
- No external dependencies
- Supports both privileged and unprivileged ICMP
[dependencies]
surge-ping = "0.9"
Option 2: fastping-rs
- Simple API, battle-tested
- Requires raw sockets (may need elevated privileges)
Option 3: pnet (lower-level)
- Full network protocol suite
- More complex but very flexible
- Requires privileged access
Example Implementation Skeleton
use anyhow::Result;
use surge_ping::{Client, Config, ICMP, PingIdentifier, PingSequence};
use std::net::IpAddr;
use std::time::Duration;
use tokio::time::timeout;
pub async fn network_ping_nodes(
nodes: Vec<Node>,
count: u32,
ping_timeout: u64,
interval: f64,
max_parallel: usize,
) -> Result<()> {
// Create ICMP client
let client = Client::new(&Config::default())?;
// Create tasks for each node
let tasks: Vec<_> = nodes.iter().map(|node| {
let client = client.clone();
let host = node.host.clone();
tokio::spawn(async move {
ping_host(&client, &host, count, ping_timeout).await
})
}).collect();
// Execute with concurrency limit
// ... parallel execution logic ...
// Display results with statistics
// ... formatting and output ...
Ok(())
}
async fn ping_host(
client: &Client,
host: &str,
count: u32,
timeout_secs: u64,
) -> Result<PingStats> {
// NOTE (2026-08-02): `Node.host` is a hostname string, not necessarily a
// literal IP. Nothing in the current SSH path parses it as `IpAddr` (russh
// and tokio resolve `(host, port)` for us), so this line must become a DNS
// lookup (e.g. `tokio::net::lookup_host`) or every hostname-based node
// fails to parse. Which resolved address to pick is an open decision, see
// "Decisions still required" below.
let addr: IpAddr = host.parse()?;
let mut pinger = client.pinger(addr, PingIdentifier(rand::random())).await;
let mut latencies = Vec::new();
let mut lost = 0;
for seq in 0..count {
match timeout(
Duration::from_secs(timeout_secs),
pinger.ping(PingSequence(seq as u16), &[])
).await {
Ok(Ok((_, duration))) => latencies.push(duration),
_ => lost += 1,
}
}
Ok(PingStats::from_latencies(latencies, lost, count))
}
Note that Config::default() selects ICMPv4. Honoring the root -4/-6 flags requires selecting ICMP::V4 or ICMP::V6 explicitly, which is why the ICMP import above is currently unused.
Output Format
▶ Network Ping Test Results (12 nodes)
● 10.100.64.101 4/4 packets min/avg/max/stddev = 0.5/1.2/2.1/0.6 ms
● 10.100.64.102 4/4 packets min/avg/max/stddev = 0.8/1.5/2.3/0.5 ms
● 10.100.64.103 3/4 packets min/avg/max/stddev = 1.2/2.1/3.0/0.8 ms (25% loss)
● 10.100.64.104 0/4 packets - Host unreachable
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary: 3 reachable, 1 unreachable (25% success rate)
Average latency: 1.6 ms (mean of per-host averages)
Usage Examples
# Basic network ping to all cluster nodes
bssh -C production network-ping
# Custom packet count and reply timeout
bssh -C production network-ping -c 10 -W 1
# Slower probing
bssh -C production network-ping -c 10 --interval 1.0
# Force IPv6 (requires the -4/-6 wiring issue to land first)
bssh -6 -C production network-ping
# Quick alias
bssh -C production nping
# With specific hosts
bssh -H "host1,host2,host3" network-ping
Comparison: ping vs network-ping
| Command | Purpose | Speed | Tests |
|---|---|---|---|
ping |
SSH connectivity test | 2-5s per node | SSH service + auth |
network-ping |
Network connectivity test | ~0.6s total at defaults (4 packets at 0.2s interval, hosts probed in parallel), plus up to -W per unresponsive host |
ICMP reachability |
The original draft claimed "<100ms per node", which is not achievable with a multi-packet probe: the floor is count * interval. The corrected figure reflects the settled defaults.
Files to Modify/Create
src/
├── cli/
│ └── bssh.rs # Add NetworkPing variant to Commands enum
├── app/
│ └── dispatcher.rs # Route NetworkPing (see match sites below)
└── commands/
├── mod.rs # Export network_ping module
└── network_ping.rs # New: ICMP ping implementation
Cargo.toml # Add surge-ping dependency
README.md # Document under "Built-in Commands"
docs/man/bssh.1 # Document under .SH COMMANDS
Command routing moved out of src/main.rs into src/app/dispatcher.rs in 5f3c320 (main.rs split, Phase 2, 2025-10-20). A new variant must be handled at four match sites in dispatcher.rs, not one:
- the main
match &cli.commanddispatch arm subcommand_name()(diagnostics label)sudo_password_is_applicable()(should returnfalse; ICMP has no sudo hook)ssh_password_is_applicable()(should returnfalse; ICMP opens no SSH connection)
Dependencies to Add
[dependencies]
surge-ping = "0.9" # ICMP ping library (0.9.0 released 2026-06-29)
rand is already a direct dependency (rand = "0.10"), so the rand::random() call in the skeleton needs no new dependency.
Security Considerations
ICMP Raw Sockets:
- On Linux: May require
CAP_NET_RAWcapability or root privileges - On macOS: Generally works without special permissions
On Windows: Requires administrator privileges(not applicable: bssh does not build for Windows.nixis an unconditional dependency inCargo.toml, and the release matrix ships only linux-gnu/musl x86_64+aarch64 and aarch64-apple-darwin.)
Solutions:
- Use
surge-pingwith unprivileged mode (SOCK_DGRAM) when possible - Document privilege requirements in README
- Fall back to TCP ping if ICMP is unavailable
- Provide clear error messages if permissions are insufficient
Note that items 1, 3, and 4 are not yet a chosen strategy; see "Decisions still required".
Blocking dependency
Honoring -4/-6 requires those flags to actually work. They are currently parsed and then ignored: src/cli/bssh.rs:281-296 declares them, docs/man/bssh.1 documents them, and no code in the repository reads cli.ipv4 or cli.ipv6. #246 tracks wiring them into the connection path (along with the AddressFamily SSH config keyword, which is parsed and resolved but likewise never consumed). Until #246 lands, network-ping either hardcodes ICMPv4 or becomes the first consumer of the preference, which is a scheduling decision between the two issues.
Testing Plan
-
Unit Tests:
- Ping parsing and statistics calculation (min/avg/max/stddev)
- Timeout handling
- Packet loss detection
- Exit code mapping for the all-success, partial-failure, and total-failure cases
-
Integration Tests:
- Single host ping
- Multi-host parallel ping
- Timeout scenarios
- Unreachable hosts
- Note: whether these can run in CI is unresolved, see "Decisions still required"
-
Manual Testing:
- Test on Linux (various distros)
- Test on macOS
- Test with different privilege levels
- Compare with system
pingcommand
Alternative Implementations
If ICMP proves problematic, consider:
- TCP Ping: Connect to SSH port without authentication
- HTTP Ping: If nodes have web services
- Hybrid: Try ICMP first, fall back to TCP
Decisions still required
Nothing below is settled. These need answering before or during implementation. They are recorded here so the decision can be made later from the issue alone.
- Whether to build this at all. The issue is
priority:low/status:backlogand its own closing paragraph notes thatpingalready covers SSH connectivity testing. No one has committed to the feature. - Which ICMP library. Three candidates are listed above with
surge-pingmarked "Recommended", but no evaluation has been done. Deciding factors: unprivileged SOCK_DGRAM support on both Linux and macOS, IPv6 support (needed for-6), and maintenance activity. - What happens when ICMP is unavailable. The four "Solutions" above are parallel options, not a strategy. The choice is material: adding an automatic TCP fallback changes what the command measures, so the comparison table's claim of "ICMP reachability" would no longer hold. Options: hard error with a clear message and exit 255; automatic fallback with the transport labeled per host in the output; or an explicit opt-in flag.
- When, if ever, the TCP/HTTP/Hybrid alternatives trigger. Related to 3, but broader: "if ICMP proves problematic" has no defined trigger.
- Jump host behavior. ICMP cannot traverse
-J/--jump-host, which bssh supports. For a node reachable only through a jump host,network-pingmust either report it unreachable (accurate for ICMP, misleading for the user), skip it with a notice, or fall back to a TCP probe tunneled through the jump host. This is the largest open question. - Which resolved address to probe. A hostname may resolve to several addresses.
pingprobes one. Options: first address in resolver order (matching the existing SSH connect loop's preference order), all addresses, or the address family forced by-4/-6. Interacts with #246. - CI feasibility. The integration tests need an ICMP socket. Whether GitHub's
ubuntu-latestrunners permit unprivileged ICMP (vianet.ipv4.ping_group_range) has not been checked. If they do not, the integration tests must be feature-gated or moved to manual testing. - Final exit code mapping. The 0/1/255 scheme above is proposed, not ratified. #245 asks the same question for
pingand frames it as Option A (0/1 only, preserving the currently documented text) versus Option B (0/1/255, OpenSSH-aligned). Whichever option #245 adopts governs here too;network-pingshould not diverge from its sibling.
Related Issues
- Complements existing
pingcommand (SSH connectivity test) - Blocked on #246 (
-4/-6address family flags are parsed but ignored) for address family support - Exit code semantics must agree with #245 (
pingexit code contract) - Part of broader cluster management features
Priority
Low - Nice to have feature for quick network checks, though ping command already provides SSH connectivity testing.
Refresh log
-
2026-08-02 - Refreshed against
mainatc3b8ac2. Feature is still entirely unimplemented: nonetwork_pingmodule, noNetworkPingvariant, nosurge-pingdependency. All items remain open.- Renamed references: 3.
src/cli.rstosrc/cli/bssh.rs(split in #105, 2025-12-17, after this issue was filed);src/main.rsrouting tosrc/app/dispatcher.rs(5f3c320, 2025-10-20, which already predated this issue, so the original body was pointing at a stale path from the start);src/commands/mod.rspath re-nested under the tree. - Corrected facts: 3.
pingexecutestrue, notecho 'pong';surge-pingbumped 0.8 to 0.9 (0.9.0, 2026-06-29);rand 0.10is already a direct dependency. - Marked obsolete: 1. The Windows administrator-privileges bullet. bssh does not build for Windows (unconditional
nixdependency) and ships no Windows release artifact. - Added implementation notes: dispatcher requires four match arms, not one;
-talready means--ttyat the root; no existing subcommand uses a clap alias;Node.hostis a hostname so the skeleton'sIpAddrparse needs a DNS lookup; docs targets are README "Built-in Commands" anddocs/man/bssh.1.SH COMMANDS. - Surfaced (not added to scope): ICMP cannot traverse
-J/--jump-host, which bssh supports. Whatnetwork-pingshould do for jump-host-reachable-only nodes is undecided and may deserve its own discussion before implementation.
- Renamed references: 3.
-
2026-08-02 (second pass, verified against
mainat0eb3fac; onlyCargo.lockmoved sincec3b8ac2, so everyCargo.tomlclaim above still holds) - Resolved the flag collision and the previously unspecified behaviors, and separated what is now settled from what still needs a decision.- Flag set reworked for OpenSSH compatibility:
-trejected because the rootClibinds it to--tty. Timeout becomes-W, --timeout(iputilsping -W), interval becomes long-only--intervalbecauseping -iwould shadow OpenSSH's-i(IdentityFile), count stays-c. Address family and parallelism inherit the root flags rather than adding subcommand duplicates. The governing rule is stated in the body: a subcommand short flag must not reuse a root letter with a different meaning. - Settled 7 previously unspecified points: packet interval (0.2s default and floor, per iputils' unprivileged limit), concurrency (inherits
--parallel), output modes (normal only, mirroringping), pdsh exposure (none), color thresholds (10ms / 100ms), statistics fields (min/avg/max/stddev, sample output corrected), and exit codes (0/1/255 with 255 following OpenSSH's "ssh itself failed" convention). - Corrected the comparison table: "<100ms per node" is unachievable for a multi-packet probe whose floor is
count * interval. Replaced with the actual figure at the settled defaults. - Added a "Blocking dependency" section:
-4/-6are declared atsrc/cli/bssh.rs:281-296and read by nothing, so address family support depends on #246. - Added a "Decisions still required" section with 8 numbered items, so the remaining choices can be made later from the issue alone rather than rediscovered.
- Filed two issues found during this pass: #245 (
pingdocuments exit codes 0/1 but always exits 0) and #246 (-4/-6parsed but never consumed, along with theAddressFamilyconfig keyword). Both govern decisions listed here.
- Flag set reworked for OpenSSH compatibility:
- Dominant language
- Rust
- Stars
- 65
- Forks
- 7
- Avg merge
- 1h 30m
- Merged PRs (30d)
- 25
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lablup/bssh
-
priority:medium status:backlog type:bug
Difficulty 5/5 Over a week Newbie friendliness 28/100
-
priority:high status:in-progress type:enhancement
Difficulty 5/5 Over a week Newbie friendliness 25/100
-
priority:medium status:backlog type:enhancement
-
priority:low status:backlog type:enhancement
-
priority:medium status:backlog type:enhancement
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100