mod codeentry;
mod ctl;
mod diag;
mod direct;
mod doctor;
mod expose;
mod holepunch;
mod interact;
mod l2;
mod mount;
mod mount_proto;
#[cfg(target_os = "linux")]
mod mount_fuse;
#[cfg(all(target_os = "windows", feature = "mount-windows"))]
mod mount_winfsp;
mod backup;
mod net;
mod overlay;
mod platform;
mod pake;
mod pake_ceremony;
mod ping;
mod sdnotify;
mod protocol;
mod resilience;
mod session;
mod settings;
#[cfg(l3)]
mod tun;
#[cfg(l3)]
mod l3;
mod shutdown;
mod sshd;
mod sshkeys;
mod local;
mod ui;
use anyhow::{anyhow, bail, Context, Result};
use clap::{Parser, Subcommand};
use net::{Ev, Peer, Transport};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};
use std::io::{IsTerminal, Read, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::{mpsc, oneshot};
#[cfg(unix)]
fn pwrite_at(file: &std::fs::File, buf: &[u8], offset: u64) -> std::io::Result<usize> {
use std::os::unix::fs::FileExt;
file.write_at(buf, offset)
}
#[cfg(windows)]
fn pwrite_at(file: &std::fs::File, buf: &[u8], offset: u64) -> std::io::Result<usize> {
use std::os::windows::fs::FileExt;
file.seek_write(buf, offset)
}
const DEFAULT_SERVER: &str = "https://api.filament.autumated.com";
const HEAD_BYTES: u64 = 256 * 1024;
const REJOIN_WINDOW: Duration = Duration::from_secs(120);
fn rejoin_unwarned() -> Duration {
std::env::var("FILAMENT_REJOIN_SECS") .ok()
.and_then(|v| v.parse().ok())
.map(Duration::from_secs)
.unwrap_or(Duration::from_secs(45))
}
fn quiet_exit_window() -> Duration {
std::env::var("FILAMENT_QUIET_EXIT_SECS") .ok()
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or(Duration::from_secs(10))
}
const MAX_ATTEMPTS: u32 = 5;
#[cfg(feature = "test-hooks")]
mod test_hooks {
pub fn pair_stall() -> bool {
std::env::var("FILAMENT_TEST_PAIR_STALL").is_ok()
}
pub fn webrtc_relay_only() -> bool {
std::env::var("FILAMENT_TEST_WEBRTC_RELAY_ONLY").map(|v| v == "1").unwrap_or(false)
}
pub fn disable_modeb_drop() -> bool {
std::env::var("FILAMENT_TEST_DISABLE_MODEB_DROP").is_ok()
}
pub fn no_defer() -> bool {
std::env::var("FILAMENT_TEST_NO_DEFER").is_ok()
}
pub fn inject_peer_left_at() -> Option<u64> {
std::env::var("FILAMENT_TEST_INJECT_PEER_LEFT").ok().and_then(|v| v.parse::<u64>().ok())
}
pub fn no_signaling_reconnect() -> bool {
std::env::var("FILAMENT_TEST_NO_SIGNALING_RECONNECT").is_ok()
}
pub fn wedge_loop_on_shutdown() -> bool {
std::env::var("FILAMENT_TEST_WEDGE_LOOP").is_ok()
}
pub fn churn_after_complete() -> bool {
std::env::var("FILAMENT_TEST_CHURN_AFTER_COMPLETE").is_ok()
}
pub fn drop_file_end() -> bool {
std::env::var("FILAMENT_TEST_DROP_FILE_END").is_ok()
}
pub fn drop_peer_left() -> bool {
std::env::var("FILAMENT_TEST_DROP_PEER_LEFT").is_ok()
}
pub fn suppress_delivery_ack() -> bool {
std::env::var("FILAMENT_TEST_SUPPRESS_ACK").is_ok()
}
pub fn premature_close_after_ack() -> bool {
matches!(std::env::var("FILAMENT_TEST_PREMATURE_CLOSE").as_deref(), Ok("1") | Ok("once"))
}
static PREMATURE_FIRED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn premature_close_once() -> bool {
std::env::var("FILAMENT_TEST_PREMATURE_CLOSE").as_deref() == Ok("once")
}
pub fn premature_already_fired() -> bool {
PREMATURE_FIRED.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn premature_mark_fired() {
PREMATURE_FIRED.store(true, std::sync::atomic::Ordering::SeqCst);
}
static CORRUPT_FIRED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn corrupt_recv_target() -> Option<String> {
std::env::var("FILAMENT_TEST_CORRUPT_RECV").ok()
}
pub fn corrupt_recv_once() -> bool {
std::env::var("FILAMENT_TEST_CORRUPT_ONCE").map(|v| v == "1").unwrap_or(false)
}
pub fn corrupt_already_fired() -> bool {
CORRUPT_FIRED.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn corrupt_mark_fired() {
CORRUPT_FIRED.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
#[cfg(not(feature = "test-hooks"))]
mod test_hooks {
#[inline] pub fn pair_stall() -> bool { false }
#[inline] pub fn webrtc_relay_only() -> bool { false }
#[inline] pub fn disable_modeb_drop() -> bool { false }
#[inline] pub fn no_defer() -> bool { false }
#[inline] pub fn inject_peer_left_at() -> Option<u64> { None }
#[inline] pub fn no_signaling_reconnect() -> bool { false }
#[inline] pub fn wedge_loop_on_shutdown() -> bool { false }
#[inline] pub fn churn_after_complete() -> bool { false }
#[inline] pub fn drop_file_end() -> bool { false }
#[inline] pub fn drop_peer_left() -> bool { false }
#[inline] pub fn suppress_delivery_ack() -> bool { false }
#[inline] pub fn premature_close_after_ack() -> bool { false }
#[inline] pub fn premature_close_once() -> bool { false }
#[inline] pub fn premature_already_fired() -> bool { false }
#[inline] pub fn premature_mark_fired() {}
#[inline] pub fn corrupt_recv_target() -> Option<String> { None }
}
static NO_RELAY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn relay_forbidden() -> bool {
NO_RELAY.load(std::sync::atomic::Ordering::Relaxed)
}
static NO_INTERACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub struct UiCapability {
pub interactive: bool,
pub json: bool,
pub yes: bool,
pub color: bool,
}
impl UiCapability {
pub(crate) fn from_cli(cli: &Cli) -> Self {
let interactive = std::io::stdin().is_terminal()
&& !cli.no_interactive
&& std::env::var_os("FILAMENT_NONINTERACTIVE").is_none();
let color = match cli.color.as_deref() {
Some("always") => true,
Some("never") => false,
_ => std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none(),
};
UiCapability { interactive, json: cli.json, yes: cli.yes, color }
}
pub fn confirm(&self, action: &str) -> Result<()> {
if self.yes { return Ok(()); }
if self.interactive {
use std::io::Write;
eprint!("{action} [y/N] ");
let _ = std::io::stderr().flush();
let mut line = String::new();
std::io::stdin().read_line(&mut line).ok();
if !matches!(line.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
anyhow::bail!("cancelled");
}
} else {
anyhow::bail!("refusing {action} without --yes (non-interactive)");
}
Ok(())
}
}
fn interactive_allowed() -> bool {
std::io::stdin().is_terminal()
&& !NO_INTERACTIVE.load(std::sync::atomic::Ordering::Relaxed)
&& std::env::var_os("FILAMENT_NONINTERACTIVE").is_none()
}
fn relay_banner() -> String {
ui::paint(
ui::Tone::Warn,
"⚠ on relay, via a TURN server, not a direct link (still end-to-end encrypted)",
)
}
const STALL_MAX_REPAIRS: u32 = 5;
const VERIFY_PROBE_SID: u32 = 0x7FFF_FFFF;
const MAX_VERIFY_FAILS: u32 = 3;
fn maybe_hint_local_wedge(shown: &mut bool) {
if *shown {
return;
}
*shown = true;
ui::say(&ui::paint(
ui::Tone::Dim,
" still can't connect, if both ends are on the SAME machine, a browser's \
mDNS (.local) ICE candidates can block this; try a different network path, \
or disable mDNS ICE in the browser (chrome://flags → \"Anonymize local IPs\").",
));
}
const VERSION: &str = env!("FILAMENT_BUILD_INFO");
const EXAMPLES: &str = "\
EXAMPLES:
filament video.mp4 send it; mints a speakable one-time code + QR
filament clever-lynx-63 claim a code and receive
filament recv <code> -o - | tar x stream straight into a pipe
filament pair --name phone remember a device (no file transfer)
filament send big.iso --to laptop send to a remembered device, no code
filament up --install always-on drop target (trusted devices only)
The other end never needs anything installed: https://filament.autumated.com
More commands (run `filament <cmd> --help`):
ssh · forward remote shell / port-forward to a device
set tun-addr auto join the encrypted L3 overlay mesh (`filament addr`)
ping · doctor diagnose a link; introduce · grant · serve-tun · netcat";
#[derive(Parser)]
#[command(name = "filament", version = VERSION, about = "P2P file transfer between terminals and browsers, no upload, no account", after_help = EXAMPLES)]
struct Cli {
#[arg(long, global = true, env = "FILAMENT_SERVER", default_value = DEFAULT_SERVER)]
server: String,
#[arg(long, global = true)]
relay: bool,
#[arg(long, global = true, conflicts_with = "relay")]
no_relay: bool,
#[arg(long, global = true)]
name_as: Option<String>,
#[arg(short = 'v', long = "verbose", global = true, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short = 'q', long = "quiet", global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(long, global = true)]
no_interactive: bool,
#[arg(long, global = true, value_name = "WHEN", value_parser = ["auto", "always", "never"])]
color: Option<String>,
#[arg(long, global = true)]
json: bool,
#[arg(short = 'y', long = "yes", global = true)]
yes: bool,
#[command(subcommand)]
cmd: Option<Cmd>,
}
#[derive(Subcommand)]
enum Cmd {
Send {
paths: Vec<String>,
#[arg(long)]
code: bool,
#[arg(long)]
word: Option<String>,
#[arg(long)]
remember: Option<String>,
#[arg(long)]
room: Option<String>,
#[arg(long)]
to: Option<String>,
#[arg(long)]
name: Option<String>,
},
Recv {
code: Option<String>,
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(long, short = 'y')]
yes: bool,
#[arg(long)]
room: Option<String>,
#[arg(long)]
to: Option<String>,
#[arg(long)]
keep_open: bool,
#[arg(long)]
remember: Option<String>,
#[arg(long, short = 'o')]
output: Option<String>,
},
Pair {
code: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
word: Option<String>,
},
Devices {
#[command(subcommand)]
action: Option<DevicesAction>,
#[arg(long)]
json: bool,
},
Up {
#[arg(long)]
install: bool,
#[arg(long)]
system: bool,
#[arg(long)]
userspace: bool,
#[arg(long)]
dir: Option<PathBuf>,
#[arg(long)]
shell: bool,
#[arg(long, value_name = "DEVICES")]
shell_only: Option<String>,
#[arg(long, value_name = "PROGRAM")]
shell_program: Option<String>,
#[arg(long, value_name = "USER")]
shell_user: Option<String>,
#[arg(long, hide = true)]
install_system: bool,
#[arg(long)]
no_proxy_fallback: bool,
},
Status {
#[arg(long)]
json: bool,
},
Down,
#[command(hide = true)]
Introduce { a: String, b: String },
#[command(after_help = "\x1b[1mExamples:\x1b[0m\n \
filament set show every setting + where it came from\n \
filament set auto-extract on change one setting (partial, never resets others)\n \
filament set shell on --peer laptop per-device override\n \
filament get drop-dir --show-origin read one value (bare value on stdout)\n \
filament unset relay revert one setting to its default\n\n\
Keys: name, server, drop-dir, relay, auto-extract, shell, shell-user")]
Set {
key: Option<String>,
value: Option<String>,
#[arg(long, value_name = "DEVICE", value_delimiter = ',')]
peer: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
reset: bool,
#[arg(long)]
yes: bool,
#[arg(long)]
json: bool,
#[arg(long, conflicts_with = "soft")]
hard: bool,
#[arg(long, conflicts_with = "hard")]
soft: bool,
},
Addr {
device: Option<String>,
#[arg(long)]
v4: bool,
},
#[command(hide = true)]
Get {
key: String,
#[arg(long, value_name = "DEVICE")]
peer: Option<String>,
#[arg(long)]
show_origin: bool,
#[arg(long, value_name = "VALUE")]
default: Option<String>,
#[arg(long)]
json: bool,
},
#[command(hide = true)]
Unset {
key: String,
#[arg(long, value_name = "DEVICE", value_delimiter = ',')]
peer: Vec<String>,
},
#[command(hide = true)]
ServeTun {
#[arg(long, value_name = "CIDR")]
tun_addr: String,
#[arg(long, value_name = "BIND", conflicts_with = "connect")]
listen: Option<String>,
#[arg(long, value_name = "HOST:PORT")]
connect: Option<String>,
#[arg(long)]
psk: String,
#[arg(long, default_value = "filament0")]
dev: String,
#[arg(long, default_value_t = 1280)]
mtu: u32,
},
#[command(hide = true)]
Config { key: Option<String>, value: Option<String> },
Update {
#[arg(long)]
check: bool,
#[arg(long)]
beta: bool,
},
#[command(hide = true)]
Completions {
shell: clap_complete::Shell,
},
#[command(hide = true)]
Man {
page: Option<String>,
},
#[command(hide = true)]
Netcat {
peer: String,
rport: u16,
},
Dial {
peer: String,
port: u16,
},
Pty {
peer: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
cmd: Vec<String>,
},
Forward {
lport: u16,
peer: String,
rport: u16,
},
Expose {
port: Option<u16>,
#[arg(long, value_name = "HOST:PORT")]
to: Option<String>,
#[arg(long, value_name = "DEVICE", value_delimiter = ',')]
peer: Vec<String>,
#[arg(long)]
list: bool,
},
Unexpose {
port: u16,
},
Proxy {
#[arg(long, default_value_t = 1080)]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
bind: String,
#[arg(long, default_value_t = 0)]
http_port: u16,
},
Ssh {
peer: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(hide = true)]
Ping {
peer: String,
#[arg(long, default_value_t = 1)]
count: u32,
#[arg(long)]
json: bool,
},
#[command(hide = true)]
Doctor {
device: Option<String>,
#[arg(long)]
watch: bool,
#[arg(long)]
repeat: Option<u32>,
#[arg(long)]
json: bool,
},
#[command(hide = true)]
Grant {
device: String,
capability: String,
},
#[command(hide = true)]
Revoke {
device: String,
capability: String,
},
Mount {
peer: Option<String>,
remote: Option<String>,
local: Option<String>,
#[arg(long)]
read_only: bool,
#[arg(long)]
options: Option<String>,
#[arg(long)]
foreground: bool,
#[arg(long)]
save_auto: bool,
#[arg(long)]
list: bool,
#[arg(long, value_name = "PATH")]
check: Option<String>,
#[arg(long = "save-profile", alias = "save", value_name = "NAME")]
save_profile: Option<String>,
#[arg(long = "apply-profile", alias = "apply", value_name = "NAME")]
apply_profile: Option<String>,
#[arg(long)]
profiles: bool,
#[arg(long, value_name = "NAME")]
delete_profile: Option<String>,
},
Unmount {
path: String,
},
Backup {
peer: String,
source: String,
dest: String,
#[arg(long)]
exclude: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
delete: bool,
#[arg(long)]
options: Option<String>,
},
}
#[derive(Subcommand)]
enum DevicesAction {
Forget { name: String },
Rename { old: String, new: String },
}
fn regex_lite_code(s: &str) -> bool {
let parts: Vec<&str> = s.split('-').collect();
parts.len() == 3
&& parts[0].chars().all(|c| c.is_ascii_lowercase())
&& parts[1].chars().all(|c| c.is_ascii_lowercase())
&& !parts[0].is_empty()
&& !parts[1].is_empty()
&& parts[2].len() >= 2
&& parts[2].chars().all(|c| c.is_ascii_digit())
}
fn trailing_num_width(s: &str) -> usize {
match s.rsplit('-').next() {
Some(t) if !t.is_empty() && t.chars().all(|c| c.is_ascii_digit()) => t.len(),
_ => 0,
}
}
fn looks_like_pake_code(s: &str) -> bool {
regex_lite_code(s) && trailing_num_width(s) >= 4
}
pub(crate) fn password_word_tokens(normalized_password: &str) -> usize {
let mut tokens = 0;
let mut run = 0;
for c in normalized_password.chars() {
if c.is_ascii_lowercase() {
run += 1;
if run == 2 {
tokens += 1; }
} else {
run = 0;
}
}
tokens
}
fn install_id() -> String {
let p = devices_path().with_file_name("device.id");
if let Ok(id) = std::fs::read_to_string(&p) {
let id = id.trim().to_string();
if !id.is_empty() {
return id;
}
}
let id: String = fresh_secret()[..8].to_string();
let _ = crate::platform::SecretFile::write_str(&p, &id);
id
}
pub(crate) fn mk_uid(prefix: &str) -> String {
if let Ok(forced) = std::env::var("FILAMENT_UID") {
return format!("cli-{prefix}-{forced}");
}
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
format!("cli-{prefix}-{}-{:x}{:x}", install_id(), std::process::id(), nanos)
}
pub(crate) fn is_self_uid(my_uid: &str, peer_uid: Option<&str>) -> bool {
if std::env::var("FILAMENT_UID").is_ok() {
return false; }
let id = install_id();
let _ = my_uid;
peer_uid.map(|p| p.contains(&format!("-{id}-"))).unwrap_or(false)
}
fn config_path() -> PathBuf {
devices_path().with_file_name("config")
}
fn config_get(key: &str) -> Option<String> {
std::fs::read_to_string(config_path()).ok()?.lines().find_map(|l| {
let (k, v) = l.split_once(char::is_whitespace)?;
(k == key && !v.trim().is_empty()).then(|| v.trim().to_string())
})
}
fn config_set(key: &str, value: &str) -> Result<()> {
let p = config_path();
if let Some(d) = p.parent() {
std::fs::create_dir_all(d)?;
}
let mut lines: Vec<String> = std::fs::read_to_string(&p)
.unwrap_or_default()
.lines()
.filter(|l| l.split_whitespace().next() != Some(key))
.map(|l| l.to_string())
.collect();
lines.push(format!("{key} {value}"));
std::fs::write(&p, lines.join("\n") + "\n")?;
Ok(())
}
pub(crate) fn display_name() -> String {
if let Ok(n) = std::env::var("FILAMENT_NAME") {
return n;
}
if let Some(n) = config_get("name") {
return n;
}
default_display_name()
}
pub(crate) fn default_display_name() -> String {
let user = std::env::var("USER").unwrap_or_else(|_| "user".into());
let host = std::fs::read_to_string("/etc/hostname")
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| "cli".into());
format!("{user}@{host}")
}
fn sha256_hex(data: &[u8]) -> String {
let mut h = Sha256::new();
h.update(data);
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
pub(crate) fn human(bytes: u64) -> String {
const U: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut v = bytes as f64;
let mut i = 0;
while v >= 1024.0 && i < U.len() - 1 {
v /= 1024.0;
i += 1;
}
if i == 0 { format!("{bytes} B") } else { format!("{v:.1} {}", U[i]) }
}
fn head_hash(path: &Path) -> Option<String> {
let mut f = std::fs::File::open(path).ok()?;
let mut buf = vec![0u8; HEAD_BYTES as usize];
let mut got = 0usize;
while got < buf.len() {
match f.read(&mut buf[got..]) {
Ok(0) => break,
Ok(n) => got += n,
Err(_) => return None,
}
}
let mut h = Sha256::new();
h.update(&buf[..got]);
Some(h.finalize().iter().map(|b| format!("{b:02x}")).collect())
}
fn full_hash(path: &Path) -> Option<String> {
let mut f = std::fs::File::open(path).ok()?;
let mut h = Sha256::new();
let mut buf = vec![0u8; 1 << 20];
loop {
match f.read(&mut buf) {
Ok(0) => break,
Ok(n) => h.update(&buf[..n]),
Err(_) => return None,
}
}
Some(h.finalize().iter().map(|b| format!("{b:02x}")).collect())
}
struct PartMeta {
size: u64,
head: Option<String>,
full: Option<String>,
}
impl PartMeta {
fn load(path: &Path) -> Option<PartMeta> {
let raw = std::fs::read_to_string(path).ok()?;
if let Ok(v) = serde_json::from_str::<Value>(&raw) {
if let Some(size) = v["size"].as_u64() {
return Some(PartMeta {
size,
head: v["head"].as_str().map(|s| s.to_string()),
full: v["full"].as_str().map(|s| s.to_string()),
});
}
}
raw.trim().parse::<u64>().ok().map(|size| PartMeta { size, head: None, full: None })
}
fn store(&self, path: &Path) -> std::io::Result<()> {
std::fs::write(path, json!({ "size": self.size, "head": self.head, "full": self.full }).to_string())
}
}
fn unique_path(dir: &Path, name: &str) -> PathBuf {
let candidate = dir.join(name);
if !candidate.exists() {
return candidate;
}
for i in 1..1000 {
let c = dir.join(format!("{name}.{i}"));
if !c.exists() {
return c;
}
}
dir.join(format!("{name}.dup"))
}
fn direct_ok_for(daemon: bool, l2_enabled: bool) -> bool {
direct::direct_enabled() || l2_enabled || daemon
}
fn devices_path() -> PathBuf {
crate::platform::Paths::config_path("devices.json")
}
pub(crate) fn devices_load() -> Vec<(String, String)> {
let Ok(raw) = std::fs::read_to_string(devices_path()) else { return Vec::new() };
serde_json::from_str::<Value>(&raw)
.ok()
.and_then(|v| {
v.as_array().map(|a| {
a.iter()
.filter_map(|d| Some((d["name"].as_str()?.to_string(), d["secret"].as_str()?.to_string())))
.collect()
})
})
.unwrap_or_default()
}
fn devices_name_taken(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
devices_load().iter().any(|(n, _)| n.to_ascii_lowercase() == lower)
}
fn sanitize_device_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut chars = name.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
if chars.peek() == Some(&'[') {
chars.next();
while let Some(&n) = chars.peek() {
chars.next();
if n.is_ascii_alphabetic() {
break;
}
}
}
continue; }
if !c.is_control() {
out.push(c);
}
}
out.trim().to_string()
}
fn devices_store(name: &str, secret: &str) -> Result<()> {
let clean = sanitize_device_name(name);
let name = clean.as_str();
let p = devices_path();
if let Some(dir) = p.parent() {
std::fs::create_dir_all(dir)?;
}
let mut arr: Vec<Value> = std::fs::read_to_string(&p)
.ok()
.and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
.and_then(|v| v.as_array().cloned())
.unwrap_or_default();
let final_name = if arr.iter().any(|d| d["name"].as_str() == Some(name)) {
let mut suffix = 2;
let mut new_name = format!("{name}-{suffix}");
while arr.iter().any(|d| d["name"].as_str() == Some(&new_name)) {
suffix += 1;
new_name = format!("{name}-{suffix}");
}
eprintln!(" note: '{name}' already exists, pairing as '{new_name}'");
new_name
} else {
name.to_string()
};
match arr.iter_mut().find(|d| d["name"].as_str() == Some(&final_name)) {
Some(existing) => existing["secret"] = json!(secret),
None => arr.push(json!({"name": &final_name, "secret": secret})),
}
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr)?)?;
Ok(())
}
fn devices_store_v2(name: &str, secret: &str, caps: &[String]) -> Result<()> {
let clean = sanitize_device_name(name);
let name = clean.as_str();
let p = devices_path();
if let Some(dir) = p.parent() {
std::fs::create_dir_all(dir)?;
}
let mut arr: Vec<Value> = std::fs::read_to_string(&p)
.ok()
.and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
.and_then(|v| v.as_array().cloned())
.unwrap_or_default();
let final_name = if arr.iter().any(|d| d["name"].as_str() == Some(name)) {
let mut suffix = 2;
let mut new_name = format!("{name}-{suffix}");
while arr.iter().any(|d| d["name"].as_str() == Some(&new_name)) {
suffix += 1;
new_name = format!("{name}-{suffix}");
}
eprintln!(" note: '{name}' already exists, pairing as '{new_name}'");
new_name
} else {
name.to_string()
};
arr.retain(|d| d["name"].as_str() != Some(&final_name));
arr.push(json!({"name": &final_name, "secret": secret, "v": 2, "caps": caps,
"addedAt": SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)}));
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr)?)?;
Ok(())
}
fn devices_touch(name: &str, v6: Option<std::net::Ipv6Addr>, v4: Option<std::net::Ipv4Addr>) {
let p = devices_path();
let Ok(raw) = std::fs::read_to_string(&p) else { return };
let Ok(val) = serde_json::from_str::<Value>(&raw) else { return };
let Some(arr) = val.as_array() else { return };
let mut arr = arr.clone();
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
for d in arr.iter_mut() {
if d["name"].as_str() == Some(name) {
d["lastSeen"] = json!(now);
if let Some(v6) = v6 { d["overlayV6"] = json!(v6.to_string()); }
if let Some(v4) = v4 { d["overlayV4"] = json!(v4.to_string()); }
break;
}
}
let _ = crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr).unwrap_or_default());
}
fn devices_info(name: &str) -> Option<(u64, Option<String>, Option<String>)> {
let p = devices_path();
let raw = std::fs::read_to_string(p).ok()?;
let arr: Vec<Value> = serde_json::from_str(&raw).ok()?;
let d = arr.iter().find(|d| d["name"].as_str() == Some(name))?;
let last_seen = d["lastSeen"].as_u64();
let v6 = d["overlayV6"].as_str().map(|s| s.to_string());
let v4 = d["overlayV4"].as_str().map(|s| s.to_string());
Some((last_seen.unwrap_or(0), v6, v4))
}
fn any_shell_grant() -> bool {
any_shell_grant_at(&devices_path())
}
fn any_shell_grant_at(path: &Path) -> bool {
let Ok(raw) = std::fs::read_to_string(path) else { return false };
let Ok(arr) = serde_json::from_str::<Value>(&raw) else { return false };
arr.as_array()
.map(|a| {
a.iter().any(|d| {
d.get("caps")
.and_then(|c| c.as_array())
.map(|list| list.iter().any(|c| c.as_str() == Some("shell")))
.unwrap_or(false)
})
})
.unwrap_or(false)
}
fn l2_open_allowed(blanket: bool, peer_has_shell: bool) -> bool {
blanket || peer_has_shell
}
fn l2_allow_path() -> PathBuf {
devices_path().with_file_name("l2-allow.json")
}
fn l2_allow_load() -> Value {
std::fs::read_to_string(l2_allow_path())
.ok()
.and_then(|s| serde_json::from_str::<Value>(&s).ok())
.unwrap_or(Value::Null)
}
fn l2_target_allowed_in(allow: &Value, device: &str, host: &str, port: u16) -> bool {
let matches = |list: &Value| -> bool {
list.as_array()
.map(|a| {
a.iter().any(|e| {
let s = e.as_str().unwrap_or("");
match s.rsplit_once(':') {
Some((h, "*")) => h.eq_ignore_ascii_case(host),
Some((h, p)) => {
h.eq_ignore_ascii_case(host) && p.parse::<u16>().ok() == Some(port)
}
None => false,
}
})
})
.unwrap_or(false)
};
allow.get(device).map(&matches).unwrap_or(false) || allow.get("*").map(&matches).unwrap_or(false)
}
fn l2_target_allowed(device: &str, host: &str, port: u16) -> bool {
l2_target_allowed_in(&l2_allow_load(), device, host, port)
}
#[allow(dead_code)] fn device_caps(name: &str) -> Option<Vec<String>> {
device_caps_at(&devices_path(), name)
}
#[allow(dead_code)]
fn device_caps_at(path: &Path, name: &str) -> Option<Vec<String>> {
let raw = std::fs::read_to_string(path).ok()?;
let arr = serde_json::from_str::<Value>(&raw).ok()?;
for d in arr.as_array()? {
if d["name"].as_str() == Some(name) {
return Some(match d.get("caps").and_then(|c| c.as_array()) {
Some(list) => list.iter().filter_map(|c| c.as_str().map(String::from)).collect(),
None => vec!["transfer".to_string()], });
}
}
None
}
#[allow(dead_code)]
fn device_allows_at(path: &Path, name: &str, capability: &str) -> bool {
if capability == "transfer" {
return true; }
device_caps_at(path, name).map(|c| c.iter().any(|g| g == capability)).unwrap_or(false)
}
#[allow(dead_code)] fn device_allows(name: &str, capability: &str) -> bool {
if capability == "transfer" {
return true; }
device_caps(name).map(|c| c.iter().any(|g| g == capability)).unwrap_or(false)
}
fn device_set_cap(name: &str, capability: &str, grant: bool) -> Result<()> {
let p = devices_path();
let raw = std::fs::read_to_string(&p)
.map_err(|_| anyhow::anyhow!("no known device named '{name}', pair first"))?;
let mut arr: Vec<Value> = serde_json::from_str::<Value>(&raw)
.ok()
.and_then(|v| v.as_array().cloned())
.unwrap_or_default();
let mut found = false;
for d in arr.iter_mut() {
if d["name"].as_str() != Some(name) {
continue;
}
found = true;
let mut caps: Vec<String> = match d.get("caps").and_then(|c| c.as_array()) {
Some(list) => list.iter().filter_map(|c| c.as_str().map(String::from)).collect(),
None => vec!["transfer".to_string()],
};
caps.retain(|c| c != capability);
if grant {
caps.push(capability.to_string());
}
if let Some(obj) = d.as_object_mut() {
obj.insert("v".into(), json!(2));
obj.insert("caps".into(), json!(caps));
}
}
if !found {
return Err(anyhow::anyhow!(
"no known device named '{name}', run `filament devices` to see who you've paired"
));
}
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr)?)?;
Ok(())
}
fn devices_remove(name: &str) -> Result<()> {
let p = devices_path();
if let Some(dir) = p.parent() {
std::fs::create_dir_all(dir)?;
}
let mut arr: Vec<Value> = std::fs::read_to_string(&p)
.ok()
.and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
.and_then(|v| v.as_array().cloned())
.unwrap_or_default();
arr.retain(|d| d["name"].as_str() != Some(name));
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr)?)?;
Ok(())
}
pub(crate) fn channel_of(secret: &str) -> String {
let mut h = Sha256::new();
h.update(b"filament-pair:");
h.update(secret.as_bytes());
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
fn hmac_sha256(key: &[u8], msg: &[u8]) -> String {
let mut k = [0u8; 64];
if key.len() > 64 {
let mut h = Sha256::new();
h.update(key);
k[..32].copy_from_slice(&h.finalize());
} else {
k[..key.len()].copy_from_slice(key);
}
let ipad: Vec<u8> = k.iter().map(|b| b ^ 0x36).collect();
let opad: Vec<u8> = k.iter().map(|b| b ^ 0x5c).collect();
let mut inner = Sha256::new();
inner.update(&ipad);
inner.update(msg);
let mut outer = Sha256::new();
outer.update(&opad);
outer.update(inner.finalize());
outer.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
pub(crate) fn proof_for(secret: &str, prover_uid: &str, a_uid: &str, b_uid: &str, fp1: &str, fp2: &str) -> String {
let (lo, hi) = if a_uid < b_uid { (a_uid, b_uid) } else { (b_uid, a_uid) };
let (f_lo, f_hi) = if fp1 < fp2 { (fp1, fp2) } else { (fp2, fp1) };
hmac_sha256(
secret.as_bytes(),
format!("filament-proof2:{prover_uid}|{lo}|{hi}|{f_lo}|{f_hi}").as_bytes(),
)
}
pub(crate) fn fresh_secret() -> String {
let mut buf = [0u8; 32];
if std::fs::File::open("/dev/urandom")
.and_then(|mut f| f.read_exact(&mut buf))
.is_err()
{
let mut h = Sha256::new();
h.update(format!("{:?}{}", SystemTime::now(), std::process::id()));
buf.copy_from_slice(&h.finalize()[..32]);
}
buf.iter().map(|b| format!("{b:02x}")).collect()
}
fn drop_dir(flag: Option<PathBuf>) -> PathBuf {
flag.or_else(|| config_get("dir").map(PathBuf::from)).unwrap_or_else(default_drop_dir)
}
pub(crate) fn default_drop_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join("Filament")
}
pub(crate) fn daemon_running() -> bool {
daemon_alive().is_some()
}
fn chrono_now() -> String {
let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
let (days, rem) = (secs / 86400, secs % 86400);
let (hh, mm) = (rem / 3600, (rem % 3600) / 60);
let z = days as i64 + 719_468;
let era = z / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}")
}
fn pidfile() -> PathBuf {
devices_path().with_file_name("up.pid")
}
fn up_log() -> PathBuf {
devices_path().with_file_name("up.log")
}
fn daemon_alive() -> Option<u32> {
let pid: u32 = std::fs::read_to_string(pidfile()).ok()?.trim().parse().ok()?;
let cmd = std::fs::read_to_string(format!("/proc/{pid}/cmdline")).ok()?;
cmd.contains("filament").then_some(pid)
}
fn shell_argv(shell_program: Option<&str>, shell_user: Option<&str>) -> Vec<String> {
let shell_config = settings::get_str("shell-program", None);
let (argv, _can_user) = platform::Paths::shell_argv(shell_program, shell_config.as_deref(), shell_user);
argv
}
fn spawn_session_pumps(
sess: l2::PtySessionHandle,
mut rx: tokio::sync::mpsc::Receiver<Option<bytes::Bytes>>,
mut rrx: tokio::sync::mpsc::UnboundedReceiver<(u16, u16)>,
) {
let s_in = sess.clone();
tokio::spawn(async move {
while let Some(item) = rx.recv().await {
match item {
Some(bytes) => s_in.feed_input(bytes.to_vec()),
None => break, }
}
});
tokio::spawn(async move {
while let Some((c, r)) = rrx.recv().await {
sess.resize(c, r);
}
});
}
#[derive(Clone, Debug)]
enum ShellPolicy {
Granted,
All,
Only(std::collections::HashSet<String>),
}
impl ShellPolicy {
fn auto_allows(&self, name: &str) -> bool {
match self {
ShellPolicy::Granted => false,
ShellPolicy::All => true,
ShellPolicy::Only(set) => set.contains(name),
}
}
fn enables_l2(&self) -> bool {
!matches!(self, ShellPolicy::Granted)
}
}
#[cfg(target_os = "linux")]
fn install_system_service(shell: bool, shell_only: &Option<String>, shell_user: &Option<String>) -> Result<()> {
let exe = std::env::current_exe()?.display().to_string();
let user = std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.unwrap_or_else(|_| "root".into());
let home = std::env::var("HOME").unwrap_or_else(|_| format!("/home/{user}"));
let mut up_args = String::from(" up");
if let Some(csv) = shell_only {
up_args.push_str(&format!(" --shell-only {csv}"));
} else if shell {
up_args.push_str(" --shell");
}
if let Some(u) = shell_user {
up_args.push_str(&format!(" --shell-user {u}"));
}
let unit = format!(
"[Unit]\n\
Description=Filament drop target (trusted devices only)\n\
After=network-online.target\n\
Wants=network-online.target\n\n\
[Service]\n\
Type=notify\n\
User={user}\n\
Environment=HOME={home}\n\
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n\
ExecStart={exe}{up_args}\n\
AmbientCapabilities=CAP_NET_ADMIN\n\
CapabilityBoundingSet=CAP_NET_ADMIN\n\
Restart=always\n\
RestartSec=2\n\
WatchdogSec=45\n\n\
[Install]\n\
WantedBy=multi-user.target\n"
);
let unit_path = "/etc/systemd/system/filament.service";
let am_root = unsafe { libc::geteuid() } == 0;
let run_priv = |args: &[&str]| -> bool {
let mut cmd = if am_root {
std::process::Command::new(args[0])
} else {
let mut c = std::process::Command::new("sudo");
c.arg(args[0]);
c
};
cmd.args(&args[1..]).status().map(|s| s.success()).unwrap_or(false)
};
ui::say(&format!("filament: installing system service at {unit_path}"));
if !am_root {
ui::say(" (one-time sudo for the system unit; updates afterward need none)");
}
let wrote = {
use std::io::Write;
use std::process::Stdio;
let mut cmd = if am_root {
std::process::Command::new("tee")
} else {
let mut c = std::process::Command::new("sudo");
c.arg("tee");
c
};
match cmd.arg(unit_path).stdin(Stdio::piped()).stdout(Stdio::null()).spawn() {
Ok(mut child) => {
if let Some(mut si) = child.stdin.take() {
let _ = si.write_all(unit.as_bytes());
}
child.wait().map(|s| s.success()).unwrap_or(false)
}
Err(_) => false,
}
};
if wrote {
let _ = run_priv(&["chmod", "644", unit_path]);
}
if !wrote {
ui::say("filament: could not elevate; install the system unit by hand:");
ui::say(&format!(" sudo tee {unit_path} >/dev/null <<'UNIT'\n{unit}UNIT"));
ui::say(&format!(" sudo setcap -r {exe} 2>/dev/null || true"));
ui::say(" sudo systemctl daemon-reload && sudo systemctl enable --now filament");
return Ok(());
}
let _ = run_priv(&["setcap", "-r", &exe]);
let _ = std::process::Command::new("systemctl").args(["--user", "disable", "--now", "filament"]).status();
let enabled =
run_priv(&["systemctl", "daemon-reload"]) && run_priv(&["systemctl", "enable", "--now", "filament"]);
if enabled {
ui::say(&format!(
" {} system service enabled; CAP_NET_ADMIN comes from systemd, so no setcap on update",
ui::paint(ui::Tone::Ok, ui::glyph_ok())
));
ui::say(" logs: journalctl -u filament");
} else {
ui::say(" wrote the unit; enable it with: sudo systemctl enable --now filament");
}
let systemctl = ["/usr/bin/systemctl", "/bin/systemctl"]
.iter()
.find(|p| std::path::Path::new(p).exists())
.copied()
.unwrap_or("/usr/bin/systemctl");
let sudoers_path = "/etc/sudoers.d/filament";
let sudoers = format!(
"{user} ALL=(root) NOPASSWD: {systemctl} restart filament, {systemctl} daemon-reload\n"
);
let wrote_sudoers = {
use std::io::Write;
use std::process::Stdio;
let mut cmd = if am_root {
std::process::Command::new("tee")
} else {
let mut c = std::process::Command::new("sudo");
c.arg("tee");
c
};
match cmd.arg(sudoers_path).stdin(Stdio::piped()).stdout(Stdio::null()).spawn() {
Ok(mut ch) => {
if let Some(mut si) = ch.stdin.take() {
let _ = si.write_all(sudoers.as_bytes());
}
ch.wait().map(|s| s.success()).unwrap_or(false)
}
Err(_) => false,
}
};
if wrote_sudoers {
let _ = run_priv(&["chmod", "0440", sudoers_path]);
if run_priv(&["visudo", "-cf", sudoers_path]) {
ui::say(&format!(
" {} passwordless `systemctl restart filament` for {user}",
ui::paint(ui::Tone::Ok, ui::glyph_ok())
));
} else {
let _ = run_priv(&["rm", "-f", sudoers_path]);
ui::say(" (skipped the restart sudoers rule: visudo validation failed)");
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn install_system_service(_shell: bool, _shell_only: &Option<String>, _shell_user: &Option<String>) -> Result<()> {
let hint = platform::ServiceHost::detect().install_instructions();
bail!("--install --system (ambient-cap system service) is not supported on this platform. {hint}");
}
async fn up_cmd(
server: &str,
install: bool,
system: bool,
dir: Option<PathBuf>,
relay: bool,
shell: bool,
shell_only: Option<String>,
shell_program: Option<String>,
shell_user: Option<String>,
install_system_flag: bool,
no_proxy_fallback: bool,
) -> Result<()> {
if install_system_flag {
let host = platform::ServiceHost::detect();
let exe = std::env::current_exe()?;
let mut up_args = String::new();
if let Some(csv) = &shell_only {
up_args.push_str(&format!(" --shell-only {csv}"));
} else if shell {
up_args.push_str(" --shell");
}
if let Some(u) = &shell_user {
up_args.push_str(&format!(" --shell-user {u}"));
}
host.install_system(&exe, &up_args)?;
return Ok(());
}
if let Some(ref prog) = shell_program {
settings::set("shell-program", prog, None).ok();
}
let shell_policy = match &shell_only {
Some(csv) => ShellPolicy::Only(
csv.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
),
None if shell => ShellPolicy::All,
None => ShellPolicy::Granted,
};
if shell_user.is_some() && cfg!(windows) {
ui::say(&format!(
" {} --shell-user is not supported on Windows.\n\
\n\
Windows requires CreateProcessAsUser or CreateProcessWithLogonW to run\n\
a process as another user. CreateProcessAsUser needs SE_INCREASE_QUOTA_NAME\n\
and SE_ASSIGNPRIMARYTOKEN_NAME privileges (typically requires admin).\n\
CreateProcessWithLogonW needs the target user's credentials (username +\n\
password), which is a security risk if stored or passed via CLI.\n\
\n\
The PTY will run as the current user. To run as a different user,\n\
start filament from that user's session, or use 'runas /user:<name> filament'.",
ui::paint(ui::Tone::Warn, "WARNING:")
));
}
if install && system {
return install_system_service(shell, &shell_only, &shell_user);
}
if install {
let host = platform::ServiceHost::detect();
if !host.supports_install() {
let hint = host.install_instructions();
eprintln!("filament: --install is not supported on this platform. {hint}");
return Ok(());
}
let exe = std::env::current_exe()?;
let mut up_args = String::new();
if let Some(csv) = &shell_only {
up_args.push_str(&format!(" --shell-only {csv}"));
} else if shell {
up_args.push_str(" --shell");
}
if let Some(u) = &shell_user {
up_args.push_str(&format!(" --shell-user {u}"));
}
match host.install_system(&exe, &up_args) {
Ok(platform::InstallResult::System) => {
ui::say(&format!(" {} installed as a system service (autostart at boot)", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
}
Ok(platform::InstallResult::User) | Err(_) => {
host.install_user(&exe, &up_args)?;
ui::say(&format!(" {} installed as a user-level autostart", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
ui::say(&format!(" {} run `filament up --install` again to grant admin for kernel overlay",
ui::paint(ui::Tone::Dim, "note:")));
}
}
#[cfg(target_os = "windows")]
platform::add_firewall_rule(&exe);
return Ok(());
}
if let Some(pid) = daemon_alive() {
eprintln!("[up] already-up: pidfile={:?} pid={pid} cmdline={:?}", pidfile(), std::fs::read_to_string(format!("/proc/{pid}/cmdline")).unwrap_or_default());
bail!("already up (pid {pid}), `filament status` / `filament down`");
}
let dir = drop_dir(dir);
std::fs::create_dir_all(&dir)?;
std::fs::write(pidfile(), std::process::id().to_string())?;
match &shell_policy {
ShellPolicy::All => ui::say(&format!(
" {} seamless shell ON, ANY paired device (now or paired later) can `filament ssh` into this machine",
ui::paint(ui::Tone::Warn, "!"),
)),
ShellPolicy::Only(set) => {
let mut names: Vec<&String> = set.iter().collect();
names.sort();
let list = names.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ");
ui::say(&format!(
" {} seamless shell ON for: {list}, they can `filament ssh` into this machine",
ui::paint(ui::Tone::Warn, "!"),
));
}
ShellPolicy::Granted => {}
}
if shell_policy.enables_l2() && shell_user.is_none() {
ui::say(&format!(
" {} shell PTYs run as THIS user (root if the daemon is root), pass `--shell-user <name>` to drop to a non-root account",
ui::paint(ui::Tone::Warn, "!"),
));
}
{
let server = server.to_string();
tokio::spawn(async move { direct::warm_public_ip(&server).await; });
}
let res = recv_cmd(server, None, dir, false, None, None, true, relay, None, true, None, shell_policy, shell_user, no_proxy_fallback).await;
let _ = std::fs::remove_file(pidfile());
res
}
fn tour_cmd() -> Result<()> {
let color = ui::stdout_color();
ui::say(&format!(
" {} {}",
ui::paint_when(color, ui::Tone::Brand, "filament"),
ui::paint_when(color, ui::Tone::Dim, "· send files and reach your devices, no account"),
));
ui::say("");
match daemon_alive() {
Some(pid) => ui::say(&format!(" {} daemon up (pid {pid})", ui::paint_when(color, ui::Tone::Ok, ui::glyph_ok()))),
None => ui::say(&format!(" {} daemon not running", ui::paint_when(color, ui::Tone::Dim, "·"))),
}
let n = devices_load().len();
ui::say(&format!(" {n} known device{}", if n == 1 { "" } else { "s" }));
ui::say("");
ui::say(&ui::paint_when(color, ui::Tone::Dim, " do this:"));
let act = |cmd: &str, desc: &str| ui::say(&format!(" {:<24} {}", cmd, desc));
act("filament send <file>", "send files (to a browser or a paired device)");
if n == 0 {
act("filament pair", "remember a device (unlocks ssh + expose)");
} else {
act("filament <device>", "shell into a paired device (e.g. filament dovm)");
act("filament expose <port>", "publish a local port on the mesh");
}
act("filament up", "receive in the background");
ui::say(&ui::paint_when(color, ui::Tone::Dim, " more: filament --help · filament status · filament devices"));
Ok(())
}
fn status_cmd(json: bool) -> Result<()> {
if json {
let pid = daemon_alive();
let exposed: Vec<Value> = expose::load()
.iter()
.map(|b| json!({ "port": b.port, "target": b.target, "peers": b.peers.clone().unwrap_or_default() }))
.collect();
let mut recent: Vec<String> = std::fs::read_to_string(up_log())
.map(|log| log.lines().rev().take(8).map(str::to_string).collect())
.unwrap_or_default();
recent.reverse();
println!(
"{}",
serde_json::to_string_pretty(&json!({
"running": pid.is_some(),
"pid": pid,
"devices": devices_load().len(),
"exposed": exposed,
"recent": recent,
}))?
);
return Ok(());
}
match daemon_alive() {
Some(pid) => ui::say(&format!(" {} up (pid {pid})", ui::paint(ui::Tone::Ok, ui::glyph_ok()))),
None => ui::say(&format!(" {} not running, start with: filament up", ui::paint(ui::Tone::Dim, "·"))),
}
let n = devices_load().len();
ui::say(&format!(" {} known device{}", n, if n == 1 { "" } else { "s" }));
let exposed = expose::load();
if !exposed.is_empty() {
ui::say(&ui::paint(ui::Tone::Dim, " exposed on .mesh:"));
for b in exposed {
let scope = match &b.peers {
Some(p) if !p.is_empty() => p.join(","),
_ => "any".into(),
};
ui::say(&format!(" :{} {} {} ({})", b.port, ui::glyph_arrow(), b.target, scope));
}
}
if let Ok(log) = std::fs::read_to_string(up_log()) {
let recent: Vec<&str> = log.lines().rev().take(8).collect();
if !recent.is_empty() {
ui::say(&ui::paint(ui::Tone::Dim, " recent receives:"));
for l in recent.iter().rev() {
ui::say(&format!(" {l}"));
}
}
}
Ok(())
}
fn down_cmd() -> Result<()> {
match daemon_alive() {
Some(pid) => {
std::process::Command::new("kill").arg(pid.to_string()).status()?;
let _ = std::fs::remove_file(pidfile());
ui::say(&format!(" {} stopped (pid {pid})", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
Ok(())
}
None => {
ui::say(" not running");
Ok(())
}
}
}
async fn introduce_cmd(server: &str, a: &str, b: &str, relay: bool) -> Result<()> {
let store = devices_load();
let find = |n: &str| store.iter().find(|(name, _)| name.eq_ignore_ascii_case(n)).cloned();
let (a_name, a_sec) = find(a).ok_or_else(|| anyhow!("'{a}' is not a known device (see: filament devices)"))?;
let (b_name, b_sec) = find(b).ok_or_else(|| anyhow!("'{b}' is not a known device"))?;
let my_uid = mk_uid("s");
let (tx, mut rx) = mpsc::unbounded_channel::<Ev>();
let sio = net::connect_signaling(server, tx.clone()).await?;
let solo = format!("intro-{}", fresh_secret());
let mut sess = session::Session::new(&display_name(), &my_uid);
sess.room = Some(solo.clone());
sess.channels = vec![channel_of(&a_sec), channel_of(&b_sec)];
sess.emit(&sio, "join", json!({ "room": solo, "name": display_name(), "uid": my_uid })).await;
sess.emit(&sio, "subscribe", json!({ "channels": [channel_of(&a_sec), channel_of(&b_sec)] })).await;
ui::say(&format!(" waiting for {} and {} to be online...", ui::paint(ui::Tone::Bold, &a_name), ui::paint(ui::Tone::Bold, &b_name)));
let mut conn = Conn {
server: server.to_string(),
sio: sio.clone(),
tx: tx.clone(),
my_uid: my_uid.clone(),
my_id: String::new(),
relay_only: relay,
to_filter: None,
links: HashMap::new(),
roster: HashMap::new(),
active: None,
next_gen: 0,
rejoin: RejoinState { waiting_rejoin: None, rejoin_window: REJOIN_WINDOW, away: None },
chunk_size: net::MAX_DC_PAYLOAD,
deferred_left: HashMap::new(),
recv_done: false,
direct_pending: HashMap::new(),
resil: ResilienceState {
stall_repairs: HashMap::new(),
relay_committed: std::collections::HashSet::new(),
warm_standby: net::warm_standby_override().unwrap_or(false),
warm_cutover: std::collections::HashSet::new(),
upgrade_probe: HashMap::new(),
iface_snapshot: Vec::new(),
},
direct_ok: direct::direct_enabled(),
local_port: None,
local_listener: None,
direct_endpoint: None,
warm_hold: WarmHold::default(),
worker_port_tx: HashMap::new(),
};
let mut who: HashMap<String, bool> = HashMap::new();
let mut sent: [bool; 2] = [false, false];
let fresh = fresh_secret();
let deadline = Instant::now() + Duration::from_secs(120);
loop {
if Instant::now() > deadline {
bail!("timed out, both devices must be online (e.g. running `filament up`)");
}
let ev = match tokio::time::timeout(Duration::from_secs(2), rx.recv()).await {
Ok(Some(ev)) => ev,
Ok(None) => bail!("signaling closed"),
Err(_) => continue,
};
sess.tick(&sio).await; conn.reap_deferred(); match ev {
Ev::Welcome(v) => {
conn.my_id = v["id"].as_str().unwrap_or_default().to_string();
sess.invalidate();
}
Ev::Synced(v) => { sess.on_synced(&v); }
Ev::KnownPeer(v) => {
if is_self_uid(&conn.my_uid, v["uid"].as_str()) {
continue; }
let ch = v["channel"].as_str().unwrap_or_default().to_string();
let pid = v["id"].as_str().unwrap_or_default().to_string();
let is_b = if ch == channel_of(&a_sec) { false } else if ch == channel_of(&b_sec) { true } else { continue };
conn.maybe_adopt(&v, false).await?;
if let Some(l) = conn.link_mut(&pid) {
l.expected_secret = Some(if is_b { (b_name.clone(), b_sec.clone()) } else { (a_name.clone(), a_sec.clone()) });
}
who.insert(pid, is_b);
}
Ev::Signal(v) => {
let from = v["from"].as_str().unwrap_or_default().to_string();
let data = v["data"].clone();
conn.ensure_responder(&from, &data).await?;
conn.apply_signal(&from, data).await;
}
Ev::ChannelReady(pid, t) => {
if let Some(l) = conn.link_mut(&pid) {
l.transport = Some(t.clone());
l.presence = Presence::Ready;
}
let Some(&is_b) = who.get(&pid) else { continue };
let (dev_name, sec) = if is_b { (&b_name, &b_sec) } else { (&a_name, &a_sec) };
let other_name = if is_b { &a_name } else { &b_name };
if let Some(l) = conn.link(&pid) {
if let Some((my_fp, their_fp)) = match &l.peer { Some(p) => p.fingerprints().await, None => None } {
t.send_control(&json!({
"type": "pair-proof",
"mac": proof_for(sec, &conn.my_uid, &conn.my_uid, l.uid.as_deref().unwrap_or(""), &my_fp, &their_fp),
})).await?;
t.send_control(&json!({
"type": "pair-intro", "name": other_name, "secret": fresh,
})).await?;
sent[is_b as usize] = true;
ui::say(&format!(" {} vouched to {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), ui::paint(ui::Tone::Bold, dev_name)));
}
}
if sent[0] && sent[1] {
tokio::time::sleep(Duration::from_millis(800)).await; ui::say(&format!(
" {} {} and {} now know each other (no codes needed)",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
a_name, b_name,
));
let _ = sio.disconnect().await;
return Ok(());
}
}
Ev::Stuck(pid, g) => { conn.on_stuck(&pid, g, "stuck").await?; }
Ev::GraceExpired(pid, g) => { conn.on_stuck(&pid, g, "lost").await?; }
Ev::PcState(pid, st) => conn.on_pc_state(&pid, &st).await,
Ev::PeerLeft(v) => { conn.on_peer_left(&v); }
Ev::Interrupted => bail!("interrupted"),
_ => {}
}
}
}
use pake_ceremony::{pair_v2_caps, Ceremony, Inbound as PakeInbound};
fn malformed_entry_banner(arg: &str) {
ui::say(&ui::paint(
ui::Tone::Dim,
&format!(
"couldn't parse '{arg}', opening guided entry · set FILAMENT_NONINTERACTIVE=1 to fail fast in scripts"
),
));
}
fn cancelled() -> anyhow::Error {
anyhow!("cancelled")
}
async fn pair_cmd(server: &str, mut code: Option<String>, name: Option<String>, mut word: Option<String>, relay: bool) -> Result<()> {
let mut preview_nameplate: Option<String> = None;
if word.is_none() && interactive_allowed() {
let malformed = code.as_deref().filter(|c| {
let normalized = crate::pake::norm_code(c);
let (np, pw) = crate::pake::split_code(&normalized);
pw.is_empty() || np.is_empty()
});
match (&code, malformed) {
(None, _) => {
let auto_np = crate::pake::words::mint_nameplate();
match codeentry::run(" pair · choose words ", codeentry::Mode::Create, "", &auto_np)? {
codeentry::Outcome::Submitted(words) => {
word = Some(words);
preview_nameplate = Some(auto_np);
}
codeentry::Outcome::Empty => preview_nameplate = Some(auto_np),
codeentry::Outcome::Cancelled => return Err(cancelled()),
}
}
(Some(raw), Some(_)) => {
malformed_entry_banner(raw);
let prefill = crate::pake::norm_code(raw);
match codeentry::run(" pair · code ", codeentry::Mode::Claim, &prefill, "")? {
codeentry::Outcome::Submitted(c) => code = Some(c),
codeentry::Outcome::Empty => return Err(cancelled()),
codeentry::Outcome::Cancelled => return Err(cancelled()),
}
}
(Some(_), None) => {}
}
}
if word.is_some() && code.is_some() {
bail!("--word chooses your OWN pairing words (creator); it can't be combined with a code to claim. Drop one.");
}
let custom_words: Option<String> = match &word {
Some(w) => {
let (words, _np) =
crate::pake::split_chosen_code(&crate::pake::norm_code(w));
if password_word_tokens(&words) < 2 {
bail!(
"'{w}' is too weak. Use at least two words, e.g. gigantic-element \
(easier to say, harder to guess). A single word falls below the \
strength floor the rate-limit relies on."
);
}
Some(words)
}
None => None,
};
let my_uid = mk_uid("p");
let (tx, mut rx) = mpsc::unbounded_channel::<Ev>();
let sio = net::connect_signaling(server, tx.clone()).await?;
let solo = format!("pairc-{}", fresh_secret());
let mut sess = session::Session::new(&display_name(), &my_uid);
sess.room = Some(solo.clone());
sess.emit(&sio, "join", json!({ "room": solo, "name": display_name(), "uid": my_uid })).await;
if let Some(c) = &code {
if regex_lite_code(c) && !looks_like_pake_code(c) {
bail!(
"'{c}' looks like a one-time TRANSFER code (from `filament send --code`), not a pairing code.\n \
To receive that transfer: run `filament {c}` (or `filament recv {c}`)\n \
A pairing code ends in a 4-digit number, e.g. `brave-otter-3141`."
);
}
}
let creator = code.is_none();
let mut my_words; let mut my_nameplate;
match &code {
Some(c) => {
let normalized = crate::pake::norm_code(c);
let (np, pw) = crate::pake::split_code(&normalized);
if pw.is_empty() || np.is_empty() {
bail!("that code doesn't look right, expected something like brave-otter-ruby-3141");
}
my_words = pw;
my_nameplate = np.clone();
ui::say(&format!(" claiming {}...", ui::paint(ui::Tone::Brand, c)));
sio.emit("pair-claim", json!({ "nameplate": np, "v": 2 })).await.ok();
}
None => {
my_words = custom_words.clone().unwrap_or_else(crate::pake::words::mint_words);
my_nameplate = preview_nameplate
.clone()
.unwrap_or_else(crate::pake::words::mint_nameplate);
if custom_words.is_some() {
ui::say(&format!(
" using your words: {}",
ui::paint(ui::Tone::Brand, &format!("{my_words}-{my_nameplate}"))
));
}
sio.emit("pair-create", json!({ "nameplate": my_nameplate, "v": 2 })).await.ok();
}
}
let mut conn = Conn::for_command(
server,
sio.clone(),
tx.clone(),
my_uid.clone(),
relay, None, false, direct::direct_enabled(), );
{
let tx = tx.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
shutdown::arm_force_exit(130, shutdown::grace());
let _ = tx.send(Ev::Interrupted);
});
}
let mut petname = name; let mut prompted = false;
let mut peer: Option<(String, String)> = None;
let mut agreed_secret: Option<String> = None;
let mut pake_peer: Option<String> = None;
let caps = pair_v2_caps();
let mut cer = Ceremony::new(&my_words, &my_nameplate, caps.clone());
let deadline = Instant::now() + Duration::from_secs(600); let ceremony_budget = Duration::from_secs(
std::env::var("FILAMENT_PAIR_GRACE_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(60),
);
let mut ceremony_deadline: Option<Instant> = None;
let stall = test_hooks::pair_stall();
loop {
if let Some(n) = petname.clone() {
if let Some(sec) = agreed_secret.clone() {
devices_store_v2(&n, &sec, &caps)?;
ui::say(&format!(
" {} {} mutually remembered, verified end-to-end (no key ever crossed the server)",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ui::paint(ui::Tone::Bold, &n),
));
ui::say(&ui::paint(ui::Tone::Dim, &format!(" try: filament send <file> --to {n} · filament up")));
tokio::time::sleep(Duration::from_millis(300)).await; let _ = sio.disconnect().await;
return Ok(());
}
}
if Instant::now() > deadline {
bail!("timed out, the code was never used (codes expire after 10 minutes)");
}
if let Some(dl) = ceremony_deadline {
if Instant::now() > dl {
bail!("the other device disconnected or could not connect before pairing finished, make sure both run `filament pair` at the same time, then try again");
}
}
sess.tick(&sio).await; conn.reap_deferred();
if let Some(pid) = pake_peer.clone() {
if !stall {
if let Some(data) = cer.take_msg_payload() {
sio.emit("signal", json!({ "to": pid, "data": data })).await.ok();
}
}
if cer.has_k() {
if let Some(l) = conn.link(&pid) {
if let Some((my_fp, their_fp)) = match &l.peer { Some(p) => p.fingerprints().await, None => None } {
if let Some(data) = cer.take_confirm_payload(&my_fp, &their_fp) {
sio.emit("signal", json!({ "to": pid, "data": data })).await.ok();
}
}
}
}
}
let ev = match tokio::time::timeout(Duration::from_secs(2), rx.recv()).await {
Ok(Some(ev)) => ev,
Ok(None) => bail!("signaling closed"),
Err(_) => continue,
};
match ev {
Ev::Welcome(v) => {
conn.my_id = v["id"].as_str().unwrap_or_default().to_string();
if let Some(peers) = v["peers"].as_array() {
for p in peers {
conn.maybe_adopt(p, true).await?;
}
}
sess.invalidate(); }
Ev::Synced(v) => { sess.on_synced(&v); }
Ev::PairOk(_v) => {
let full = format!("{my_words}-{my_nameplate}");
ui::clipboard(&full);
ui::say("");
ui::say(&format!(" {}", ui::paint(ui::Tone::Brand, &full.to_uppercase())));
ui::say("");
ui::say(&ui::paint(ui::Tone::Dim, " on the other device: type it into the web app, or `filament pair <code>`"));
ui::say(&ui::paint(ui::Tone::Dim, " one claim · expires in 10 min · paired end-to-end (no key crosses the server)"));
}
Ev::PairCode(v) => {
let c = v["code"].as_str().unwrap_or("?");
let _ = c;
bail!("this server returned a legacy code. Update the server (or the peer) to pair securely.");
}
Ev::PairUsed(_) => {
ui::say(&ui::paint(ui::Tone::Dim, " code claimed, connecting..."));
ceremony_deadline.get_or_insert_with(|| Instant::now() + ceremony_budget);
}
Ev::PairMatched(v) => {
let room = v["room"].as_str().unwrap_or_default().to_string();
ceremony_deadline.get_or_insert_with(|| Instant::now() + ceremony_budget);
ui::say(&format!(" {} code accepted, connecting", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
sess.room = Some(room.clone()); sess.touch();
sess.emit(&sio, "join", json!({ "room": room, "name": display_name(), "uid": my_uid })).await;
}
Ev::PairError(v) => {
if creator && v["error"].as_str() == Some("taken") {
if custom_words.is_none() {
my_words = crate::pake::words::mint_words();
}
my_nameplate = crate::pake::words::mint_nameplate();
cer.restart(&my_words, &my_nameplate);
sio.emit("pair-create", json!({ "nameplate": my_nameplate, "v": 2 })).await.ok();
continue;
}
let hint = match v["why"].as_str() {
Some("sender-gone") => "that code's creator already left, ask them for a fresh one".to_string(),
_ => format!("{}, codes burn after one use; a failed pairing needs a FRESH code (re-run `filament pair`)", v["error"].as_str().unwrap_or("?")),
};
bail!("code rejected: {hint}");
}
Ev::PeerJoined(v) => {
conn.maybe_adopt(&v, true).await?;
}
Ev::Signal(v) => {
let from = v["from"].as_str().unwrap_or_default().to_string();
let data = v["data"].clone();
if matches!(data["type"].as_str(), Some("pake-msg") | Some("pake-confirm")) {
pake_peer.get_or_insert(from.clone());
let fps = match conn.link(&from) {
Some(l) => match &l.peer { Some(p) => p.fingerprints().await, None => None },
None => None,
};
let fp_ref = fps.as_ref().map(|(a, b)| (a.as_str(), b.as_str()));
match cer.on_signal(&data, fp_ref) {
PakeInbound::Consumed => {
if let Some(sec) = cer.secret() {
agreed_secret = Some(sec.clone());
}
}
PakeInbound::Abort(why) => {
bail!("pairing REFUSED: {why}. Nothing was stored; ask for a FRESH code.");
}
PakeInbound::Ignored => {}
}
continue;
}
conn.ensure_responder(&from, &data).await?;
conn.apply_signal(&from, data).await;
}
Ev::ChannelReady(pid, t) => {
let display = match conn.link_mut(&pid) {
Some(l) => {
l.transport = Some(t.clone());
l.presence = Presence::Ready;
l.name.clone()
}
None => continue,
};
ui::say(&format!(" {} {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), ui::paint(ui::Tone::Bold, &display)));
peer = Some((pid.clone(), display.clone()));
let _ = &t; if stall {
continue; }
pake_peer.get_or_insert(pid.clone());
if petname.is_none() && !prompted {
prompted = true;
if std::io::stdin().is_terminal() {
eprint!(" remember this device as [{display}]: ");
let tx = tx.clone();
tokio::spawn(async move {
use tokio::io::AsyncBufReadExt;
let mut line = String::new();
let mut reader = tokio::io::BufReader::new(tokio::io::stdin());
if reader.read_line(&mut line).await.is_ok() {
let _ = tx.send(Ev::StdinLine(line.trim().to_string()));
}
});
} else {
petname = Some(display.clone());
}
}
}
Ev::StdinLine(line) => {
if petname.is_none() && prompted {
let n = if line.is_empty() {
peer.as_ref().map(|(_, d)| d.clone()).unwrap_or_else(|| "device".into())
} else {
line
};
petname = Some(n);
}
}
Ev::Control(pid, v) if stall => {
let _ = (pid, v); }
Ev::Control(_pid, v) => match v["type"].as_str() {
Some("pair-keep") => {
bail!("the other device uses an older version and can't pair securely. Update it (or this CLI) so first-pairing runs the encrypted handshake. Nothing was stored.");
}
_ => {}
},
Ev::Stuck(pid, g) => {
conn.on_stuck(&pid, g, "stuck").await?;
}
Ev::GraceExpired(pid, g) => {
conn.on_stuck(&pid, g, "lost").await?;
}
Ev::PcState(pid, st) => conn.on_pc_state(&pid, &st).await,
Ev::PeerLeft(v) => {
let gone = v["id"].as_str().and_then(|p| conn.link(p)).map(|l| l.name.clone());
conn.on_peer_left(&v);
let n = gone.unwrap_or_else(|| "the other device".into());
ui::say(&ui::paint(ui::Tone::Dim, &format!(" {n} disconnected, waiting briefly in case it reconnects...")));
}
Ev::Interrupted => bail!("interrupted"),
_ => {}
}
}
}
struct Link {
peer: Option<Arc<Peer>>,
info: Value, name: String,
uid: Option<String>,
transport: Option<Arc<dyn Transport>>,
generation: u32,
attempts: u32,
trusted: bool,
expected_secret: Option<(String, String)>,
verified_name: Option<String>,
presence: Presence,
direct: bool,
direct_route: &'static str,
workers: Vec<Arc<dyn Transport>>,
established_at: Option<Instant>,
}
impl Link {
fn shown(&self) -> &str {
self.verified_name.as_deref().unwrap_or(&self.name)
}
}
#[derive(Clone, Copy, PartialEq)]
enum Presence {
Connecting,
Ready,
Away,
Reconnecting,
}
fn presence_glyph(p: Presence) -> (&'static str, ui::Tone, &'static str) {
match p {
Presence::Ready => (ui::glyph_ok(), ui::Tone::Ok, ""),
Presence::Away => ("●", ui::Tone::Warn, "away"),
Presence::Reconnecting => ("◌", ui::Tone::Warn, "reconnecting..."),
Presence::Connecting => ("◌", ui::Tone::Dim, "connecting..."),
}
}
const MAX_LINKS: usize = 16;
#[derive(Default)]
struct ResilienceState {
stall_repairs: HashMap<String, StallState>,
relay_committed: std::collections::HashSet<String>,
warm_standby: bool,
warm_cutover: std::collections::HashSet<String>,
upgrade_probe: HashMap<String, UpgradeProbe>,
iface_snapshot: Vec<String>,
}
struct RejoinState {
waiting_rejoin: Option<Instant>,
rejoin_window: Duration,
away: Option<(String, Instant)>,
}
const WARM_RECENT_CAP: usize = 5;
const WARM_MAX_BACKOFF: Duration = Duration::from_secs(30);
const WARM_DORMANT_THRESHOLD: u32 = 5;
const WARM_DORMANT_RETRY: Duration = Duration::from_secs(300);
fn warm_max() -> usize {
settings::get_str("warm-max", None)
.and_then(|s| s.trim().parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(12)
.min(MAX_LINKS - 2)
}
#[derive(Default)]
struct WarmHold {
configured: std::collections::HashSet<String>,
auto: std::collections::HashSet<String>,
recent: std::collections::VecDeque<String>,
last_use: HashMap<String, Instant>,
backoff: HashMap<String, WarmBackoff>,
}
#[derive(Clone)]
struct WarmBackoff {
duration: Duration,
failures: u32,
last_attempt: Option<Instant>,
dormant: bool,
}
impl Default for WarmBackoff {
fn default() -> Self {
Self {
duration: Duration::from_secs(1),
failures: 0,
last_attempt: None,
dormant: false,
}
}
}
impl WarmHold {
fn note_use(&mut self, peer: &str) {
let peer = peer.to_string();
self.last_use.insert(peer.clone(), Instant::now());
self.recent.retain(|p| p != &peer);
self.recent.push_back(peer.clone());
while self.recent.len() > WARM_RECENT_CAP {
self.recent.pop_front();
}
if let Some(b) = self.backoff.get_mut(&peer) {
b.dormant = false;
b.failures = 0;
b.duration = Duration::from_secs(1);
}
}
fn note_failure(&mut self, peer: &str) {
let b = self.backoff.entry(peer.to_string()).or_default();
b.failures += 1;
b.last_attempt = Some(Instant::now());
b.duration = (b.duration * 2).min(WARM_MAX_BACKOFF);
if b.failures >= WARM_DORMANT_THRESHOLD {
b.dormant = true;
}
}
fn note_success(&mut self, peer: &str) {
if let Some(b) = self.backoff.get_mut(peer) {
b.failures = 0;
b.duration = Duration::from_secs(1);
b.dormant = false;
}
}
fn should_connect(&self, peer: &str) -> bool {
if self.configured.contains(peer) {
return true;
}
if self.auto.contains(peer) {
return true;
}
if self.recent.contains(&peer.to_string()) {
if let Some(b) = self.backoff.get(peer) {
return !b.dormant;
}
return true;
}
false
}
fn peers_to_connect(&self) -> Vec<String> {
let mut peers: Vec<String> = self.configured.iter().cloned().collect();
for p in &self.auto {
if !peers.contains(p) {
peers.push(p.clone());
}
}
for p in &self.recent {
if !peers.contains(p) {
if let Some(b) = self.backoff.get(p) {
if !b.dormant {
peers.push(p.clone());
}
} else {
peers.push(p.clone());
}
}
}
peers
}
fn is_dormant(&self, peer: &str) -> bool {
self.backoff.get(peer).map(|b| b.dormant).unwrap_or(false)
}
fn due(&self, peer: &str) -> bool {
match self.backoff.get(peer) {
None => true,
Some(b) => {
let since = b.last_attempt.map(|t| t.elapsed());
if b.dormant {
since.map_or(true, |d| d >= WARM_DORMANT_RETRY)
} else {
since.map_or(true, |d| d >= b.duration)
}
}
}
}
fn resume(&mut self, peer: &str) {
if let Some(b) = self.backoff.get_mut(peer) {
b.dormant = false;
b.failures = 0;
b.duration = Duration::from_secs(1);
}
}
}
#[cfg(test)]
mod warm_hold_tests {
use super::*;
#[test]
fn should_connect_configured() {
let mut wh = WarmHold::default();
wh.configured.insert("dovm".into());
assert!(wh.should_connect("dovm"));
assert!(!wh.should_connect("popos"));
}
#[test]
fn should_connect_l3() {
let mut wh = WarmHold::default();
wh.auto.insert("dovm".into());
assert!(wh.should_connect("dovm"));
assert!(!wh.should_connect("popos"));
}
#[test]
fn should_connect_recent_not_dormant() {
let mut wh = WarmHold::default();
wh.note_use("dovm");
assert!(wh.should_connect("dovm"));
}
#[test]
fn should_connect_false_for_dormant() {
let mut wh = WarmHold::default();
for _ in 0..WARM_DORMANT_THRESHOLD {
wh.note_failure("dovm");
}
assert!(!wh.should_connect("dovm"));
}
#[test]
fn should_connect_false_for_unknown() {
let wh = WarmHold::default();
assert!(!wh.should_connect("unknown"));
}
#[test]
fn recent_lru_caps_at_5() {
let mut wh = WarmHold::default();
for i in 0..7 {
wh.note_use(&format!("peer{i}"));
}
assert_eq!(wh.recent.len(), WARM_RECENT_CAP);
assert!(!wh.recent.contains(&"peer0".to_string()));
assert!(!wh.recent.contains(&"peer1".to_string()));
assert!(wh.recent.contains(&"peer2".to_string()));
assert!(wh.recent.contains(&"peer6".to_string()));
}
#[test]
fn recent_evicts_oldest() {
let mut wh = WarmHold::default();
wh.note_use("a");
wh.note_use("b");
wh.note_use("c");
wh.note_use("d");
wh.note_use("e");
assert_eq!(wh.recent.len(), 5);
wh.note_use("f");
assert_eq!(wh.recent.len(), 5);
assert!(!wh.recent.contains(&"a".to_string()));
assert!(wh.recent.contains(&"f".to_string()));
}
#[test]
fn auto_set_is_unbounded() {
let mut wh = WarmHold::default();
for i in 0..10 {
wh.auto.insert(format!("peer{i}"));
}
let peers = wh.peers_to_connect();
for i in 0..10 {
assert!(peers.contains(&format!("peer{i}")), "peer{i} missing");
}
}
#[test]
fn auto_disabled_clears_set() {
let mut wh = WarmHold::default();
wh.auto.insert("dovm".into());
wh.auto.insert("popos".into());
assert!(wh.should_connect("dovm"));
wh.auto.clear();
assert!(!wh.should_connect("dovm"));
assert!(!wh.should_connect("popos"));
}
#[test]
fn note_failure_backoff_doubling() {
let mut wh = WarmHold::default();
wh.note_failure("dovm");
assert_eq!(wh.backoff["dovm"].duration, Duration::from_secs(2));
wh.note_failure("dovm");
assert_eq!(wh.backoff["dovm"].duration, Duration::from_secs(4));
wh.note_failure("dovm");
assert_eq!(wh.backoff["dovm"].duration, Duration::from_secs(8));
}
#[test]
fn note_failure_caps_at_30s() {
let mut wh = WarmHold::default();
for _ in 0..10 {
wh.note_failure("dovm");
}
assert_eq!(wh.backoff["dovm"].duration, WARM_MAX_BACKOFF);
}
#[test]
fn note_failure_dormant_after_threshold() {
let mut wh = WarmHold::default();
for _ in 0..WARM_DORMANT_THRESHOLD {
wh.note_failure("dovm");
}
assert!(wh.backoff["dovm"].dormant);
assert!(wh.is_dormant("dovm"));
}
#[test]
fn note_use_clears_backoff() {
let mut wh = WarmHold::default();
for _ in 0..WARM_DORMANT_THRESHOLD {
wh.note_failure("dovm");
}
assert!(wh.is_dormant("dovm"));
wh.note_use("dovm");
assert!(!wh.is_dormant("dovm"));
assert_eq!(wh.backoff["dovm"].duration, Duration::from_secs(1));
assert_eq!(wh.backoff["dovm"].failures, 0);
}
#[test]
fn resume_clears_dormant() {
let mut wh = WarmHold::default();
for _ in 0..WARM_DORMANT_THRESHOLD {
wh.note_failure("dovm");
}
assert!(wh.is_dormant("dovm"));
wh.resume("dovm");
assert!(!wh.is_dormant("dovm"));
}
#[test]
fn warm_peer_allows_instant_connection() {
let mut wh = WarmHold::default();
wh.configured.insert("dovm".into());
wh.auto.insert("dovm".into());
wh.note_use("dovm");
assert!(wh.should_connect("dovm"));
assert!(!wh.is_dormant("dovm"));
assert!(wh.configured.contains("dovm"));
assert!(wh.auto.contains("dovm"));
}
#[test]
fn due_respects_backoff_window() {
let mut wh = WarmHold::default();
assert!(wh.due("unknown"));
wh.note_failure("dovm");
assert!(!wh.due("dovm")); }
#[test]
fn dormant_peer_retries_after_interval() {
let mut wh = WarmHold::default();
for _ in 0..WARM_DORMANT_THRESHOLD {
wh.note_failure("dovm");
}
assert!(wh.is_dormant("dovm"));
assert!(!wh.due("dovm"));
assert!(wh.backoff["dovm"].dormant);
}
}
struct Conn {
server: String,
sio: rust_socketio::asynchronous::Client,
tx: mpsc::UnboundedSender<Ev>,
my_uid: String,
my_id: String,
relay_only: bool,
to_filter: Option<String>,
links: HashMap<String, Link>,
roster: HashMap<String, Value>, active: Option<String>, next_gen: u32,
rejoin: RejoinState,
chunk_size: usize,
deferred_left: HashMap<String, Value>,
recv_done: bool,
direct_pending: HashMap<String, DirectPending>,
resil: ResilienceState,
direct_ok: bool,
local_port: Option<u16>,
local_listener: Option<Arc<tokio::net::TcpListener>>,
direct_endpoint: Option<quinn::Endpoint>,
warm_hold: WarmHold,
worker_port_tx: HashMap<String, oneshot::Sender<Vec<u16>>>,
}
#[derive(Default)]
struct StallState {
attempts: u32,
pending: bool,
relayed: bool,
}
struct UpgradeProbe {
attempt: u32,
next_at: Option<Instant>,
standby: Option<Arc<dyn Transport>>,
standby_route: &'static str,
verify_started: Option<Instant>,
verify_last_idle: u64,
}
impl UpgradeProbe {
fn armed() -> Self {
UpgradeProbe {
attempt: 0,
next_at: None, standby: None,
standby_route: "direct-quic",
verify_started: None,
verify_last_idle: u64::MAX,
}
}
}
#[derive(Debug, PartialEq)]
enum Rung {
Resume,
Repaired,
Relayed,
Exhausted,
}
struct DirectPending {
secret: (String, String),
deadline: Instant,
racing: bool,
endpoint: Option<quinn::Endpoint>,
punch_sock: Option<std::net::UdpSocket>,
#[allow(dead_code)]
my_srflx: Option<std::net::SocketAddr>,
probe: bool,
}
impl Conn {
fn for_command(
server: &str,
sio: rust_socketio::asynchronous::Client,
tx: mpsc::UnboundedSender<Ev>,
my_uid: String,
relay_only: bool,
to_filter: Option<String>,
warm_standby_default: bool,
direct_ok: bool,
) -> Self {
Conn {
server: server.to_string(),
sio,
tx,
my_uid,
my_id: String::new(),
relay_only,
to_filter,
links: HashMap::new(),
roster: HashMap::new(),
active: None,
next_gen: 0,
rejoin: RejoinState { waiting_rejoin: None, rejoin_window: REJOIN_WINDOW, away: None },
chunk_size: net::MAX_DC_PAYLOAD,
deferred_left: HashMap::new(),
recv_done: false,
direct_pending: HashMap::new(),
resil: ResilienceState {
stall_repairs: HashMap::new(),
relay_committed: std::collections::HashSet::new(),
warm_standby: net::warm_standby_override().unwrap_or(warm_standby_default),
warm_cutover: std::collections::HashSet::new(),
upgrade_probe: HashMap::new(),
iface_snapshot: Vec::new(),
},
direct_ok,
local_port: None,
local_listener: None,
direct_endpoint: None,
warm_hold: WarmHold::default(),
worker_port_tx: HashMap::new(),
}
}
fn link(&self, pid: &str) -> Option<&Link> {
self.links.get(pid)
}
fn link_mut(&mut self, pid: &str) -> Option<&mut Link> {
self.links.get_mut(pid)
}
fn active_link(&self) -> Option<&Link> {
self.active.as_ref().and_then(|a| self.links.get(a))
}
fn is_active(&self, pid: &str) -> bool {
self.active.as_deref() == Some(pid)
}
fn transport(&self) -> Option<Arc<dyn Transport>> {
self.active_link().and_then(|l| l.transport.clone())
}
fn transport_of(&self, pid: &str) -> Option<Arc<dyn Transport>> {
self.links.get(pid).and_then(|l| l.transport.clone())
}
fn has_live_transport(&self, pid: &str) -> bool {
self.links.get(pid)
.map(|l| {
let primary_ok = l.transport.as_ref().map(|t| !t.is_dead()).unwrap_or(false);
let workers_ok = l.workers.iter().any(|w| !w.is_dead());
primary_ok || workers_ok
})
.unwrap_or(false)
}
fn link_flowing(&self, pid: &str) -> bool {
let threshold = std::env::var("FILAMENT_ADOPT_ACTIVE_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(3000);
self.transport_of(pid)
.map(|t| t.idle_ms() < threshold)
.unwrap_or(false)
}
fn targetable(&self, name: &str, peer_uid: Option<&str>) -> bool {
if let Some(filter) = &self.to_filter {
if !name.to_lowercase().contains(&filter.to_lowercase()) {
return false;
}
}
if let (Some(pu), Some(my_role)) = (peer_uid, self.my_uid.get(..6)) {
if pu.starts_with(my_role) {
return false;
}
}
true
}
async fn maybe_adopt(&mut self, v: &Value, want_active: bool) -> Result<bool> {
let peer_id = v["id"].as_str().unwrap_or_default().to_string();
let peer_uid = v["uid"].as_str().map(|s| s.to_string());
let name = v["name"].as_str().unwrap_or("peer").to_string();
if peer_id.is_empty() || peer_id == self.my_id {
return Ok(false);
}
self.roster.insert(peer_id.clone(), v.clone());
let stale: Option<String> = self
.links
.iter()
.find(|(sid, l)| l.uid.is_some() && l.uid == peer_uid && **sid != peer_id)
.map(|(sid, _)| sid.clone());
if let Some(old_sid) = stale {
if self.link_flowing(&old_sid) {
ui::debug(&format!("{name} reconnected, keeping active link"));
return Ok(self.is_active(&old_sid));
}
ui::debug(&format!("{name} reconnected, superseding old link"));
let was_active = self.is_active(&old_sid);
let secret = self.links.get(&old_sid).and_then(|l| l.expected_secret.clone());
self.drop_link(&old_sid);
self.establish(v.clone()).await?;
if let Some(l) = self.links.get_mut(&peer_id) {
l.expected_secret = secret;
}
if was_active {
self.active = Some(peer_id.clone());
}
return Ok(self.is_active(&peer_id));
}
if !self.links.contains_key(&peer_id) {
if self.links.len() >= MAX_LINKS {
return Ok(false);
}
self.establish(v.clone()).await?;
}
let active_deferred = self
.active
.as_ref()
.is_some_and(|a| self.deferred_left.contains_key(a));
if want_active && (self.active.is_none() || active_deferred) && self.targetable(&name, peer_uid.as_deref()) {
if active_deferred {
if let Some(old) = self.active.clone() {
if old != peer_id {
self.drop_link(&old);
}
}
}
self.active = Some(peer_id.clone());
self.rejoin.waiting_rejoin = None;
}
Ok(self.is_active(&peer_id))
}
fn drop_link(&mut self, pid: &str) {
self.deferred_left.remove(pid);
if let Some(old) = self.links.remove(pid) {
if let Some(p) = old.peer.clone() {
p.mark_closed();
tokio::spawn(async move { p.close().await });
}
}
if self.is_active(pid) {
self.active = None;
}
}
fn note_warm_use(&mut self, peer: &str) {
self.warm_hold.note_use(peer);
}
fn load_warm_peers_config(&mut self) {
if let Some(setting) = settings::get_str("warm-peers", None) {
self.warm_hold.configured.clear();
for p in setting.split(',') {
let p = p.trim().to_string();
if !p.is_empty() {
self.warm_hold.configured.insert(p);
}
}
}
}
async fn warm_hold_tick(&mut self, auto_warm: bool) -> Vec<String> {
self.load_warm_peers_config();
if auto_warm {
let mut online: Vec<(String, Option<Instant>)> = self.roster.values()
.filter_map(|v| v["name"].as_str())
.map(|s| (s.to_string(), self.warm_hold.last_use.get(s).copied()))
.collect();
let cap = warm_max();
if online.len() > cap {
online.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
online.truncate(cap);
}
let current: std::collections::HashSet<String> =
online.into_iter().map(|(n, _)| n).collect();
for name in ¤t {
if !self.warm_hold.auto.contains(name) {
self.warm_hold.auto.insert(name.clone());
ui::debug(&format!("warm-hold: auto-warming peer '{name}'"));
}
}
self.warm_hold.auto.retain(|name| current.contains(name));
} else {
if !self.warm_hold.auto.is_empty() {
ui::debug("warm-hold: auto-warm off, clearing auto tier");
self.warm_hold.auto.clear();
}
}
let mut connected = Vec::new();
let peers = self.warm_hold.peers_to_connect();
for peer in peers {
const WARM_PAIR_PROOF_GRACE: std::time::Duration = std::time::Duration::from_secs(30);
let skip = self.links.iter().any(|(_, l)| {
l.name.eq_ignore_ascii_case(&peer)
&& (l.transport.as_ref().map(|t| t.is_alive()).unwrap_or(false)
|| l.peer.is_some()
|| l.direct)
&& (
l.verified_name.is_some() || l.established_at .map(|t| t.elapsed() < WARM_PAIR_PROOF_GRACE)
.unwrap_or(false)
)
});
if skip {
ui::debug(&format!("warm-hold: skip '{peer}' (link alive, within grace)"));
continue;
} else {
ui::debug(&format!("warm-hold: will establish '{peer}' (no alive link found)"));
}
if !self.warm_hold.due(&peer) {
continue;
}
if let Some(info) = self.roster.values().find(|v| {
v["name"].as_str().map(|n| n.eq_ignore_ascii_case(&peer)).unwrap_or(false)
|| v["id"].as_str().map(|id| id.eq_ignore_ascii_case(&peer)).unwrap_or(false)
}) {
let info = info.clone();
let _peer_id = info["id"].as_str().unwrap_or_default().to_string();
match self.establish(info).await {
Ok(()) => {
self.warm_hold.note_success(&peer);
ui::debug(&format!("warm-hold: established connection to '{peer}'"));
connected.push(peer);
}
Err(e) => {
self.warm_hold.note_failure(&peer);
ui::debug(&format!("warm-hold: failed to connect to '{peer}': {e}"));
}
}
}
}
connected
}
fn resume_warm_peer(&mut self, peer: &str) {
self.warm_hold.resume(peer);
}
async fn establish(&mut self, info: Value) -> Result<()> {
self.establish_as(info, None).await
}
async fn establish_as(&mut self, info: Value, force_polite: Option<bool>) -> Result<()> {
let peer_id = info["id"].as_str().unwrap_or_default().to_string();
if self.direct_pending.contains_key(&peer_id) {
return Ok(());
}
if self.resil.relay_committed.contains(&peer_id) && self.links.contains_key(&peer_id) {
return Ok(());
}
self.drop_link(&peer_id); let peer_uid = info["uid"].as_str().map(|s| s.to_string());
let name = info["name"].as_str().unwrap_or("peer").to_string();
let mut cfg = net::fetch_config(&self.server).await?;
self.chunk_size = cfg.chunk_size;
let polite = force_polite.unwrap_or_else(|| {
net::polite_role(&self.my_uid, peer_uid.as_deref(), &self.my_id, &peer_id)
});
self.next_gen += 1;
let generation = self.next_gen;
let relay_ice = self.relay_only || test_hooks::webrtc_relay_only();
if relay_forbidden() {
cfg.ice_servers.retain(|s| net::is_stun_only(s));
}
let peer = Peer::connect(
peer_id.clone(),
polite,
cfg.ice_servers,
relay_ice,
self.sio.clone(),
self.tx.clone(),
generation,
)
.await?;
self.links.insert(
peer_id,
Link {
peer: Some(peer),
info,
name,
uid: peer_uid,
transport: None,
workers: vec![],
generation,
attempts: 0,
trusted: false,
expected_secret: None,
verified_name: None,
presence: Presence::Connecting,
direct: false,
direct_route: "direct-quic", established_at: Some(Instant::now()),
},
);
Ok(())
}
async fn start_direct(&mut self, pid: &str, name: &str, secret: &str) {
self.start_direct_inner(pid, name, secret, false).await
}
async fn start_upgrade_probe(&mut self, pid: &str, name: &str, secret: &str) {
if self.direct_pending.contains_key(pid) {
return;
}
self.start_direct_inner(pid, name, secret, true).await
}
async fn ensure_local_listener(&mut self) -> u16 {
if let Some(p) = self.local_port {
return p;
}
let (listener, port) = crate::local::listen_local().await.unwrap();
self.local_listener = Some(Arc::new(listener));
self.local_port = Some(port);
port
}
async fn start_direct_inner(&mut self, pid: &str, name: &str, secret: &str, probe: bool) {
if !self.direct_ok {
return;
}
if !probe && self.resil.relay_committed.contains(pid) {
if !self.links.contains_key(pid) {
let info = json!({ "id": pid, "name": name });
let _ = self.establish(info).await;
}
return;
}
let link_dead = self.links.get(pid)
.map(|l| l.transport.as_ref().map(|t| t.is_dead()).unwrap_or(true) && l.workers.iter().all(|w| w.is_dead()))
.unwrap_or(false);
if link_dead {
self.direct_pending.remove(pid);
self.drop_link(pid);
}
let link_alive = self.has_live_transport(pid);
if self.direct_pending.contains_key(pid) || (!probe && link_alive) {
return; }
let peer_uid = self.roster.get(pid).and_then(|info| info["uid"].as_str());
let is_local = is_self_uid(&self.my_uid, peer_uid);
let (ep, port) = if is_local {
(None, 0u16)
} else {
match direct::bind_endpoint() {
Ok(v) => {
if net::direct_streams() > 1 {
self.direct_endpoint = Some(v.0.clone());
}
(Some(v.0), v.1)
}
Err(e) => {
ui::trace(&format!("filament: direct disabled (endpoint bind failed: {e})"));
return;
}
}
};
let (cands, srflx) = if is_local {
if self.local_port.is_none() {
let (listener, port) = crate::local::listen_local().await.unwrap();
self.local_listener = Some(Arc::new(listener));
self.local_port = Some(port);
}
let cands = vec![format!("{{\"type\":\"tcp-localhost\",\"port\":{}}}", self.local_port.unwrap())];
(cands, None)
} else {
let srflx_fut = async {
if holepunch::holepunch_enabled() {
self.gather_srflx().await
} else {
None
}
};
tokio::join!(direct::gather_candidates(&self.server, port), srflx_fut)
};
let (punch_sock, my_srflx) = match srflx {
Some((sock, srflx)) => (Some(sock), Some(srflx)),
None => (None, None),
};
let mut offer = json!({ "type": "transport-offer", "v": 1, "addrs": cands });
if let Some(s) = my_srflx {
offer["srflx"] = json!(s.to_string());
}
if probe {
offer["probe"] = json!(true);
}
let _ = self
.sio
.emit("signal", json!({ "to": pid, "data": offer.clone() }))
.await;
ui::trace(&format!(
"filament: {} sent to {name} ({pid}), port {} srflx {}",
if probe { "UPGRADE-PROBE-OFFER" } else { "DIRECT-OFFER" },
port,
my_srflx.map(|s| s.to_string()).unwrap_or_else(|| "-".into())
));
{
let sio = self.sio.clone();
let pid_c = pid.to_string();
tokio::spawn(async move {
for _ in 0..6 {
tokio::time::sleep(Duration::from_millis(1200)).await;
let _ = sio
.emit("signal", json!({ "to": pid_c, "data": offer.clone() }))
.await;
}
});
}
let deadline = if holepunch::holepunch_enabled() {
Instant::now() + direct::DIRECT_BUDGET + holepunch::PUNCH_BUDGET + Duration::from_secs(3)
} else {
Instant::now() + direct::DIRECT_BUDGET
};
self.direct_pending.insert(
pid.to_string(),
DirectPending {
secret: (name.to_string(), secret.to_string()),
deadline,
racing: false,
endpoint: ep,
punch_sock,
my_srflx,
probe,
},
);
}
async fn gather_srflx(&self) -> Option<(std::net::UdpSocket, std::net::SocketAddr)> {
let cfg = net::fetch_config(&self.server).await.ok()?;
let stun_urls: Vec<String> = cfg
.ice_servers
.iter()
.flat_map(|s| s.urls.iter().cloned())
.collect();
let stun_addrs = holepunch::stun_server_addrs(&stun_urls);
if stun_addrs.is_empty() {
return None;
}
let sock = holepunch::bind_punch_socket().ok()?;
tokio::task::spawn_blocking(move || {
holepunch::stun_srflx_any(&sock, &stun_addrs).map(|srflx| (sock, srflx))
})
.await
.ok()?
.ok()
}
fn on_transport_offer(&mut self, pid: &str, peer_cands: Vec<String>, peer_srflx: Option<String>) {
let Some(p) = self.direct_pending.get_mut(pid) else { return };
if p.racing {
return;
}
let Some(ep) = p.endpoint.take() else { return };
p.racing = true;
let secret = p.secret.1.clone();
let is_probe = p.probe;
let punch_sock = p.punch_sock.take();
let peer_srflx_addr = peer_srflx
.as_deref()
.and_then(|s| s.parse::<std::net::SocketAddr>().ok());
let tx = self.tx.clone();
let pid_s = pid.to_string();
let mk = move |pid: String, t: Arc<dyn Transport>, route: &'static str| {
if is_probe {
Ev::DirectUpgradeReady(pid, t, route)
} else {
Ev::DirectReady(pid, t, route)
}
};
tokio::spawn(async move {
for cand in &peer_cands {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(cand) {
if v["type"] == "tcp-localhost" {
if let Some(port) = v["port"].as_u64() {
let addr = format!("127.0.0.1:{port}");
ui::trace(&format!("filament: trying TCP localhost to {addr}"));
match crate::local::LocalTransport::connect(&addr).await {
Ok(t) => {
let _ = tx.send(mk(pid_s, Arc::new(t), "local-tcp"));
return;
}
Err(e) => {
ui::trace(&format!("filament: TCP localhost failed: {e}"));
}
}
}
}
}
}
if let Some(t) =
direct::race_connect(ep, peer_cands, &secret, pid_s.clone(), tx.clone(), true).await
{
let _ = tx.send(mk(pid_s, t, "direct-quic"));
return;
}
if holepunch::holepunch_enabled() {
if let (Some(sock), Some(peer_srflx)) = (punch_sock, peer_srflx_addr) {
ui::trace(&format!("filament: rung-1 failed, attempting hole-punch to {peer_srflx}"));
if let Some(t) = holepunch::connect(
sock,
peer_srflx,
&secret,
pid_s.clone(),
tx.clone(),
true, )
.await
{
let _ = tx.send(mk(pid_s, t, "holepunched"));
return;
}
}
}
});
}
async fn answer_upgrade_probe(&mut self, pid: &str) {
if !net::upgrade_prober_enabled() || relay_forbidden() {
return;
}
if !self.resil.relay_committed.contains(pid) || !self.links.contains_key(pid) {
return;
}
if self.direct_pending.contains_key(pid) {
return; }
let known = self.links.get(pid).and_then(|l| l.expected_secret.clone());
if let Some((name, secret)) = known {
self.start_upgrade_probe(pid, &name, &secret).await;
}
}
fn adopt_direct(&mut self, pid: &str, t: Arc<dyn Transport>, route: &'static str) {
let pend = self.direct_pending.remove(pid);
let (name, secret) = match pend {
Some(p) => p.secret,
None => ("peer".to_string(), String::new()),
};
let info = self
.roster
.get(pid)
.cloned()
.unwrap_or_else(|| json!({ "id": pid, "name": name }));
let uid = info["uid"].as_str().map(|s| s.to_string());
let expected_secret = if secret.is_empty() {
None
} else {
Some((name.clone(), secret))
};
self.next_gen += 1;
let generation = self.next_gen;
let _existing_generation = self.links.get(pid).map(|l| l.generation).unwrap_or(0);
if let Some(existing) = self.links.get_mut(pid) {
if existing.transport.as_ref().map(|t2| t2.is_dead()).unwrap_or(true) { existing.transport = Some(t); }
existing.direct = true;
existing.direct_route = route;
existing.generation = generation;
existing.attempts = 0;
existing.trusted = true;
if let Some((n, _)) = &expected_secret {
existing.verified_name = Some(n.clone());
}
if expected_secret.is_some() {
existing.expected_secret = expected_secret;
}
} else {
direct::spawn_mesh_accept(pid.to_string(), &t, self.tx.clone());
self.links.insert(
pid.to_string(),
Link {
peer: None,
info,
name,
uid,
transport: Some(t),
workers: vec![],
generation,
attempts: 0,
trusted: true,
verified_name: expected_secret.as_ref().map(|(n, _)| n.clone()),
expected_secret,
presence: Presence::Ready,
direct: true,
direct_route: route,
established_at: Some(Instant::now()),
},
);
}
}
fn spawn_direct_workers(
&mut self,
pid: &str,
primary: &Arc<dyn Transport>,
_tkey: [u8; 32],
) {
let k = net::direct_streams();
if k <= 1 {
return;
}
let Some(_peer_addr) = primary.remote_addr() else { return };
let peer_uid = self.roster.get(pid).and_then(|i| i["uid"].as_str());
let answerer = net::polite_role(&self.my_uid, peer_uid, &self.my_id, pid);
#[cfg(debug_assertions)]
eprintln!("[T] spawn_direct_workers: pid={pid} my_uid={} peer_uid={peer_uid:?} my_id={} answerer={answerer} k={k}", self.my_uid, self.my_id);
let count = k - 1;
let pid = pid.to_string();
let tx = self.tx.clone();
let primary = primary.clone();
if !answerer {
#[cfg(debug_assertions)]
eprintln!("[T] worker DIALER branch: mesh streams accepted by background loop");
return;
}
tokio::spawn(async move {
let mut workers = Vec::with_capacity(count);
#[cfg(debug_assertions)]
eprintln!("[T] worker ACCEPTOR branch: opening {count} mesh streams on primary");
for _ in 0..count {
if let Some((send, recv, conn)) = primary.open_stream().await {
let worker = direct::make_transport(pid.clone(), conn, send, recv, tx.clone(), true, None);
workers.push(worker);
}
}
#[cfg(debug_assertions)]
eprintln!("[T] acceptor mesh streams: {} workers", workers.len());
if !workers.is_empty() {
let _ = tx.send(net::Ev::DirectWorkersReady(pid, workers));
}
});
}
fn expired_direct(&mut self) -> Vec<(String, Value, (String, String))> {
let now = Instant::now();
let mut fell_back = Vec::new();
let expired_probes: Vec<String> = self
.direct_pending
.iter()
.filter(|(_, p)| p.probe && now >= p.deadline)
.map(|(pid, _)| pid.clone())
.collect();
for pid in expired_probes {
self.direct_pending.remove(&pid);
ui::debug(&format!("filament: UPGRADE-PROBE for {pid} found no direct path in budget, staying on relay"));
self.mark_probe_failed(&pid);
}
let expired: Vec<String> = self
.direct_pending
.iter()
.filter(|(pid, p)| !p.probe && now >= p.deadline && !self.links.contains_key(*pid))
.map(|(pid, _)| pid.clone())
.collect();
for pid in expired {
if let Some(p) = self.direct_pending.remove(&pid) {
let info = self
.roster
.get(&pid)
.cloned()
.unwrap_or_else(|| json!({ "id": pid, "name": p.secret.0 }));
ui::debug(&format!(
"filament: DIRECT-FALLBACK for {}, no authenticated QUIC in budget, using WebRTC",
p.secret.0
));
fell_back.push((pid, info, p.secret));
}
}
let linked: Vec<String> = self
.direct_pending
.iter()
.filter(|(pid, p)| !p.probe && self.links.contains_key(*pid))
.map(|(pid, _)| pid.clone())
.collect();
for pid in linked {
self.direct_pending.remove(&pid);
}
fell_back
}
async fn on_stuck(&mut self, pid: &str, generation: u32, why: &str) -> Result<bool> {
if self.is_away(pid) {
return Ok(false);
}
let Some(l) = self.links.get(pid) else { return Ok(false) };
if l.direct {
return Ok(false);
}
if l.generation != generation || l.peer.as_ref().map(|p| p.is_connected()).unwrap_or(true) {
return Ok(false); }
if self.recv_done && !test_hooks::disable_modeb_drop() {
let was_active = self.is_active(pid);
ui::debug(&ui::paint(ui::Tone::Dim, &format!("dropping peer (connection {why} after completion, nothing left to fetch)")));
self.drop_link(pid);
return Ok(was_active);
}
let attempts = l.attempts + 1;
if attempts >= MAX_ATTEMPTS {
let was_active = self.is_active(pid);
ui::debug(&ui::paint(ui::Tone::Dim, &format!("dropping peer (connection {why} after {attempts} attempts)")));
self.drop_link(pid);
return Ok(was_active);
}
ui::debug(&format!("connection {why}, retrying ({}/{})", attempts + 1, MAX_ATTEMPTS));
let info = l.info.clone();
let secret = l.expected_secret.clone();
let prev = match l.presence {
Presence::Connecting => Presence::Connecting,
_ => Presence::Reconnecting,
};
let was_active = self.is_active(pid);
let backoff = std::cmp::min(
Duration::from_secs(2u64.pow(attempts - 1)),
Duration::from_secs(10),
);
tokio::time::sleep(backoff).await;
self.establish(info).await?;
if let Some(nl) = self.links.get_mut(pid) {
nl.attempts = attempts;
nl.expected_secret = secret;
nl.presence = prev;
}
if was_active {
self.active = Some(pid.to_string());
}
Ok(false)
}
fn detect_stall(&mut self, pid: &str, in_flight: bool) -> Option<u64> {
let in_episode = self.resil.stall_repairs.get(pid).map(|s| s.pending).unwrap_or(false);
let link = self.links.get(pid);
let transport = link.and_then(|l| l.transport.as_ref());
let workers: Vec<Arc<dyn Transport>> = link.map(|l| l.workers.clone()).unwrap_or_default();
let transport_up = transport.is_some() || !workers.is_empty();
let flowed = transport.map(|t| t.has_flowed()).unwrap_or(false)
|| workers.iter().any(|w| w.has_flowed());
let idle_ms = transport.map(|t| t.idle_ms()).into_iter()
.chain(workers.iter().map(|w| w.idle_ms()))
.min()
.unwrap_or(u64::MAX);
#[cfg(debug_assertions)]
eprintln!("[STALL] pid={pid} in_flight={in_flight} transport_up={transport_up} flowed={flowed} idle_ms={idle_ms} workers={} cluster={:?}",
workers.len(),
transport.map(|t| t.idle_ms()).into_iter().chain(workers.iter().map(|w| w.idle_ms())).collect::<Vec<_>>()
);
let obs = resilience::LiveObs {
in_flight,
transport_up,
flowed,
idle_ms,
grace_ms: net::establish_grace_ms(),
stall_ms: net::stall_ms(),
};
match resilience::classify(&obs) {
resilience::Liveness::Flowing | resilience::Liveness::Establishing => {
self.note_progress(pid);
None
}
resilience::Liveness::Idle => {
self.resil.stall_repairs.remove(pid);
None
}
resilience::Liveness::Stalled => {
if in_episode && self.repair_in_flight(pid) {
None
} else {
Some(obs.idle_ms)
}
}
}
}
fn repair_in_flight(&self, pid: &str) -> bool {
if self.direct_pending.contains_key(pid) {
return true;
}
self.resil.stall_repairs.get(pid).map(|s| s.relayed).unwrap_or(false)
}
fn note_progress(&mut self, pid: &str) {
self.resil.stall_repairs.remove(pid);
self.resil.warm_cutover.remove(pid);
}
async fn link_alive(&self, pid: &str) -> bool {
match self.transport_of(pid) {
Some(t) => t
.send_control(&json!({ "type": "ping", "v": 1, "reason": "stall-probe" }))
.await
.is_ok(),
None => false,
}
}
async fn correct_stall(&mut self, pid: &str) -> Rung {
if self.resil.warm_cutover.contains(pid) {
return Rung::Repaired;
}
let st = self.resil.stall_repairs.entry(pid.to_string()).or_default();
st.pending = true;
let attempt = st.attempts;
st.attempts += 1;
let warm_eligible = self.resil.warm_standby
&& !relay_forbidden()
&& !self.relay_only
&& !self.resil.warm_cutover.contains(pid);
match resilience::decide_stall_action(
attempt,
STALL_MAX_REPAIRS,
warm_eligible,
relay_forbidden(),
self.relay_only,
) {
resilience::StallAction::WarmCutover => {
ui::debug(&ui::paint(
ui::Tone::Warn,
" transfer stalled, cutting over to the warm relay standby (instant failover)",
));
self.resil.warm_cutover.insert(pid.to_string());
if let Some(st) = self.resil.stall_repairs.get_mut(pid) {
st.relayed = true;
}
self.escalate_to_relay(pid).await;
Rung::Relayed
}
resilience::StallAction::Resume => {
ui::debug(&ui::paint(ui::Tone::Warn, " transfer stalled, resuming on the same link"));
Rung::Resume
}
resilience::StallAction::ExhaustedRelayForbidden => {
ui::critical(&ui::paint(
ui::Tone::Warn,
" couldn't establish a direct path; relay disabled (--no-relay), \
partial kept on disk. Re-run to resume, or drop --no-relay to \
allow relay fallback.",
));
Rung::Exhausted
}
resilience::StallAction::ExhaustedAlreadyRelay => {
ui::critical(&ui::paint(
ui::Tone::Warn,
" transfer still stalled on the relay route, partial kept on disk. \
Re-run to resume.",
));
Rung::Exhausted
}
resilience::StallAction::RelayEscalate => {
ui::critical(&ui::paint(
ui::Tone::Warn,
" direct paths exhausted, falling back to the TURN relay",
));
if let Some(st) = self.resil.stall_repairs.get_mut(pid) {
st.relayed = true;
}
self.escalate_to_relay(pid).await;
Rung::Relayed
}
resilience::StallAction::Repair => {
ui::debug(&ui::paint(
ui::Tone::Warn,
&format!(" transfer stalled, repairing the link in place (attempt {}/{})", attempt, STALL_MAX_REPAIRS),
));
self.repair_link_in_place(pid).await;
Rung::Repaired
}
}
}
async fn repair_link_in_place(&mut self, pid: &str) {
let Some(l) = self.links.get(pid) else { return };
if l.direct {
let known = l.expected_secret.clone();
let info = l.info.clone();
let was_active = self.is_active(pid);
self.drop_link(pid); if let Some((name, secret)) = known {
self.start_direct(pid, &name, &secret).await;
if was_active {
self.active = Some(pid.to_string());
}
} else {
let _ = self.establish(info).await;
if was_active {
self.active = Some(pid.to_string());
}
}
} else if let Some(p) = l.peer.clone() {
p.restart_ice().await;
}
}
async fn escalate_to_relay(&mut self, pid: &str) {
self.relay_only = true;
self.resil.relay_committed.insert(pid.to_string());
let Some(l) = self.links.get(pid) else { return };
let info = l.info.clone();
let known = l.expected_secret.clone();
let was_active = self.is_active(pid);
self.drop_link(pid);
self.direct_pending.remove(pid);
let _ = self.establish(info).await;
if let (Some(l), Some(ks)) = (self.links.get_mut(pid), known) {
l.expected_secret = Some(ks);
}
if was_active {
self.active = Some(pid.to_string());
}
ui::critical(&format!(" {}", relay_banner()));
if self.upgrade_eligible() {
self.resil.upgrade_probe
.entry(pid.to_string())
.or_insert_with(UpgradeProbe::armed);
ui::say(&ui::paint(
ui::Tone::Dim,
" on relay, will keep trying for a direct path and upgrade automatically",
));
}
}
fn upgrade_eligible(&self) -> bool {
self.resil.warm_standby && net::upgrade_prober_enabled() && !relay_forbidden()
}
fn reprobe_on_network_event(&mut self) {
if !net::upgrade_prober_enabled() || relay_forbidden() {
return;
}
for up in self.resil.upgrade_probe.values_mut() {
if up.standby.is_none() {
up.next_at = Some(Instant::now());
}
}
}
fn mark_probe_failed(&mut self, pid: &str) {
let Some(up) = self.resil.upgrade_probe.get_mut(pid) else { return };
up.attempt = up.attempt.saturating_add(1);
up.standby = None;
up.verify_started = None;
up.verify_last_idle = u64::MAX;
let first = net::upgrade_first_ms();
let steady = net::upgrade_steady_ms();
let backoff = first.saturating_mul(1u64 << up.attempt.min(8)).min(steady);
up.next_at = Some(Instant::now() + Duration::from_millis(backoff));
}
async fn tick_upgrade_prober(&mut self) {
if !net::upgrade_prober_enabled() || relay_forbidden() {
return;
}
if self.resil.upgrade_probe.is_empty() {
return;
}
let snap = direct::local_ip_snapshot();
if snap != self.resil.iface_snapshot {
if !self.resil.iface_snapshot.is_empty() {
ui::debug(&ui::paint(
ui::Tone::Dim,
" network changed, re-probing for a direct path now",
));
for up in self.resil.upgrade_probe.values_mut() {
if up.standby.is_none() {
up.next_at = Some(Instant::now()); }
}
}
self.resil.iface_snapshot = snap;
}
let now = Instant::now();
let pids: Vec<String> = self.resil.upgrade_probe.keys().cloned().collect();
for pid in pids {
if !self.resil.relay_committed.contains(&pid) || !self.links.contains_key(&pid) {
self.resil.upgrade_probe.remove(&pid);
self.direct_pending.remove(&pid);
continue;
}
let verifying = self
.resil.upgrade_probe
.get(&pid)
.map(|u| u.standby.is_some())
.unwrap_or(false);
if verifying {
self.judge_upgrade_standby(&pid).await;
continue;
}
if self.direct_pending.contains_key(&pid) {
continue;
}
let due = match self.resil.upgrade_probe.get(&pid).and_then(|u| u.next_at) {
None => true, Some(at) => now >= at, };
if let Some(up) = self.resil.upgrade_probe.get_mut(&pid) {
if up.next_at.is_none() {
up.next_at = Some(now + Duration::from_millis(net::upgrade_first_ms()));
continue;
}
}
if !due {
continue;
}
let known = self.links.get(&pid).and_then(|l| l.expected_secret.clone());
let Some((name, secret)) = known else {
self.resil.upgrade_probe.remove(&pid);
continue;
};
ui::debug(&ui::paint(
ui::Tone::Dim,
" probing for a direct path (alongside the relay)...",
));
self.start_upgrade_probe(&pid, &name, &secret).await;
}
}
async fn judge_upgrade_standby(&mut self, pid: &str) {
let verify_ms = net::upgrade_verify_ms();
let verify_idle_ms = net::upgrade_verify_idle_ms();
let standby = self.resil.upgrade_probe.get(pid).and_then(|u| u.standby.clone());
let Some(standby) = standby else { return };
let beat_ok = matches!(
tokio::time::timeout(
Duration::from_millis(500),
standby.send_frame(VERIFY_PROBE_SID, 0, b"upgrade-verify"),
)
.await,
Ok(Ok(()))
);
let idle = standby.idle_ms();
let Some(up) = self.resil.upgrade_probe.get_mut(pid) else { return };
let started = match up.verify_started {
Some(t) => t,
None => {
up.verify_started = Some(Instant::now());
up.verify_last_idle = idle;
return;
}
};
if !beat_ok || idle >= verify_idle_ms {
ui::debug(&ui::paint(
ui::Tone::Warn,
" direct path connected but didn't hold, staying on relay (no flap)",
));
self.direct_pending.remove(pid);
self.mark_probe_failed(pid);
return;
}
up.verify_last_idle = up.verify_last_idle.min(idle);
if started.elapsed() >= Duration::from_millis(verify_ms) {
let t = standby;
let route = self.resil.upgrade_probe.get(pid).map(|u| u.standby_route).unwrap_or("direct-quic");
self.perform_upgrade(pid, t, route).await;
}
}
async fn perform_upgrade(&mut self, pid: &str, t: Arc<dyn Transport>, route: &'static str) {
let was_active = self.is_active(pid);
let known = self.links.get(pid).and_then(|l| l.expected_secret.clone());
self.resil.relay_committed.remove(pid);
self.relay_only = false;
self.resil.upgrade_probe.remove(pid);
self.direct_pending.remove(pid);
self.drop_link(pid);
self.adopt_direct_transport(pid, t.clone(), route, known);
if was_active {
self.active = Some(pid.to_string());
}
ui::critical(&ui::paint(
ui::Tone::Ok,
&format!(" upgraded back to a direct path (route: {route}), relay released"),
));
let _ = self.tx.send(Ev::ChannelReady(pid.to_string(), t));
}
fn adopt_direct_transport(
&mut self,
pid: &str,
t: Arc<dyn Transport>,
route: &'static str,
known: Option<(String, String)>,
) {
let info = self
.roster
.get(pid)
.cloned()
.unwrap_or_else(|| json!({ "id": pid, "name": known.as_ref().map(|(n, _)| n.clone()).unwrap_or_else(|| "peer".into()) }));
let name = known
.as_ref()
.map(|(n, _)| n.clone())
.or_else(|| info["name"].as_str().map(String::from))
.unwrap_or_else(|| "peer".into());
let uid = info["uid"].as_str().map(|s| s.to_string());
self.next_gen += 1;
let generation = self.next_gen;
self.links.insert(
pid.to_string(),
Link {
peer: None,
info,
name,
uid,
transport: Some(t),
workers: vec![],
generation,
attempts: 0,
trusted: true,
verified_name: known.as_ref().map(|(n, _)| n.clone()),
expected_secret: known,
presence: Presence::Ready,
direct: true,
direct_route: route,
established_at: Some(Instant::now()),
},
);
}
fn stash_upgrade_standby(&mut self, pid: &str, t: Arc<dyn Transport>, route: &'static str) {
self.direct_pending.remove(pid);
let Some(up) = self.resil.upgrade_probe.get_mut(pid) else {
return;
};
if up.standby.is_some() {
return; }
up.standby = Some(t);
up.standby_route = route;
up.verify_started = None; up.verify_last_idle = u64::MAX;
ui::debug(&ui::paint(
ui::Tone::Dim,
" direct path connected, verifying it holds before upgrading...",
));
}
async fn on_pc_state(&mut self, pid: &str, s: &str) {
let away = self.is_away(pid);
let Some(l) = self.links.get_mut(pid) else { return };
let mut announce: Option<(&'static str, ui::Tone, &'static str)> = None;
match s {
"connected" => {
if l.presence == Presence::Reconnecting {
announce = Some((ui::glyph_ok(), ui::Tone::Ok, "recovered"));
}
l.presence = Presence::Ready;
l.attempts = 0;
}
"disconnected" => {
let grace = if away {
l.presence = Presence::Away;
if let Some((_, until)) = &self.rejoin.away {
until.duration_since(Instant::now()) + Duration::from_secs(15)
} else {
Duration::from_secs(6)
}
} else {
l.presence = Presence::Reconnecting;
announce = Some(("◌", ui::Tone::Warn, "reconnecting..."));
Duration::from_secs(6)
};
if let Some(p) = &l.peer {
if !p.polite && !away {
p.restart_ice().await;
}
}
let tx = self.tx.clone();
let pid = pid.to_string();
let generation = l.generation;
tokio::spawn(async move {
tokio::time::sleep(grace).await;
let _ = tx.send(Ev::GraceExpired(pid, generation));
});
}
_ => {}
}
if let Some((mark, tone, note)) = announce {
ui::say(&self.roster(pid, mark, tone, note, "peer"));
}
}
fn on_peer_left(&mut self, v: &Value) -> bool {
let Some(pid) = v["id"].as_str() else { return false };
let pid = pid.to_string();
self.roster.remove(&pid);
if !self.links.contains_key(&pid) {
return false;
}
let forced = v["__fil_force_drop"].as_bool() == Some(true);
let defer_disabled = test_hooks::no_defer();
if !forced && !defer_disabled && self.link_flowing(&pid) {
self.deferred_left.entry(pid.clone()).or_insert_with(|| v.clone());
let name = self.link(&pid).map(|l| l.name.clone()).unwrap_or_else(|| "peer".into());
ui::debug(&format!("{name} signaling left, data channel still flowing, deferring drop"));
return false;
}
let was_active = self.is_active(&pid);
self.drop_link(&pid);
if was_active {
self.rejoin.rejoin_window = match &self.rejoin.away {
Some((apid, until)) if *apid == pid && *until > Instant::now() => {
until.duration_since(Instant::now()) + Duration::from_secs(15)
}
_ => rejoin_unwarned(),
};
self.rejoin.waiting_rejoin = Some(Instant::now());
}
was_active
}
fn reap_deferred(&mut self) {
if self.deferred_left.is_empty() {
return;
}
let ready: Vec<(String, Value)> = self
.deferred_left
.iter()
.filter(|(sid, _)| !self.links.contains_key(*sid) || !self.link_flowing(sid))
.map(|(sid, v)| (sid.clone(), v.clone()))
.collect();
for (sid, mut payload) in ready {
self.deferred_left.remove(&sid);
if !self.links.contains_key(&sid) {
continue; }
if let Some(obj) = payload.as_object_mut() {
obj.insert("__fil_force_drop".into(), Value::Bool(true));
}
let name = self.link(&sid).map(|l| l.name.clone()).unwrap_or_else(|| "peer".into());
ui::debug(&format!("{name} link went idle after deferred leave, dropping now"));
let _ = self.tx.send(Ev::PeerLeft(payload));
}
}
fn only_deferred_links(&self) -> bool {
self.links.keys().all(|k| self.deferred_left.contains_key(k))
}
fn note_alive(&mut self, pid: &str) {
if matches!(&self.rejoin.away, Some((apid, _)) if apid == pid) {
self.rejoin.away = None;
}
}
fn link_presence(&mut self, pid: &str, p: Presence) -> String {
match self.links.get_mut(pid) {
Some(l) => {
l.presence = p;
l.name.clone()
}
None => String::new(),
}
}
fn is_away(&self, pid: &str) -> bool {
matches!(&self.rejoin.away, Some((apid, until)) if apid == pid && *until > Instant::now())
}
fn roster(&self, pid: &str, mark: &str, tone: ui::Tone, note: &str, fallback_name: &str) -> String {
let mut links: Vec<(&String, &Link)> = self.links.iter().collect();
links.sort_by(|a, b| a.1.name.cmp(&b.1.name));
let mut parts = Vec::new();
let mut seen = false;
for (id, l) in links {
if id == pid {
seen = true;
parts.push(peer_entry(l.shown(), mark, tone, note));
} else {
let (m, t, n) = presence_glyph(l.presence);
parts.push(peer_entry(l.shown(), m, t, n));
}
}
if !seen {
parts.push(peer_entry(fallback_name, mark, tone, note));
}
format!(" {}", parts.join(" "))
}
async fn apply_signal(&mut self, from: &str, data: Value) {
let peer = match self.link(from).and_then(|l| l.peer.clone()) {
Some(p) => p,
None => return,
};
match peer.handle_signal(data).await {
Ok(net::SignalOutcome::Handled) => {}
Ok(net::SignalOutcome::Glare(offer)) => {
self.drop_link(from);
if let Err(e) = self.ensure_responder(from, &offer).await {
ui::trace(&format!("signal: glare rebuild failed: {e} (recovering)"));
return;
}
if let Some(p) = self.link(from).and_then(|l| l.peer.clone()) {
if let Err(e) = p.handle_signal(offer).await {
ui::trace(&format!("signal failed to apply: {e} (recovering)"));
}
}
}
Err(e) => ui::trace(&format!("signal failed to apply: {e} (recovering)")),
}
}
async fn ensure_responder(&mut self, from: &str, data: &Value) -> Result<()> {
if self.links.contains_key(from) {
return Ok(());
}
if data["type"].as_str() == Some("description")
&& data["description"]["type"].as_str() == Some("offer")
{
let info = self
.roster
.get(from)
.cloned()
.unwrap_or_else(|| json!({ "id": from }));
if self.links.len() < MAX_LINKS {
self.establish_as(info, Some(true)).await?;
}
}
Ok(())
}
}
async fn next_ev(
rx: &mut mpsc::UnboundedReceiver<Ev>,
conn: &Conn,
suppress_countdown: bool,
) -> Result<Option<Ev>> {
if let Some(since) = conn.rejoin.waiting_rejoin {
if since.elapsed() > conn.rejoin.rejoin_window {
ui::clear_sticky();
bail!(
"peer did not come back within {}s (partial state kept for resume)",
conn.rejoin.rejoin_window.as_secs()
);
}
if !suppress_countdown {
let left = conn.rejoin.rejoin_window.saturating_sub(since.elapsed()).as_secs();
ui::sticky(&ui::paint(
ui::Tone::Dim,
&format!(" {} holding the line, {left}s for them to come back (Ctrl-C to stop)", ui::spinner_frame()),
));
}
match tokio::time::timeout(Duration::from_secs(1), rx.recv()).await {
Ok(Some(ev)) => Ok(Some(ev)),
Ok(None) => Err(anyhow!("signaling channel closed")),
Err(_) => Ok(None), }
} else {
match tokio::time::timeout(Duration::from_secs(2), rx.recv()).await {
Ok(Some(ev)) => Ok(Some(ev)),
Ok(None) => Err(anyhow!("signaling channel closed")),
Err(_) => Ok(None), }
}
}
type WarmPtys = std::sync::Arc<std::sync::Mutex<HashMap<String, (String, u32)>>>;
struct DaemonMountEntry {
local: String,
peer: String,
remote: String,
pid: u32,
read_only: bool,
auto_restore: bool,
created: String,
}
struct DaemonMounts {
entries: HashMap<String, DaemonMountEntry>,
children: HashMap<String, tokio::process::Child>,
}
#[cfg(not(unix))]
async fn handle_warm_req(
_conn: &Conn,
_l2_muxes: &mut HashMap<String, Arc<l2::Mux>>,
_warm_ptys: &WarmPtys,
_tx: &mpsc::UnboundedSender<Ev>,
req: ctl::Req,
) {
match req {}
}
#[cfg(unix)]
async fn handle_warm_req(
conn: &Conn,
l2_muxes: &mut HashMap<String, Arc<l2::Mux>>,
warm_ptys: &WarmPtys,
tx: &mpsc::UnboundedSender<Ev>,
req: ctl::Req,
) {
match &req.kind {
ctl::ReqKind::Open { .. } => handle_warm_open(conn, l2_muxes, tx, req).await,
ctl::ReqKind::Dial { .. } => req.reject("dial not handled here").await,
ctl::ReqKind::Pty { .. } => handle_warm_pty(conn, l2_muxes, warm_ptys, tx, req).await,
ctl::ReqKind::Resize { .. } => handle_warm_resize(l2_muxes, warm_ptys, req).await,
ctl::ReqKind::Ping { .. } => handle_warm_ping(conn, req).await,
ctl::ReqKind::Bootstrap { .. } => req.reject("bootstrap not handled here").await,
ctl::ReqKind::Reconfigure { .. } => req.reply(&json!({ "ok": true, "live": false })).await,
ctl::ReqKind::ReloadExpose => req.reply(&json!({ "ok": true, "live": false, "count": 0 })).await,
ctl::ReqKind::Reload => req.reply(&json!({ "ok": true, "reloading": false })).await,
ctl::ReqKind::Mount { .. } => req.reject("mount not handled here").await,
ctl::ReqKind::Unmount { .. } => req.reject("unmount not handled here").await,
ctl::ReqKind::ListMounts => req.reject("list-mounts not handled here").await,
ctl::ReqKind::MountHealth { .. } => req.reject("mount-health not handled here").await,
}
}
#[cfg(unix)]
async fn handle_mount(
req: ctl::Req,
server: &str,
relay: bool,
daemon_mounts: &mut DaemonMounts,
last_mount_check: &mut Instant,
) {
let ctl::ReqKind::Mount { peer, remote, local, read_only, auto_restore, port } = &req.kind else { return };
let peer = peer.clone();
let remote = remote.clone();
let local = local.clone();
let read_only = *read_only;
let auto_restore = *auto_restore;
let port = *port;
if !Path::new(&local).exists() {
if let Err(e) = std::fs::create_dir_all(&local) {
req.reject(&format!("failed to create mount point: {e}")).await;
return;
}
crate::ui::say(&format!("created mount point: {local}"));
}
if std::process::Command::new("which")
.arg("sshfs")
.output()
.map(|o| !o.status.success())
.unwrap_or(true)
{
req.reject("sshfs not found").await;
return;
}
let info = match crate::l2::ensure_peer_bootstrap_port(server, &peer, relay, port).await {
Ok(info) => info,
Err(e) => {
req.reject(&format!("bootstrap failed: {e}")).await;
return;
}
};
let peer_name = peer.strip_suffix(".mesh").unwrap_or(&peer);
let mut args: Vec<String> = Vec::new();
args.extend_from_slice(&[
"-o".into(), format!("IdentityFile={}", info.key_path.display()),
"-o".into(), "IdentitiesOnly=yes".into(),
"-o".into(), format!("UserKnownHostsFile={}", info.known_hosts_path.display()),
"-o".into(), "GlobalKnownHostsFile=/dev/null".into(),
"-o".into(), "StrictHostKeyChecking=accept-new".into(),
"-o".into(), "ConnectTimeout=10".into(),
"-o".into(), "ServerAliveInterval=15".into(),
"-o".into(), "ServerAliveCountMax=4".into(),
]);
let dest = if let Some(d) = crate::l2::l3_dest(&info) {
d } else {
let exe = std::env::current_exe().unwrap();
let exe = exe.to_string_lossy();
let mut proxy = format!("{exe} --server {server}");
if relay {
proxy.push_str(" --relay");
}
proxy.push_str(&format!(" netcat {peer_name} {}", info.rport));
args.push("-o".into());
args.push(format!("ProxyCommand={proxy}"));
format!("{}@{}", info.login, info.host)
};
args.push(format!("{dest}:{remote}"));
args.push(local.clone());
if read_only {
args.push("-o".into());
args.push("ro".into());
}
let child = match tokio::process::Command::new("sshfs")
.args(&args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(c) => c,
Err(e) => {
req.reject(&format!("failed to spawn sshfs: {e}")).await;
return;
}
};
let pid = child.id().unwrap_or(0);
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let mount_id = mount::unique_mount_id();
let parent_id = mount::find_parent_mount(&local);
let _ = mount::add_mount(mount::MountEntry {
id: mount_id.clone(),
parent_id,
local: local.clone(),
peer: peer_name.to_string(),
remote: remote.clone(),
pid,
read_only,
auto_restore,
created: now.clone(),
});
let entry = DaemonMountEntry {
local: local.clone(),
peer: peer_name.to_string(),
remote: remote.clone(),
pid,
read_only,
auto_restore,
created: now,
};
daemon_mounts.entries.insert(local.clone(), entry);
daemon_mounts.children.insert(local.clone(), child);
*last_mount_check = Instant::now();
crate::ui::say(&format!("mounted {peer_name}:{remote} at {local} (id: {mount_id})"));
req.reply(&json!({ "ok": true })).await;
}
#[cfg(unix)]
async fn handle_unmount(req: ctl::Req, daemon_mounts: &mut DaemonMounts) {
let ctl::ReqKind::Unmount { target } = &req.kind else { return };
let target = target.clone();
if let Some(mut child) = daemon_mounts.children.remove(&target) {
let _ = child.kill().await;
}
daemon_mounts.entries.remove(&target);
match mount::unmount_cmd_async(&target).await {
Ok(()) => req.reply(&json!({ "ok": true })).await,
Err(e) => req.reject(&format!("unmount failed: {e}")).await,
}
}
#[cfg(unix)]
async fn handle_list_mounts(req: ctl::Req, daemon_mounts: &DaemonMounts) {
let mounts: Vec<Value> = daemon_mounts.entries.values().map(|e| {
let is_alive = mount::is_mount_point(&e.local);
let status = if is_alive { "healthy" } else { "dead" };
json!({
"local": e.local,
"peer": e.peer,
"remote": e.remote,
"read_only": e.read_only,
"auto_restore": e.auto_restore,
"created": e.created,
"status": status,
})
}).collect();
req.reply(&json!({ "ok": true, "mounts": mounts })).await;
}
#[cfg(unix)]
async fn handle_mount_health(req: ctl::Req, daemon_mounts: &DaemonMounts) {
let ctl::ReqKind::MountHealth { target } = &req.kind else { return };
let target = target.clone();
let entry = daemon_mounts.entries.get(&target);
match entry {
Some(e) => {
let is_alive = mount::is_mount_point(&e.local);
let path_exists = Path::new(&e.local).exists();
let status = if !path_exists {
"missing"
} else if !is_alive {
"dead"
} else {
match std::fs::metadata(&e.local) {
Ok(_) => "healthy",
Err(_) => "stale",
}
};
req.reply(&json!({ "ok": true, "status": status, "local": e.local, "peer": e.peer, "remote": e.remote })).await;
}
None => {
if mount::is_mount_point(&target) {
req.reply(&json!({ "ok": true, "status": "untracked", "local": target })).await;
} else {
req.reject(&format!("no mount found for '{target}'")).await;
}
}
}
}
async fn sshd_listening(port: u16) -> bool {
let addrs: [(&str, std::net::SocketAddr); 2] = [
("127.0.0.1", (std::net::Ipv4Addr::LOCALHOST, port).into()),
("[::1]", (std::net::Ipv6Addr::LOCALHOST, port).into()),
];
let rt = tokio::runtime::Handle::current();
for (_label, addr) in addrs {
let ok = rt
.spawn_blocking(move || {
std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(400))
.is_ok()
})
.await
.unwrap_or(false);
if ok {
return true;
}
}
false
}
#[cfg(unix)]
async fn handle_warm_ping(conn: &Conn, req: ctl::Req) {
let ctl::ReqKind::Ping { peer } = &req.kind else { return };
let peer = peer.clone();
let Some((pid, t)) = warm_link_for(conn, &peer) else {
req.reject("no warm link").await;
return;
};
let link = conn.link(&pid);
let direct = link.map(|l| l.direct).unwrap_or(false);
let route = if direct {
link.map(|l| l.direct_route.to_string()).unwrap_or_else(|| "direct".into())
} else if let Some(p) = link.and_then(|l| l.peer.clone()) {
p.route().await.unwrap_or_else(|| "relay".into())
} else {
"relay".to_string()
};
let peer_ref = link.and_then(|l| l.peer.clone());
let path = net::describe_path(t.as_ref(), peer_ref.as_deref()).await.to_json();
let reply = json!({
"ok": true,
"warm": true,
"direct": direct,
"route": route,
"remote_addr": t.remote_addr().map(|a| a.to_string()),
"rtt_ms": t.rtt_ms(),
"verified": link.and_then(|l| l.verified_name.clone()),
"path": path,
});
req.reply(&reply).await;
}
#[cfg(unix)]
const WARM_RELAY_STALE_MS: u64 = 8_000;
#[cfg(unix)]
fn warm_link_for(conn: &Conn, peer: &str) -> Option<(String, Arc<dyn net::Transport>)> {
conn.links
.iter()
.filter(|(_, l)| {
l.trusted
&& l.verified_name.as_deref().map(|n| n.eq_ignore_ascii_case(peer)).unwrap_or(false)
&& l.transport.as_ref().map(|t| {
t.is_alive() && (l.direct || t.idle_ms() < WARM_RELAY_STALE_MS)
}).unwrap_or(false)
})
.max_by_key(|(_, l)| l.direct as u8)
.map(|(pid, l)| (pid.clone(), l.transport.clone().unwrap()))
}
#[cfg(unix)]
fn log_warm_miss(conn: &Conn, peer: &str) {
for (p, l) in conn.links.iter() {
ui::debug(&format!(
"warm-miss '{peer}': pid={p} name={:?} verified={:?} trusted={} has_transport={} alive={} direct={}",
l.name, l.verified_name, l.trusted,
l.transport.is_some(),
l.transport.as_ref().map(|t| t.is_alive()).unwrap_or(false),
l.direct,
));
}
}
#[cfg(unix)]
async fn handle_warm_open(
conn: &Conn,
l2_muxes: &mut HashMap<String, Arc<l2::Mux>>,
tx: &mpsc::UnboundedSender<Ev>,
req: ctl::Req,
) {
let ctl::ReqKind::Open { peer, rport } = &req.kind else { return };
let (peer, rport) = (peer.clone(), *rport);
let Some((pid, t)) = warm_link_for(conn, &peer) else {
log_warm_miss(conn, &peer);
req.reject("no warm link to that peer").await;
return;
};
let mux = l2_muxes.entry(pid.clone()).or_insert_with(|| l2::Mux::new(t)).clone();
let tx = tx.clone();
tokio::spawn(async move {
match l2::open_stream_verified(&mux, rport, l2::warm_verify_window()).await {
Ok((sid, first, rx)) => {
let sock = req.accept().await;
l2::serve_verified_stream(mux, sid, sock, first, rx).await;
}
Err(e) => {
ui::debug(&format!(
"filament: warm link to '{peer}' is a zombie ({e}); dropping + establishing fresh"
));
let _ = tx.send(Ev::DropLink(pid));
req.reject("warm link unresponsive; establishing fresh").await;
}
}
});
}
#[cfg(unix)]
async fn handle_warm_pty(
conn: &Conn,
l2_muxes: &mut HashMap<String, Arc<l2::Mux>>,
warm_ptys: &WarmPtys,
tx: &mpsc::UnboundedSender<Ev>,
req: ctl::Req,
) {
let ctl::ReqKind::Pty { peer, session, cols, rows, term, cmd } = &req.kind else { return };
let (peer, session, cols, rows, term, cmd) = (peer.clone(), session.clone(), *cols, *rows, term.clone(), cmd.clone());
let Some((pid, t)) = warm_link_for(conn, &peer) else {
log_warm_miss(conn, &peer);
req.reject("no warm link to that peer").await;
return;
};
let mux = l2_muxes.entry(pid.clone()).or_insert_with(|| l2::Mux::new(t)).clone();
let warm_ptys = warm_ptys.clone();
let tx = tx.clone();
let verify = l2::warm_verify_window();
tokio::spawn(async move {
match l2::open_pty_stream_verified(&mux, &session, cols, rows, &term, &cmd, verify).await {
Ok((sid, first, rx_pipe)) => {
if let Ok(mut m) = warm_ptys.lock() {
m.insert(session.clone(), (pid, sid));
}
let sock = req.accept().await;
l2::serve_verified_stream(mux, sid, sock, first, rx_pipe).await;
if let Ok(mut m) = warm_ptys.lock() {
if m.get(&session).map(|(_, s)| *s == sid).unwrap_or(false) {
m.remove(&session);
}
}
}
Err(e) => {
ui::debug(&format!(
"filament: warm pty link to '{peer}' is a zombie ({e}); dropping + establishing fresh"
));
let _ = tx.send(Ev::DropLink(pid));
req.reject("warm link unresponsive; establishing fresh").await;
}
}
});
}
#[cfg(unix)]
async fn handle_warm_resize(
l2_muxes: &HashMap<String, Arc<l2::Mux>>,
warm_ptys: &WarmPtys,
req: ctl::Req,
) {
let ctl::ReqKind::Resize { session, cols, rows } = &req.kind else { return };
let (cols, rows) = (*cols, *rows);
let target = warm_ptys.lock().ok().and_then(|m| m.get(session).cloned());
if let Some((pid, sid)) = target {
if let Some(mux) = l2_muxes.get(&pid) {
let _ = mux
.transport()
.send_control(&json!({ "type": "pty-resize", "sid": sid, "cols": cols, "rows": rows }))
.await;
}
}
req.accept().await; }
#[cfg(unix)]
type PendingBootstraps =
HashMap<String, Vec<(tokio::net::UnixStream, std::time::Instant)>>;
#[cfg(unix)]
async fn handle_warm_bootstrap(conn: &Conn, pending: &mut PendingBootstraps, req: ctl::Req) {
let (peer, pubkey, ssh_port) = match &req.kind {
ctl::ReqKind::Bootstrap { peer, pubkey, ssh_port } => (peer.clone(), pubkey.clone(), *ssh_port),
_ => return,
};
let Some((pid, t)) = warm_link_for(conn, &peer) else {
log_warm_miss(conn, &peer);
req.reject("no warm link to that peer").await;
return;
};
if t.send_control(&json!({ "type": "shell-bootstrap", "v": 1, "pubkey": pubkey, "ssh_port": ssh_port }))
.await
.is_err()
{
req.reject("warm link send failed").await;
return;
}
let deadline = std::time::Instant::now() + Duration::from_secs(12);
pending.entry(pid).or_default().push((req.sock, deadline));
}
#[cfg(unix)]
async fn complete_warm_bootstrap(pending: &mut PendingBootstraps, pid: &str, reply: &Value) {
if let Some(waiters) = pending.remove(pid) {
for (mut sock, _) in waiters {
ctl::send_reply(&mut sock, reply).await;
}
}
}
#[cfg(unix)]
fn reap_warm_bootstraps(pending: &mut PendingBootstraps) {
if pending.is_empty() {
return;
}
let now = std::time::Instant::now();
for waiters in pending.values_mut() {
waiters.retain(|(_, deadline)| *deadline > now);
}
pending.retain(|_, waiters| !waiters.is_empty());
}
#[tokio::main]
async fn main() -> Result<()> {
rustls::crypto::ring::default_provider().install_default().ok();
platform::Paths::migrate_legacy();
let mut argv: Vec<String> = std::env::args().collect();
if let Some(first) = argv.get(1) {
use clap::CommandFactory;
let cmd_names: std::collections::HashSet<String> = {
let c = Cli::command();
let mut s = std::collections::HashSet::new();
for sc in c.get_subcommands() {
s.insert(sc.get_name().to_string());
s.extend(sc.get_all_aliases().map(str::to_string));
}
s
};
if !first.starts_with('-') && !cmd_names.contains(first.as_str()) {
if std::path::Path::new(first).exists() {
argv.insert(1, "send".into());
argv.push("--code".into());
} else if looks_like_pake_code(first) {
argv.insert(1, "pair".into());
} else if regex_lite_code(first) {
argv.insert(1, "recv".into());
} else if devices_load().iter().any(|(n, _)| n == first) {
argv.insert(1, "pty".into());
} else {
let first = first.clone();
let mut cands: Vec<String> = cmd_names.iter().cloned().collect();
cands.extend(devices_load().into_iter().map(|(n, _)| n));
let hint = cands
.iter()
.map(|c| (settings::levenshtein(&first, c), c))
.filter(|(d, _)| *d <= 2)
.min_by_key(|(d, _)| *d)
.map(|(_, c)| c.clone());
eprintln!("filament: unknown command or device '{first}'");
if let Some(h) = hint {
eprintln!(" did you mean '{h}'?");
}
eprintln!(" see what you can do: filament · filament --help · filament devices");
std::process::exit(2);
}
}
}
let cli = Cli::parse_from(argv);
let ui_caps = UiCapability::from_cli(&cli);
ui::init_verbosity(cli.verbose, cli.quiet);
if let Some(n) = &cli.name_as {
unsafe { std::env::set_var("FILAMENT_NAME", n) };
}
if let Some(when) = &cli.color {
unsafe { std::env::set_var("FILAMENT_COLOR", when) };
}
let relay = if cli.no_relay {
NO_RELAY.store(true, std::sync::atomic::Ordering::Relaxed);
false
} else if cli.relay {
true
} else {
match settings::get_str("relay", None).as_deref() {
Some("always") => true,
Some("never") => {
NO_RELAY.store(true, std::sync::atomic::Ordering::Relaxed);
false
}
_ => false,
}
};
if cli.no_interactive {
NO_INTERACTIVE.store(true, std::sync::atomic::Ordering::Relaxed);
}
let server = if cli.server == DEFAULT_SERVER {
config_get("server").unwrap_or(cli.server.clone())
} else {
cli.server.clone()
};
let server = server.trim_end_matches('/').to_string();
let Some(cmd) = cli.cmd else {
return tour_cmd();
};
match cmd {
Cmd::Send { paths, code, word, room, to, name, remember } => {
send_cmd(&server, paths, code || word.is_some(), word, room, to, name, relay, remember).await
}
Cmd::Recv { code, dir, yes, room, to, keep_open, remember, output } => {
recv_cmd(&server, code, dir, yes, room, to, keep_open, relay, remember, false, output, ShellPolicy::Granted, None, false).await
}
Cmd::Set { key, value, peer, dry_run, reset, hard, .. } => settings::run_set(
key.as_deref(),
value.as_deref(),
&peer,
dry_run,
reset,
hard,
ui_caps.yes,
ui_caps.json || cli.json,
).await,
Cmd::Get { key, peer, show_origin, default, json } => {
settings::run_get(&key, peer.as_deref(), show_origin, default.as_deref(), json)
}
Cmd::Addr { device, v4 } => {
if let Some(name) = device {
let all = devices_load();
let entry = all.iter().find(|(n, _)| n == &name);
let Some((_, secret)) = entry else {
bail!("no device named '{name}', see `filament devices`");
};
let caps = device_caps(&name).unwrap_or_else(|| vec!["transfer".to_string()]);
let channel = channel_of(secret);
let (last_seen, stored_v6, stored_v4) = devices_info(&name).unwrap_or((0, None, None));
let last_seen_str = if last_seen == 0 { "never".to_string() } else {
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
let ago = now.saturating_sub(last_seen);
if ago < 60 { "just now".to_string() }
else if ago < 3600 { format!("{}m ago", ago / 60) }
else if ago < 86400 { format!("{}h ago", ago / 3600) }
else { format!("{}d ago", ago / 86400) }
};
println!(" {}", ui::paint(ui::Tone::Bold, &name));
println!(" channel: {}", &channel[..12.min(channel.len())]);
if let Some(v6) = &stored_v6 {
let v4_str = stored_v4.as_ref().map(|a| format!(" / {a}")).unwrap_or_default();
println!(" overlay: {v6}{v4_str}");
println!(" mesh: {name}.mesh");
}
println!(" granted: {}", caps.join(", "));
println!(" last seen: {last_seen_str}");
} else {
let id = overlay::Identity::load_or_create()?;
let my_name = config_get("name").unwrap_or_else(|| l3::hostname());
let mesh_name = l3::sanitize_host(&my_name);
if v4 {
println!("{}", id.addr_v4());
} else {
println!(" {}", ui::paint(ui::Tone::Bold, &mesh_name));
println!(" overlay: {} (v4) / {} (v6)", id.addr_v4(), id.addr());
println!(" mesh: {mesh_name}.mesh");
}
}
Ok(())
}
Cmd::Unset { key, peer } => settings::run_unset(&key, &peer).await,
Cmd::ServeTun { tun_addr, listen, connect, psk, dev, mtu } => {
#[cfg(l3)]
{
let mut h = Sha256::new();
h.update(psk.as_bytes());
let secret: [u8; 32] = h.finalize().into();
let conn = match (listen, connect) {
(Some(b), None) => {
ui::say(&format!(" serve-tun: listening on {b} (dev {dev}, {tun_addr})"));
direct::serve_tun_listen(b.parse().context("bad --listen address")?, &secret).await?
}
(None, Some(p)) => {
ui::say(&format!(" serve-tun: connecting to {p} (dev {dev}, {tun_addr})"));
direct::serve_tun_connect(p.parse().context("bad --connect address")?, &secret).await?
}
_ => bail!("serve-tun needs exactly one of --listen <bind> or --connect <host:port>"),
};
ui::say(&format!(" {} serve-tun link up", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
l3::run_point_to_point(conn, &dev, &tun_addr, mtu).await
}
#[cfg(not(l3))]
{
let _ = (tun_addr, listen, connect, psk, dev, mtu);
bail!("serve-tun (L3) is Linux-only")
}
}
Cmd::Config { key, value } => {
match (key, value) {
(Some(k), Some(v)) => {
config_set(&k, &v)?;
println!("{k} = {v}");
}
(Some(k), None) => println!("{}", config_get(&k).unwrap_or_default()),
(None, _) => {
for k in ["name", "server", "dir"] {
if let Some(v) = config_get(k) {
println!("{k} {v}");
}
}
}
}
Ok(())
}
Cmd::Up { install, system, userspace, dir, shell, shell_only, shell_program, shell_user, install_system, no_proxy_fallback } => {
if userspace {
unsafe { std::env::set_var("FILAMENT_L3_USERSPACE", "1") };
}
let shell = shell || settings::get_bool("shell", None);
let shell_user = shell_user.or_else(|| settings::get_str("shell-user", None));
let peer_shell = settings::peers_with("shell", "on");
let shell_only = match (shell_only, peer_shell.is_empty()) {
(existing, true) => existing,
(Some(list), false) => Some(format!("{list},{}", peer_shell.join(","))),
(None, false) => Some(peer_shell.join(",")),
};
up_cmd(&server, install, system, dir, relay, shell, shell_only, shell_program, shell_user, install_system, no_proxy_fallback).await
}
Cmd::Status { json } => status_cmd(json),
Cmd::Down => { ui_caps.confirm("shut down the daemon")?; down_cmd() },
Cmd::Introduce { a, b } => introduce_cmd(&server, &a, &b, relay).await,
Cmd::Pair { code, name, word } => pair_cmd(&server, code, name, word, relay).await,
Cmd::Devices { action, json } => {
match action {
None => {
let all = devices_load();
if json {
let arr: Vec<Value> = all
.iter()
.map(|(n, s)| {
let (last_seen, v6, v4) = devices_info(n).unwrap_or((0, None, None));
let addr = v6.clone().or_else(|| v4.clone()).unwrap_or_default();
let mesh = format!("{n}.mesh");
json!({
"name": n,
"channel": channel_of(s),
"caps": device_caps(n).unwrap_or_else(|| vec!["transfer".to_string()]),
"lastSeen": last_seen,
"address": addr,
"mesh": mesh,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&arr)?);
} else {
if all.is_empty() {
println!("no known devices yet, run `filament pair` to add one");
} else {
let mut table = ui::Table::new(&["NAME", "ADDRESS", "GRANTED", "LAST SEEN"]);
for (n, _) in &all {
let (last_seen, v6, v4) = devices_info(n).unwrap_or((0, None, None));
let addr = v6.as_deref().or(v4.as_deref()).unwrap_or("-");
let caps = device_caps(n).unwrap_or_else(|| vec!["transfer".to_string()]);
let last_seen_str = if last_seen == 0 {
"never".to_string()
} else {
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
let ago = now.saturating_sub(last_seen);
if ago < 60 { "just now".to_string() }
else if ago < 3600 { format!("{}m ago", ago / 60) }
else if ago < 86400 { format!("{}h ago", ago / 3600) }
else { format!("{}d ago", ago / 86400) }
};
table.row(&[n, addr, &caps.join(", "), &last_seen_str]);
}
table.print();
}
}
}
Some(DevicesAction::Forget { name }) => {
let had = devices_load().iter().any(|(n, _)| n == &name);
if !had {
bail!("no device named '{name}', see `filament devices`");
}
devices_remove(&name)?;
println!("forgot '{name}', it can no longer find or auto-connect to this machine");
println!("(their side still holds its half; it will hear \"never met you\" on the next proof)");
}
Some(DevicesAction::Rename { old, new }) => {
let p = devices_path();
let mut arr: Vec<Value> = std::fs::read_to_string(&p)
.ok()
.and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
.and_then(|v| v.as_array().cloned())
.unwrap_or_default();
if !arr.iter().any(|d| d["name"].as_str() == Some(old.as_str())) {
bail!("no device named '{old}', see `filament devices`");
}
if arr.iter().any(|d| d["name"].as_str() == Some(new.as_str())) {
bail!("'{new}' already exists, forget it first or pick another name");
}
for d in arr.iter_mut() {
if d["name"].as_str() == Some(old.as_str()) {
d["name"] = json!(new);
}
}
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr)?)?;
println!("renamed '{old}' -> '{new}' (local alias only, the secret, and the other side, are unchanged)");
}
}
Ok(())
}
Cmd::Update { check, beta } => update_cmd(check, beta).await,
Cmd::Completions { shell } => {
use clap::CommandFactory;
clap_complete::generate(shell, &mut Cli::command(), "filament", &mut std::io::stdout());
Ok(())
}
Cmd::Man { page } => {
if let Some(p) = page {
if p == "routing" {
println!("{}", include_str!("../docs/filament-routing.md"));
return Ok(());
}
if std::io::stdout().is_terminal() {
eprintln!("no manual page '{p}'; available: routing");
eprintln!("try `filament man` for the full help, or `filament man routing` for the connection model.");
} else {
use clap::CommandFactory;
clap_mangen::Man::new(Cli::command()).render(&mut std::io::stdout())?;
}
return Ok(());
}
if std::io::stdout().is_terminal() {
use clap::CommandFactory;
Cli::command().print_long_help()?;
} else {
use clap::CommandFactory;
clap_mangen::Man::new(Cli::command()).render(&mut std::io::stdout())?;
}
Ok(())
}
Cmd::Netcat { peer, rport } => l2::netcat_cmd(&server, &peer, rport, relay).await,
Cmd::Dial { peer, port } => l2::dial_cmd(&peer, port).await,
Cmd::Pty { peer, cmd } => l2::pty_cmd(&server, &peer, relay, cmd).await,
Cmd::Forward { lport, peer, rport } => l2::forward_cmd(&server, lport, &peer, rport, relay).await,
Cmd::Expose { port, to, peer, list } => expose::expose_cmd(port, to, peer, list).await,
Cmd::Unexpose { port } => { ui_caps.confirm("unexpose a port")?; expose::unexpose_cmd(port).await },
Cmd::Proxy { port, bind, http_port } => l2::proxy_cmd(&server, &bind, port, http_port, relay).await,
Cmd::Ssh { peer, args } => l2::ssh_cmd(&server, &peer, &args, relay).await,
Cmd::Ping { peer, count, json } => ping::ping_cmd(&server, &peer, count, json, relay).await,
Cmd::Doctor { device, watch, repeat, json } => {
doctor::doctor_cmd(&server, device, watch, repeat, json, relay).await
}
Cmd::Grant { device, capability } => {
device_set_cap(&device, &capability, true)?;
println!(
"granted '{capability}' to '{device}'. {}",
if capability == "shell" {
"they can now `filament ssh` into this machine (their key is installed on first connect)."
} else {
""
}
);
Ok(())
}
Cmd::Revoke { device, capability } => {
ui_caps.confirm(&format!("revoke {capability} from {device}"))?;
device_set_cap(&device, &capability, false)?;
if capability == "shell" {
sshkeys::remove_authorized_key(&device)?;
println!("revoked 'shell' from '{device}' and removed its filament-managed authorized_keys block.");
} else {
println!("revoked '{capability}' from '{device}'.");
}
Ok(())
}
Cmd::Mount { peer, remote, local, read_only: _, options: _, foreground: _, save_auto, list, check, save_profile, apply_profile, profiles, delete_profile } => {
if let Some(name) = save_profile {
mount::save_profile_cmd(&name)
} else if let Some(name) = apply_profile {
mount::apply_profile_cmd(&name, &server, relay).await
} else if profiles {
mount::profiles_cmd()
} else if let Some(name) = delete_profile {
mount::delete_profile_cmd(&name)
} else if list {
mount::list_cmd()
} else if let Some(path) = check {
mount::check_cmd(&path)
} else if peer.is_none() && remote.is_none() {
if std::io::stdin().is_terminal() {
mount::interactive_mount_fancy(&server, relay).await
} else {
mount::print_mount_help();
Ok(())
}
} else {
let peer = peer.ok_or_else(|| anyhow::anyhow!("peer is required"))?;
let remote = remote.ok_or_else(|| anyhow::anyhow!("remote path is required"))?;
let _auto_restore = save_auto;
let mut client = l2::mount_cmd(&server, &peer, relay, &remote).await?;
if let Some(local) = local {
#[cfg(any(target_os = "linux", all(target_os = "macos", feature = "mount-macos"), all(target_os = "windows", feature = "mount-windows")))]
{
return mount_fuse_cmd(client, &peer, &remote, &local).await;
}
#[cfg(not(any(target_os = "linux", all(target_os = "macos", feature = "mount-macos"), all(target_os = "windows", feature = "mount-windows"))))]
{
let _ = &local;
ui::say(&format!(
" {} mesh-native mount protocol connected to {peer}:{remote}",
ui::paint(ui::Tone::Ok, ui::glyph_ok())
));
ui::say(&format!(
" {} local mount adapter not available on this OS; listing directory instead",
ui::paint(ui::Tone::Warn, "!")
));
}
}
use crate::mount_proto::MountOp;
let root_enc = mount_proto::path_encode(std::path::Path::new("."));
let resp = client.call(MountOp::Open { path: root_enc, flags: 0 }).await?;
match resp.result {
crate::mount_proto::MountResult::Ok(v) => {
let fh = v["fh"].as_u64().unwrap_or(0);
ui::say(&format!(" {} {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), remote));
let entries = client.call(MountOp::ReadDir { fh, offset: 0 }).await?;
match entries.result {
crate::mount_proto::MountResult::Ok(v) => {
if let Some(arr) = v.as_array() {
for entry in arr {
let name_enc = entry["name"].as_str().unwrap_or("?");
let name = mount_proto::path_decode(name_enc)
.map(|p| p.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "?".into()))
.unwrap_or_else(|_| "?".into());
let kind = entry["stat"]["kind"].as_str().unwrap_or("file");
let size = entry["stat"]["size"].as_u64().unwrap_or(0);
ui::say(&format!(" {name} ({kind}, {size}B)"));
}
}
}
_ => {}
}
}
crate::mount_proto::MountResult::Err(e) => {
ui::say(&format!(" {} {}: {}", ui::paint(ui::Tone::Err, ui::glyph_err()), remote, e.msg));
}
}
Ok(())
}
}
Cmd::Unmount { path } => { ui_caps.confirm(&format!("unmount {path}"))?; mount::unmount_cmd(&path) },
Cmd::Backup { peer, source, dest, exclude, dry_run, delete, options } => {
backup::backup_cmd(&server, &peer, &source, &dest, exclude, dry_run, delete, options, relay).await
}
}
}
#[cfg(any(target_os = "linux", all(target_os = "macos", feature = "mount-macos"), all(target_os = "windows", feature = "mount-windows")))]
async fn mount_fuse_cmd(
mut client: crate::mount_proto::MountClient,
peer: &str,
remote: &str,
local: &str,
) -> Result<()> {
use crate::mount_proto::{MountOp, MountResult};
let root_enc = mount_proto::path_encode(std::path::Path::new("."));
let probe = tokio::time::timeout(
Duration::from_secs(10),
client.call(MountOp::GetAttr { path: root_enc }),
)
.await;
match probe {
Ok(Ok(resp)) => match resp.result {
MountResult::Ok(_) => {}
MountResult::Err(e) => bail!(
"mount refused by {peer}: {} (remote path {remote})",
e.msg
),
},
Ok(Err(e)) => bail!(
"mount to {peer} failed before it started: {e} \
(peer may be untrusted or the link dropped; nothing was mounted)"
),
Err(_) => bail!(
"mount to {peer} timed out waiting for the remote filesystem \
(no response in 10s; nothing was mounted)"
),
}
let mnt = std::path::PathBuf::from(local);
let created_dir = !mnt.exists();
if created_dir {
std::fs::create_dir_all(&mnt)
.with_context(|| format!("failed to create mount point {local}"))?;
}
ui::say(&format!(
" {} mesh-native mount: {peer}:{remote} -> {local} (FUSE, no sshd/sshfs)",
ui::paint(ui::Tone::Ok, ui::glyph_ok())
));
ui::say(&format!(
" {} mounted. unmount with `filament unmount {local}` or ctrl-c",
ui::paint(ui::Tone::Ok, ui::glyph_ok())
));
let mnt_run = mnt.clone();
#[cfg(target_os = "linux")]
let session = tokio::task::spawn_blocking(move || crate::mount_fuse::run_mount(client, &mnt_run));
#[cfg(all(target_os = "windows", feature = "mount-windows"))]
let session = tokio::task::spawn_blocking(move || crate::mount_winfsp::run_mount(client, &mnt_run));
let result: Result<()> = tokio::select! {
joined = session => {
match joined {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(e),
Err(e) => Err(anyhow::anyhow!("mount session panicked: {e}")),
}
}
_ = tokio::signal::ctrl_c() => {
ui::say("\n unmounting...");
let _ = unmount_fuse(local);
Ok(())
}
};
if result.is_err() && created_dir {
let _ = unmount_fuse(local);
let _ = std::fs::remove_dir(&mnt);
}
result
}
#[cfg(any(target_os = "linux", all(target_os = "macos", feature = "mount-macos")))]
fn unmount_fuse(local: &str) -> std::io::Result<()> {
#[cfg(target_os = "linux")]
{
for bin in ["fusermount3", "fusermount"] {
if std::process::Command::new(bin)
.args(["-u", local])
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return Ok(());
}
}
let _ = std::process::Command::new("fusermount3")
.args(["-uz", local])
.status();
}
#[cfg(target_os = "macos")]
{
if std::process::Command::new("diskutil")
.args(["unmount", "force", local])
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return Ok(());
}
let _ = std::process::Command::new("umount")
.args([local])
.status();
}
Ok(())
}
#[cfg(all(target_os = "windows", feature = "mount-windows"))]
fn unmount_fuse(_local: &str) -> std::io::Result<()> {
Ok(())
}
const REPO: &str = "Abdk4Moura/filament";
fn release_target() -> Option<&'static str> {
match (std::env::consts::OS, std::env::consts::ARCH) {
("linux", "x86_64") => Some("x86_64-unknown-linux-musl"),
("macos", "aarch64") => Some("aarch64-apple-darwin"),
("macos", "x86_64") => Some("x86_64-apple-darwin"),
("windows", "x86_64") => Some("x86_64-pc-windows-msvc"),
_ => None,
}
}
async fn update_cmd(check_only: bool, beta: bool) -> Result<()> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.user_agent(format!("filament/{}", env!("CARGO_PKG_VERSION")))
.build()?;
let beta_ok = beta || env!("CARGO_PKG_VERSION").contains('-');
let releases: Value = client
.get(format!("https://api.github.com/repos/{REPO}/releases?per_page=20"))
.send()
.await?
.json()
.await?;
fn key(v: &str) -> (u64, u64, u64, bool, u64) {
let (core, pre) = v.split_once('-').map(|(c, p)| (c, Some(p))).unwrap_or((v, None));
let mut it = core.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
let pre_num = pre.and_then(|p| p.rsplit('.').next()).and_then(|n| n.parse().ok()).unwrap_or(0);
(it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0), pre.is_none(), pre_num)
}
let latest = releases
.as_array()
.and_then(|a| {
a.iter()
.filter(|r| {
r["tag_name"].as_str().is_some_and(|t| t.starts_with("cli-v"))
&& (beta_ok || !r["prerelease"].as_bool().unwrap_or(false))
})
.max_by_key(|r| key(r["tag_name"].as_str().unwrap_or_default().trim_start_matches("cli-v")))
})
.ok_or_else(|| anyhow!("no CLI release found"))?;
let tag = latest["tag_name"].as_str().unwrap_or_default().to_string();
let latest_ver = tag.trim_start_matches("cli-v").to_string();
let current = env!("CARGO_PKG_VERSION");
if key(&latest_ver) <= key(current) {
println!("filament {current} is already the latest (released: {latest_ver})");
return Ok(());
}
println!("update available: {current} -> {latest_ver}");
if check_only {
return Ok(());
}
let source = platform::InstallSource::detect();
if source != platform::InstallSource::SelfInstalled {
let hint = source.upgrade_hint();
println!("filament was installed via a package manager — update with: {hint}");
return Ok(());
}
let target = release_target().ok_or_else(|| anyhow!("no prebuilt binary for this platform; build from source"))?;
let (asset, inner) = if cfg!(windows) {
(format!("filament-{target}.zip"), "filament.exe")
} else {
(format!("filament-{target}.tar.gz"), "filament")
};
let base = format!("https://github.com/{REPO}/releases/download/{tag}");
ui::say(&format!("downloading {asset} ..."));
let bytes = client.get(format!("{base}/{asset}")).send().await?.error_for_status()?.bytes().await?;
let sums = client.get(format!("{base}/SHA256SUMS")).send().await?.error_for_status()?.text().await?;
let got = sha256_hex(&bytes);
let expected = sums
.lines()
.find(|l| l.contains(&asset))
.and_then(|l| l.split_whitespace().next())
.ok_or_else(|| anyhow!("{asset} missing from SHA256SUMS"))?;
if got != expected {
bail!("checksum mismatch for {asset}: got {got}, expected {expected}");
}
ui::say("checksum ok");
let new_bin: Vec<u8> = if asset.ends_with(".tar.gz") {
let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(&bytes[..]));
let mut ar = tar::Archive::new(gz);
let mut out = None;
for entry in ar.entries()? {
let mut e = entry?;
if e.path()?.file_name().map(|n| n == inner).unwrap_or(false) {
let mut v = Vec::new();
e.read_to_end(&mut v)?;
out = Some(v);
break;
}
}
out.ok_or_else(|| anyhow!("{inner} not found in archive"))?
} else {
use std::io::Read;
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&bytes))
.map_err(|e| anyhow!("zip reader: {e}"))?;
let mut found = None;
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| anyhow!("zip entry {i}: {e}"))?;
if entry.name().ends_with(inner) {
let mut v = Vec::new();
entry.read_to_end(&mut v)?;
found = Some(v);
break;
}
}
found.ok_or_else(|| anyhow!("{inner} not found in zip archive"))?
};
let me = std::env::current_exe()?;
let staging = me.with_extension("update-staging");
std::fs::write(&staging, &new_bin)?;
#[cfg(target_os = "linux")]
let had_cap = std::process::Command::new("getcap")
.arg(&me)
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("cap_net_admin"))
.unwrap_or(false);
#[cfg(windows)]
{
let old = me.with_extension("old");
let _ = std::fs::remove_file(&old);
std::fs::rename(&me, &old)
.with_context(|| format!("renaming {} -> {}", me.display(), old.display()))?;
std::fs::rename(&staging, &me)
.with_context(|| format!("installing new {}", me.display()))?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&staging, std::fs::Permissions::from_mode(0o755))?;
std::fs::rename(&staging, &me)
.with_context(|| format!("replacing {}", me.display()))?;
}
println!("updated to {latest_ver} -> {}", me.display());
#[cfg(target_os = "linux")]
if had_cap {
let is_root = unsafe { libc::geteuid() } == 0;
let ok = if is_root {
std::process::Command::new("setcap").args(["cap_net_admin+eip"]).arg(&me).status().map(|s| s.success()).unwrap_or(false)
} else {
std::process::Command::new("sudo").args(["setcap", "cap_net_admin+eip"]).arg(&me).status().map(|s| s.success()).unwrap_or(false)
};
if ok {
println!("re-applied CAP_NET_ADMIN (L3 overlay)");
} else {
println!("note: re-grant L3's capability:\n sudo setcap cap_net_admin+eip {}", me.display());
}
crate::tun::ensure_hosts_writable();
}
#[cfg(unix)]
{
let reloading = matches!(ctl::try_reload().await, Some(ref v) if v["reloading"].as_bool() == Some(true));
if reloading {
println!("reloading the daemon onto the new binary (graceful restart, no sudo)");
} else if daemon_alive().is_some() {
println!("restart the daemon to run the new binary: `systemctl restart filament` (or `filament down` then `filament up ...`)");
}
}
Ok(())
}
struct Outgoing {
id: String,
sid: u32,
name: String,
size: u64,
head: Option<String>,
full: Option<String>,
path: PathBuf,
temp: bool, accepted_once: bool, sent: bool,
acked: bool,
done: bool,
}
#[allow(clippy::too_many_arguments)]
async fn send_cmd(
server: &str,
paths: Vec<String>,
mut use_code: bool,
mut word: Option<String>,
room: Option<String>,
mut to: Option<String>,
name: Option<String>,
relay: bool,
remember: Option<String>,
) -> Result<()> {
if paths.is_empty() {
bail!("nothing to send, pass files, directories, or '-' for stdin");
}
if !use_code && to.is_none() && !paths.iter().any(|p| p == "-") && interactive_allowed() {
let names: Vec<String> = devices_load().into_iter().map(|(n, _)| n).collect();
let mut chose_device = false;
if !names.is_empty() {
let mut items = names.clone();
items.push("shareable code / local network".into());
let header = ui::paint(ui::Tone::Dim, " send to which device? (up/down, enter, esc)");
if let Some(i) = codeentry::pick(&header, &items)? {
if i < names.len() {
ui::say(&format!(" {} sending to {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), names[i]));
to = Some(names[i].clone());
chose_device = true;
}
}
}
if !chose_device {
ui::say(&ui::paint(
ui::Tone::Dim,
" press enter to send over the local network, or type words to create a shareable code",
));
let auto_np = crate::pake::words::mint_nameplate();
match codeentry::run(" send · code ", codeentry::Mode::Create, "", &auto_np)? {
codeentry::Outcome::Submitted(words) => {
use_code = true;
word = Some(words);
}
codeentry::Outcome::Empty => { }
codeentry::Outcome::Cancelled => return Err(cancelled()),
}
}
}
if name.is_some() && paths.len() > 1 {
ui::say(&ui::paint(ui::Tone::Warn, "--name is ignored when sending multiple paths"));
}
let single = paths.len() == 1;
let my_uid = mk_uid("s");
let mut outgoing: Vec<Outgoing> = Vec::new();
for (i, p) in paths.iter().enumerate() {
let sid = (i + 1) as u32;
let id = format!("{}-{}", my_uid, sid);
if p == "-" {
let spool = std::env::temp_dir().join(format!("filament-stdin-{}", std::process::id()));
let mut f = std::fs::File::create(&spool)?;
let n = std::io::copy(&mut std::io::stdin().lock(), &mut f)?;
drop(f);
let head = head_hash(&spool);
let full = full_hash(&spool);
let offered = name.clone().filter(|_| single).unwrap_or_else(|| "stdin.bin".into());
outgoing.push(Outgoing { id, sid, name: offered, size: n, head, full, path: spool, temp: true, accepted_once: false, sent: false, acked: false, done: false });
} else {
let path = PathBuf::from(p);
let meta = std::fs::metadata(&path).with_context(|| format!("stat {p}"))?;
if meta.is_dir() {
if name.is_some() && single {
ui::say(&ui::paint(ui::Tone::Warn, "--name is ignored for a directory (it's tarred under the directory name)"));
}
let dirname = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "dir".into());
let spool = std::env::temp_dir().join(format!("filament-tar-{}-{}.tar", std::process::id(), i));
ui::say(&format!("packing {p} -> {dirname}.tar ..."));
{
let f = std::fs::File::create(&spool)?;
let mut b = tar::Builder::new(f);
b.append_dir_all(&dirname, &path)?;
b.finish()?;
}
let size = std::fs::metadata(&spool)?.len();
let head = head_hash(&spool);
let full = full_hash(&spool);
outgoing.push(Outgoing { id, sid, name: format!("{dirname}.tar"), size, head, full, path: spool, temp: true, accepted_once: false, sent: false, acked: false, done: false });
} else {
let offered = name.clone().filter(|_| single).unwrap_or_else(|| {
path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| p.clone())
});
let head = head_hash(&path);
let full = full_hash(&path);
outgoing.push(Outgoing { id, sid, name: offered, size: meta.len(), head, full, path, temp: false, accepted_once: false, sent: false, acked: false, done: false });
}
}
}
for o in &outgoing {
ui::say(&format!("send: {} ({})", o.name, human(o.size)));
}
let room = match room {
Some(r) => r,
None => net::fetch_auto_room(server).await?,
};
let (tx, mut rx) = mpsc::unbounded_channel::<Ev>();
let sio = net::connect_signaling(server, tx.clone()).await?;
let mut sess = session::Session::new(&display_name(), &my_uid);
sess.room = Some(room.clone());
sess.emit(&sio, "join", json!({ "room": room, "name": display_name(), "uid": my_uid })).await;
let known_target: Option<(String, String)> =
to.as_ref().and_then(|t| devices_load().into_iter().find(|(n, _)| n.eq_ignore_ascii_case(t)));
let mut send_words = String::new(); let mut send_nameplate = String::new();
if let Some((n, sec)) = &known_target {
ui::say(&format!(" waiting for known device {}", ui::paint(ui::Tone::Bold, n)));
sess.channels = vec![channel_of(sec)];
sess.emit(&sio, "subscribe", json!({ "channels": [channel_of(sec)] })).await;
} else if use_code {
send_words = match &word {
Some(w) => {
let (words, _np) =
crate::pake::split_chosen_code(&crate::pake::norm_code(w));
if password_word_tokens(&words) < 2 {
bail!(
"'{w}' is too weak. Use at least two words, e.g. gigantic-element \
(easier to say, harder to guess). A single word falls below the \
strength floor the rate-limit relies on."
);
}
words
}
None => crate::pake::words::mint_words(),
};
send_nameplate = crate::pake::words::mint_nameplate();
sio.emit("pair-create", json!({ "nameplate": send_nameplate, "v": 2 })).await.ok();
} else {
ui::say(&format!("waiting for a peer in room {room} (same network auto-discovers; or use --code)"));
}
let waiting = Arc::new(std::sync::atomic::AtomicBool::new(true));
{
let waiting = waiting.clone();
tokio::spawn(async move {
while waiting.load(std::sync::atomic::Ordering::Relaxed) {
ui::status(&format!(" {} waiting...", ui::spinner_frame()));
tokio::time::sleep(Duration::from_millis(120)).await;
}
});
}
let mut conn = Conn::for_command(
server,
sio.clone(),
tx.clone(),
my_uid,
relay, to, false, direct::direct_enabled(), );
if known_target.is_some() {
conn.to_filter = None; }
let mut code_used = !use_code && known_target.is_none();
let mut send_cer: Option<Ceremony> = if use_code {
Some(Ceremony::new(&send_words, &send_nameplate, pair_v2_caps()))
} else {
None
};
let mut pake_peer: Option<String> = None;
let mut pake_done = !use_code;
let pake_budget = Duration::from_secs(
std::env::var("FILAMENT_PAIR_GRACE_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(60),
);
let mut pake_deadline: Option<Instant> = None;
let mut last_state_ping = Instant::now();
let mut reproved: std::collections::HashSet<String> = Default::default();
{
let tx = tx.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
shutdown::arm_force_exit(130, shutdown::grace());
let _ = tx.send(Ev::Interrupted);
});
}
let outgoing = Arc::new(tokio::sync::Mutex::new(outgoing));
let started = Instant::now();
let claim_deadline = Duration::from_secs(600);
let establish_deadline = std::env::var("FILAMENT_SEND_TIMEOUT")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or(Duration::from_secs(60));
let mut established = false;
let mut stuck_while_connecting = 0u32;
let mut wedge_hint_shown = false;
let mut saw_known_peer: HashSet<String> = HashSet::new();
let ack_wait = std::env::var("FILAMENT_ACK_TIMEOUT")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or(Duration::from_secs(15));
let ack_reprobe = Duration::from_secs(5);
let mut sent_all_at: Option<Instant> = None;
let mut ack_reprobed = false; let mut reprobed_at: Option<Instant> = None;
loop {
if !established
&& !establish_deadline.is_zero()
&& started.elapsed() >= establish_deadline
{
ui::clear_sticky();
bail!(
"no peer connected within {}s, is a receiver running / the page open? \
(set FILAMENT_SEND_TIMEOUT to change or 0 to disable)",
establish_deadline.as_secs()
);
}
let ev = if conn.active.is_none() && conn.rejoin.waiting_rejoin.is_none() {
if started.elapsed() >= claim_deadline {
bail!("timed out waiting for a peer, is the other device online and on the same server? (--code makes pairing explicit)");
}
let slice = Duration::from_secs(2).min(claim_deadline.saturating_sub(started.elapsed()));
match tokio::time::timeout(slice, rx.recv()).await {
Ok(Some(ev)) => Some(ev),
Ok(None) => bail!("signaling channel closed"),
Err(_) => None, }
} else {
next_ev(&mut rx, &conn, false).await?
};
sess.tick(&sio).await;
conn.reap_deferred();
if let (Some(cer), Some(pid)) = (send_cer.as_mut(), pake_peer.clone()) {
if let Some(data) = cer.take_msg_payload() {
sio.emit("signal", json!({ "to": pid, "data": data })).await.ok();
}
if cer.has_k() {
if let Some(l) = conn.link(&pid) {
if let Some((my_fp, their_fp)) = match &l.peer { Some(p) => p.fingerprints().await, None => None } {
if let Some(data) = cer.take_confirm_payload(&my_fp, &their_fp) {
sio.emit("signal", json!({ "to": pid, "data": data })).await.ok();
}
}
}
}
}
if !pake_done {
if let Some(dl) = pake_deadline {
if Instant::now() > dl {
ui::clear_sticky();
bail!("the other device uses an older version and can't receive securely over a code. Update it (or this CLI) so the transfer runs the encrypted handshake. Nothing was sent.");
}
}
}
for (pid, info, (n, sec)) in conn.expired_direct() {
conn.establish(info).await?;
if let Some(l) = conn.link_mut(&pid) {
l.expected_secret = Some((n, sec));
}
if conn.to_filter.is_none() && conn.active.is_none() {
conn.active = Some(pid.clone());
}
}
if last_state_ping.elapsed() >= Duration::from_secs(10) {
last_state_ping = Instant::now();
for l in conn.links.values() {
if let Some(t) = &l.transport {
let _ = t
.send_control(&json!({
"type": "state", "v": 1,
"transfers": {},
"trusted": l.trusted,
"away": false,
}))
.await;
}
}
}
if let Some(active) = conn.active.clone() {
let in_flight = {
let out = outgoing.lock().await;
out.iter().any(|o| o.accepted_once && !o.sent)
};
if let Some(idle) = conn.detect_stall(&active, in_flight) {
if conn.link_alive(&active).await {
let _ = tx.send(Ev::TransferStalled(active, idle));
} else {
conn.note_progress(&active);
}
}
}
conn.tick_upgrade_prober().await;
let Some(ev) = ev else { continue };
match ev {
Ev::Welcome(v) => {
conn.my_id = v["id"].as_str().unwrap_or_default().to_string();
conn.reprobe_on_network_event();
if let Some(peers) = v["peers"].as_array() {
for p in peers {
conn.maybe_adopt(p, code_used).await?;
}
}
sess.invalidate();
}
Ev::Synced(v) => { sess.on_synced(&v); }
Ev::PairOk(v) => {
let ttl = v["ttl"].as_u64().unwrap_or(600);
let full = format!("{send_words}-{send_nameplate}");
let site = if server == DEFAULT_SERVER { "https://filament.autumated.com".to_string() } else { server.to_string() };
ui::clipboard(&full);
ui::say("");
ui::say(&format!(" code {} {}", ui::paint(ui::Tone::Brand, &full), ui::paint(ui::Tone::Dim, "(copied to clipboard)")));
ui::say(&format!(" {}", ui::paint(ui::Tone::Dim, &format!("terminal: filament recv {full} browser: {} (RECEIVE WITH CODE)", ui::link(&site, &site.replace("https://", ""))))));
ui::say(&format!(" {}", ui::paint(ui::Tone::Dim, &format!("one claim · expires in {} min · authenticated end-to-end (no key crosses the server)", ttl / 60))));
ui::say("");
}
Ev::PairCode(_v) => {
bail!("this server returned a legacy transfer code and can't authenticate the transfer. Update the server (or the peer) to transfer securely.");
}
Ev::PairError(v) => {
if use_code && v["error"].as_str() == Some("taken") {
if word.is_none() {
send_words = crate::pake::words::mint_words();
}
send_nameplate = crate::pake::words::mint_nameplate();
if let Some(cer) = send_cer.as_mut() {
cer.restart(&send_words, &send_nameplate);
}
sio.emit("pair-create", json!({ "nameplate": send_nameplate, "v": 2 })).await.ok();
continue;
}
bail!("pairing failed: {}", v["error"].as_str().unwrap_or("?"));
}
Ev::PairUsed(_) => {
ui::say("code claimed, connecting...");
code_used = true;
}
Ev::PeerJoined(v) => {
conn.maybe_adopt(&v, code_used).await?;
}
Ev::KnownPeer(v) => {
if is_self_uid(&conn.my_uid, v["uid"].as_str()) {
continue; }
if let Some((n, sec)) = &known_target {
if v["channel"].as_str() == Some(channel_of(sec).as_str()) {
let pid = v["id"].as_str().unwrap_or_default().to_string();
let link_alive = conn.link(&pid)
.and_then(|l| l.transport.as_ref())
.map(|t| !t.is_dead())
.unwrap_or(false);
if link_alive {
if !saw_known_peer.contains(n) {
saw_known_peer.insert(n.clone());
}
continue;
}
if !saw_known_peer.contains(n) {
ui::say(&format!("known device '{n}' is online, connecting"));
}
saw_known_peer.insert(n.clone());
let (n, sec) = (n.clone(), sec.clone());
conn.start_direct(&pid, &n, &sec).await;
conn.maybe_adopt(&v, true).await?;
if let Some(l) = conn.link_mut(&pid) {
l.expected_secret = Some((n.clone(), sec.clone()));
}
}
}
}
Ev::Signal(v) => {
let from = v["from"].as_str().unwrap_or_default().to_string();
let data = v["data"].clone();
if data["type"].as_str() == Some("transport-offer") {
let cands: Vec<String> = data["addrs"]
.as_array()
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
.unwrap_or_default();
let srflx = data["srflx"].as_str().map(String::from);
if data["probe"].as_bool() == Some(true) {
conn.answer_upgrade_probe(&from).await;
}
conn.on_transport_offer(&from, cands, srflx);
continue;
}
if matches!(data["type"].as_str(), Some("pake-msg") | Some("pake-confirm")) {
if let Some(cer) = send_cer.as_mut() {
pake_peer.get_or_insert(from.clone());
let fps = match conn.link(&from) {
Some(l) => match &l.peer { Some(p) => p.fingerprints().await, None => None },
None => None,
};
let fp_ref = fps.as_ref().map(|(a, b)| (a.as_str(), b.as_str()));
match cer.on_signal(&data, fp_ref) {
PakeInbound::Consumed => {
if cer.secret().is_some() && !pake_done {
pake_done = true;
ui::say(&ui::paint(ui::Tone::Dim, " authenticated, sending"));
if let (Some(name), Some(t)) = (&remember, conn.transport_of(&from)) {
let sec = fresh_secret();
let _ = t.send_control(&json!({ "type": "pair-keep", "secret": sec })).await;
devices_store(name, &sec)?;
ui::say(&format!("remembered this device as '{name}' (they must also pass --remember)"));
}
if let Some(t) = conn.transport_of(&from) {
let _ = tx.send(Ev::ChannelReady(from.clone(), t));
}
}
}
PakeInbound::Abort(why) => {
ui::clear_sticky();
bail!("transfer REFUSED: {why}. Nothing was sent; ask for a FRESH code.");
}
PakeInbound::Ignored => {}
}
continue;
}
}
conn.ensure_responder(&from, &data).await?;
conn.apply_signal(&from, data).await;
}
Ev::DirectReady(pid, t, route) => {
let tkey = conn.direct_pending.get(&pid)
.map(|p| direct::transport_key(&p.secret.1));
conn.adopt_direct(&pid, t.clone(), route);
if let Some(k) = tkey {
conn.spawn_direct_workers(&pid, &t, k);
}
let _ = tx.send(Ev::ChannelReady(pid, t));
}
Ev::DirectWorkersReady(pid, workers) => {
if let Some(link) = conn.link_mut(&pid) {
link.workers = workers;
crate::ui::debug(&format!("worker transports ready: {pid} {} workers", link.workers.len()));
}
}
Ev::DirectUpgradeReady(pid, t, route) => {
conn.stash_upgrade_standby(&pid, t, route);
}
Ev::ChannelReady(pid, t) => {
if let Some(l) = conn.link_mut(&pid) {
l.transport = Some(t.clone());
l.presence = Presence::Ready;
}
if !conn.is_active(&pid) {
continue;
}
established = true;
waiting.store(false, std::sync::atomic::Ordering::Relaxed);
if let Some(l) = conn.link(&pid) {
ui::say(&format!(" {} {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), ui::paint(ui::Tone::Bold, l.shown())));
let is_direct = l.direct;
let direct_route = l.direct_route;
if let Some(p) = l.peer.clone() {
tokio::spawn(async move {
for _ in 0..6 {
tokio::time::sleep(Duration::from_millis(400)).await;
if let Some(r) = p.route().await {
ui::debug(&format!(" {}", ui::paint(ui::Tone::Dim, &format!("route: {r}"))));
if r == "relayed" {
ui::critical(&format!(" {}", relay_banner()));
}
break;
}
}
});
} else if is_direct {
ui::debug(&format!(" {}", ui::paint(ui::Tone::Dim, &format!("route: {direct_route}"))));
}
if use_code && !is_direct && !pake_done {
pake_peer.get_or_insert(pid.clone());
pake_deadline.get_or_insert_with(|| Instant::now() + pake_budget);
ui::say(&ui::paint(ui::Tone::Dim, " authenticating..."));
continue; }
if is_direct {
} else if let Some((_n, sec)) = &l.expected_secret {
if let Some((my_fp, their_fp)) = match &l.peer { Some(p) => p.fingerprints().await, None => None } {
t.send_control(&json!({
"type": "pair-proof",
"mac": proof_for(sec, &conn.my_uid, &conn.my_uid, l.uid.as_deref().unwrap_or(""), &my_fp, &their_fp),
})).await?;
} else {
ui::say(&ui::paint(ui::Tone::Warn, "no DTLS fingerprints available, skipping identity proof"));
}
}
for o in outgoing.lock().await.iter() {
if o.done {
continue;
}
let offer = protocol::offer_msg(
&o.id, o.sid, &o.name, o.size,
o.head.as_deref(), o.full.as_deref(), o.accepted_once,
);
t.send_control(&offer).await?;
}
}
}
Ev::Control(pid, v) => match v["type"].as_str() {
Some("worker-ports") => {
let pid = v["for"].as_str().unwrap_or_default();
eprintln!("[T:SERVE] worker-ports handler: looking up key={pid}");
if let Some(tx) = conn.worker_port_tx.remove(pid) {
eprintln!("[T:SERVE] worker-ports handler: FOUND key={pid}");
let ports: Vec<u16> = v["ports"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|p| p.as_u64().map(|x| x as u16))
.collect()
})
.unwrap_or_default();
let _ = tx.send(ports);
}
}
_ if !conn.is_active(&pid) => {}
Some("brb") => {
let ttl = v["ttl"].as_u64().unwrap_or(120).min(300);
conn.rejoin.away = Some((pid.clone(), Instant::now() + Duration::from_secs(ttl)));
let n = conn.link_presence(&pid, Presence::Away);
ui::say(&conn.roster(&pid, "●", ui::Tone::Warn, "away, holding the line", &n));
}
Some("back") => {
let was_away = conn.is_away(&pid);
conn.note_alive(&pid);
if was_away {
let n = conn.link_presence(&pid, Presence::Ready);
ui::say(&conn.roster(&pid, ui::glyph_ok(), ui::Tone::Ok, "back", &n));
}
}
Some("state") => {
let was_away = conn.is_away(&pid);
conn.note_alive(&pid); if was_away {
let n = conn.link_presence(&pid, Presence::Ready);
ui::say(&conn.roster(&pid, ui::glyph_ok(), ui::Tone::Ok, "back", &n));
}
if let Some(obj) = v["transfers"].as_object() {
let mut out = outgoing.lock().await;
for o in out.iter_mut() {
if let Some(b) = obj.get(&o.id).and_then(|x| x.as_u64()) {
if o.done && b < o.size {
o.done = false; ui::debug(&ui::paint(ui::Tone::Warn, &format!(" state-diverged: {}, peer holds {b}/{}; re-offering", o.name, o.size)));
if let Some(t) = conn.transport_of(&pid) {
let offer = protocol::offer_msg(
&o.id, o.sid, &o.name, o.size,
o.head.as_deref(), o.full.as_deref(), true,
);
let _ = t.send_control(&offer).await;
}
}
}
}
}
if v["trusted"].as_bool() == Some(false) && !reproved.contains(&pid) {
let proof = match conn.link(&pid) {
Some(l) => match &l.expected_secret {
Some((_n, sec)) => (match &l.peer { Some(p) => p.fingerprints().await, None => None })
.map(|(my_fp, their_fp)| proof_for(sec, &conn.my_uid, &conn.my_uid, l.uid.as_deref().unwrap_or(""), &my_fp, &their_fp)),
None => None,
},
None => None,
};
if let Some(mac) = proof {
if let Some(t) = conn.transport_of(&pid) {
let _ = t.send_control(&json!({ "type": "pair-proof", "mac": mac })).await;
reproved.insert(pid.clone());
ui::debug(&ui::paint(ui::Tone::Dim, " state-diverged: re-proving identity"));
}
}
}
}
Some("pair-keep-ack") => {
if let Some(name) = &remember {
let n = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
if v["ok"].as_bool() == Some(false) {
devices_remove(name)?;
ui::say(&conn.roster(&pid, ui::glyph_err(), ui::Tone::Warn, "declined to be remembered, nothing stored", &n));
} else {
ui::say(&conn.roster(&pid, ui::glyph_ok(), ui::Tone::Ok, "mutually remembered, you'll reconnect automatically", &n));
}
}
}
Some("pair-proof-ack") => {
if v["ok"].as_bool() == Some(false) {
let n = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
if let Some(l) = conn.link_mut(&pid) {
l.expected_secret = None;
}
ui::say(&conn.roster(&pid, ui::glyph_err(), ui::Tone::Warn, "doesn't recognize this device, re-pair with --remember", &n));
}
}
Some("file-accept") => {
let Some(t) = conn.transport() else { continue };
let workers = conn.link(&pid)
.map(|l| l.workers.clone())
.unwrap_or_default();
let mut transports = vec![t];
transports.extend(workers);
let offset = v["offset"].as_u64().unwrap_or(0);
let id = v["id"].as_str().unwrap_or_default().to_string();
{
let mut out = outgoing.lock().await;
if let Some(o) = out.iter_mut().find(|o| o.id == id) {
o.accepted_once = true;
}
}
let out = outgoing.clone();
let chunk = transports[0].max_payload();
let tx2 = tx.clone();
let active_sid = conn.active.clone();
tokio::spawn(async move {
match stream_one(out, transports, id.clone(), offset, chunk, active_sid, tx2.clone()).await {
Ok(()) => {
let _ = tx2.send(Ev::TransferDone(id));
}
Err(e) => {
let _ = tx2.send(Ev::TransferFailed { id, err: e.to_string() });
}
}
});
}
Some("file-decline") => {
let id = v["id"].as_str().unwrap_or_default();
let mut out = outgoing.lock().await;
if let Some(o) = out.iter_mut().find(|o| o.id == id) {
ui::say(&format!("declined: {}", o.name));
o.done = true;
}
}
Some("delivery-ack") => {
let id = v["id"].as_str().unwrap_or_default();
let mut out = outgoing.lock().await;
if let Some(o) = out.iter_mut().find(|o| o.id == id) {
if !o.acked {
o.acked = true;
o.done = true;
ui::say(&ui::paint(ui::Tone::Dim, &format!(" {} delivered + verified (whole-file sha256 matched)", o.name)));
}
}
}
_ => {}
},
Ev::TransferFailed { id, err } => {
let out = outgoing.lock().await;
let name = out.iter().find(|o| o.id == id).map(|o| o.name.as_str()).unwrap_or("?");
ui::debug(&format!("{name}: interrupted ({err}), will resume on reconnect"));
}
Ev::TransferStalled(pid, idle_ms) => {
if !conn.is_active(&pid) {
continue; }
ui::debug(&ui::paint(ui::Tone::Warn, &format!(" stall detected: {idle_ms}ms with no data, correcting")));
match conn.correct_stall(&pid).await {
Rung::Resume => {
if conn.transport_of(&pid).is_none() {
ui::debug(&format!(" resume skipped: no live transport for {pid}, awaiting next repair cycle"));
} else if let Some(t) = conn.transport_of(&pid) {
let out = outgoing.lock().await;
for o in out.iter().filter(|o| o.accepted_once && !o.done) {
let offer = protocol::offer_msg(
&o.id, o.sid, &o.name, o.size,
o.head.as_deref(), o.full.as_deref(), true,
);
let _ = t.send_control(&offer).await;
}
}
}
Rung::Repaired => {}
Rung::Relayed => {}
Rung::Exhausted => {
let _ = tokio::time::timeout(Duration::from_secs(2), sio.disconnect()).await;
if relay_forbidden() {
bail!("couldn't establish a direct path and relay is disabled (--no-relay), partial kept; re-run to resume, or drop --no-relay");
}
bail!("transfer stalled and no usable path remains, partial kept; re-run to resume");
}
}
}
Ev::Interrupted => {
ui::say(&format!(" {} interrupted, the receiver keeps its partial; re-run the same command to resume", ui::paint(ui::Tone::Warn, "!")));
let _ = sio.disconnect().await;
std::process::exit(130);
}
Ev::Stuck(pid, generation) => {
if !established {
stuck_while_connecting += 1;
if stuck_while_connecting >= 2 {
maybe_hint_local_wedge(&mut wedge_hint_shown);
}
}
if conn.on_stuck(&pid, generation, "stuck while connecting").await? {
bail!("lost the receiving peer after {} attempts; the partial is kept, re-run the same `filament send` to resume", MAX_ATTEMPTS);
}
}
Ev::GraceExpired(pid, generation) => {
if conn.on_stuck(&pid, generation, "lost").await? {
bail!("lost the receiving peer after {} attempts; the partial is kept, re-run the same `filament send` to resume", MAX_ATTEMPTS);
}
}
Ev::PcState(pid, s) => conn.on_pc_state(&pid, &s).await,
Ev::PeerLeft(v) => {
let gone = v["id"].as_str().and_then(|p| conn.link(p)).map(|l| l.name.clone());
if conn.on_peer_left(&v) {
let all_done = outgoing.lock().await.iter().all(|o| o.done);
if !all_done {
let secs = REJOIN_WINDOW.as_secs();
let gid = v["id"].as_str().unwrap_or_default();
match gone {
Some(n) => ui::say(&conn.roster(gid, "○", ui::Tone::Dim, &format!("disconnected, waiting up to {secs}s"), &n)),
None => ui::debug(&format!("peer disconnected, waiting up to {secs}s for them to come back")),
}
}
}
}
_ => {}
}
{
let all_sent;
let all_acked;
let mut do_flush = false;
let mut do_reprobe = false;
let mut give_up = false;
{
let out = outgoing.lock().await;
all_sent = !out.is_empty() && out.iter().all(|o| o.sent);
all_acked = !out.is_empty() && out.iter().all(|o| o.done);
if all_sent && !all_acked {
if sent_all_at.is_none() {
sent_all_at = Some(Instant::now());
do_flush = true;
}
let window_elapsed = sent_all_at.map(|t| t.elapsed() >= ack_wait).unwrap_or(false);
let reprobe_elapsed = reprobed_at.map(|t| t.elapsed() >= ack_reprobe).unwrap_or(false);
if (!ack_reprobed && window_elapsed) || (ack_reprobed && reprobe_elapsed) {
let link_alive = conn.transport().is_some();
match protocol::decide_ack_fallback(link_alive, ack_reprobed) {
protocol::AckFallback::Reprobe => do_reprobe = true,
protocol::AckFallback::FailUnconfirmed => give_up = true,
}
}
}
}
if do_flush {
if let Some(t) = conn.transport() {
let _ = t.flush().await;
}
}
if do_reprobe {
ack_reprobed = true;
reprobed_at = Some(Instant::now());
let pending: Vec<(String, u32, String)> = {
let out = outgoing.lock().await;
out.iter().filter(|o| !o.done).map(|o| (o.id.clone(), o.sid, o.name.clone())).collect()
};
if let Some(t) = conn.transport() {
for (id, sid, name) in &pending {
ui::debug(&format!(" {name}: no delivery-ack yet, re-probing (re-sending file-end)"));
let _ = t.send_control(&protocol::end_msg(id, *sid)).await;
}
let _ = t.flush().await;
}
}
if give_up {
let names: Vec<String> = {
let out = outgoing.lock().await;
out.iter().filter(|o| !o.done).map(|o| o.name.clone()).collect()
};
for name in &names {
ui::critical(&ui::paint(ui::Tone::Warn, &format!(
" {name}: delivery not confirmed (no whole-file delivery-ack), the receiver may have gotten nothing; NOT marking complete"
)));
}
let _ = sio.disconnect().await;
bail!(
"delivery not confirmed: {} file(s) sent but never delivery-acked by the receiver (treating as unconfirmed, not delivered)",
names.len().max(1)
);
}
}
{
let out = outgoing.lock().await;
if !out.is_empty() && out.iter().all(|o| o.done) {
if let Some(t) = conn.transport() {
if let Err(e) = t.drain_finish().await {
ui::critical(&ui::paint(ui::Tone::Warn, &format!("warning: transfer may be incomplete, {e}")));
}
}
for o in out.iter().filter(|o| o.temp) {
let _ = std::fs::remove_file(&o.path);
}
ui::say("done.");
tokio::time::sleep(Duration::from_millis(300)).await;
let _ = sio.disconnect().await;
return Ok(());
}
}
}
}
async fn stream_one(
outgoing: Arc<tokio::sync::Mutex<Vec<Outgoing>>>,
transports: Vec<Arc<dyn Transport>>,
id: String,
offset: u64,
chunk: usize,
active_sid: Option<String>,
tx: mpsc::UnboundedSender<Ev>,
) -> Result<()> {
let (sid, name, size, path) = {
let out = outgoing.lock().await;
let o = out.iter().find(|o| o.id == id).ok_or_else(|| anyhow!("unknown transfer {id}"))?;
(o.sid, o.name.clone(), o.size, o.path.clone())
};
if offset > 0 {
ui::debug(&format!("{name}: resuming at {} ({:.0}%)", human(offset), offset as f64 / size.max(1) as f64 * 100.0));
}
use tokio::io::{AsyncSeekExt, AsyncReadExt};
let inject_at: Option<u64> = test_hooks::inject_peer_left_at();
let num = transports.len().max(1);
let bar = std::sync::Arc::new(tokio::sync::Mutex::new(ui::Progress::new(&name, size)));
let progress = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(offset));
let injected = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let remaining = size.saturating_sub(offset);
let per = remaining / num as u64;
let mut handles = Vec::with_capacity(num);
for i in 0..num {
let start = offset + i as u64 * per;
let end = if i == num - 1 { size } else { start + per };
if start >= end {
continue;
}
let t = transports[i].clone();
let path = path.clone();
let bar = bar.clone();
let progress = progress.clone();
let injected = injected.clone();
let tx = tx.clone();
let active_sid = active_sid.clone();
handles.push(tokio::spawn(async move {
let mut f = tokio::fs::File::open(&path).await?;
f.seek(SeekFrom::Start(start)).await?;
let mut pos = start;
let mut buf = vec![0u8; chunk];
while pos < end {
let want = std::cmp::min(chunk as u64, end - pos) as usize;
let n = f.read(&mut buf[..want]).await?;
if n == 0 {
break;
}
t.send_frame(sid, pos, &buf[..n]).await?;
pos += n as u64;
let total =
progress.fetch_add(n as u64, std::sync::atomic::Ordering::Relaxed) + n as u64;
bar.lock().await.tick(total);
if let (Some(at), Some(asid)) = (inject_at, active_sid.as_ref()) {
if total >= at
&& !injected.swap(true, std::sync::atomic::Ordering::SeqCst)
{
eprintln!("[test] injecting synthetic peer-left for active sid at {total} bytes");
let _ = tx.send(Ev::PeerLeft(json!({ "id": asid })));
}
}
}
t.flush().await?;
Ok::<(), anyhow::Error>(())
}));
}
for h in handles {
h.await.map_err(|e| anyhow!("stream task join: {e}"))??;
}
transports[0].send_control(&protocol::end_msg(&id, sid)).await?;
for t in &transports {
t.flush().await?;
}
bar.lock().await.done(size.saturating_sub(offset));
let mut out = outgoing.lock().await;
if let Some(o) = out.iter_mut().find(|o| o.id == id) {
o.sent = true;
if o.full.is_none() {
o.acked = true;
o.done = true;
}
}
Ok(())
}
struct IncomingFile {
id: String,
name: String,
size: u64,
received: Arc<AtomicU64>,
ranges: Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
file: Arc<std::fs::File>,
part_path: PathBuf,
full: Option<String>,
inflight: Arc<AtomicI64>,
end_seen: Arc<AtomicBool>,
ack_sid: u32,
last_tick: u64,
bar: ui::Progress,
}
fn record_range(ranges: &mut Vec<(u64, u64)>, pos: u64, len: usize) -> u64 {
let end = pos + len as u64;
let mut i = 0;
while i < ranges.len() && ranges[i].1 < pos {
i += 1;
}
let mut new_s = pos;
let mut new_e = end;
while i < ranges.len() && ranges[i].0 <= end {
let (s, e) = ranges.remove(i);
new_s = new_s.min(s);
new_e = new_e.max(e);
}
ranges.insert(i, (new_s, new_e));
ranges.iter().map(|(s, e)| e - s).sum()
}
fn shell_policy_from_settings() -> ShellPolicy {
if settings::get_bool("shell", None) {
return ShellPolicy::All;
}
let peers = settings::peers_with("shell", "on");
if peers.is_empty() {
ShellPolicy::Granted
} else {
ShellPolicy::Only(peers.into_iter().collect())
}
}
#[allow(clippy::too_many_arguments)]
async fn apply_reconfigure(
key: &str,
dir: &mut PathBuf,
shell_policy: &mut ShellPolicy,
shell_user: &mut Option<String>,
l2_enabled: bool,
sess: &mut session::Session,
sio: &rust_socketio::asynchronous::Client,
my_uid: &str,
) -> bool {
match key {
"drop-dir" => {
let nd = settings::get_str("drop-dir", None)
.map(PathBuf::from)
.unwrap_or_else(default_drop_dir);
let _ = std::fs::create_dir_all(&nd);
*dir = nd;
true
}
"shell-user" => {
*shell_user = settings::get_str("shell-user", None);
true
}
"auto-extract" => true,
"shell" => {
*shell_policy = shell_policy_from_settings();
l2_enabled || !shell_policy.enables_l2()
}
"name" => {
if let Some(room) = sess.room.clone() {
sess.emit(sio, "join", json!({ "room": room, "name": display_name(), "uid": my_uid })).await;
}
true
}
_ => false,
}
}
#[allow(clippy::too_many_arguments)]
async fn recv_cmd(
server: &str,
mut code: Option<String>,
mut dir: PathBuf,
yes: bool,
room: Option<String>,
to: Option<String>,
keep_open: bool,
relay: bool,
remember: Option<String>,
daemon: bool,
output: Option<String>,
mut shell_policy: ShellPolicy,
mut shell_user: Option<String>,
no_proxy_fallback: bool,
) -> Result<()> {
let to_stdout = output.as_deref() == Some("-");
if !daemon && code.is_none() && interactive_allowed() {
ui::say(&ui::paint(
ui::Tone::Dim,
" enter a code to connect to a specific person, or press enter to use the local network",
));
match codeentry::run(" recv · code ", codeentry::Mode::Claim, "", "")? {
codeentry::Outcome::Submitted(c) => code = Some(c),
codeentry::Outcome::Empty => { }
codeentry::Outcome::Cancelled => return Err(cancelled()),
}
}
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
let my_uid = mk_uid("r");
let (tx, mut rx) = mpsc::unbounded_channel::<Ev>();
let mut sio = net::connect_signaling(server, tx.clone()).await?;
let mut paired = code.is_some();
let mut claim_in_flight = false;
let mut ceremony: Option<bool> = None;
let mut ceremony_pid: Option<String> = None;
let mut ceremony_secret = fresh_secret();
let mut question_shown = Instant::now();
let mut devices = devices_load(); let mut sess = session::Session::new(&display_name(), &my_uid);
sess.channels = devices.iter().map(|(_, s)| channel_of(s)).collect();
let recv_code_path = code.is_some();
let recv_pake_template: Option<(String, String)>; let mut recv_cers: HashMap<String, Ceremony> = HashMap::new();
let mut recv_deadlines: HashMap<String, Instant> = HashMap::new();
let mut recv_pake_peer: Option<String> = None;
let mut recv_pending_offers: HashMap<String, Value> = HashMap::new();
let mut recv_pake_done = code.is_none(); let recv_pake_budget = Duration::from_secs(
std::env::var("FILAMENT_PAIR_GRACE_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(60),
);
let mut recv_pake_overall_deadline: Option<Instant> = None;
const RECV_MAX_CANDIDATES: usize = 8;
match &code {
Some(c) => {
let normalized = crate::pake::norm_code(c);
let (np, pw) = crate::pake::split_code(&normalized);
if pw.is_empty() || np.is_empty() {
bail!("that code doesn't look right, expected something like brave-otter-371");
}
recv_pake_template = Some((pw.clone(), np.clone()));
sio.emit("pair-claim", json!({ "nameplate": np, "v": 2 })).await.ok();
}
None if daemon => {
recv_pake_template = None; let solo = format!("up-{}", fresh_secret());
sess.room = Some(solo.clone());
sess.emit(&sio, "join", json!({ "room": solo, "name": display_name(), "uid": my_uid })).await;
if devices.is_empty() && !std::io::stdin().is_terminal() && !shell_policy.enables_l2() {
bail!("no known devices, run `filament pair` once, or `filament up` in a terminal to pair interactively");
}
let chans: Vec<String> = devices.iter().map(|(_, s)| channel_of(s)).collect();
sess.emit(&sio, "subscribe", json!({ "channels": chans })).await;
ui::say(&format!(
" {} filament up, {} known device{} {} {}",
ui::paint(ui::Tone::Brand, "●"),
devices.len(),
if devices.len() == 1 { "" } else { "s" },
ui::glyph_arrow(),
ui::paint(ui::Tone::Bold, &dir.display().to_string()),
));
ui::say(&ui::paint(ui::Tone::Dim, " trusted devices only · invisible to strangers · Ctrl-C or `filament down` to stop"));
if std::io::stdin().is_terminal() {
ui::say(&ui::paint(
ui::Tone::Dim,
" type a code to pair a new device · `pair` mints one · `devices` · `forget <name>`",
));
}
}
None => {
recv_pake_template = None; let room = match &room {
Some(r) => r.clone(),
None => net::fetch_auto_room(server).await?,
};
ui::say(&format!(
" {} listening, same-network devices appear automatically {}",
ui::paint(ui::Tone::Brand, "●"),
ui::paint(ui::Tone::Dim, &format!("(room {room} · dir {})", dir.display())),
));
ui::say(&ui::paint(
ui::Tone::Dim,
" have a code? just type it here (like brave-otter-123) and press Enter",
));
sess.room = Some(room.clone());
sess.emit(&sio, "join", json!({ "room": room, "name": display_name(), "uid": my_uid })).await;
if !devices.is_empty() {
let chans: Vec<String> = devices.iter().map(|(_, s)| channel_of(s)).collect();
ui::say(&format!("watching for {} known device(s)", devices.len()));
sess.emit(&sio, "subscribe", json!({ "channels": chans })).await;
}
}
}
let l2_enabled = shell_policy.enables_l2()
|| std::env::var("FILAMENT_L2").map(|v| v == "1").unwrap_or(false)
|| any_shell_grant();
let mut conn = Conn::for_command(
server,
sio.clone(),
tx.clone(),
my_uid.clone(),
relay, to, daemon, direct_ok_for(daemon, l2_enabled),
);
if daemon {
conn.load_warm_peers_config();
}
{
let tx = tx.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
shutdown::arm_force_exit(130, shutdown::grace());
let _ = tx.send(Ev::Interrupted);
});
}
#[cfg(unix)]
{
let tx = tx.clone();
tokio::spawn(async move {
if let Ok(mut term) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
term.recv().await;
shutdown::arm_force_exit(130, shutdown::grace());
let _ = tx.send(Ev::Interrupted);
}
});
}
let question_open = Arc::new(std::sync::atomic::AtomicBool::new(false));
let interactive = !daemon || std::io::stdin().is_terminal();
let tty_guard = if interactive && std::io::stdin().is_terminal() { Some(TtyGuard::raw()) } else { None };
if interactive {
let tx = tx.clone();
let q = question_open.clone();
tokio::spawn(async move {
use tokio::io::AsyncReadExt;
let mut stdin = tokio::io::stdin();
let mut buf = [0u8; 1];
let mut line = String::new();
while stdin.read(&mut buf).await.map(|n| n == 1).unwrap_or(false) {
let c = buf[0] as char;
if q.load(std::sync::atomic::Ordering::Relaxed) && "yYnN".contains(c) && line.is_empty() {
ui::answer_echo(c); let _ = tx.send(Ev::StdinLine(c.to_lowercase().to_string()));
continue;
}
match buf[0] {
b'\n' | b'\r' => {
eprintln!();
let _ = tx.send(Ev::StdinLine(line.trim().to_string()));
line.clear();
}
0x7f | 0x08 => {
if line.pop().is_some() {
eprint!("\x08 \x08");
}
}
_ if !c.is_control() => {
eprint!("{c}");
line.push(c);
}
_ => {}
}
}
});
}
#[cfg(l3)]
let mut l3_seen: HashMap<String, overlay::Announce> = HashMap::new();
#[cfg(l3)]
let l3: Option<std::sync::Arc<l3::L3>> = if daemon {
match settings::get_str("tun-addr", None) {
Some(setting) => {
let (cidr, identity) = if setting == "auto" {
match overlay::Identity::load_or_create() {
Ok(id) => (format!("{}/128", id.addr()), Some(id)),
Err(e) => {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" L3 disabled: {e}")));
(String::new(), None)
}
}
} else {
(setting.clone(), None) };
if cidr.is_empty() {
None
} else {
let mode = match settings::get_str("l3-mode", None).as_deref() {
Some("kernel") => l3::L3Mode::Kernel,
Some("userspace") => l3::L3Mode::Userspace,
_ => l3::L3Mode::Auto,
};
match l3::L3::start(&cidr, 1280, identity, mode) {
Ok(m) => {
let addr = m.my_addr().map(|a| a.to_string()).unwrap_or(cidr);
if m.is_userspace() {
ui::say(&format!(
" {} L3 overlay {} (userspace, zero privilege)",
ui::paint(ui::Tone::Brand, "●"),
addr
));
ui::say(&ui::paint(ui::Tone::Warn,
" host firewall/nftables are NOT enforced here; only mesh membership + the expose allowlist gate access"));
ui::say(" native tools reach <peer>.mesh via `filament proxy` / `filament dial` (no kernel route in userspace)");
let auto_proxy = settings::get_bool("auto-proxy", None)
&& !no_proxy_fallback;
if auto_proxy {
let server = server.to_string();
tokio::spawn(async move {
if let Err(e) = l2::proxy_cmd(&server, "127.0.0.1", 1080, 0, relay).await {
let msg = e.to_string();
if !msg.contains("already in use") {
ui::debug(&format!("auto-proxy: {e}"));
}
}
});
ui::say(&format!(
" {} started SOCKS5 proxy on 127.0.0.1:1080 (set your tools' proxy to this)",
ui::paint(ui::Tone::Ok, ui::glyph_ok())
));
ui::say(&format!(
" e.g. curl --socks5-hostname 127.0.0.1:1080 http://<peer>.mesh:8080/"
));
}
} else {
let v4 = m.my_addr_v4().map(|a| format!(" / {a}")).unwrap_or_default();
ui::say(&format!(
" {} L3 overlay {}{} on filament0",
ui::paint(ui::Tone::Brand, "●"),
addr,
v4
));
let my_name = l3::hostname();
ui::say(&format!(
" this machine resolves as {}{}",
l3::sanitize_host(&my_name),
".mesh"
));
}
if let Some(id) = m.identity_ref() {
let my_name = config_get("name").unwrap_or_else(|| l3::hostname());
let v6 = id.addr();
let v4 = Some(id.addr_v4());
m.names_insert("__self__", &l3::sanitize_host(&my_name), v6, v4).await;
if !m.is_userspace() {
m.refresh_hosts().await;
}
if settings::get_bool("sshd-overlay", None) {
let v6_str = id.addr().to_string();
let v4_str = id.addr_v4().to_string();
if let Err(e) = sshd::configure_sshd_overlay(&v6_str, &v4_str) {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" sshd-overlay: {e}")));
}
}
}
Some(m)
}
Err(e) => {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" L3 disabled: {e}")));
None
}
}
}
}
None => None,
}
} else {
None
};
#[cfg(l3)]
let exposer: Option<std::sync::Arc<expose::Exposer>> = match l3.as_ref() {
Some(m) => {
let userspace_opt_in = std::env::var("FILAMENT_L3_USERSPACE").as_deref() == Ok("1")
|| settings::get_str("l3-mode", None).as_deref() == Some("userspace");
if m.is_userspace() && !userspace_opt_in {
if !expose::load().is_empty() {
ui::say(&ui::paint(ui::Tone::Warn,
" expose.json NOT honored: L3 fell back to userspace (host firewall is bypassed there)."));
ui::say(" opt in with `filament up --userspace` or `filament set l3-mode userspace` to expose in userspace mode");
}
let ex = expose::Exposer::new(m.clone());
Some(ex) } else {
let ex = expose::Exposer::new(m.clone());
let n = ex.reconcile().await;
if n > 0 {
ui::say(&format!(
" {} exposing {} port{} on the overlay",
ui::paint(ui::Tone::Brand, "●"),
n,
if n == 1 { "" } else { "s" }
));
}
Some(ex)
}
}
None => None,
};
let mut by_sid: HashMap<(String, u32), IncomingFile> = HashMap::new();
let mut verify_fails: HashMap<String, u32> = HashMap::new();
let mut pending: std::collections::VecDeque<(String, Value)> = Default::default();
let mut completed = 0usize;
let mut last_quiet: Option<Instant> = None;
let quiet_window = quiet_exit_window();
let mut digest_absent: HashMap<String, u8> = HashMap::new();
let mut digest_alone = false;
let mut last_state_ping = Instant::now();
let mut l2_muxes: HashMap<String, Arc<l2::Mux>> = HashMap::new();
let warm_ptys: WarmPtys = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
#[cfg(unix)]
let mut pending_bootstrap: PendingBootstraps = HashMap::new();
let (ctl_tx, mut ctl_rx) = mpsc::unbounded_channel::<ctl::Req>();
#[cfg(unix)]
{
if daemon_alive() == Some(std::process::id()) {
let ctl_tx = ctl_tx.clone();
tokio::spawn(async move {
if let Err(e) = ctl::serve(ctl_tx).await {
crate::ui::trace(&format!("filament: control socket disabled: {e}"));
}
});
}
}
#[cfg(not(unix))]
let _ = &ctl_tx;
let pty_sessions = l2::PtySessions::new();
let mut pty_bindings: HashMap<String, HashMap<u32, String>> = HashMap::new();
let mut stuck_while_connecting = 0u32;
let mut wedge_hint_shown = false;
let _saw_known_peer: HashSet<String> = HashSet::new();
let mut ever_received = false;
let mut known_channels: std::collections::HashSet<String> =
devices.iter().map(|(_, s)| channel_of(s)).collect();
let mut last_devices_scan = Instant::now();
let mut daemon_mounts = DaemonMounts {
entries: HashMap::new(),
children: HashMap::new(),
};
let mut last_mount_check = Instant::now();
let mut last_warm_hold_tick = Instant::now();
let signaling_self_heal = daemon && !test_hooks::no_signaling_reconnect();
let mut last_signaling = Instant::now();
let mut signaling_down_since: Option<Instant> = None;
let mut reconnect_attempt: u32 = 0;
let mut last_reconnect_try = Instant::now();
let mut probed_silence = false; let mut last_watchdog = Instant::now();
let mut last_link_health = Instant::now();
if daemon {
sdnotify::ready();
sdnotify::status("up - serving");
}
if let Err(e) = mount::restore_mounts(server, relay).await {
crate::ui::say(&format!("warning: failed to restore mounts: {e}"));
}
loop {
if daemon && last_watchdog.elapsed() >= Duration::from_secs(5) {
sdnotify::watchdog();
last_watchdog = Instant::now();
}
if test_hooks::wedge_loop_on_shutdown() && !conn.links.is_empty() {
std::future::pending::<()>().await;
}
let ev = tokio::select! {
biased;
req = ctl_rx.recv() => {
if let Some(req) = req {
#[cfg(unix)]
{
if let ctl::ReqKind::Reconfigure { key } = &req.kind {
let key = key.clone();
let live = apply_reconfigure(
&key, &mut dir, &mut shell_policy, &mut shell_user,
l2_enabled, &mut sess, &sio, &my_uid,
).await;
req.reply(&json!({ "ok": true, "live": live })).await;
} else if matches!(&req.kind, ctl::ReqKind::ReloadExpose) {
let (live, count): (bool, usize) = {
#[cfg(l3)]
{
match exposer.as_ref() {
Some(ex) => (true, ex.reconcile().await),
None => (false, 0),
}
}
#[cfg(not(l3))]
{
(false, 0)
}
};
req.reply(&json!({ "ok": true, "live": live, "count": count })).await;
} else if matches!(&req.kind, ctl::ReqKind::Reload) {
let supervised = std::env::var("INVOCATION_ID").is_ok();
req.reply(&json!({ "ok": true, "reloading": supervised })).await;
if supervised {
ui::say("filament: reloading onto the updated binary (graceful restart)");
#[cfg(unix)]
unsafe { libc::raise(libc::SIGTERM); }
}
} else if matches!(&req.kind, ctl::ReqKind::Dial { .. }) {
#[cfg(l3)]
if let ctl::ReqKind::Dial { peer, port } = &req.kind {
let (peer, port) = (peer.clone(), *port);
match l3.as_ref() {
Some(m) => {
let m = m.clone();
tokio::spawn(async move {
let Some(addr) = m.addr_of(&peer).await else {
req.reject("unknown overlay peer").await;
return;
};
match m.dial(addr, port).await {
Ok(mut stream) => {
let mut sock = req.accept().await;
let _ = tokio::io::copy_bidirectional(&mut sock, &mut stream).await;
}
Err(e) => req.reject(&format!("overlay dial failed: {e}")).await,
}
});
}
None => req.reject("L3 overlay is not up").await,
}
}
#[cfg(not(l3))]
req.reject("L3 overlay not supported on this build").await;
} else if matches!(&req.kind, ctl::ReqKind::Bootstrap { .. }) {
handle_warm_bootstrap(&conn, &mut pending_bootstrap, req).await;
} else if matches!(&req.kind, ctl::ReqKind::Mount { .. }) {
handle_mount(req, &server, relay, &mut daemon_mounts, &mut last_mount_check).await;
} else if matches!(&req.kind, ctl::ReqKind::Unmount { .. }) {
handle_unmount(req, &mut daemon_mounts).await;
} else if matches!(&req.kind, ctl::ReqKind::ListMounts) {
handle_list_mounts(req, &daemon_mounts).await;
} else if matches!(&req.kind, ctl::ReqKind::MountHealth { .. }) {
handle_mount_health(req, &daemon_mounts).await;
} else {
if let ctl::ReqKind::Pty { peer, .. } = &req.kind {
conn.note_warm_use(peer);
}
handle_warm_req(&conn, &mut l2_muxes, &warm_ptys, &tx, req).await;
}
}
#[cfg(not(unix))]
handle_warm_req(&conn, &mut l2_muxes, &warm_ptys, &tx, req).await;
}
None
}
res = tokio::time::timeout(
Duration::from_secs(2),
next_ev(&mut rx, &conn, !pending.is_empty()),
) => match res {
Ok(res) => res?,
Err(_) => None, },
};
sess.tick(&sio).await;
if recv_code_path && !recv_pake_done {
let pids: Vec<String> = recv_cers.keys().cloned().collect();
for pid in pids {
let send_msg = recv_cers.get_mut(&pid).and_then(|c| c.take_msg_payload());
if let Some(data) = send_msg {
sio.emit("signal", json!({ "to": pid, "data": data })).await.ok();
}
let has_k = recv_cers.get(&pid).map(|c| c.has_k()).unwrap_or(false);
if has_k {
let fps = match conn.link(&pid) {
Some(l) => match &l.peer { Some(p) => p.fingerprints().await, None => None },
None => None,
};
if let Some((my_fp, their_fp)) = fps {
let conf = recv_cers
.get_mut(&pid)
.and_then(|c| c.take_confirm_payload(&my_fp, &their_fp));
if let Some(data) = conf {
sio.emit("signal", json!({ "to": pid, "data": data })).await.ok();
}
}
}
}
}
if recv_code_path && !recv_pake_done {
let now = Instant::now();
let expired: Vec<String> = recv_deadlines
.iter()
.filter(|(_, dl)| now > **dl)
.map(|(pid, _)| pid.clone())
.collect();
for pid in expired {
recv_deadlines.remove(&pid);
recv_cers.remove(&pid);
recv_pending_offers.remove(&pid);
ui::debug(&format!("recv: candidate {pid} did not authenticate in budget, dropped"));
}
}
if recv_code_path && !recv_pake_done {
if let Some(dl) = recv_pake_overall_deadline {
if Instant::now() > dl {
bail!("the other device uses an older version and can't send securely over a code. Update it (or this CLI) so the transfer runs the encrypted handshake. Nothing was received.");
}
}
}
if signaling_self_heal {
let mut saw_down = false;
match &ev {
Some(Ev::SignalingDown(_)) => saw_down = true,
Some(
Ev::Welcome(_) | Ev::Synced(_) | Ev::SignalingAlive | Ev::PeerJoined(_)
| Ev::PeerLeft(_) | Ev::Signal(_) | Ev::KnownPeer(_) | Ev::KnownPeerLeft(_)
| Ev::PairMatched(_) | Ev::PairOk(_) | Ev::PairCode(_) | Ev::PairUsed(_)
| Ev::PairError(_),
) => {
last_signaling = Instant::now();
signaling_down_since = None;
probed_silence = false;
reconnect_attempt = 0;
}
_ => {}
}
let silence = net::signaling_silence_ms();
let silent_ms = last_signaling.elapsed().as_millis() as u64;
if signaling_down_since.is_none() {
if saw_down {
signaling_down_since = Some(Instant::now());
last_reconnect_try = Instant::now() - Duration::from_secs(60); ui::say(&ui::paint(ui::Tone::Warn, "signaling link closed, reconnecting..."));
sdnotify::status("signaling down - reconnecting");
} else if silent_ms >= silence {
if !probed_silence {
probed_silence = true;
net::heartbeat(&sio, sess.heartbeat_payload(), tx.clone()).await;
} else if silent_ms >= silence.saturating_mul(2) {
signaling_down_since = Some(Instant::now());
last_reconnect_try = Instant::now() - Duration::from_secs(60);
ui::say(&ui::paint(ui::Tone::Warn, &format!("signaling silent for {silent_ms}ms, reconnecting...")));
sdnotify::status("signaling silent - reconnecting");
}
}
}
if let Some(down_at) = signaling_down_since {
let base = 500u64.saturating_mul(1 << reconnect_attempt.min(4)).min(8_000);
let jitter = (down_at.elapsed().as_nanos() as u64 % (base / 2 + 1)) as i64 - (base as i64 / 4);
let backoff = Duration::from_millis((base as i64 + jitter).max(100) as u64);
if last_reconnect_try.elapsed() >= backoff {
last_reconnect_try = Instant::now();
reconnect_attempt = reconnect_attempt.saturating_add(1);
let _ = sio.disconnect().await; match net::reconnect_signaling(server, tx.clone()).await {
Ok(new_sio) => {
sio = new_sio;
conn.sio = sio.clone();
sess.invalidate();
if let Some(room) = sess.room.clone() {
sess.emit(&sio, "join", json!({ "room": room, "name": display_name(), "uid": my_uid })).await;
}
if !sess.channels.is_empty() {
let chans = sess.channels.clone();
sess.emit(&sio, "subscribe", json!({ "channels": chans })).await;
}
sess.tick(&sio).await;
last_signaling = Instant::now();
signaling_down_since = None;
probed_silence = false;
ui::say(&ui::paint(ui::Tone::Ok, "signaling reconnected, re-announcing presence"));
sdnotify::status("up - serving");
}
Err(e) => {
ui::debug(&ui::paint(ui::Tone::Dim, &format!(" signaling reconnect failed ({e}), retrying with backoff")));
}
}
}
}
}
if daemon && last_link_health.elapsed() >= Duration::from_secs(8) {
last_link_health = Instant::now();
let dead: Vec<String> = conn
.links
.iter()
.filter(|(_, l)| l.transport.as_ref().is_some_and(|t| !t.is_alive()))
.map(|(pid, _)| pid.clone())
.collect();
for pid in dead {
ui::debug(&format!("filament: link to '{pid}' died, dropping so it can re-connect"));
conn.drop_link(&pid);
l2_muxes.remove(&pid);
}
}
if daemon && last_mount_check.elapsed() >= Duration::from_secs(30) {
last_mount_check = Instant::now();
let dead_locals: Vec<String> = daemon_mounts.entries.iter()
.filter_map(|(local, _entry)| {
let is_alive = mount::is_mount_point(local);
let path_exists = Path::new(local).exists();
if !path_exists {
Some(local.clone())
} else if !is_alive {
Some(local.clone())
} else {
None
}
})
.collect();
for local in dead_locals {
if let Some(entry) = daemon_mounts.entries.remove(&local) {
ui::say(&format!("mount {local} is gone, removing from daemon tracking (was {}:{})", entry.peer, entry.remote));
if let Some(mut child) = daemon_mounts.children.remove(&local) {
let _ = child.kill().await;
}
let _ = mount::remove_mount(&local);
}
}
}
if daemon && last_warm_hold_tick.elapsed() >= Duration::from_secs(10) {
last_warm_hold_tick = Instant::now();
let auto_warm = l3.is_some() || settings::get_bool("auto-warm", None);
let _ = conn.warm_hold_tick(auto_warm).await;
}
if daemon && last_devices_scan.elapsed() >= Duration::from_secs(2) {
last_devices_scan = Instant::now();
let mut new_chans: Vec<String> = Vec::new();
for (n, s) in devices_load() {
let ch = channel_of(&s);
if known_channels.insert(ch.clone()) {
ui::say(&format!("new device '{n}' paired, now reachable"));
devices.push((n, s));
new_chans.push(ch);
}
}
if !new_chans.is_empty() {
for ch in &new_chans {
if !sess.channels.contains(ch) {
sess.channels.push(ch.clone());
}
}
sess.emit(&sio, "subscribe", json!({ "channels": new_chans })).await;
}
}
conn.reap_deferred();
#[cfg(unix)]
reap_warm_bootstraps(&mut pending_bootstrap);
for (pid, info, (n, sec)) in conn.expired_direct() {
conn.maybe_adopt(&info, true).await?;
if let Some(l) = conn.link_mut(&pid) {
l.expected_secret = Some((n, sec));
}
}
if last_state_ping.elapsed() >= Duration::from_secs(10) {
last_state_ping = Instant::now();
for (pid, l) in &conn.links {
if let Some(t) = &l.transport {
let mut transfers = serde_json::Map::new();
for ((p0, _), inc) in &by_sid {
if p0 == pid {
transfers.insert(inc.id.clone(), json!(inc.received.load(Ordering::Relaxed)));
}
}
let _ = tokio::time::timeout(
Duration::from_secs(2),
t.send_control(&json!({
"type": "state", "v": 1,
"transfers": Value::Object(transfers),
"trusted": l.trusted,
"away": false,
})),
)
.await;
}
}
}
sweep_completed_streams(&mut by_sid, &conn, &dir, &output, to_stdout, daemon, &mut completed).await?;
conn.recv_done = protocol::recv_transfer_done(completed, keep_open, by_sid.is_empty());
if completed > 0 {
let warm_peers: Vec<String> = by_sid.keys()
.filter_map(|(pid, _)| conn.links.get(pid)?.verified_name.clone())
.collect();
for name in &warm_peers {
conn.note_warm_use(name);
}
}
if completed > 0 || !by_sid.is_empty() {
ever_received = true; }
{
let all_pids: Vec<String> = conn.links.keys().cloned().collect();
for pid in all_pids {
let in_flight = by_sid.keys().any(|(p, _)| *p == pid);
if let Some(idle) = conn.detect_stall(&pid, in_flight) {
if conn.link_alive(&pid).await {
let _ = tx.send(Ev::TransferStalled(pid, idle));
} else {
conn.note_progress(&pid);
}
}
}
}
conn.tick_upgrade_prober().await;
if conn.recv_done && test_hooks::churn_after_complete() {
let churn: Vec<(String, u32)> = conn
.links
.iter()
.map(|(pid, l)| (pid.clone(), l.generation))
.collect();
for (pid, generation) in churn {
if let Some(l) = conn.links.get_mut(&pid) {
l.attempts = 0; if let Some(p) = &l.peer {
p.close().await;
}
}
let _ = conn.tx.send(Ev::Stuck(pid, generation));
}
}
if completed > 0 && !keep_open && by_sid.is_empty() && pending.is_empty()
&& !conn.links.is_empty() && conn.only_deferred_links()
{
ui::say(&format!("done ({completed} file{}).", if completed == 1 { "" } else { "s" }));
let _ = sio.disconnect().await;
return Ok(());
}
if completed > 0 && !keep_open && by_sid.is_empty() && pending.is_empty()
&& conn.links.is_empty()
{
conn.rejoin.waiting_rejoin = None;
ui::clear_sticky();
ui::say(&format!("done ({completed} file{}).", if completed == 1 { "" } else { "s" }));
let _ = sio.disconnect().await;
return Ok(());
}
let digest_says_alone = digest_alone && conn.links.values().all(|l| l.expected_secret.is_none());
let no_healthy_link = conn.links.values().all(|l| !matches!(l.presence, Presence::Ready));
let links_clear = if !test_hooks::disable_modeb_drop() {
no_healthy_link
} else {
conn.links.is_empty() || digest_says_alone
};
if completed > 0 && !keep_open && by_sid.is_empty() && pending.is_empty() && links_clear {
match last_quiet {
None => last_quiet = Some(Instant::now()),
Some(since) if since.elapsed() > quiet_window => {
ui::say(&ui::paint(ui::Tone::Dim, " (peer-left never arrived, exiting on quiet)"));
ui::say(&format!("done ({completed} file{}).", if completed == 1 { "" } else { "s" }));
let _ = sio.disconnect().await;
return Ok(());
}
Some(_) => {}
}
} else {
last_quiet = None;
}
let Some(ev) = ev else { continue };
if !pending.is_empty() {
let front_id = pending.front().map(|(_, v)| v["id"].clone());
pending.retain(|(p, _)| conn.links.contains_key(p));
if pending.front().map(|(_, v)| v["id"].clone()) != front_id {
ui::clear_sticky();
if let Some((qpid, qv)) = pending.front() {
let s = conn.link(qpid).map(|l| l.name.clone()).unwrap_or_default();
{
let q = offer_question(&s, qv["name"].as_str().unwrap_or("file"), qv["size"].as_u64().unwrap_or(0), paired);
ui::say(&q); ui::sticky(&q);
question_shown = Instant::now();
}
} else {
question_open.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
}
match ev {
Ev::DropLink(pid) => {
if conn.links.contains_key(&pid) {
ui::debug(&format!("filament: dropping zombie warm link to '{pid}' (black-holed a stream)"));
conn.drop_link(&pid);
l2_muxes.remove(&pid);
#[cfg(l3)]
l3_seen.remove(&pid);
}
}
Ev::PairMatched(v) => {
claim_in_flight = false;
let room = v["room"].as_str().unwrap_or_default().to_string();
ui::say(&format!(" {} code accepted, joining sender", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
sess.room = Some(room.clone()); sess.touch();
sess.emit(&sio, "join", json!({ "room": room, "name": display_name(), "uid": my_uid })).await;
}
Ev::Synced(v) => {
if let Some(peers) = sess.on_synced(&v) {
digest_alone = peers.is_empty();
let present: std::collections::HashSet<String> = peers
.iter()
.filter_map(|p| p["id"].as_str().map(String::from))
.collect();
for p in &peers {
let id = p["id"].as_str().unwrap_or_default();
if !id.is_empty() && !conn.links.contains_key(id) {
ui::debug(&ui::paint(ui::Tone::Dim, " (digest: adopting a peer we never heard join)"));
conn.maybe_adopt(p, true).await?;
}
}
let mut gone: Vec<String> = Vec::new();
for (pid, l) in &conn.links {
if l.expected_secret.is_none() && !present.contains(pid) {
let c = digest_absent.entry(pid.clone()).or_insert(0);
*c += 1;
if *c >= 2 {
gone.push(pid.clone());
}
} else {
digest_absent.remove(pid);
}
}
for pid in gone {
digest_absent.remove(&pid);
let name = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
conn.drop_link(&pid);
ui::say(&conn.roster(&pid, "○", ui::Tone::Dim, "left (digest reconcile), still listening", &name));
}
}
}
Ev::PairCode(v) => {
let c = v["code"].as_str().unwrap_or("?");
ui::clipboard(c);
ui::say("");
ui::say(&format!(" {}", ui::paint(ui::Tone::Brand, &c.to_uppercase())));
ui::say("");
ui::say(&ui::paint(ui::Tone::Dim, " say it aloud, they type it in the web app or `filament pair <code>` · one claim · 10 min"));
}
Ev::PairUsed(_) => {
ui::say(&ui::paint(ui::Tone::Dim, " code claimed, connecting..."));
}
Ev::PairError(v) => {
let why = v["error"].as_str().unwrap_or("?").to_string();
let hint = match v["why"].as_str() {
Some("sender-gone") => "the sender who made that code already left, ask them for a fresh one".to_string(),
_ => format!("{why}, codes burn after one use and expire after 10 min"),
};
if code.is_some() && conn.links.is_empty() && completed == 0 {
bail!("code rejected: {hint}");
}
paired = false;
claim_in_flight = false;
ui::say(&format!(
" {} code rejected: {hint}; still listening",
ui::paint(ui::Tone::Err, ui::glyph_err()),
));
}
Ev::Welcome(v) => {
conn.my_id = v["id"].as_str().unwrap_or_default().to_string();
conn.reprobe_on_network_event();
if let Some(peers) = v["peers"].as_array() {
for p in peers {
conn.maybe_adopt(p, true).await?;
}
}
sess.invalidate();
}
Ev::KnownPeer(v) => {
if is_self_uid(&conn.my_uid, v["uid"].as_str()) {
continue; }
if let Some((n, sec)) = devices.iter().find(|(_, s)| channel_of(s) == v["channel"].as_str().unwrap_or("")) {
let pid = v["id"].as_str().unwrap_or_default().to_string();
let auto_warm = l3.is_some() || settings::get_bool("auto-warm", None);
if auto_warm {
conn.warm_hold.auto.insert(n.clone());
}
if conn.warm_hold.should_connect(n) {
conn.warm_hold.resume(n);
last_warm_hold_tick = Instant::now()
.checked_sub(Duration::from_secs(10))
.unwrap_or_else(Instant::now);
}
let fresh = !conn.links.contains_key(&pid)
&& !conn.direct_pending.contains_key(&pid);
let link_dead = conn.links.get(&pid)
.and_then(|l| l.transport.as_ref())
.map(|t| t.is_dead())
.unwrap_or(false);
if fresh || link_dead {
ui::say(&format!("known device '{n}' appeared, connecting"));
devices_touch(n, None, None); } else {
ui::trace(&format!("known device '{n}' re-announced (link already up)"));
}
let (n, sec) = (n.clone(), sec.clone());
conn.start_direct(&pid, &n, &sec).await;
conn.maybe_adopt(&v, true).await?;
if let Some(l) = conn.link_mut(&pid) {
l.expected_secret = Some((n.clone(), sec.clone()));
}
}
}
Ev::PeerJoined(v) => {
let had_partials = !by_sid.is_empty();
if conn.maybe_adopt(&v, true).await? && had_partials {
flush_inflight(&mut by_sid).await;
}
}
Ev::Signal(v) => {
let from = v["from"].as_str().unwrap_or_default().to_string();
let data = v["data"].clone();
if data["type"].as_str() == Some("transport-offer") {
let cands: Vec<String> = data["addrs"]
.as_array()
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
.unwrap_or_default();
let srflx = data["srflx"].as_str().map(String::from);
if data["probe"].as_bool() == Some(true) {
conn.answer_upgrade_probe(&from).await;
}
conn.on_transport_offer(&from, cands, srflx);
continue;
}
if recv_code_path
&& matches!(data["type"].as_str(), Some("pake-msg") | Some("pake-confirm"))
{
if recv_pake_done {
continue;
}
if !recv_cers.contains_key(&from) {
if recv_cers.len() >= RECV_MAX_CANDIDATES {
ui::debug("recv: candidate cap reached, ignoring extra peer's PAKE");
continue;
}
if let Some((pw, np)) = &recv_pake_template {
recv_cers.insert(from.clone(), Ceremony::new(pw, np, pair_v2_caps()));
recv_deadlines
.entry(from.clone())
.or_insert_with(|| Instant::now() + recv_pake_budget);
recv_pake_overall_deadline
.get_or_insert_with(|| Instant::now() + recv_pake_budget);
}
}
let fps = match conn.link(&from) {
Some(l) => match &l.peer { Some(p) => p.fingerprints().await, None => None },
None => None,
};
let fp_ref = fps.as_ref().map(|(a, b)| (a.as_str(), b.as_str()));
if let Some(cer) = recv_cers.get_mut(&from) {
match cer.on_signal(&data, fp_ref) {
PakeInbound::Consumed => {
if cer.secret().is_some() {
recv_pake_peer = Some(from.clone());
recv_pake_done = true;
recv_cers.clear();
recv_deadlines.clear();
ui::say(&ui::paint(ui::Tone::Dim, " authenticated, receiving"));
let buffered = recv_pending_offers.remove(&from);
recv_pending_offers.clear();
if let Some(offer) = buffered {
let _ = tx.send(Ev::Control(from.clone(), offer));
}
}
}
PakeInbound::Abort(why) => {
recv_cers.remove(&from);
recv_deadlines.remove(&from);
recv_pending_offers.remove(&from);
ui::debug(&format!("recv: candidate {from} refused ({why}), dropped"));
}
PakeInbound::Ignored => {}
}
}
continue;
}
conn.ensure_responder(&from, &data).await?;
conn.apply_signal(&from, data).await;
}
Ev::DirectReady(pid, t, route) => {
let tkey = conn.direct_pending.get(&pid)
.map(|p| direct::transport_key(&p.secret.1));
conn.adopt_direct(&pid, t.clone(), route);
if let Some(k) = tkey {
conn.spawn_direct_workers(&pid, &t, k);
}
let _ = tx.send(Ev::ChannelReady(pid, t));
}
Ev::DirectWorkersReady(pid, workers) => {
if let Some(link) = conn.link_mut(&pid) {
link.workers = workers;
crate::ui::debug(&format!("worker transports ready: {pid} {} workers", link.workers.len()));
}
}
Ev::DirectUpgradeReady(pid, t, route) => {
conn.stash_upgrade_standby(&pid, t, route);
}
Ev::ChannelReady(pid, t) => {
let _ = t.send_control(&json!({ "type": "caps", "shell": l2_enabled })).await;
#[cfg(l3)]
if let Some(l3) = l3.as_ref() {
if let Some(cb) = t.channel_binding() {
if let Some(ann) = l3.make_announce(&cb) {
let _ = t.send_control(&ann.to_json()).await;
}
if let Some(pending) = l3_seen.get(&pid) {
if let Ok(ip) = pending.verify(&cb) {
let who = conn.link(&pid).map(|l| l.shown()).unwrap_or_default();
let v4 = pending.addr_v4();
l3.add_peer(&pid, &who, ip.into(), Some(v4.into()), t.clone()).await;
devices_touch(&who, Some(ip), Some(v4));
}
}
}
}
if let Some(l) = conn.link_mut(&pid) {
ui::say(&format!(" {} {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), ui::paint(ui::Tone::Bold, l.shown())));
l.transport = Some(t.clone());
l.presence = Presence::Ready;
let is_direct = l.direct;
let direct_route = l.direct_route;
if let Some(p) = l.peer.clone() {
tokio::spawn(async move {
for _ in 0..6 {
tokio::time::sleep(Duration::from_millis(400)).await;
if let Some(r) = p.route().await {
ui::debug(&format!(" {}", ui::paint(ui::Tone::Dim, &format!("route: {r}"))));
if r == "relayed" {
ui::critical(&format!(" {}", relay_banner()));
}
break;
}
}
});
} else if is_direct {
ui::debug(&format!(" {}", ui::paint(ui::Tone::Dim, &format!("route: {direct_route}"))));
}
}
let proof_creds = conn.link(&pid).and_then(|l| {
if l.direct {
return None;
}
let (_n, sec) = l.expected_secret.clone()?;
Some((sec, l.uid.clone().unwrap_or_default(), l.peer.clone()))
});
if let Some((sec, uid, peer)) = proof_creds {
if let Some((my_fp, their_fp)) = match peer {
Some(p) => p.fingerprints().await,
None => None,
} {
let mac = proof_for(&sec, &conn.my_uid, &conn.my_uid, &uid, &my_fp, &their_fp);
let _ = t.send_control(&json!({ "type": "pair-proof", "mac": mac })).await;
}
}
if recv_code_path && !recv_pake_done {
let (is_direct, is_trusted) = conn
.link(&pid)
.map(|l| (l.direct, l.trusted))
.unwrap_or((false, false));
if is_direct && is_trusted {
recv_pake_peer = Some(pid.clone());
recv_pake_done = true; recv_cers.clear();
recv_deadlines.clear();
let buffered = recv_pending_offers.remove(&pid);
recv_pending_offers.clear();
if let Some(offer) = buffered {
let _ = tx.send(Ev::Control(pid.clone(), offer));
}
} else if recv_cers.contains_key(&pid) {
recv_deadlines
.entry(pid.clone())
.or_insert_with(|| Instant::now() + recv_pake_budget);
recv_pake_overall_deadline
.get_or_insert_with(|| Instant::now() + recv_pake_budget);
} else if recv_cers.len() < RECV_MAX_CANDIDATES {
if let Some((pw, np)) = &recv_pake_template {
recv_cers.insert(pid.clone(), Ceremony::new(pw, np, pair_v2_caps()));
recv_deadlines
.insert(pid.clone(), Instant::now() + recv_pake_budget);
recv_pake_overall_deadline
.get_or_insert_with(|| Instant::now() + recv_pake_budget);
ui::say(&ui::paint(ui::Tone::Dim, " authenticating..."));
}
}
}
let fresh_link = conn.link(&pid).map(|l| l.expected_secret.is_none()).unwrap_or(false);
if fresh_link {
match ceremony {
Some(true) => {
ceremony = None;
ceremony_pid = Some(pid.clone());
t.send_control(&json!({ "type": "pair-keep", "secret": ceremony_secret })).await.ok();
}
Some(false) => {
let tx = tx.clone();
let pid = pid.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(3)).await;
let _ = tx.send(Ev::Control(pid, json!({ "type": "__pair_fallback" })));
});
}
None => {}
}
}
}
Ev::Control(pid, v) => match v["type"].as_str() {
#[cfg(l3)]
Some("l3-announce") if l3.is_some() => {
match overlay::Announce::from_json(&v) {
Ok(ann) => {
l3_seen.insert(pid.clone(), ann.clone());
if let Some(l3) = l3.as_ref() {
match conn.transport_of(&pid).and_then(|t| t.channel_binding().map(|cb| (t, cb))) {
Some((t, cb)) => match ann.verify(&cb) {
Ok(ip) => {
let who = conn.link(&pid).map(|l| l.shown()).unwrap_or_default();
let v4 = ann.addr_v4();
l3.add_peer(&pid, &who, ip.into(), Some(v4.into()), t).await;
ui::say(&format!(" {} L3 peer {who}.mesh ({ip} / {v4})", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
devices_touch(&who, Some(ip), Some(v4));
}
Err(e) => ui::debug(&ui::paint(ui::Tone::Warn, &format!(" L3 announce rejected: {e}"))),
},
None => {}
}
}
}
Err(e) => ui::debug(&format!(" L3 malformed announce: {e}")),
}
}
Some("worker-ports") => {
let pid = v["for"].as_str().unwrap_or_default();
eprintln!("[T:CLI] worker-ports handler: looking up key={pid}");
if let Some(tx) = conn.worker_port_tx.remove(pid) {
eprintln!("[T:CLI] worker-ports handler: FOUND key={pid}");
let ports: Vec<u16> = v["ports"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|p| p.as_u64().map(|x| x as u16))
.collect()
})
.unwrap_or_default();
let _ = tx.send(ports);
}
}
_ if !conn.links.contains_key(&pid) => {}
Some("l2-open-ack") if l2_enabled => {
if let Some(sid) = v["sid"].as_u64() {
if let Some(mux) = l2_muxes.get(&pid) {
mux.on_open_ack(sid as u32).await;
}
}
}
Some("l2-open") | Some("l2-close") if l2_enabled => {
let Some(t) = conn.transport_of(&pid) else { continue };
let trusted = conn.link(&pid).map(|l| l.trusted).unwrap_or(false);
let authorized = v["type"].as_str() != Some("l2-open") || {
let blanket = shell_policy.enables_l2()
|| std::env::var("FILAMENT_L2").map(|x| x == "1").unwrap_or(false);
let peer_has_shell = conn
.link(&pid)
.and_then(|l| l.verified_name.as_deref())
.map(|n| device_allows(n, "shell"))
.unwrap_or(false);
l2_open_allowed(blanket, peer_has_shell)
};
if !authorized {
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
ui::say(&format!("l2: refused stream {sid:#x}: device not granted shell"));
let _ = t
.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "not authorized: device lacks shell grant" }))
.await;
} else {
let allow_nonloopback = {
let host = v["host"].as_str().unwrap_or("127.0.0.1");
let port = v["rport"].as_u64().or_else(|| v["port"].as_u64()).unwrap_or(0) as u16;
let name = conn
.link(&pid)
.and_then(|l| l.verified_name.clone())
.unwrap_or_default();
l2_target_allowed(&name, host, port)
};
let mux = l2_muxes
.entry(pid.clone())
.or_insert_with(|| l2::Mux::new(t.clone()))
.clone();
match mux.accept_control(&v, trusted, allow_nonloopback).await {
l2::OpenVerdict::Accept { sid, host, port, rx } => {
tokio::spawn(mux.clone().dial_and_serve(sid, host, port, rx));
}
l2::OpenVerdict::Deny { sid, err } => {
ui::say(&format!("l2: refused stream {sid:#x}: {err}"));
let _ = t
.send_control(&json!({ "type": "l2-close", "sid": sid, "err": err }))
.await;
}
l2::OpenVerdict::Ignore => {}
}
}
}
#[cfg(unix)]
Some("shell-bootstrap-ack") => {
let reply = json!({
"ok": true,
"hostkeys": v["hostkeys"].clone(),
"user": v["user"].clone(),
"sshd": v["sshd"].clone(),
});
complete_warm_bootstrap(&mut pending_bootstrap, &pid, &reply).await;
}
#[cfg(unix)]
Some("shell-bootstrap-deny") => {
let reply = json!({
"ok": false,
"err": v["reason"].as_str().unwrap_or("shell bootstrap denied"),
});
complete_warm_bootstrap(&mut pending_bootstrap, &pid, &reply).await;
}
Some("shell-bootstrap") if l2_enabled => {
let Some(t) = conn.transport_of(&pid) else { continue };
let trusted = conn.link(&pid).map(|l| l.trusted).unwrap_or(false);
let dev = conn.link(&pid).and_then(|l| l.verified_name.clone());
let granted = trusted
&& dev
.as_deref()
.map(|n| shell_policy.auto_allows(n) || device_allows(n, "shell"))
.unwrap_or(false);
if !granted {
let who = dev.as_deref().unwrap_or("<unverified>");
ui::say(&format!("l2: shell bootstrap refused: {who} (no shell cap / untrusted)"));
let _ = t
.send_control(&json!({
"type": "shell-bootstrap-deny",
"reason": "shell capability not granted"
}))
.await;
continue;
}
let device = dev.unwrap();
let pubkey = v["pubkey"].as_str().unwrap_or_default().to_string();
let pubkey = match sshkeys::validate_pubkey(&pubkey) {
Ok(k) => k,
Err(e) => {
ui::say(&format!("l2: shell bootstrap refused: malformed pubkey from '{device}': {e}"));
let _ = t
.send_control(&json!({
"type": "shell-bootstrap-deny",
"reason": "malformed pubkey"
}))
.await;
continue;
}
};
match sshkeys::install_authorized_key(&device, &pubkey) {
Ok(()) => {
let hostkeys = sshkeys::host_pubkeys();
let login = std::env::var("USER").unwrap_or_else(|_| "root".into());
let ssh_port = v["ssh_port"].as_u64().and_then(|n| u16::try_from(n).ok()).unwrap_or(22);
let sshd = sshd_listening(ssh_port).await;
ui::say(&format!("l2: shell granted to '{device}', installed managed key (filament-managed block)"));
let _ = t
.send_control(&json!({
"type": "shell-bootstrap-ack",
"hostkeys": hostkeys,
"user": login,
"sshd": sshd,
"ssh_port": ssh_port
}))
.await;
}
Err(e) => {
ui::say(&format!("l2: shell bootstrap install failed for '{device}': {e}"));
let _ = t
.send_control(&json!({
"type": "shell-bootstrap-deny",
"reason": "install failed"
}))
.await;
}
}
}
Some("pty-open") if l2_enabled => {
let Some(t) = conn.transport_of(&pid) else { continue };
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
if !l2::is_l2_sid(sid) {
continue;
}
let trusted = conn.link(&pid).map(|l| l.trusted).unwrap_or(false);
let dev = conn.link(&pid).and_then(|l| l.verified_name.clone());
let granted = trusted
&& dev
.as_deref()
.map(|n| shell_policy.auto_allows(n) || device_allows(n, "shell"))
.unwrap_or(false);
if !granted {
let who = dev.as_deref().unwrap_or("<unverified>");
ui::say(&format!("l2: pty refused: {who} (no shell cap / untrusted)"));
let _ = t
.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "shell capability not granted" }))
.await;
continue;
}
let cols = v["cols"].as_u64().unwrap_or(80) as u16;
let rows = v["rows"].as_u64().unwrap_or(24) as u16;
let session_id = match v["session"].as_str().filter(|s| !s.is_empty() && s.len() <= 128) {
Some(s) => format!("{}\u{1}{}", dev.as_deref().unwrap_or(&pid), s),
None => format!("{pid}:{sid:#x}"),
};
let term = v["term"]
.as_str()
.filter(|s| !s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_graphic()))
.unwrap_or("xterm-256color")
.to_string();
let pty_cmd = v["cmd"]
.as_str()
.unwrap_or("")
.to_string();
let resume = v["resume"].as_bool().unwrap_or(false);
let mux = l2_muxes
.entry(pid.clone())
.or_insert_with(|| l2::Mux::new(t.clone()))
.clone();
if let Some(sess) = pty_sessions.get_live(&session_id).await {
if mux.at_stream_cap().await {
ui::say("l2: pty reattach refused: too many streams on this link");
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "too many streams" })).await;
continue;
}
let rx = mux.register_stream(sid).await;
let (rtx, rrx) = tokio::sync::mpsc::unbounded_channel::<(u16, u16)>();
mux.register_resizer(sid, rtx).await;
let _ = t.send_control(&json!({ "type": "pty-open-ack", "sid": sid })).await;
sess.attach(t.clone(), sid);
sess.resize(cols, rows);
pty_bindings.entry(pid.clone()).or_default().insert(sid, session_id.clone());
spawn_session_pumps(sess.clone(), rx, rrx);
ui::say(&format!("l2: pty REATTACHED to '{}', {cols}x{rows}", dev.unwrap_or_default()));
continue;
}
if resume {
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "no such session" })).await;
continue;
}
if mux.at_stream_cap().await {
ui::say("l2: pty refused: too many streams on this link");
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "too many streams" })).await;
continue;
}
let Some(pty_guard) = l2::PtyGuard::try_acquire() else {
ui::say(&format!("l2: pty refused: too many PTYs (global cap {})", l2::MAX_PTYS_GLOBAL));
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "too many streams" })).await;
continue;
};
let rx = mux.register_stream(sid).await; let (rtx, rrx) = tokio::sync::mpsc::unbounded_channel::<(u16, u16)>();
mux.register_resizer(sid, rtx).await;
let _ = t.send_control(&json!({ "type": "pty-open-ack", "sid": sid })).await;
let shell_argv = shell_argv(None, shell_user.as_deref());
let host = platform::ShellHost::new(&shell_argv);
let argv = if pty_cmd.is_empty() {
host.interactive_args()
} else {
host.exec_cmd_args(&pty_cmd)
};
match l2::spawn_pty_session(
pty_sessions.clone(),
session_id.clone(),
t.clone(),
sid,
cols,
rows,
&term,
argv,
pty_guard,
)
.await
{
Some(sess) => {
pty_bindings.entry(pid.clone()).or_default().insert(sid, session_id.clone());
spawn_session_pumps(sess, rx, rrx);
ui::say(&format!("l2: pty granted to '{}', {cols}x{rows}", dev.unwrap_or_default()));
}
None => {
mux.drop_pty(sid).await;
}
}
}
Some("mount-open") if l2_enabled => {
let Some(t) = conn.transport_of(&pid) else { continue };
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
if !l2::is_l2_sid(sid) { continue; }
let trusted = conn.link(&pid).map(|l| l.trusted).unwrap_or(false);
if !trusted {
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "mount: untrusted peer" })).await;
continue;
}
let root_encoded = v["root"].as_str().unwrap_or(".");
let root_path = mount_proto::path_decode(root_encoded).unwrap_or_else(|_| std::path::PathBuf::from("."));
let caps = mount_proto::mount_caps_for_root(&root_path);
let mux = l2_muxes.entry(pid.clone())
.or_insert_with(|| l2::Mux::new(t.clone()))
.clone();
if mux.at_stream_cap().await {
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "too many streams" })).await;
continue;
}
let rx = mux.register_stream(sid).await;
let _ = t.send_control(&json!({ "type": "mount-open-ack", "sid": sid, "caps": caps })).await;
let transport = t.clone();
let spawn_sid = sid;
let proto_version = caps.protocol_version;
mount_proto::spawn_mount_server(root_path, transport, spawn_sid, rx, proto_version);
}
Some("pty-resize") if l2_enabled => {
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
let cols = v["cols"].as_u64().unwrap_or(80) as u16;
let rows = v["rows"].as_u64().unwrap_or(24) as u16;
if let Some(sid_map) = pty_bindings.get(&pid) {
if let Some(session_id) = sid_map.get(&sid) {
if let Some(sess) = pty_sessions.get_live(session_id).await {
sess.resize(cols, rows);
}
}
}
if let Some(mux) = l2_muxes.get(&pid) {
mux.resize_pty(sid, cols, rows).await;
}
}
Some("pty-close") if l2_enabled => {
if let Some(session_id) = v["session"].as_str() {
if let Some(sess) = pty_sessions.get_live(session_id).await {
sess.end(); }
pty_sessions.remove(session_id).await;
if let Some(sid_map) = pty_bindings.get_mut(&pid) {
sid_map.retain(|_, v| v != session_id);
}
}
}
Some("mount-cap-ack") if l2_enabled => {}
Some("brb") => {
let ttl = v["ttl"].as_u64().unwrap_or(120).min(300);
conn.rejoin.away = Some((pid.clone(), Instant::now() + Duration::from_secs(ttl)));
let n = conn.link_presence(&pid, Presence::Away);
ui::say(&conn.roster(&pid, "●", ui::Tone::Warn, "away, choosing a file · holding the line", &n));
}
Some("back") => {
let was_away = conn.is_away(&pid);
conn.note_alive(&pid);
if was_away {
let n = conn.link_presence(&pid, Presence::Ready);
ui::say(&conn.roster(&pid, ui::glyph_ok(), ui::Tone::Ok, "back", &n));
}
}
Some("state") => {
let was_away = conn.is_away(&pid);
conn.note_alive(&pid);
if was_away {
let n = conn.link_presence(&pid, Presence::Ready);
ui::say(&conn.roster(&pid, ui::glyph_ok(), ui::Tone::Ok, "back", &n));
}
}
Some("pair-keep") => {
let sec = v["secret"].as_str().unwrap_or_default().to_string();
if sec.len() == 64 {
let kept = if let Some(name) = &remember {
devices_store(name, &sec)?;
ui::say(&format!("remembered this device as '{name}', future sends auto-accept after proof"));
true
} else if ceremony == Some(false) {
ceremony = None;
let n = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_else(|| "device".into());
devices_store(&n, &sec)?;
devices.push((n.clone(), sec.clone()));
sess.channels.push(channel_of(&sec)); sess.touch();
sio.emit("subscribe", json!({ "channels": [channel_of(&sec)] })).await.ok();
ui::say(&format!(
" {} {} mutually remembered, rename anytime: filament devices rename {n} <new>",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ui::paint(ui::Tone::Bold, &n),
));
true
} else {
ui::say("(sender offered to be remembered; re-run with --remember <name> to keep it)");
false
};
if let Some(t) = conn.transport_of(&pid) {
t.send_control(&json!({ "type": "pair-keep-ack", "ok": kept })).await.ok();
}
}
}
Some("__pair_fallback") => {
if ceremony == Some(false) {
ceremony = None;
ceremony_pid = Some(pid.clone());
if let Some(t) = conn.transport_of(&pid) {
t.send_control(&json!({ "type": "pair-keep", "secret": ceremony_secret })).await.ok();
}
}
}
Some("pair-keep-ack") => {
if ceremony_pid.as_deref() == Some(pid.as_str()) {
ceremony_pid = None;
let n = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_else(|| "device".into());
if v["ok"].as_bool() == Some(false) {
ui::say(&conn.roster(&pid, ui::glyph_err(), ui::Tone::Warn, "declined to be remembered, nothing stored", &n));
} else {
devices_store(&n, &ceremony_secret)?;
devices.push((n.clone(), ceremony_secret.clone()));
sess.channels.push(channel_of(&ceremony_secret)); sess.touch();
sio.emit("subscribe", json!({ "channels": [channel_of(&ceremony_secret)] })).await.ok();
ceremony_secret = fresh_secret(); ui::say(&format!(
" {} {} mutually remembered, rename anytime: filament devices rename {n} <new>",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ui::paint(ui::Tone::Bold, &n),
));
}
}
}
Some("pair-proof") => {
let mac = v["mac"].as_str().unwrap_or_default();
let peer_uid = conn.link(&pid).and_then(|l| l.uid.clone()).unwrap_or_default();
let fps = match conn.link(&pid) {
Some(l) => match &l.peer { Some(p) => p.fingerprints().await, None => None },
None => None,
};
let Some((my_fp, their_fp)) = fps else {
ui::debug("pair-proof received before fingerprints known, ignoring");
continue;
};
let hit = if is_self_uid(&conn.my_uid, Some(peer_uid.as_str())) {
ui::debug("pair-proof from our own install, refusing (self-connect)");
None
} else {
devices
.iter()
.find(|(_, s)| proof_for(s, &peer_uid, &peer_uid, &conn.my_uid, &my_fp, &their_fp) == mac)
};
let ok = if let Some((n, _)) = hit {
if let Some(l) = conn.link_mut(&pid) {
l.trusted = true;
l.verified_name = Some(n.clone());
}
ui::say(&format!("identity verified: '{n}' (auto-accepting)"));
true
} else {
ui::critical(&ui::paint(ui::Tone::Warn, "pair-proof FAILED verification, treating peer as untrusted"));
false
};
if let Some(t) = conn.transport_of(&pid) {
t.send_control(&json!({ "type": "pair-proof-ack", "ok": ok })).await.ok();
}
}
Some("pair-intro") => {
let trusted = conn.link(&pid).map(|l| l.trusted).unwrap_or(false);
let iname = v["name"].as_str().unwrap_or_default().to_string();
let isec = v["secret"].as_str().unwrap_or_default().to_string();
let hub = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
if trusted && isec.len() == 64 && !iname.is_empty() {
devices_store(&iname, &isec)?;
devices.push((iname.clone(), isec.clone()));
sess.channels.push(channel_of(&isec)); sess.touch();
sio.emit("subscribe", json!({ "channels": [channel_of(&isec)] })).await.ok();
ui::say(&format!(
" {} introduced to '{}' by {}, now a known device",
ui::paint(ui::Tone::Ok, ui::glyph_ok()), iname, hub
));
} else {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" ignored pair-intro from unverified peer {hub}")));
}
}
Some("file-offer") => {
let Some(t) = conn.transport_of(&pid) else { continue };
if recv_code_path
&& (!recv_pake_done || recv_pake_peer.as_deref() != Some(pid.as_str()))
{
if !recv_pake_done && recv_cers.contains_key(&pid) {
recv_pending_offers.insert(pid.clone(), v.clone());
ui::debug("buffering a pre-auth file-offer until its ceremony confirms");
} else {
ui::debug("ignoring file-offer from a peer that has not authenticated via ephemeral PAKE");
}
continue;
}
let id = v["id"].as_str().unwrap_or_default().to_string();
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
let raw = v["name"].as_str().unwrap_or("file.bin");
let name = Path::new(raw)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "file.bin".into());
let size = v["size"].as_u64().unwrap_or(0);
let offer_head = v["head"].as_str().map(|s| s.to_string());
let offer_full = v["full"].as_str().map(|s| s.to_string());
let is_resume = v["resume"].as_bool().unwrap_or(false);
let part_path = dir.join(format!("{name}.part"));
let meta_path = dir.join(format!("{name}.part.meta"));
let mut offset = 0u64;
let mut prior_full: Option<String> = None;
if part_path.is_file() {
let prior = std::fs::metadata(&part_path).map(|m| m.len()).unwrap_or(0);
match PartMeta::load(&meta_path) {
Some(m) if m.size == size && prior <= size => {
let head_ok = match (&m.head, &offer_head) {
(Some(a), Some(b)) => a == b,
_ => true, };
if head_ok {
offset = prior;
prior_full = m.full;
} else {
ui::debug(&format!("{name}: same name+size but different content, restarting from 0"));
}
}
_ => {}
}
}
let sender_name = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
let link_trusted = conn.link(&pid).map(|l| l.trusted).unwrap_or(false);
let consented = v["__consent"].as_str() == Some(consent_token());
let ok = if daemon {
link_trusted
} else {
yes || consented || link_trusted || (is_resume && offset > 0)
};
if !ok {
if !daemon && std::io::stdin().is_terminal() {
pending.push_back((pid.clone(), v.clone()));
question_open.store(true, std::sync::atomic::Ordering::Relaxed);
if pending.len() == 1 {
let q = offer_question(&sender_name, &name, size, paired);
ui::say(&q);
ui::sticky(&q);
question_shown = Instant::now();
}
continue; }
ui::say(&ui::paint(ui::Tone::Dim, &format!(
" declined {name} from {sender_name} ({})",
if daemon { "unverified peer" } else { "no tty, use -y to auto-accept" }
)));
t.send_control(&protocol::decline_msg(&id)).await?;
continue;
}
let want_part = dir.join(format!("{name}.part"));
if !to_stdout {
let dup_keys: Vec<(String, u32)> = by_sid
.iter()
.filter(|(_, inc)| inc.part_path == want_part)
.map(|(k, _)| k.clone())
.collect();
if !dup_keys.is_empty() {
let any_flowing = dup_keys.iter().any(|(p, _)| {
conn.transport_of(p)
.map(|t| t.idle_ms() < net::stall_ms())
.unwrap_or(false)
});
if any_flowing {
ui::say(&ui::paint(ui::Tone::Dim, &format!(" (duplicate offer for {name} ignored, already receiving it)")));
continue;
}
for k in dup_keys {
if let Some(inc) = by_sid.remove(&k) {
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
}
}
}
}
if to_stdout {
#[cfg(unix)]
let out = tokio::fs::OpenOptions::new().write(true).open("/dev/stdout").await?;
#[cfg(not(unix))]
{
bail!("-o - (stdout streaming) is not supported on this platform yet");
}
#[cfg(unix)]
{
by_sid.insert((pid.clone(), sid), IncomingFile {
id: id.clone(),
name,
size,
received: Arc::new(AtomicU64::new(0)),
ranges: Arc::new(std::sync::Mutex::new(vec![])),
file: Arc::new(out.into_std().await),
part_path: PathBuf::new(),
full: None,
inflight: Arc::new(AtomicI64::new(0)),
end_seen: Arc::new(AtomicBool::new(false)),
ack_sid: 0,
last_tick: 0,
bar: ui::Progress::new("(stdout)", size),
});
t.send_control(&protocol::accept_msg(&id, 0)).await?;
continue;
}
}
let effective_full = offer_full.clone().or(prior_full);
let file = if offset > 0 {
ui::debug(&format!("{name}: resuming at {} ({:.0}%)", human(offset), offset as f64 / size.max(1) as f64 * 100.0));
tokio::fs::OpenOptions::new().write(true).open(&part_path).await?
} else {
PartMeta { size, head: offer_head, full: effective_full.clone() }.store(&meta_path)?;
tokio::fs::File::create(&part_path).await?
};
let bar = ui::Progress::new(&name, size);
let file = Arc::new(file.into_std().await);
let received = Arc::new(AtomicU64::new(offset));
let ranges = Arc::new(std::sync::Mutex::new(
if offset > 0 { vec![(0, offset)] } else { vec![] }
));
by_sid.insert((pid.clone(), sid), IncomingFile {
id: id.clone(),
name,
size,
received,
ranges,
file,
part_path,
full: effective_full,
inflight: Arc::new(AtomicI64::new(0)),
end_seen: Arc::new(AtomicBool::new(false)),
ack_sid: 0,
last_tick: 0,
bar,
});
t.send_control(&protocol::accept_msg(&id, offset)).await?;
}
Some("file-end") => {
if test_hooks::drop_file_end() {
continue;
}
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
let process_now = {
match by_sid.get_mut(&(pid.clone(), sid)) {
None => false, Some(inc) => {
inc.ack_sid = sid;
if inc.inflight.load(Ordering::Relaxed) > 0 {
inc.end_seen.store(true, Ordering::Relaxed);
false } else {
true }
}
}
};
if !process_now { continue; }
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
let mut inc = match by_sid.remove(&(pid.clone(), sid)) {
Some(i) => i,
None => continue,
};
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
if to_stdout {
completed += 1;
continue;
}
let id = inc.id.clone();
if inc.full.is_some() {
let verdict = verify_incoming(&inc).await;
match verdict {
protocol::VerifyResult::Match => {
verify_fails.remove(&id);
let rename_to = if completed == 0 { output.clone() } else { None };
let from = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
let nm = inc.name.clone();
if finalize_incoming(inc, &dir, rename_to.as_deref(), daemon, &from).await? {
completed += 1;
if test_hooks::suppress_delivery_ack() {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" [test] {nm} verified but SUPPRESSING delivery-ack")));
} else if let Some(t) = conn.transport_of(&pid) {
let _ = t.send_control(&protocol::delivery_ack_msg(&id, sid)).await;
let _ = t.flush().await;
ui::say(&ui::paint(ui::Tone::Dim, &format!(" {nm} verified (whole-file sha256 matched), acked")));
}
}
}
protocol::VerifyResult::Mismatch { restart_from_zero } => {
let fails = verify_fails.entry(id.clone()).or_insert(0);
*fails += 1;
if *fails > MAX_VERIFY_FAILS {
ui::critical(&ui::paint(ui::Tone::Err, &format!(
" {}: whole-file checksum still wrong after {MAX_VERIFY_FAILS} re-fetches, refusing to accept a corrupt file (partial kept)",
inc.name
)));
verify_fails.remove(&id);
continue;
}
let mut req_offset = inc.received.load(Ordering::Relaxed);
if restart_from_zero {
let _ = tokio::fs::File::create(&inc.part_path).await;
inc.received.store(0, Ordering::Relaxed);
inc.ranges.lock().unwrap().clear();
req_offset = 0;
ui::debug(&ui::paint(ui::Tone::Warn, &format!(
" {}: received all bytes but whole-file checksum FAILED (corrupt), re-fetching from 0 (attempt {})",
inc.name, *fails
)));
} else {
ui::debug(&ui::paint(ui::Tone::Warn, &format!(
" {}: TRUNCATED ({}/{}), checksum can't match yet; re-requesting the rest (attempt {})",
inc.name, human(req_offset), human(inc.size), *fails
)));
}
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
if req_offset == 0 {
if let Ok(f) = tokio::fs::OpenOptions::new().write(true).open(&inc.part_path).await {
inc.file = Arc::new(f.into_std().await);
}
}
inc.end_seen.store(false, Ordering::Relaxed);
by_sid.insert((pid.clone(), sid), inc);
if let Some(t) = conn.transport_of(&pid) {
let _ = t.send_control(&protocol::accept_msg(&id, req_offset)).await;
}
}
}
} else {
let rename_to = if completed == 0 { output.clone() } else { None };
let from = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
if finalize_incoming(inc, &dir, rename_to.as_deref(), daemon, &from).await? {
completed += 1;
}
}
}
_ => {}
},
Ev::MaybeComplete(pid, sid) => {
let ack_sid = {
by_sid.get(&(pid.clone(), sid)).map(|inc| inc.ack_sid).unwrap_or(0)
};
if let Some(mut inc) = by_sid.remove(&(pid.clone(), sid)) {
if to_stdout {
completed += 1;
continue;
}
let id = inc.id.clone();
if inc.full.is_some() {
let verdict = verify_incoming(&inc).await;
match verdict {
protocol::VerifyResult::Match => {
verify_fails.remove(&id);
let rename_to = if completed == 0 { output.clone() } else { None };
let from = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
let nm = inc.name.clone();
if finalize_incoming(inc, &dir, rename_to.as_deref(), daemon, &from).await? {
completed += 1;
if test_hooks::suppress_delivery_ack() {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" [test] {nm} verified but SUPPRESSING delivery-ack")));
} else if let Some(t) = conn.transport_of(&pid) {
let _ = t.send_control(&protocol::delivery_ack_msg(&id, ack_sid)).await;
ui::say(&ui::paint(ui::Tone::Dim, &format!(" {nm} verified (whole-file sha256 matched), acked")));
}
}
}
protocol::VerifyResult::Mismatch { restart_from_zero } => {
let fails = verify_fails.entry(id.clone()).or_insert(0);
*fails += 1;
if *fails > MAX_VERIFY_FAILS {
ui::critical(&ui::paint(ui::Tone::Err, &format!(
" {}: whole-file checksum still wrong after {MAX_VERIFY_FAILS} re-fetches, refusing to accept a corrupt file (partial kept)",
inc.name
)));
verify_fails.remove(&id);
continue;
}
let mut req_offset = inc.received.load(Ordering::Relaxed);
if restart_from_zero {
let _ = tokio::fs::File::create(&inc.part_path).await;
if let Ok(f) = tokio::fs::OpenOptions::new().write(true).open(&inc.part_path).await {
inc.file = Arc::new(f.into_std().await);
}
inc.received.store(0, Ordering::Relaxed);
inc.ranges.lock().unwrap().clear();
req_offset = 0;
ui::debug(&ui::paint(ui::Tone::Warn, &format!(
" {}: received all bytes but whole-file checksum FAILED (corrupt), re-fetching from 0 (attempt {})",
inc.name, *fails
)));
} else {
ui::debug(&ui::paint(ui::Tone::Warn, &format!(
" {}: TRUNCATED ({}/{}), checksum can't match yet; re-requesting the rest (attempt {})",
inc.name, human(req_offset), human(inc.size), *fails
)));
}
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
inc.end_seen.store(false, Ordering::Relaxed);
by_sid.insert((pid.clone(), sid), inc);
if let Some(t) = conn.transport_of(&pid) {
let _ = t.send_control(&protocol::accept_msg(&id, req_offset)).await;
}
}
}
} else {
let rename_to = if completed == 0 { output.clone() } else { None };
let from = conn.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
if finalize_incoming(inc, &dir, rename_to.as_deref(), daemon, &from).await? {
completed += 1;
}
}
}
}
Ev::Chunk(pid, sid, offset, data) => {
if l2_enabled && l2::is_l2_sid(sid) {
if let Some(mux) = l2_muxes.get(&pid) {
mux.on_frame(sid, data).await;
}
} else if let Some(inc) = by_sid.get_mut(&(pid.clone(), sid)) {
let pos = offset.unwrap_or_else(|| inc.received.load(Ordering::Relaxed));
inc.inflight.fetch_add(1, Ordering::Relaxed);
let file = Arc::clone(&inc.file);
let ranges = offset.map(|_| Arc::clone(&inc.ranges));
let received = Arc::clone(&inc.received);
let inflight = Arc::clone(&inc.inflight);
let end_seen = Arc::clone(&inc.end_seen);
let tx = tx.clone();
let pid = pid.clone();
tokio::task::spawn_blocking(move || {
let _ = pwrite_at(&file, &data, pos);
if let Some(ranges) = ranges {
let total = {
let mut r = ranges.lock().unwrap();
record_range(&mut *r, pos, data.len())
};
received.store(total, Ordering::Relaxed);
} else {
received.fetch_add(data.len() as u64, Ordering::Relaxed);
}
let prev = inflight.fetch_sub(1, Ordering::Relaxed);
if prev == 1 && end_seen.load(Ordering::Relaxed) {
let _ = tx.send(Ev::MaybeComplete(pid, sid));
}
});
let r = inc.received.load(Ordering::Relaxed);
if r != inc.last_tick {
inc.bar.tick(r);
inc.last_tick = r;
}
} else {
ui::debug(&format!(" dropping chunk for unknown sid {sid} from {pid} ({} bytes)", data.len()));
}
}
Ev::StdinLine(line) => {
let ans = line.to_lowercase();
if !pending.is_empty() {
if question_shown.elapsed() < Duration::from_millis(300) {
continue;
}
let mut answered = false;
if ans == "y" || ans == "yes" {
answered = true;
let (qpid, mut qv) = pending.pop_front().unwrap();
ui::clear_sticky();
qv["__consent"] = json!(consent_token());
let _ = tx.send(Ev::Control(qpid, qv)); } else if ans == "n" || ans == "no" {
answered = true;
let (qpid, qv) = pending.pop_front().unwrap();
ui::clear_sticky();
ui::say(&ui::paint(ui::Tone::Dim, &format!(" declined {}", qv["name"].as_str().unwrap_or("file"))));
if let Some(t) = conn.transport_of(&qpid) {
t.send_control(&protocol::decline_msg(qv["id"].as_str().unwrap_or_default())).await?;
}
}
if let Some((qpid, qv)) = pending.front() {
let s = conn.link(qpid).map(|l| l.name.clone()).unwrap_or_default();
let q = offer_question(&s, qv["name"].as_str().unwrap_or("file"), qv["size"].as_u64().unwrap_or(0), paired);
if answered {
ui::say(&q); question_shown = Instant::now();
}
ui::sticky(&q);
} else {
question_open.store(false, std::sync::atomic::Ordering::Relaxed);
}
} else if ans == "devices" {
if devices.is_empty() {
ui::say(&ui::paint(ui::Tone::Dim, " no known devices yet, type a code or `pair` to add one"));
}
for (n, s) in &devices {
ui::say(&format!(" {} {}", ui::paint(ui::Tone::Bold, n), ui::paint(ui::Tone::Dim, &format!("(channel {})", &channel_of(s)[..12]))));
}
} else if let Some(n) = ans.strip_prefix("forget ") {
let n = n.trim();
if devices.iter().any(|(dn, _)| dn == n) {
devices_remove(n)?;
devices.retain(|(dn, _)| dn != n);
ui::say(&format!(" {} forgot '{n}', it can no longer find this machine", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
} else {
ui::say(&ui::paint(ui::Tone::Dim, &format!(" no device named '{n}' (try `devices`)")));
}
} else if ans == "pair" || ans == "code" {
if daemon {
ceremony = Some(true);
}
sio.emit("pair-create", json!({})).await.ok();
} else if regex_lite_code(&line) {
if claim_in_flight {
ui::say(&ui::paint(ui::Tone::Dim, " (a claim is already in flight, wait for it to resolve)"));
} else {
ui::say(&format!(" claiming {}...", ui::paint(ui::Tone::Brand, &line)));
paired = true;
claim_in_flight = true;
if daemon {
ceremony = Some(false); }
sio.emit("pair-claim", json!({ "code": line.to_lowercase() })).await.ok();
}
} else if !line.is_empty() {
ui::say(&ui::paint(ui::Tone::Dim, " (type a code like brave-otter-123 to claim it · `pair` · `devices` · `forget <name>`)"));
}
}
Ev::Interrupted => {
flush_inflight(&mut by_sid).await;
ui::say(&format!(" {} interrupted, partials kept; run the same command to resume", ui::paint(ui::Tone::Warn, "!")));
if let Some(g) = &tty_guard {
g.restore(); }
let _ = tokio::time::timeout(Duration::from_secs(1), sio.disconnect()).await;
std::process::exit(130);
}
Ev::TransferStalled(pid, idle_ms) => {
ui::debug(&ui::paint(ui::Tone::Warn, &format!(" inbound stall: {idle_ms}ms with no data from peer, repairing link")));
let stale: Vec<(String, u32)> =
by_sid.keys().filter(|(p, _)| *p == pid).cloned().collect();
for key in stale {
if let Some(inc) = by_sid.remove(&key) {
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
ui::debug(&format!("{}: parked at {} for resume", inc.name, human(inc.received.load(Ordering::Relaxed))));
}
}
match conn.correct_stall(&pid).await {
Rung::Resume => {}
Rung::Repaired => {}
Rung::Relayed => {}
Rung::Exhausted => {}
}
}
Ev::Stuck(pid, generation) => {
if !ever_received {
stuck_while_connecting += 1;
if stuck_while_connecting >= 2 {
maybe_hint_local_wedge(&mut wedge_hint_shown);
}
}
if conn.on_stuck(&pid, generation, "stuck while connecting").await? && paired && !keep_open {
sweep_completed_streams(&mut by_sid, &conn, &dir, &output, to_stdout, daemon, &mut completed).await?;
if completed == 0 {
bail!("lost the sender after {} attempts; the partial is kept, re-run `filament recv <code>` to resume", MAX_ATTEMPTS);
}
}
}
Ev::GraceExpired(pid, generation) => {
if conn.on_stuck(&pid, generation, "lost").await? && paired && !keep_open {
sweep_completed_streams(&mut by_sid, &conn, &dir, &output, to_stdout, daemon, &mut completed).await?;
if completed == 0 {
bail!("lost the sender after {} attempts; the partial is kept, re-run `filament recv <code>` to resume", MAX_ATTEMPTS);
}
}
}
Ev::PcState(pid, s) => {
if l2_enabled && (s == "failed" || s == "closed" || s == "disconnected") {
if let Some(mux) = l2_muxes.remove(&pid) {
mux.shutdown_all().await;
}
if (s == "failed" || s == "closed") && !pty_bindings.is_empty() {
if let Some(sid_map) = pty_bindings.remove(&pid) {
for session_id in sid_map.values() {
if let Some(sess) = pty_sessions.get_live(session_id).await {
sess.detach();
}
}
}
}
}
conn.on_pc_state(&pid, &s).await;
}
Ev::PeerLeft(v) => {
if test_hooks::drop_peer_left() {
continue;
}
let gone = v["id"].as_str().and_then(|p| conn.link(p)).map(|l| l.name.clone());
if conn.on_peer_left(&v) {
let secs = conn.rejoin.rejoin_window.as_secs();
if !by_sid.is_empty() {
ui::say(&ui::paint(ui::Tone::Dim, &format!(" sender disconnected mid-transfer, waiting up to {secs}s")));
flush_inflight(&mut by_sid).await;
} else if completed > 0 && !keep_open {
ui::say(&format!("done ({completed} file{}).", if completed == 1 { "" } else { "s" }));
let _ = sio.disconnect().await;
return Ok(());
} else if paired && !keep_open {
let gid = v["id"].as_str().unwrap_or_default();
let n = gone.unwrap_or_else(|| "sender".into());
ui::say(&conn.roster(gid, "●", ui::Tone::Warn, &format!("stepped away, holding the line up to {secs}s (Ctrl-C to stop)"), &n));
} else {
conn.rejoin.waiting_rejoin = None; let gid = v["id"].as_str().unwrap_or_default();
match gone {
Some(n) => ui::say(&conn.roster(gid, "○", ui::Tone::Dim, "left, still listening", &n)),
None => ui::say(&ui::paint(ui::Tone::Dim, " peer left, still listening")),
}
}
}
}
_ => {}
}
}
}
async fn verify_incoming(inc: &IncomingFile) -> protocol::VerifyResult {
let want = match &inc.full { Some(w) => w.clone(), None => return protocol::VerifyResult::Match };
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
let recvd = inc.received.load(Ordering::Relaxed);
if let Some(target) = test_hooks::corrupt_recv_target() {
#[cfg(feature = "test-hooks")]
{
let once = test_hooks::corrupt_recv_once();
let already = test_hooks::corrupt_already_fired();
if target == inc.id && recvd == inc.size && !(once && already) {
if let Ok(mut bytes) = std::fs::read(&inc.part_path) {
if let Some(b) = bytes.last_mut() {
*b ^= 0xFF;
let _ = std::fs::write(&inc.part_path, &bytes);
eprintln!("[test] CORRUPT-RECV: flipped the last byte of {} (id {})", inc.name, inc.id);
if once { test_hooks::corrupt_mark_fired(); }
}
}
}
}
let _ = ⌖
}
if recvd < inc.size {
return protocol::decide_verify(recvd, inc.size, None);
}
let path = inc.part_path.clone();
let got = tokio::task::spawn_blocking(move || full_hash(&path)).await.ok().flatten();
protocol::decide_verify(recvd, inc.size, Some(got.as_deref() == Some(want.as_str())))
}
async fn finalize_incoming(
inc: IncomingFile,
dir: &Path,
rename_to: Option<&str>,
daemon: bool,
from_name: &str,
) -> Result<bool> {
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
drop(inc.file);
let final_path = unique_path(dir, rename_to.unwrap_or(&inc.name));
if let Err(e) = tokio::fs::rename(&inc.part_path, &final_path).await {
ui::say(&ui::paint(ui::Tone::Dim, &format!(" (stream for {} already finalized, duplicate discarded: {e})", inc.name)));
return Ok(false);
}
let _ = tokio::fs::remove_file(dir.join(format!("{}.part.meta", inc.name))).await;
let recvd = inc.received.load(Ordering::Relaxed);
let ok = recvd == inc.size;
inc.bar.done(recvd);
let shown = final_path.display().to_string();
ui::say(&format!(
" {} {}{}",
ui::paint(ui::Tone::Dim, ui::glyph_arrow()),
ui::link(&format!("file://{shown}"), &shown),
if ok { String::new() } else { ui::paint(ui::Tone::Err, " SIZE MISMATCH") },
));
if ok {
let lname = final_path
.file_name()
.map(|n| n.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
let is_archive =
lname.ends_with(".tar") || lname.ends_with(".tar.gz") || lname.ends_with(".tgz");
let peer = (!from_name.is_empty()).then_some(from_name);
if is_archive && settings::get_bool("auto-extract", peer) {
match settings::extract_archive(&final_path, dir) {
Ok(n) => ui::say(&format!(
" {} extracted {n} file{} into {}",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
if n == 1 { "" } else { "s" },
dir.display()
)),
Err(e) => ui::say(&ui::paint(
ui::Tone::Warn,
&format!(" auto-extract skipped ({e}); the archive is kept as-is"),
)),
}
}
}
if daemon {
use std::io::Write as _;
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(up_log()) {
let _ = writeln!(f, "{} {} {} from {}", chrono_now(), inc.name, human(recvd), from_name);
}
}
Ok(true)
}
async fn sweep_completed_streams(
by_sid: &mut HashMap<(String, u32), IncomingFile>,
conn: &Conn,
dir: &Path,
output: &Option<String>,
to_stdout: bool,
daemon: bool,
completed: &mut usize,
) -> Result<()> {
let done_sids: Vec<(String, u32)> = by_sid
.iter()
.filter(|((pid, _), inc)| inc.received.load(Ordering::Relaxed) == inc.size && !conn.links.contains_key(pid))
.map(|(k, _)| k.clone())
.collect();
for key in done_sids {
if let Some(inc) = by_sid.remove(&key) {
if to_stdout {
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
*completed += 1;
continue;
}
let rename_to = if *completed == 0 { output.clone() } else { None };
ui::say(&ui::paint(ui::Tone::Dim, &format!(" ({} fully received, sender left before file-end; finalizing)", inc.name)));
if finalize_incoming(inc, dir, rename_to.as_deref(), daemon, "").await? {
*completed += 1;
}
}
}
Ok(())
}
async fn flush_inflight(by_sid: &mut HashMap<(String, u32), IncomingFile>) {
for (_sid, inc) in by_sid.drain() {
let f = inc.file.clone();
let _ = tokio::task::spawn_blocking(move || { let _ = f.sync_all(); }).await;
ui::debug(&format!("{}: parked at {} for resume", inc.name, human(inc.received.load(Ordering::Relaxed))));
}
}
struct TtyGuard {
saved: Option<String>,
}
impl TtyGuard {
fn raw() -> TtyGuard {
let saved = std::process::Command::new("stty")
.arg("-g")
.stdin(std::process::Stdio::inherit())
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string());
if saved.is_some() {
let _ = std::process::Command::new("stty")
.args(["-icanon", "-echo", "min", "1", "time", "0"])
.stdin(std::process::Stdio::inherit())
.status();
}
TtyGuard { saved }
}
fn restore(&self) {
if let Some(s) = &self.saved {
let _ = std::process::Command::new("stty")
.arg(s)
.stdin(std::process::Stdio::inherit())
.status();
}
}
}
impl Drop for TtyGuard {
fn drop(&mut self) {
self.restore();
}
}
fn consent_token() -> &'static str {
static T: std::sync::OnceLock<String> = std::sync::OnceLock::new();
T.get_or_init(fresh_secret)
}
fn peer_entry(name: &str, mark: &str, tone: ui::Tone, note: &str) -> String {
let mut s = format!("{} {}", ui::paint(tone, mark), ui::paint(ui::Tone::Bold, name));
if !note.is_empty() {
s.push_str(&format!(" {}", ui::paint(ui::Tone::Dim, note)));
}
s
}
fn offer_question(sender: &str, name: &str, size: u64, paired: bool) -> String {
let sender = if sender.is_empty() { "unknown peer" } else { sender };
let hint = if paired { " [paired]" } else { "" };
format!(
" {}{} offers {} ({}), accept? [y/N] ",
ui::paint(ui::Tone::Bold, sender),
hint,
name,
human(size)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_device_name_strips_escape_junk() {
let dirty = "\u{1b}[?1;2c\u{1b}[?1;2c\u{1b}[>0;276;0cpixel";
assert_eq!(sanitize_device_name(dirty), "pixel");
assert_eq!(sanitize_device_name(" lap\u{7}top \n"), "laptop");
assert_eq!(sanitize_device_name("agboola@pop-os"), "agboola@pop-os");
}
#[test]
fn capability_deny_by_default() {
let dir = std::env::temp_dir().join(format!("fil-caps-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("devices.json");
let sec = "a".repeat(64);
std::fs::write(
&p,
serde_json::to_string(&json!([
{"name": "empty", "secret": sec, "v": 2, "caps": []},
{"name": "xfer", "secret": sec, "v": 2, "caps": ["transfer"]},
{"name": "execcap", "secret": sec, "v": 2, "caps": ["transfer", "remote-exec"]},
{"name": "legacy", "secret": sec} ]))
.unwrap(),
)
.unwrap();
assert!(device_allows_at(&p, "empty", "transfer"), "transfer is the L0 baseline");
assert!(device_allows_at(&p, "xfer", "transfer"));
assert_eq!(device_caps_at(&p, "legacy"), Some(vec!["transfer".to_string()]));
assert!(device_allows_at(&p, "legacy", "transfer"));
assert!(!device_allows_at(&p, "empty", "remote-exec"), "empty caps must deny remote-exec");
assert!(!device_allows_at(&p, "xfer", "remote-exec"), "transfer-only must deny remote-exec");
assert!(!device_allows_at(&p, "legacy", "remote-exec"), "v1 record must deny remote-exec");
assert!(device_allows_at(&p, "execcap", "remote-exec"), "explicitly granted cap is allowed");
assert!(!device_allows_at(&p, "ghost", "remote-exec"));
assert!(device_allows_at(&p, "ghost", "transfer"), "transfer baseline is universal");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn any_shell_grant_detects_a_shell_cap() {
let dir = std::env::temp_dir().join(format!("fil-anyshell-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let p = dir.join("devices.json");
let sec = "b".repeat(64);
std::fs::write(
&p,
serde_json::to_string(&json!([
{"name": "xfer", "secret": sec, "v": 2, "caps": ["transfer"]},
{"name": "legacy", "secret": sec}
]))
.unwrap(),
)
.unwrap();
assert!(!any_shell_grant_at(&p), "no shell cap -> L2 stays off");
std::fs::write(
&p,
serde_json::to_string(&json!([
{"name": "xfer", "secret": sec, "v": 2, "caps": ["transfer"]},
{"name": "popos", "secret": sec, "v": 2, "caps": ["transfer", "shell"]}
]))
.unwrap(),
)
.unwrap();
assert!(any_shell_grant_at(&p), "a shell grant enables L2");
assert!(!any_shell_grant_at(&dir.join("nope.json")));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn l2_open_gate_scopes_grant_mode() {
assert!(l2_open_allowed(true, false));
assert!(l2_open_allowed(true, true));
assert!(l2_open_allowed(false, true), "granted device may open");
assert!(!l2_open_allowed(false, false), "ungranted device denied in grant mode");
}
#[test]
fn l2_target_allowlist_matches() {
let allow = json!({
"laptop": ["10.0.0.5:5432", "192.168.1.10:*"],
"*": ["db.internal:5432"]
});
assert!(l2_target_allowed_in(&allow, "laptop", "10.0.0.5", 5432));
assert!(l2_target_allowed_in(&allow, "laptop", "192.168.1.10", 9999));
assert!(l2_target_allowed_in(&allow, "phone", "db.internal", 5432));
assert!(!l2_target_allowed_in(&allow, "laptop", "10.0.0.5", 22));
assert!(!l2_target_allowed_in(&allow, "laptop", "10.0.0.9", 5432));
assert!(!l2_target_allowed_in(&allow, "phone", "10.0.0.5", 5432));
assert!(!l2_target_allowed_in(&Value::Null, "laptop", "10.0.0.5", 5432));
assert!(l2_target_allowed_in(&allow, "phone", "DB.INTERNAL", 5432));
}
#[test]
fn shell_policy_gates_auto_shell() {
let g = ShellPolicy::Granted;
assert!(!g.auto_allows("popos"));
assert!(!g.enables_l2(), "default must not silently enable the L2 acceptor");
let a = ShellPolicy::All;
assert!(a.auto_allows("popos") && a.auto_allows("anything"));
assert!(a.enables_l2());
let o = ShellPolicy::Only(["popos".to_string(), "laptop".to_string()].into_iter().collect());
assert!(o.auto_allows("popos") && o.auto_allows("laptop"));
assert!(!o.auto_allows("stranger"), "shell-only must not auto-shell unlisted devices");
assert!(o.enables_l2());
}
#[test]
fn direct_ok_for_covers_daemon_and_l2_acceptors() {
unsafe {
std::env::remove_var("FILAMENT_DIRECT");
std::env::remove_var("FILAMENT_L2");
}
assert!(direct_ok_for(true, false), "plain `up` daemon must answer direct-QUIC (anti-glare)");
assert!(direct_ok_for(true, true));
assert!(direct_ok_for(false, true), "L2 acceptor must take direct even when not a daemon");
assert!(direct_ok_for(false, false), "default direct-ON for any session");
unsafe { std::env::set_var("FILAMENT_DIRECT", "0") };
assert!(!direct_ok_for(false, false), "FILAMENT_DIRECT=0 disables direct");
unsafe { std::env::remove_var("FILAMENT_DIRECT") };
}
#[test]
fn forget_and_store_preserve_other_devices_caps() {
let dir = std::env::temp_dir().join(format!("fil-store-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
unsafe { std::env::set_var("FILAMENT_CONFIG_DIR", &dir) };
let p = dir.join("devices.json");
let sec = "b".repeat(64);
std::fs::write(
&p,
serde_json::to_string(&json!([
{"name": "shellbox", "secret": sec, "v": 2, "caps": ["transfer", "shell"]},
{"name": "dupe", "secret": sec, "v": 2, "caps": ["transfer"]},
]))
.unwrap(),
)
.unwrap();
devices_remove("dupe").unwrap();
assert!(device_allows_at(&p, "shellbox", "shell"), "forget wiped a survivor's shell cap");
assert!(device_caps_at(&p, "dupe").is_none(), "dupe should be gone");
devices_store("newpeer", &sec).unwrap();
assert!(device_allows_at(&p, "shellbox", "shell"), "store wiped a survivor's shell cap");
devices_store("shellbox", &"c".repeat(64)).unwrap();
assert!(device_allows_at(&p, "shellbox", "shell"), "re-store dropped the device's own caps");
unsafe { std::env::remove_var("FILAMENT_CONFIG_DIR") };
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn proof_matches_browser() {
let want = "f98c3b6b7a70ebdf4b200680e83383881bdb1a11476283507359c55ef03a8474";
assert_eq!(proof_for("s3cret", "u1", "u2", "u1", "FPB", "FPA"), want);
assert_eq!(proof_for("s3cret", "u1", "u1", "u2", "FPA", "FPB"), want);
assert_eq!(
channel_of("topsecret"),
"1e32e46e93691c29d9c0305545a10c86a00ae9f3c43d4eea3c7423c1528f9b5d"
);
}
#[test]
fn polite_role_matches_browser() {
assert!(net::polite_role("b", Some("a"), "x", "y")); assert!(!net::polite_role("a", Some("b"), "x", "y"));
assert!(net::polite_role("a", Some("a"), "y", "x"));
assert!(!net::polite_role("a", None, "x", "y"));
for (a, b) in [("a", "b"), ("cli-1", "cli-2"), ("zz", "aa")] {
let p1 = net::polite_role(a, Some(b), "s1", "s2");
let p2 = net::polite_role(b, Some(a), "s2", "s1");
assert_ne!(p1, p2, "{a} vs {b} must disagree");
}
}
#[test]
fn part_meta_roundtrip_and_legacy() {
let dir = std::env::temp_dir().join(format!("filament-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("x.part.meta");
PartMeta { size: 42, head: Some("abc".into()), full: Some("deadbeef".into()) }.store(&p).unwrap();
let m = PartMeta::load(&p).unwrap();
assert_eq!(m.size, 42);
assert_eq!(m.head.as_deref(), Some("abc"));
assert_eq!(m.full.as_deref(), Some("deadbeef"));
std::fs::write(&p, "1234").unwrap();
let m = PartMeta::load(&p).unwrap();
assert_eq!(m.size, 1234);
assert!(m.head.is_none());
assert!(m.full.is_none());
std::fs::write(&p, "{not json").unwrap();
assert!(PartMeta::load(&p).is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn head_hash_is_prefix_stable() {
let dir = std::env::temp_dir().join(format!("filament-test-h-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let a = dir.join("a.bin");
let b = dir.join("b.bin");
let mut base = vec![7u8; (HEAD_BYTES + 10) as usize];
std::fs::write(&a, &base).unwrap();
base[(HEAD_BYTES + 5) as usize] = 9;
std::fs::write(&b, &base).unwrap();
assert_eq!(head_hash(&a), head_hash(&b));
base[0] = 1;
std::fs::write(&b, &base).unwrap();
assert_ne!(head_hash(&a), head_hash(&b));
std::fs::write(&a, b"tiny").unwrap();
std::fs::write(&b, b"tinY").unwrap();
assert_ne!(head_hash(&a), head_hash(&b));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn full_hash_whole_file_integrity() {
let dir = std::env::temp_dir().join(format!("filament-test-fh-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let a = dir.join("a.bin");
let b = dir.join("b.bin");
let mut base = vec![3u8; (HEAD_BYTES + 4096) as usize];
std::fs::write(&a, &base).unwrap();
base[(HEAD_BYTES + 2048) as usize] = 4;
std::fs::write(&b, &base).unwrap();
assert_eq!(head_hash(&a), head_hash(&b), "tails past the head don't change the head hash");
assert_ne!(full_hash(&a), full_hash(&b), "full_hash sees the whole file");
std::fs::write(&b, &base[..base.len() - 100]).unwrap();
assert_ne!(full_hash(&a), full_hash(&b), "truncation changes the full hash");
assert_eq!(full_hash(&a), Some(sha256_hex(&vec![3u8; (HEAD_BYTES + 4096) as usize])));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unique_path_suffixes() {
let dir = std::env::temp_dir().join(format!("filament-test-u-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(unique_path(&dir, "f.txt"), dir.join("f.txt"));
std::fs::write(dir.join("f.txt"), b"x").unwrap();
assert_eq!(unique_path(&dir, "f.txt"), dir.join("f.txt.1"));
std::fs::write(dir.join("f.txt.1"), b"x").unwrap();
assert_eq!(unique_path(&dir, "f.txt"), dir.join("f.txt.2"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn route_address_classification() {
for a in ["127.0.0.1", "10.1.2.3", "192.168.1.9", "172.16.0.1", "169.254.1.1", "100.99.1.2", "::1", "fe80::1", "fd00::5"] {
assert!(net::is_private_addr(a), "{a} should be private");
}
for a in ["1.2.3.4", "165.22.207.231", "2606:4700::1", "8.8.8.8", "not-an-ip", ""] {
assert!(!net::is_private_addr(a), "{a} should be public/invalid");
}
}
#[test]
fn filename_sanitization() {
let evil = "../../etc/passwd";
let name = Path::new(evil).file_name().map(|n| n.to_string_lossy().into_owned());
assert_eq!(name.as_deref(), Some("passwd"));
let evil2 = "/absolute/path.bin";
let name2 = Path::new(evil2).file_name().map(|n| n.to_string_lossy().into_owned());
assert_eq!(name2.as_deref(), Some("path.bin"));
}
#[test]
fn send_name_override_for_single_file() {
let offered = |name: Option<&str>, single: bool, basename: &str| -> String {
name.map(String::from)
.filter(|_| single)
.unwrap_or_else(|| basename.to_string())
};
assert_eq!(offered(Some("renamed.bin"), true, "original.txt"), "renamed.bin");
assert_eq!(offered(None, true, "original.txt"), "original.txt");
assert_eq!(offered(Some("renamed.bin"), false, "original.txt"), "original.txt");
let stdin = |name: Option<&str>, single: bool| {
name.map(String::from).filter(|_| single).unwrap_or_else(|| "stdin.bin".into())
};
assert_eq!(stdin(Some("logs.tar"), true), "logs.tar");
assert_eq!(stdin(None, true), "stdin.bin");
}
#[test]
fn transfer_and_pairing_codes_are_distinguishable() {
assert!(regex_lite_code("brave-otter-371"));
assert!(!looks_like_pake_code("brave-otter-371"));
assert!(regex_lite_code("brave-otter-3141"));
assert!(looks_like_pake_code("brave-otter-3141"));
let transfer_hint = |s: &str| regex_lite_code(s) && !looks_like_pake_code(s);
assert!(transfer_hint("brave-otter-371")); assert!(!transfer_hint("brave-otter-3141")); assert!(looks_like_pake_code("brave-otter-3141")); assert!(!looks_like_pake_code("brave-otter-37")); assert!(trailing_num_width("brave-otter-37") == 2);
assert!(!looks_like_pake_code("brave-otter-37"));
assert!(looks_like_pake_code("calm-lynx-1000"));
assert!(!regex_lite_code("hello"));
assert!(!looks_like_pake_code("hello"));
assert!(!regex_lite_code("a-b-c-d")); assert!(!regex_lite_code("brave-otter-ruby-3141")); assert!(!looks_like_pake_code("Brave-otter-3141")); }
#[test]
fn password_word_tokens_counts_real_words() {
assert_eq!(password_word_tokens("cat"), 1);
assert_eq!(password_word_tokens("gigantic"), 1);
assert_eq!(password_word_tokens("gigantic-element"), 2);
assert_eq!(password_word_tokens("brave-otter"), 2);
assert_eq!(password_word_tokens("brave-strong-otter"), 3);
assert_eq!(password_word_tokens("a-b-c"), 0);
assert_eq!(password_word_tokens("ok1234"), 1);
assert_eq!(password_word_tokens(""), 0);
}
#[test]
fn devices_store_collision_auto_suffixes() {
let mut arr: Vec<serde_json::Value> = serde_json::from_str(
r#"[{"name":"host1","secret":"aaa"}]"#
).unwrap();
let new_secret = "bbb";
let name = "host1";
let final_name = if arr.iter().any(|d| d["name"].as_str() == Some(name)) {
let mut suffix = 2;
let mut new_name = format!("{name}-{suffix}");
while arr.iter().any(|d| d["name"].as_str() == Some(&new_name)) {
suffix += 1;
new_name = format!("{name}-{suffix}");
}
new_name
} else {
name.to_string()
};
arr.push(serde_json::json!({"name": &final_name, "secret": new_secret}));
assert_eq!(arr.len(), 2, "both entries preserved");
assert_eq!(arr[0]["name"].as_str().unwrap(), "host1", "original unchanged");
assert_eq!(arr[1]["name"].as_str().unwrap(), "host1-2", "new device auto-suffixed");
}
#[test]
fn devices_store_collision_increments_suffix() {
let mut arr: Vec<serde_json::Value> = serde_json::from_str(
r#"[{"name":"host1","secret":"aaa"},{"name":"host1-2","secret":"bbb"}]"#
).unwrap();
let name = "host1";
let final_name = if arr.iter().any(|d| d["name"].as_str() == Some(name)) {
let mut suffix = 2;
let mut new_name = format!("{name}-{suffix}");
while arr.iter().any(|d| d["name"].as_str() == Some(&new_name)) {
suffix += 1;
new_name = format!("{name}-{suffix}");
}
new_name
} else {
name.to_string()
};
arr.push(serde_json::json!({"name": &final_name, "secret": "ccc"}));
assert_eq!(arr[2]["name"].as_str().unwrap(), "host1-3", "suffix incremented");
}
#[test]
fn known_peer_first_event_connects() {
let mut saw: HashSet<String> = HashSet::new();
let n = "popos";
assert!(!saw.contains(n), "first event should connect");
saw.insert(n.to_string());
assert!(saw.contains(n));
}
#[test]
fn known_peer_repeat_while_seen_is_ignored() {
let mut saw: HashSet<String> = HashSet::new();
let n = "dovm";
saw.insert(n.to_string());
for _ in 0..5 {
assert!(saw.contains(n), "repeat KnownPeer must be ignored");
}
}
#[test]
fn known_peer_uses_device_name_not_pid() {
let mut saw: HashSet<String> = HashSet::new();
let n = "other-do";
assert!(!saw.contains(n));
saw.insert(n.to_string());
assert!(saw.contains(n), "should skip regardless of signaling pid");
}
#[test]
fn known_peer_different_devices_not_skipped() {
let mut saw: HashSet<String> = HashSet::new();
saw.insert("dovm".to_string());
assert!(!saw.contains("popos"), "different device must not be skipped");
saw.insert("popos".to_string());
assert!(saw.contains("popos"));
}
#[test]
fn confirm_yes_passes() {
let caps = UiCapability { interactive: false, json: false, yes: true, color: false };
assert!(caps.confirm("delete it").is_ok());
}
#[test]
fn confirm_non_interactive_without_yes_fails() {
let caps = UiCapability { interactive: false, json: false, yes: false, color: false };
assert!(caps.confirm("delete it").is_err());
}
#[test]
fn known_peer_liveness_allows_reconnect() {
let mut saw: HashSet<String> = HashSet::new();
let n = "peer";
assert!(!saw.contains(n));
saw.insert(n.to_string());
saw.remove(n);
assert!(!saw.contains(n), "dead link must be reconnectable");
}
}