mod codeentry;
mod ctl;
mod diag;
mod direct;
mod doctor;
mod ephemeral;
mod expose;
mod holepunch;
pub(crate) use filament_id as identity;
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 capability;
mod net;
mod overlay;
mod platform;
pub(crate) use filament_pair as pake;
mod pake_ceremony;
mod ping;
mod sdnotify;
mod protocol;
mod resilience;
mod session;
mod settings;
#[cfg(l3)]
mod tun;
#[cfg(l3)]
mod l3;
#[cfg(l3)]
mod wg;
mod shutdown;
mod sshd;
mod sshkeys;
mod local;
mod ui;
mod fleet_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, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use std::net::SocketAddr;
use tokio::sync::{mpsc, oneshot};
#[cfg(unix)]
fn pwrite_at(file: &std::fs::File, buf: &[u8], offset: u64) -> std::io::Result<()> {
use std::os::unix::fs::FileExt;
let mut written = 0usize;
let mut iters = 0u32;
while written < buf.len() {
iters += 1;
match file.write_at(&buf[written..], offset + written as u64) {
Ok(0) => return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "pwrite wrote 0 bytes")),
Ok(n) => written += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
if iters > 1 {
eprintln!("[pwrite-diag] SHORT WRITE: {iters} iterations to write {} bytes @ off {offset}", buf.len());
}
Ok(())
}
#[cfg(windows)]
fn pwrite_at(file: &std::fs::File, buf: &[u8], offset: u64) -> std::io::Result<()> {
use std::os::windows::fs::FileExt;
let mut written = 0usize;
while written < buf.len() {
match file.seek_write(&buf[written..], offset + written as u64) {
Ok(0) => return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "seek_write wrote 0 bytes")),
Ok(n) => written += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(())
}
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 = "\
COMMANDS
Connect
pair <name> remember a device (run on both ends; exchanges the pair secret)
up [--install] always-on receiver for your trusted devices
Share
<file> / send send files (mints a one-time code, or --to <device>)
recv <code> claim a code and receive
shell <device> open a shell on a device (native PTY; --ssh for real ssh)
reach <device>:<port> tunnel to a peer's port (--socks for a local proxy)
expose <port> publish a local port on your mesh address
mount <device>:<dir> mount a remote folder over the mesh
Devices
devices list your known devices
requests approve or deny access others asked for
grant / revoke give or take a capability on a device
status what the daemon is doing / recently received
Identity
identity manage your user key + device certs
Mesh
addr show your overlay address (or a device's)
doctor diagnose a link
EXAMPLES
filament video.mp4 send it; mints a speakable one-time code + QR
filament clever-lynx-63 claim a code and receive
filament send big.iso --to laptop send to a remembered device, no code
filament pair --name phone remember a device
filament up --install always-on drop target
filament shell laptop open a shell on a known device
filament reach laptop:5432 tunnel to a peer's localhost port
The other end never needs anything installed: https://filament.autumated.com
Run `filament <command> --help` for details.";
#[derive(Parser)]
#[command(
name = "filament",
version = VERSION,
about = "Peer-to-peer between your terminals and browsers: send files, open a shell, forward a port, mount a folder. No upload, no account \u{2014} your own devices form a fleet that just works.",
after_help = EXAMPLES,
help_template = "{about-with-newline}\n{usage-heading} {usage}\n\n{after-help}\n\nOptions:\n{options}"
)]
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 {
#[command(next_help_heading = "Share")]
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>,
#[arg(long, hide = true)]
auth_key: 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>,
},
#[command(next_help_heading = "Connect")]
Pair {
code: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
word: Option<String>,
},
#[command(next_help_heading = "Devices")]
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(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,
},
#[command(next_help_heading = "Mesh")]
Addr {
device: Option<String>,
#[arg(long)]
v4: bool,
},
#[command(next_help_heading = "Identity")]
Identity {
#[command(subcommand)]
action: IdentityAction,
},
#[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>,
},
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,
#[arg(long)]
off: bool,
},
Reach {
dev_port: Option<String>,
#[arg(long)]
socks: bool,
#[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,
},
#[command(hide = true)]
Doctor {
device: Option<String>,
#[arg(long)]
watch: bool,
#[arg(long)]
repeat: Option<u32>,
#[arg(long)]
json: bool,
},
Grant {
device: String,
capability: String,
#[arg(long)]
tag: Option<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>,
#[arg(long, value_name = "PATH")]
off: Option<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>,
},
Shell {
peer: String,
#[arg(long)]
ssh: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Requests {
#[command(subcommand)]
action: Option<RequestsAction>,
},
#[command(hide = true)]
Ephemeral {
#[command(subcommand)]
action: EphemeralAction,
},
Reset,
}
#[derive(Subcommand)]
enum EphemeralAction {
Mint {
#[arg(long, value_delimiter = ',')]
caps: Vec<String>,
#[arg(long, value_delimiter = ',')]
audience: Vec<String>,
#[arg(long, default_value = "86400")]
ttl: u64,
#[arg(long, default_value = "Once")]
reuse: String,
#[arg(long, default_value = "ci")]
tag: String,
},
Enroll {
auth_key: String,
#[arg(long)]
to: Option<String>,
},
}
#[derive(Subcommand)]
enum IdentityAction {
Init,
Show,
Certify {
device: String,
},
}
#[derive(Subcommand)]
enum RequestsAction {
List {
#[arg(long)]
all: bool,
},
Approve {
id: u64,
},
Deny {
id: u64,
},
}
#[derive(Subcommand)]
enum DevicesAction {
Forget { name: String },
Rename { old: String, new: String },
Vouch { a: String, b: 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 upsert_peer_record(
arr: &mut Vec<Value>,
name: &str,
secret: Option<&str>,
cert: Option<&identity::DeviceCert>,
caps: Option<&[String]>,
scope: Option<u8>,
user_key_hex: Option<&str>,
) -> String {
if let Some(existing) = arr.iter_mut().find(|d| d["name"].as_str() == Some(name)) {
if let Some(s) = secret {
existing["secret"] = json!(s);
}
if let Some(c) = cert {
existing["userKey"] = json!(hex::encode(c.user_pub));
existing["deviceCert"] = c.to_json();
} else if let Some(uk) = user_key_hex {
existing["userKey"] = json!(uk);
}
if let Some(caps) = caps {
existing["caps"] = json!(caps);
existing["v"] = json!(2);
}
if let Some(sc) = scope {
existing["identityScope"] = json!(sc);
}
return existing["name"].as_str().unwrap_or(name).to_string();
}
let mut final_name = name.to_string();
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}'");
final_name = new_name;
}
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), json!(&final_name));
if let Some(s) = secret {
obj.insert("secret".to_string(), json!(s));
}
if let Some(c) = cert {
obj.insert("userKey".to_string(), json!(hex::encode(c.user_pub)));
obj.insert("deviceCert".to_string(), c.to_json());
} else if let Some(uk) = user_key_hex {
obj.insert("userKey".to_string(), json!(uk));
}
obj.insert("v".to_string(), json!(2));
if let Some(caps) = caps {
obj.insert("caps".to_string(), json!(caps));
}
if let Some(sc) = scope {
obj.insert("identityScope".to_string(), json!(sc));
}
obj.insert("addedAt".to_string(), json!(SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)));
arr.push(Value::Object(obj));
final_name
}
pub(crate) fn devices_upsert_atomic(
name: &str,
secret: Option<&str>,
cert: Option<&identity::DeviceCert>,
caps: Option<&[String]>,
scope: Option<u8>,
user_key_hex: Option<&str>,
) -> Result<String> {
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).context("create config 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 = upsert_peer_record(&mut arr, name, secret, cert, caps, scope, user_key_hex);
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr).context("serialize devices")?)
.context("atomic write devices.json")?;
Ok(final_name)
}
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<()> {
devices_upsert_atomic(name, Some(secret), None, None, None, None)?;
Ok(())
}
fn devices_store_v2(name: &str, secret: &str, caps: &[String]) -> Result<()> {
devices_upsert_atomic(name, Some(secret), None, Some(caps), None, None)?;
Ok(())
}
fn update_device_cert(name: &str, uk: &identity::UserKey, cert: &identity::DeviceCert) -> Result<()> {
let p = devices_path();
let raw = std::fs::read_to_string(&p).ok().unwrap_or_default();
let arr: Vec<Value> = serde_json::from_str(&raw).ok().unwrap_or_default();
if !arr.iter().any(|d| d["name"].as_str() == Some(name)) {
bail!("device '{name}' is not in the device store. Pair with it first.");
}
devices_upsert_atomic(name, None, Some(cert), None, None, Some(&uk.public_key_hex()))?;
Ok(())
}
fn device_cert_for(name: &str) -> Option<identity::DeviceCert> {
let p = devices_path();
let raw = std::fs::read_to_string(&p).ok()?;
let arr: Vec<Value> = serde_json::from_str(&raw).ok()?;
for d in arr {
if d["name"].as_str() == Some(name) {
return identity::DeviceCert::from_json(&d["deviceCert"]);
}
}
None
}
fn load_owner_key() -> Option<crate::identity::UserKey> {
crate::identity::UserKey::load(&crate::platform::PlatformKeyStore).ok().flatten()
}
fn local_device_cert() -> Option<identity::DeviceCert> {
if let Some(cert) = device_cert_for(&display_name()) {
if cert.verify(identity::now_secs()).is_ok() {
return Some(cert);
}
}
if let Ok(overlay_pub) = crate::overlay::overlay_pubkey_bytes() {
let p = devices_path();
if let Ok(raw) = std::fs::read_to_string(&p) {
if let Ok(arr) = serde_json::from_str::<Vec<Value>>(&raw) {
for d in arr {
if let Some(cert) = identity::DeviceCert::from_json(&d["deviceCert"]) {
if cert.device_pub == overlay_pub && cert.verify(identity::now_secs()).is_ok() {
return Some(cert);
}
}
}
}
}
}
if let (Ok(Some(uk)), Ok(overlay_pub)) =
(identity::UserKey::load(&crate::platform::PlatformKeyStore), crate::overlay::overlay_pubkey_bytes())
{
if let Ok(cert) =
identity::DeviceCert::certify(&uk, overlay_pub, identity::now_secs(), identity::CERT_TTL_SECS)
{
return Some(cert);
}
}
None
}
fn resolve_peer_identity(link: &mut Link) {
if link.identity_device_pub.is_some() || link.identity_user_pub.is_some() {
return; }
let name = match &link.verified_name {
Some(n) => n.clone(),
None => return, };
let Some(cert) = device_cert_for(&name) else { return };
let now = crate::identity::now_secs();
if cert.verify(now).is_err() {
return;
}
link.identity_device_pub = Some(cert.device_pub);
link.identity_user_pub = Some(cert.user_pub);
link.identity_binding = crate::capability::BindingStrength::Inferred;
link.identity_cert_expires = Some(cert.expires);
}
async fn send_identity_challenge(
conn: &crate::Conn,
pid: &str,
identity_nonces: &mut HashMap<String, ([u8; 32], Instant, [u8; 32])>,
) {
if let Ok(own_dpub) = crate::overlay::overlay_pubkey_bytes() {
if let Some(t) = conn.transport_of(pid) {
use ring::rand::{SecureRandom, SystemRandom};
let rng = SystemRandom::new();
let mut nonce = [0u8; 32];
let _ = rng.fill(&mut nonce);
identity_nonces.insert(pid.to_string(), (nonce, Instant::now(), own_dpub));
let challenge = json!({
"type": "identity-nonce-challenge",
"nonce": hex::encode(nonce),
"receiver_device_pub": hex::encode(own_dpub)
});
let _ = t.send_control(&challenge).await;
}
}
}
async fn issue_proven_challenge_and_hold(
conn: &crate::Conn,
pid: &str,
t: &Arc<dyn Transport>,
pending_proven: &Arc<Mutex<HashMap<String, (Arc<dyn Transport>, Instant)>>>,
identity_nonces: &mut HashMap<String, ([u8; 32], Instant, [u8; 32])>,
) {
{
let mut pend = pending_proven.lock().unwrap();
if let Some((_, deadline)) = pend.get(pid) {
if Instant::now() < *deadline {
return; }
}
pend.insert(pid.to_string(), (t.clone(), Instant::now() + Duration::from_secs(3)));
}
send_identity_challenge(conn, pid, identity_nonces).await;
}
fn handle_identity_expose(
conn: &mut crate::Conn,
pid: &str,
v: &Value,
identity_nonces: &mut HashMap<String, ([u8; 32], Instant, [u8; 32])>,
) -> bool {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let Ok(nonce_bytes) = hex::decode(nonce_hex) else { return false };
if nonce_bytes.len() != 32 { return false; }
let mut nonce_arr = [0u8; 32];
nonce_arr.copy_from_slice(&nonce_bytes);
let Some((held_nonce, _ts, _held_recv_dpub)) = identity_nonces.get(pid) else { return false };
if held_nonce != &nonce_arr { return false; }
let Some(cert_json) = v.get("cert") else { return false };
let Some(cert) = identity::DeviceCert::from_json(cert_json) else { return false };
if cert.verify(identity::now_secs()).is_err() { return false; }
let Some(sig_hex) = v.get("possession_sig").and_then(|x| x.as_str()) else { return false };
let Ok(sig_bytes) = hex::decode(sig_hex) else { return false };
if sig_bytes.len() != 64 { return false; }
let mut sig_arr = [0u8; 64];
sig_arr.copy_from_slice(&sig_bytes);
let scope = crate::identity::IntroScope::User.to_byte();
let caps_d = crate::identity::caps_digest("transfer");
let chash = crate::identity::cert_hash(&cert);
let Ok(own_dpub) = crate::overlay::overlay_pubkey_bytes() else { return false };
let sender_dpub = cert.device_pub;
let receiver_dpub = own_dpub;
let msg = crate::identity::possession_msg(0x02, &nonce_arr, scope, &caps_d, &chash, &sender_dpub, &receiver_dpub);
if crate::identity::verify_possession_sig(&cert.device_pub, &msg, &sig_arr).is_err() { return false; }
if cert.device_pub == own_dpub { return false; }
let _ = store_provisional_identity(&format!("peer-{}", pid), &cert);
if let Some(l) = conn.link_mut(pid) {
l.identity_device_pub = Some(cert.device_pub);
l.identity_user_pub = Some(cert.user_pub);
l.identity_binding = crate::capability::BindingStrength::Proven;
l.identity_cert_expires = Some(cert.expires);
}
ui::say(&format!(
"{} identity proven for peer {}",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
pid
));
identity_nonces.remove(pid);
true
}
pub(crate) async fn respond_to_identity_challenge(t: &Arc<dyn Transport>, v: &Value) {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let recv_dpub_hex = v["receiver_device_pub"].as_str().unwrap_or_default();
let (Ok(nonce_bytes), Ok(recv_dpub_bytes)) =
(hex::decode(nonce_hex), hex::decode(recv_dpub_hex))
else {
return;
};
if nonce_bytes.len() != 32 || recv_dpub_bytes.len() != 32 {
return;
}
let mut nonce_arr = [0u8; 32];
nonce_arr.copy_from_slice(&nonce_bytes);
let mut recv_dpub_arr = [0u8; 32];
recv_dpub_arr.copy_from_slice(&recv_dpub_bytes);
if let Ok(own_dpub) = crate::overlay::overlay_pubkey_bytes() {
if recv_dpub_arr == own_dpub {
return;
}
}
let Some(local_cert) = local_device_cert() else { return };
let scope = crate::identity::IntroScope::User.to_byte();
let caps_d = crate::identity::caps_digest("transfer");
let chash = crate::identity::cert_hash(&local_cert);
let sender_dpub = local_cert.device_pub;
let msg = crate::identity::possession_msg(
0x02,
&nonce_arr,
scope,
&caps_d,
&chash,
&sender_dpub,
&recv_dpub_arr,
);
if let Ok(sig) = crate::overlay::overlay_sign_possession(&msg) {
let payload = json!({
"type": "identity-expose",
"v": 2,
"binding_type": 0x02,
"nonce": hex::encode(nonce_arr),
"cert": local_cert.to_json(),
"possession_sig": hex::encode(sig)
});
let _ = t.send_control(&payload).await;
}
}
fn apply_peer_identity(arr: &mut Vec<Value>, name: &str, peer_cert: &identity::DeviceCert, scope: u8) -> Result<()> {
identity::apply_peer_identity(arr, name, peer_cert, scope).map_err(|e| anyhow::anyhow!("{}", e))
}
fn update_peer_identity(name: &str, peer_cert: &identity::DeviceCert, scope: u8) -> Result<()> {
devices_upsert_atomic(name, None, Some(peer_cert), None, Some(scope), None)?;
Ok(())
}
fn store_provisional_identity(name: &str, peer_cert: &identity::DeviceCert) -> Result<()> {
let p = crate::platform::Paths::config_path(format!("provisional_{}.json", name));
let data = json!({
"name": name,
"deviceCert": peer_cert.to_json(),
"storedAt": identity::now_secs()
});
crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&data)?)?;
Ok(())
}
fn load_provisional_identity(name: &str) -> Option<identity::DeviceCert> {
let p = crate::platform::Paths::config_path(format!("provisional_{}.json", name));
let raw = std::fs::read_to_string(&p).ok()?;
let v: Value = serde_json::from_str(&raw).ok()?;
identity::DeviceCert::from_json(&v["deviceCert"])
}
fn clear_provisional_identity(name: &str) {
let p = crate::platform::Paths::config_path(format!("provisional_{}.json", name));
let _ = std::fs::remove_file(&p);
}
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 fleet_share_root() -> PathBuf {
config_get("share").map(PathBuf::from).unwrap_or_else(|| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join("filament-share")
})
}
fn lexical_normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in p.components() {
use std::path::Component::*;
match comp {
ParentDir => {
out.pop();
}
CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
fn path_within(root: &Path, path: &Path) -> bool {
let root_n = lexical_normalize(root);
let path_n = lexical_normalize(path);
!root_n.as_os_str().is_empty() && path_n.starts_with(&root_n)
}
fn path_within_canonical(root: &Path, path: &Path) -> bool {
let Ok(root_c) = root.canonicalize() else {
return false;
};
let Ok(path_c) = path.canonicalize() else {
return false;
};
!root_c.as_os_str().is_empty() && path_c.starts_with(&root_c)
}
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
}
macro_rules! dlog {
($($arg:tt)*) => {
#[cfg(feature = "debug-logs")]
eprintln!($($arg)*);
};
}
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() {
dlog!("[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 config_dir = crate::settings::config_dir();
let authoritative = crate::capability::cap_authoritative();
let revoked = crate::capability::devices_with_shell_revoked(&config_dir);
let ak_path = sshkeys::authorized_keys_path();
let ak_content = std::fs::read_to_string(&ak_path).unwrap_or_default();
for device in &revoked {
if sshkeys::has_block(&ak_content, device) && !authoritative {
eprintln!("CAP-SHADOW RECONCILE (startup): WOULD remove shell key for '{device}' (cap store denies shell); NOT removing in shadow");
}
}
let new_ak = crate::capability::reconcile_shell_keys(&revoked, &ak_content, authoritative);
if new_ak != ak_content {
if let Err(e) = crate::platform::SecretFile::write_str(&ak_path, &new_ak) {
eprintln!("shell-key reconcile (startup): failed to write authorized_keys: {e}");
}
}
}
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(())
}
async fn cap_status_cmd(json: bool) -> Result<()> {
let reply = crate::ctl::try_cap_status().await;
match reply {
Some(v) if json => {
println!("{}", serde_json::to_string_pretty(&v)?);
}
Some(v) => {
if let Some(summary) = v["summary"].as_str() {
ui::say(summary);
}
let flip = v["flip_ready"].as_bool().unwrap_or(false);
if v.get("counts").is_none() || v["counts"].is_null() {
ui::say(&format!(
" flip_ready: {} counts unavailable; cannot assess flip readiness",
ui::paint(ui::Tone::Warn, "x"),
));
} else {
let widening = v["counts"]["ld_authorized"].as_u64().unwrap_or(0);
let la_authorized = v["counts"]["la_authorized"].as_u64().unwrap_or(0);
if flip && widening == 0 {
ui::say(&format!(
" flip_ready: {} no blockers detected (n={} legacy-allowed opens)",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
la_authorized,
));
} else if flip {
ui::say(&format!(
" flip_ready: {} breakage-clean, but {} WIDENING opens will be NEWLY PERMITTED; review and cite them before flipping",
ui::paint(ui::Tone::Warn, "x"),
widening,
));
} else {
ui::say(&format!(
" flip_ready: {} not ready",
ui::paint(ui::Tone::Warn, "x"),
));
}
}
if let Some(counts) = v.get("counts") {
if !counts.is_null() {
ui::say(&format!(
" la_authorized={} la_denied={} la_no_header={}",
counts["la_authorized"].as_u64().unwrap_or(0),
counts["la_denied"].as_u64().unwrap_or(0),
counts["la_no_header"].as_u64().unwrap_or(0),
));
ui::say(&format!(
" WIDENING(ld_authorized)={} ld_denied={} ld_no_header={}",
counts["ld_authorized"].as_u64().unwrap_or(0),
counts["ld_denied"].as_u64().unwrap_or(0),
counts["ld_no_header"].as_u64().unwrap_or(0),
));
}
}
if let Some(by_action) = v.get("by_action").and_then(|v| v.as_array()) {
if !by_action.is_empty() {
ui::say(&ui::paint(ui::Tone::Dim, " coverage matrix (per-action):"));
for a in by_action {
let action = a["action"].as_str().unwrap_or("?");
let la = a["la_authorized"].as_u64().unwrap_or(0);
let ld = a["la_denied"].as_u64().unwrap_or(0);
let ln = a["la_no_header"].as_u64().unwrap_or(0);
let wa = a["ld_authorized"].as_u64().unwrap_or(0);
let wd = a["ld_denied"].as_u64().unwrap_or(0);
let wn = a["ld_no_header"].as_u64().unwrap_or(0);
ui::say(&format!(
" {action}: la_ok={la} la_deny={ld} la_nh={ln} | widen={wa} ld_deny={wd} ld_nh={wn}"
));
}
}
}
}
None => {
ui::say(&format!(" {} daemon not running; counters are zero in a fresh process", ui::paint(ui::Tone::Dim, "·")));
if json {
println!("{}", serde_json::to_string_pretty(&json!({
"ok": false,
"err": "daemon not reachable; start with filament up",
}))?);
}
}
}
Ok(())
}
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PendingRequest {
id: u64,
peer: String,
capability: String,
timestamp: u64,
status: String, granted_at: Option<u64>,
}
const MAX_PENDING: usize = 100;
const REQUEST_TTL_SECS: u64 = 3600;
fn requests_path() -> PathBuf {
crate::settings::config_dir().join("requests.json")
}
fn load_requests() -> Vec<PendingRequest> {
std::fs::read_to_string(requests_path())
.ok()
.and_then(|raw| serde_json::from_str(&raw).ok())
.unwrap_or_default()
}
fn save_requests(reqs: &[PendingRequest]) {
if let Ok(json) = serde_json::to_string_pretty(reqs) {
let _ = crate::platform::SecretFile::write_str(&requests_path(), &json);
}
}
fn add_pending_request(peer: &str, capability: &str, requests: &mut Vec<PendingRequest>) {
let now = crate::capability::now_secs();
let next_id = requests.iter().map(|r| r.id).max().unwrap_or(0) + 1;
while requests.iter().filter(|r| r.status == "pending").count() >= MAX_PENDING {
if let Some(pos) = requests.iter().position(|r| r.status == "pending") {
let evicted = &requests[pos];
ui::say(&format!(
"consent queue full ({}), evicting oldest pending: id={} peer={} cap={}",
MAX_PENDING, evicted.id, evicted.peer, evicted.capability
));
requests.remove(pos);
} else {
break;
}
}
requests.push(PendingRequest {
id: next_id,
peer: peer.to_string(),
capability: capability.to_string(),
timestamp: now,
status: "pending".to_string(),
granted_at: None,
});
if let Ok(hook) = std::env::var("FILAMENT_NOTIFY_HOOK") {
if !hook.is_empty() {
let _ = std::process::Command::new(&hook)
.arg(peer)
.arg(capability)
.spawn();
}
}
save_requests(requests);
}
fn expire_requests(requests: &mut Vec<PendingRequest>) {
let now = crate::capability::now_secs();
let mut changed = false;
for r in requests.iter_mut().filter(|r| r.status == "pending") {
if now.saturating_sub(r.timestamp) > REQUEST_TTL_SECS {
r.status = "expired".to_string();
changed = true;
}
}
if changed {
save_requests(requests);
}
}
fn enqueue_if_requestable(dev_petname: &str, cap: &str) {
let name = dev_petname.trim();
if name.is_empty() || name == "<unverified>" {
return;
}
match cap {
"shell" | "mount" | "transfer" => {}
_ => return,
}
let mut requests = load_requests();
expire_requests(&mut requests);
let dup = requests.iter().any(|r| r.peer == name && r.capability == cap && r.status == "pending");
if !dup {
add_pending_request(name, cap, &mut requests);
}
}
async fn requests_cmd(action: Option<RequestsAction>) -> Result<()> {
match action {
None | Some(RequestsAction::List { all: false }) => {
let reply = crate::ctl::try_list_pending().await;
match reply {
Some(v) => {
if let Some(reqs) = v.get("requests").and_then(|v| v.as_array()) {
if reqs.is_empty() {
ui::say(" no pending requests");
} else {
for r in reqs {
let id = r["id"].as_u64().unwrap_or(0);
let peer = r["peer"].as_str().unwrap_or("?");
let cap = r["capability"].as_str().unwrap_or("?");
let status = r["status"].as_str().unwrap_or("?");
let ts = r["timestamp"].as_u64().unwrap_or(0);
let ago = crate::capability::now_secs().saturating_sub(ts);
let when = if ago < 60 { format!("{ago}s ago") }
else if ago < 3600 { format!("{}m ago", ago/60) }
else { format!("{}h ago", ago/3600) };
ui::say(&format!(" #{id} {when:>8} {peer:>16} {cap:>10} {status}"));
}
}
}
}
None => ui::say(" daemon not running; no pending request state"),
}
}
Some(RequestsAction::List { all: true }) => {
let reply = crate::ctl::try_list_pending().await;
match reply {
Some(v) => {
if let Some(reqs) = v.get("requests").and_then(|v| v.as_array()) {
if reqs.is_empty() {
ui::say(" no requests");
} else {
for r in reqs {
let id = r["id"].as_u64().unwrap_or(0);
let peer = r["peer"].as_str().unwrap_or("?");
let cap = r["capability"].as_str().unwrap_or("?");
let status = r["status"].as_str().unwrap_or("?");
let ts = r["timestamp"].as_u64().unwrap_or(0);
let ago = crate::capability::now_secs().saturating_sub(ts);
let when = if ago < 60 { format!("{ago}s ago") }
else if ago < 3600 { format!("{}m ago", ago/60) }
else { format!("{}h ago", ago/3600) };
ui::say(&format!(" #{id} {when:>8} {peer:>16} {cap:>10} {status}"));
}
}
}
}
None => ui::say(" daemon not running; no request state"),
}
}
Some(RequestsAction::Approve { id }) => {
let reply = crate::ctl::try_approve_request(id).await;
match reply {
Some(v) => {
if let Some(peer) = v.get("peer").and_then(|v| v.as_str()) {
if let Some(cap) = v.get("capability").and_then(|v| v.as_str()) {
ui::say(&format!(
" {} approved {cap} for {peer}",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
));
}
}
}
None => ui::say(&format!(" {} request {id} not found or daemon not running", ui::paint(ui::Tone::Warn, "x"))),
}
}
Some(RequestsAction::Deny { id }) => {
let reply = crate::ctl::try_deny_request(id).await;
match reply {
Some(_) => ui::say(&format!(" {} request {id} denied", ui::paint(ui::Tone::Ok, ui::glyph_ok()))),
None => ui::say(&format!(" {} request {id} not found or daemon not running", ui::paint(ui::Tone::Warn, "x"))),
}
}
}
Ok(())
}
async fn ephemeral_cmd(server: &str, action: EphemeralAction, relay: bool) -> Result<()> {
match action {
EphemeralAction::Mint { caps, audience, ttl, reuse, tag } => {
let uk = match crate::identity::UserKey::load(&crate::platform::PlatformKeyStore)? {
Some(uk) => uk,
None => bail!("no user identity. Run 'filament identity init' first."),
};
let rng = ring::rand::SystemRandom::new();
let mut seed = [0u8; 32];
ring::rand::SecureRandom::fill(&rng, &mut seed).map_err(|e| anyhow::anyhow!("{}", e))?;
let enroll_kp = ring::signature::Ed25519KeyPair::from_seed_unchecked(&seed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let enroll_pub: [u8; 32] = ring::signature::KeyPair::public_key(&enroll_kp).as_ref().try_into().unwrap();
let audience_pubs: Vec<[u8; 32]> = audience.iter()
.map(|s| {
let b = hex::decode(s).map_err(|e| anyhow::anyhow!("audience hex: {}", e))?;
let arr: [u8; 32] = b.try_into().map_err(|_| anyhow::anyhow!("audience key must be 32 bytes"))?;
Ok(arr)
})
.collect::<Result<_>>()?;
let reuse = match reuse.to_lowercase().as_str() {
"once" => crate::ephemeral::Reuse::Once,
"reusable" => crate::ephemeral::Reuse::Reusable,
s if s.starts_with("n(") || s.starts_with('n') => {
let num: u32 = s.trim_start_matches('n').trim_start_matches('(').trim_end_matches(')').parse()
.map_err(|_| anyhow::anyhow!("invalid reuse count"))?;
crate::ephemeral::Reuse::N(num)
}
_ => bail!("invalid reuse: {reuse} (Once, N(3), Reusable)"),
};
let ak = crate::ephemeral::AuthKey::mint(uk.keypair(), enroll_pub, caps, audience_pubs, ttl, reuse, tag)?;
let json = serde_json::to_string_pretty(&serde_json::json!({
"auth_key": ak.to_json(),
"enroll_private_key": hex::encode(seed),
}))?;
println!("{}", json);
eprintln!("{} auth key minted — save the JSON above. The enroll_private_key proves possession.",
ui::paint(ui::Tone::Ok, ui::glyph_ok()));
let ak = crate::ephemeral::AuthKey::from_json(&serde_json::from_str::<serde_json::Value>(&json)?.get("auth_key").unwrap_or(&serde_json::Value::Null))
.ok_or_else(|| anyhow::anyhow!("failed to re-parse minted auth key"))?;
if ctl::try_arm(hex::encode(ak.enroll_pub), ak.expires).await.is_some() {
ui::debug("local daemon armed for enrollment");
} else {
ui::say(&format!(" {} no local daemon — enrollment room not armed (start 'filament up' first)",
ui::paint(ui::Tone::Dim, "·")));
}
Ok(())
}
EphemeralAction::Enroll { auth_key, to } => {
enroll_cmd(server, &auth_key, to, relay).await
}
}
}
async fn enroll_cmd(server: &str, auth_key_json: &str, to_name: Option<String>, relay: bool) -> Result<()> {
use crate::ephemeral::AuthKey;
let v: serde_json::Value = {
let ak_str = if std::path::Path::new(auth_key_json).exists() {
std::fs::read_to_string(auth_key_json)?
} else {
auth_key_json.to_string()
};
serde_json::from_str(&ak_str)?
};
let ak = AuthKey::from_json(&v.get("auth_key").unwrap_or(&v)).ok_or_else(|| anyhow::anyhow!("invalid auth key JSON"))?;
let enroll_seed = v.get("enroll_private_key")
.and_then(|s| s.as_str())
.and_then(|s| hex::decode(s).ok())
.ok_or_else(|| anyhow::anyhow!("missing enroll_private_key in auth key JSON"))?;
let enroll_seed: [u8; 32] = enroll_seed.try_into().map_err(|_| anyhow::anyhow!("bad enroll key seed"))?;
let mut dseed = [0u8; 32];
ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), &mut dseed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let device_kp = ring::signature::Ed25519KeyPair::from_seed_unchecked(&dseed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let device_pub: [u8; 32] = ring::signature::KeyPair::public_key(&device_kp).as_ref().try_into().unwrap();
ui::say(&format!("{} auth key loaded (caps: {:?}, issuer: {})",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ak.caps.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
hex::encode(&ak.issuer[..4])));
let my_uid = mk_uid("e");
let (tx, mut rx) = mpsc::unbounded_channel::<Ev>();
let sio = net::connect_signaling(server, tx.clone()).await?;
let enroll_chan = crate::ephemeral::enroll_channel(&ak.issuer);
let mut sess = session::Session::new(&display_name(), &my_uid);
sess.room = Some(format!("enrollup-{}", fresh_secret()));
sess.channels = vec![enroll_chan.clone()];
sess.emit(&sio, "join", json!({ "room": sess.room.as_ref().unwrap(), "name": display_name(), "uid": my_uid })).await;
sess.emit(&sio, "subscribe", json!({ "channels": [enroll_chan] })).await;
let mut conn = Conn::for_command(
server,
sio.clone(),
tx.clone(),
my_uid,
relay,
to_name,
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 started = Instant::now();
let enroll_deadline = Duration::from_secs(60);
loop {
let elapsed = started.elapsed();
if elapsed >= enroll_deadline {
bail!("enrollment timed out after {}s (no response from peer)", enroll_deadline.as_secs());
}
let slice = Duration::from_secs(2).min(enroll_deadline.saturating_sub(elapsed));
let ev = match tokio::time::timeout(slice, rx.recv()).await {
Ok(Some(ev)) => Some(ev),
Ok(None) => bail!("signaling channel closed"),
Err(_) => None,
};
sess.tick(&sio).await;
let Some(ev) = ev else { 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, false).await?;
}
}
}
Ev::PeerJoined(v) => {
conn.maybe_adopt(&v, false).await?;
}
Ev::KnownPeer(v) => {
if !is_self_uid(&conn.my_uid, v["uid"].as_str())
&& v["channel"].as_str() == Some(enroll_chan.as_str())
{
let pid = v["id"].as_str().unwrap_or_default().to_string();
if !pid.is_empty() && !conn.links.contains_key(&pid) && !conn.direct_pending.contains_key(&pid) {
conn.roster.insert(pid.clone(), v.clone());
conn.establish_as(v.clone(), Some(false)).await?;
if conn.active.is_none() { conn.active = Some(pid); }
}
}
}
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::DirectReady(pid, t, route) => {
conn.adopt_direct(&pid, t.clone(), route);
let _ = tx.send(Ev::ChannelReady(pid, t));
}
Ev::ChannelReady(pid, t) => {
if let Some(l) = conn.link_mut(&pid) {
l.transport = Some(t.clone());
l.presence = Presence::Ready;
}
if conn.active.is_none() {
conn.active = Some(pid.clone());
}
crate::ephemeral::register_enrollment(
pid.clone(),
enroll_seed,
dseed,
device_pub,
ak.clone(),
);
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-request",
"auth_key": ak.to_json(),
"device_pub": hex::encode(device_pub),
})).await;
ui::say(&format!(" {} sent enrollment request to {}",
ui::paint(ui::Tone::Dim, "->"),
pid));
}
Ev::Control(pid, v) => match v["type"].as_str() {
Some("identity-auth-key-enroll-challenge") => {
if let Some(t) = conn.transport_of(&pid) {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let verifier_hex = v["verifier_pub"].as_str().unwrap_or_default();
if let (Ok(nonce_bytes), Ok(verifier_bytes)) = (hex::decode(nonce_hex), hex::decode(verifier_hex)) {
if let (Ok(nonce_arr), Ok(verifier_pub)) = (
nonce_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
verifier_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
) {
let device_cert = v.get("device_cert").cloned().unwrap_or(serde_json::Value::Null);
if let Some(response) = crate::ephemeral::build_enrollment_response(
&pid, nonce_arr, verifier_pub, &device_cert,
) {
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-response",
"auth_key": response["auth_key"],
"device_pub": response["device_pub"],
"enroll_possession_sig": response["enroll_possession_sig"],
"device_possession_sig": response["device_possession_sig"],
})).await;
}
}
}
}
}
Some("identity-auth-key-enroll-ack") => {
let name = v["name"].as_str().unwrap_or("ephemeral-device");
ui::say(&format!("{} enrolled as ephemeral device '{}' (device_pub: {})",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
name,
hex::encode(device_pub)));
return Ok(());
}
Some("identity-auth-key-enroll-error") => {
bail!("enrollment rejected: {}", v["reason"].as_str().unwrap_or("unknown reason"));
}
_ => {}
},
Ev::Interrupted => bail!("cancelled"),
Ev::SignalingDown(reason) => {
bail!("signaling connection lost: {reason}");
}
_ => {}
}
}
}
async fn enroll_and_send_cmd(
server: &str,
auth_key_path: String,
to_name: Option<String>,
paths: Vec<String>,
relay: bool,
remember: Option<String>,
) -> Result<()> {
let v: serde_json::Value = {
let ak_str = if std::path::Path::new(&auth_key_path).exists() {
std::fs::read_to_string(&auth_key_path)?
} else {
auth_key_path.clone()
};
serde_json::from_str(&ak_str)?
};
let ak = crate::ephemeral::AuthKey::from_json(&v.get("auth_key").unwrap_or(&v))
.ok_or_else(|| anyhow::anyhow!("invalid auth key JSON"))?;
let enroll_seed = v.get("enroll_private_key")
.and_then(|s| s.as_str())
.and_then(|s| hex::decode(s).ok())
.ok_or_else(|| anyhow::anyhow!("missing enroll_private_key"))?;
let enroll_seed: [u8; 32] = enroll_seed.try_into().map_err(|_| anyhow::anyhow!("bad enroll key"))?;
let mut dseed = [0u8; 32];
ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), &mut dseed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let device_kp = ring::signature::Ed25519KeyPair::from_seed_unchecked(&dseed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let device_pub: [u8; 32] = ring::signature::KeyPair::public_key(&device_kp).as_ref().try_into().unwrap();
ui::say(&format!(" {} enrolling as delegated (caps: {:?})",
ui::paint(ui::Tone::Dim, "->"),
ak.caps.iter().map(|s| s.as_str()).collect::<Vec<_>>()));
let enroll_chan = crate::ephemeral::enroll_channel(&ak.issuer);
let my_uid = mk_uid("a");
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.channels = vec![enroll_chan.clone()];
sess.room = Some(format!("enrollup-{}", fresh_secret()));
sess.emit(&sio, "join", json!({ "room": sess.room.as_ref().unwrap(), "name": display_name(), "uid": my_uid })).await;
sess.emit(&sio, "subscribe", json!({ "channels": [enroll_chan] })).await;
let mut conn = Conn::for_command(server, sio.clone(), tx.clone(), my_uid, relay, to_name.clone(), false, direct::direct_enabled());
crate::ephemeral::register_enrollment(String::new(), enroll_seed, dseed, device_pub, ak.clone());
{
let tx = tx.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
let _ = tx.send(Ev::Interrupted);
});
}
let started = Instant::now();
let deadline = Duration::from_secs(60);
loop {
if started.elapsed() >= deadline {
bail!("enrollment timed out after {}s", deadline.as_secs());
}
let ev = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
let Ok(Some(ev)) = ev else {
if conn.active.is_some() {
break;
}
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?; }
}
}
Ev::PeerJoined(v) => { conn.maybe_adopt(&v, true).await?; }
Ev::KnownPeer(v) => {
if !is_self_uid(&conn.my_uid, v["uid"].as_str())
&& v["channel"].as_str() == Some(enroll_chan.as_str())
{
let pid = v["id"].as_str().unwrap_or_default().to_string();
if !pid.is_empty() && !conn.links.contains_key(&pid) && !conn.direct_pending.contains_key(&pid) {
conn.roster.insert(pid.clone(), v.clone());
conn.establish_as(v.clone(), Some(false)).await?;
if conn.active.is_none() { conn.active = Some(pid); }
}
}
}
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::DirectReady(pid, t, route) => {
conn.adopt_direct(&pid, t.clone(), route);
let _ = tx.send(Ev::ChannelReady(pid, t));
}
Ev::ChannelReady(pid, t) => {
if let Some(l) = conn.link_mut(&pid) {
l.transport = Some(t.clone());
l.presence = Presence::Ready;
}
if conn.active.is_none() { conn.active = Some(pid.clone()); }
crate::ephemeral::register_enrollment(pid.clone(), enroll_seed, dseed, device_pub, ak.clone());
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-request",
"auth_key": ak.to_json(),
})).await;
ui::say(&format!(" {} enrollment request sent", ui::paint(ui::Tone::Dim, "->")));
}
Ev::Control(pid, v) => match v["type"].as_str() {
Some("identity-auth-key-enroll-challenge") => {
if let Some(t) = conn.transport_of(&pid) {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let verifier_hex = v["verifier_pub"].as_str().unwrap_or_default();
let cert_val = v.get("device_cert").cloned().unwrap_or(serde_json::Value::Null);
if let (Ok(nonce_bytes), Ok(verifier_bytes)) = (hex::decode(nonce_hex), hex::decode(verifier_hex)) {
if let (Ok(nonce_arr), Ok(verifier_pub)) = (
nonce_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
verifier_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
) {
if let Some(response) = crate::ephemeral::build_enrollment_response(&pid, nonce_arr, verifier_pub, &cert_val) {
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-response",
"auth_key": response["auth_key"],
"device_pub": response["device_pub"],
"enroll_possession_sig": response["enroll_possession_sig"],
"device_possession_sig": response["device_possession_sig"],
})).await;
}
}
}
}
}
Some("identity-auth-key-enroll-ack") => {
let dp_hex = v["device_pub"].as_str().unwrap_or("?");
ui::say(&format!(" {} enrolled (device_pub: {})", ui::paint(ui::Tone::Ok, ui::glyph_ok()), &dp_hex[..16]));
conn.active = Some(pid.clone());
break;
}
Some("identity-auth-key-enroll-error") => {
bail!("enrollment denied: {}", v["reason"].as_str().unwrap_or("unknown"));
}
_ => {}
},
Ev::Interrupted => bail!("interrupted"),
_ => {}
}
}
ui::say(&format!(" {} sending {} file(s)", ui::paint(ui::Tone::Dim, "->"), paths.len()));
let active_pid = conn.active.as_ref().cloned().unwrap_or_default();
let t = conn.transport_of(&active_pid)
.ok_or_else(|| anyhow::anyhow!("no transport after enrollment"))?;
for path in &paths {
let data = tokio::fs::read(path).await?;
let name = std::path::Path::new(path).file_name()
.and_then(|n| n.to_str()).unwrap_or("file");
let id = format!("ak-{}", paths.iter().position(|p| p == path).unwrap_or(0));
let offer = crate::protocol::offer_msg(&id, 0, name, data.len() as u64, None, None, false);
if let Err(e) = t.send_control(&offer).await {
bail!("failed to send file offer for {name}: {e}");
}
let offer_start = Instant::now();
let mut accepted = false;
loop {
if offer_start.elapsed() >= Duration::from_secs(30) {
bail!("file offer for {name} timed out (no accept/decline)");
}
let ev = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
let Ok(Some(ev)) = ev else { continue; };
if let Ev::Control(ref pid, ref v) = ev {
if pid == &active_pid && v["id"].as_str() == Some(&id) {
match v["type"].as_str() {
Some("file-accept") => { accepted = true; break; }
Some("file-decline") => {
bail!("file {name} declined: {}", v["reason"].as_str().unwrap_or("not authorized"));
}
_ => {}
}
}
}
if matches!(ev, Ev::Interrupted) { bail!("interrupted"); }
}
if !accepted {
bail!("transfer of {name} not accepted");
}
let total = data.len();
let max_payload = t.max_payload().max(1024);
let mut sent = 0usize;
while sent < total {
let end = total.min(sent + max_payload);
let chunk = &data[sent..end];
t.send_frame(0, sent as u64, chunk).await.map_err(|e| {
anyhow::anyhow!("data channel failed during transfer of {name} (sent {sent}/{total}): {e}")
})?;
sent = end;
}
let _ = t.flush().await;
t.send_control(&crate::protocol::end_msg(&id, 0)).await
.map_err(|e| anyhow::anyhow!("failed to send file-end for {name}: {e}"))?;
ui::say(&format!(" {} sent {} ({} bytes)", ui::paint(ui::Tone::Ok, ui::glyph_ok()), name, total));
}
if let Some(name) = remember {
ui::say(&format!(" {} delegated devices are ephemeral (--remember for enrollment not stored)", ui::paint(ui::Tone::Dim, "·")));
let _ = name;
}
Ok(())
}
async fn enroll_and_netcat_cmd(
server: &str,
auth_key_path: String,
to_name: Option<String>,
rport: u16,
relay: bool,
) -> Result<()> {
let v: serde_json::Value = {
let ak_str = if std::path::Path::new(&auth_key_path).exists() {
std::fs::read_to_string(&auth_key_path)?
} else {
auth_key_path.clone()
};
serde_json::from_str(&ak_str)?
};
let ak = crate::ephemeral::AuthKey::from_json(&v.get("auth_key").unwrap_or(&v))
.ok_or_else(|| anyhow::anyhow!("invalid auth key JSON"))?;
let enroll_seed = v.get("enroll_private_key")
.and_then(|s| s.as_str())
.and_then(|s| hex::decode(s).ok())
.ok_or_else(|| anyhow::anyhow!("missing enroll_private_key"))?;
let enroll_seed: [u8; 32] = enroll_seed.try_into().map_err(|_| anyhow::anyhow!("bad enroll key"))?;
let mut dseed = [0u8; 32];
ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), &mut dseed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let device_kp = ring::signature::Ed25519KeyPair::from_seed_unchecked(&dseed)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let device_pub: [u8; 32] = ring::signature::KeyPair::public_key(&device_kp).as_ref().try_into().unwrap();
ui::say(&format!(" {} enrolling as delegated (caps: {:?})",
ui::paint(ui::Tone::Dim, "->"),
ak.caps.iter().map(|s| s.as_str()).collect::<Vec<_>>()));
let enroll_chan = crate::ephemeral::enroll_channel(&ak.issuer);
let my_uid = mk_uid("n");
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.channels = vec![enroll_chan.clone()];
sess.room = Some(format!("enrollup-{}", fresh_secret()));
sess.emit(&sio, "join", json!({ "room": sess.room.as_ref().unwrap(), "name": display_name(), "uid": my_uid })).await;
sess.emit(&sio, "subscribe", json!({ "channels": [enroll_chan] })).await;
let mut conn = Conn::for_command(server, sio.clone(), tx.clone(), my_uid, relay, to_name.clone(), false, direct::direct_enabled());
crate::ephemeral::register_enrollment(String::new(), enroll_seed, dseed, device_pub, ak.clone());
{
let tx = tx.clone();
tokio::spawn(async move { let _ = tokio::signal::ctrl_c().await; let _ = tx.send(Ev::Interrupted); });
}
let started = Instant::now();
let deadline = Duration::from_secs(60);
loop {
if started.elapsed() >= deadline { bail!("enrollment timed out"); }
let ev = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
let Ok(Some(ev)) = ev else {
if conn.active.is_some() { break; }
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?; }
}
}
Ev::PeerJoined(v) => { conn.maybe_adopt(&v, true).await?; }
Ev::KnownPeer(v) => {
if !is_self_uid(&conn.my_uid, v["uid"].as_str())
&& v["channel"].as_str() == Some(enroll_chan.as_str())
{
let pid = v["id"].as_str().unwrap_or_default().to_string();
if !pid.is_empty() && !conn.links.contains_key(&pid) && !conn.direct_pending.contains_key(&pid) {
conn.roster.insert(pid.clone(), v.clone());
conn.establish_as(v.clone(), Some(false)).await?;
if conn.active.is_none() { conn.active = Some(pid); }
}
}
}
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::DirectReady(pid, t, route) => {
conn.adopt_direct(&pid, t.clone(), route);
let _ = tx.send(Ev::ChannelReady(pid, t));
}
Ev::ChannelReady(pid, t) => {
if let Some(l) = conn.link_mut(&pid) { l.transport = Some(t.clone()); l.presence = Presence::Ready; }
if conn.active.is_none() { conn.active = Some(pid.clone()); }
crate::ephemeral::register_enrollment(pid.clone(), enroll_seed, dseed, device_pub, ak.clone());
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-request", "auth_key": ak.to_json()})).await;
}
Ev::Control(pid, v) => match v["type"].as_str() {
Some("identity-auth-key-enroll-challenge") => {
if let Some(t) = conn.transport_of(&pid) {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let verifier_hex = v["verifier_pub"].as_str().unwrap_or_default();
let cert_val = v.get("device_cert").cloned().unwrap_or(serde_json::Value::Null);
if let (Ok(nonce_bytes), Ok(verifier_bytes)) = (hex::decode(nonce_hex), hex::decode(verifier_hex)) {
if let (Ok(nonce_arr), Ok(verifier_pub)) = (
nonce_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
verifier_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
) {
if let Some(response) = crate::ephemeral::build_enrollment_response(&pid, nonce_arr, verifier_pub, &cert_val) {
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-response",
"auth_key": response["auth_key"],
"device_pub": response["device_pub"],
"enroll_possession_sig": response["enroll_possession_sig"],
"device_possession_sig": response["device_possession_sig"],
})).await;
}
}
}
}
}
Some("identity-auth-key-enroll-ack") => {
let dp_hex = v["device_pub"].as_str().unwrap_or("?");
ui::say(&format!(" {} enrolled (device_pub: {})", ui::paint(ui::Tone::Ok, ui::glyph_ok()), &dp_hex[..16]));
conn.active = Some(pid.clone());
break;
}
Some("identity-auth-key-enroll-error") => {
bail!("enrollment denied: {}", v["reason"].as_str().unwrap_or("unknown"));
}
_ => {}
},
Ev::Interrupted => bail!("interrupted"),
_ => {}
}
}
ui::say(&format!(" {} opening shell to port {}", ui::paint(ui::Tone::Dim, "->"), rport));
let active_pid = conn.active.as_ref().cloned().unwrap_or_default();
let t = conn.transport_of(&active_pid)
.ok_or_else(|| anyhow::anyhow!("no transport after enrollment"))?;
let r = t.send_control(&json!({
"type": "l2-open",
"host": "127.0.0.1",
"rport": rport,
})).await;
match r {
Ok(()) => {
ui::say(&format!(" {} shell request sent", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
let shell_start = Instant::now();
loop {
if shell_start.elapsed() >= Duration::from_secs(30) {
bail!("shell request timed out (no response from owner)");
}
let ev = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
let Ok(Some(ev)) = ev else { continue; };
if let Ev::Control(ref pid, ref v) = ev {
if pid == &active_pid {
match v["type"].as_str() {
Some("l2-open-ack") => {
ui::say(&format!(" {} shell authorized (sid {})", ui::paint(ui::Tone::Ok, ui::glyph_ok()), v["sid"]));
break;
}
Some("l2-close") => {
bail!("shell refused: {}", v["err"].as_str().unwrap_or("not authorized"));
}
_ => {}
}
}
}
if matches!(ev, Ev::Interrupted) { bail!("interrupted"); }
}
}
Err(e) => bail!("failed to send shell request: {e}"),
}
Ok(())
}
async fn respond_to_auth_key_enroll_request(
conn: &mut Conn,
pid: String,
v: serde_json::Value,
) {
let Some(t) = conn.transport_of(&pid) else { return };
if let Err(e) = crate::ephemeral::check_rate_limit(&pid) {
ui::debug(&format!("enroll request rate-limited: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
if !crate::capability::cap_authoritative() {
ui::debug("enroll request declined: capability enforcement not authoritative (set FILAMENT_CAP_AUTHORITATIVE=1)");
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-error",
"reason": "delegated principal: capability enforcement not authoritative"
})).await;
return;
}
let ak = match crate::ephemeral::AuthKey::from_json(&v.get("auth_key").unwrap_or(&v)) {
Some(ak) => ak,
None => {
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
let owner_pub = match crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
Ok(Some(uk)) => uk.public_key_bytes(),
_ => {
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
let verifier_pub = match crate::overlay::overlay_pubkey_bytes() {
Ok(pk) => pk,
Err(e) => {
ui::debug(&format!("enroll request overlay-key error: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
if let Err(e) = ak.verify_against_owner(&owner_pub, &verifier_pub) {
ui::debug(&format!("enroll request auth-key rejected: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
let nonce = match crate::ephemeral::generate_nonce(&pid) {
Ok(n) => n,
Err(e) => {
ui::debug(&format!("enroll request nonce CSPRNG failure: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
let cert_value = local_device_cert().map(|c| c.to_json());
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-challenge",
"nonce": hex::encode(nonce),
"verifier_pub": hex::encode(verifier_pub),
"device_cert": cert_value,
})).await;
}
async fn handle_auth_key_enroll_response(
conn: &mut Conn,
pid: String,
v: serde_json::Value,
) {
let Some(t) = conn.transport_of(&pid) else { return };
let payload = match crate::ephemeral::EnrollmentPayload::from_json(&v) {
Some(p) => p,
None => {
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
let owner_pub = match crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
Ok(Some(uk)) => uk.public_key_bytes(),
_ => {
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
let verifier_pub = match crate::overlay::overlay_pubkey_bytes() {
Ok(pk) => pk,
Err(e) => {
ui::debug(&format!("enroll response overlay-key error: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
let nonce = match crate::ephemeral::consume_latest_nonce(&pid) {
Ok(n) => n,
Err(e) => {
ui::debug(&format!("enroll response nonce consumption failed: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
};
match payload.verify(&owner_pub, &nonce, &verifier_pub) {
Ok((enroll_pub, device_pub, ak)) => {
if let Err(e) = crate::ephemeral::burn_auth_key(&enroll_pub, &ak.reuse) {
ui::debug(&format!("enroll response burn failed: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
return;
}
if let Some(link) = conn.link_mut(&pid) {
link.admit_delegated(owner_pub, device_pub, ak.expires, ak.caps.clone());
}
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-ack",
"device_pub": hex::encode(device_pub),
"expires": ak.expires
})).await;
ui::say(&format!(" {} ephemeral device {pid} enrolled (caps: {:?})",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ak.caps));
}
Err(e) => {
ui::debug(&format!("enroll response verify failed: {e}"));
let _ = t.send_control(&json!({"type": "identity-auth-key-enroll-error", "reason": "enrollment denied"})).await;
}
}
}
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(())
}
}
}
fn reset_remove(path: &std::path::Path, label: &str, wiped: &mut Vec<String>) {
let removed = if path.is_dir() {
std::fs::remove_dir_all(path).is_ok()
} else if path.exists() {
std::fs::remove_file(path).is_ok()
} else {
false
};
if removed {
wiped.push(format!("{label} ({})", path.display()));
}
}
fn reset_cmd(ui_caps: &UiCapability) -> Result<()> {
if let Some(pid) = daemon_alive() {
bail!("the filament daemon is running (pid {pid}); run `filament down` first, then `filament reset`");
}
ui_caps.confirm("wipe ALL local filament state (identity, devices, caps, managed ssh keys) on this machine")?;
let cfg = crate::settings::config_dir();
let mut wiped: Vec<String> = Vec::new();
let ak_path = crate::sshkeys::authorized_keys_path();
if let Ok(existing) = std::fs::read_to_string(&ak_path) {
let mut content = existing.clone();
let mut stripped: Vec<String> = Vec::new();
for (name, _) in devices_load() {
if crate::sshkeys::has_block(&content, &name) {
content = crate::sshkeys::strip_block(&content, &name);
stripped.push(name);
}
}
if content != existing {
if crate::platform::SecretFile::write_str(&ak_path, &content).is_ok() {
wiped.push(format!(
"managed authorized_keys blocks: {} ({})",
stripped.join(", "),
ak_path.display()
));
} else {
ui::say(&ui::paint(
ui::Tone::Warn,
&format!(" could not rewrite {} — leaving it untouched", ak_path.display()),
));
}
}
}
reset_remove(&cfg.join("identity.ed25519"), "user identity key", &mut wiped);
reset_remove(&cfg.join("overlay.ed25519"), "overlay key", &mut wiped);
reset_remove(&cfg.join("devices.json"), "paired-device store (device certs)", &mut wiped);
reset_remove(&cfg.join("caps.json"), "capability store", &mut wiped);
reset_remove(&cfg.join("requests.json"), "pending consent requests", &mut wiped);
reset_remove(&cfg.join("expose.json"), "exposed-service records", &mut wiped);
reset_remove(&cfg.join("mounts.json"), "mount records", &mut wiped);
reset_remove(&cfg.join("l2-allow.json"), "L2 forward allowlist", &mut wiped);
reset_remove(&cfg.join("signaling-dns.json"), "signaling DNS cache", &mut wiped);
reset_remove(&cfg.join("peerconf"), "per-peer settings", &mut wiped);
reset_remove(&cfg.join("config"), "global settings", &mut wiped);
reset_remove(&cfg.join("diag.jsonl"), "diagnostics log", &mut wiped);
reset_remove(&cfg.join("mount-profiles"), "saved mount profiles", &mut wiped);
reset_remove(&cfg.join("ssh"), "managed ssh material (key, known_hosts, cache)", &mut wiped);
crate::capability::invalidate_cap_cache();
if wiped.is_empty() {
ui::say(" nothing to wipe — no local filament state found");
} else {
ui::say(&format!(" {} wiped local filament state:", ui::paint(ui::Tone::Ok, ui::glyph_ok())));
for line in &wiped {
ui::say(&format!(" - {line}"));
}
ui::say(" this machine is now a clean slate (re-pair / `filament identity init` to start over)");
}
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(),
buffered_offers: 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] {
use ring::rand::{SecureRandom, SystemRandom};
let rng = SystemRandom::new();
let mut a_pid: Option<String> = None;
let mut b_pid: Option<String> = None;
for (pid, is_b) in &who {
if *is_b {
b_pid = Some(pid.clone());
} else {
a_pid = Some(pid.clone());
}
}
if let (Some(a_pid), Some(b_pid)) = (a_pid, b_pid) {
if let (Some(t_a), Some(t_b)) = (conn.transport_of(&a_pid), conn.transport_of(&b_pid)) {
let mut nonce_a = [0u8; 32];
let mut nonce_b = [0u8; 32];
let _ = rng.fill(&mut nonce_a);
let _ = rng.fill(&mut nonce_b);
let a_device_pub = device_cert_for(&a_name).map(|c| c.device_pub).unwrap_or_else(|| overlay::overlay_pubkey_bytes().unwrap_or([0u8; 32]));
let b_device_pub = device_cert_for(&b_name).map(|c| c.device_pub).unwrap_or_else(|| overlay::overlay_pubkey_bytes().unwrap_or([0u8; 32]));
let challenge_a_to_b = json!({
"type": "identity-nonce-challenge",
"nonce": hex::encode(nonce_a),
"receiver_device_pub": hex::encode(a_device_pub)
});
let challenge_b_to_a = json!({
"type": "identity-nonce-challenge",
"nonce": hex::encode(nonce_b),
"receiver_device_pub": hex::encode(b_device_pub)
});
let _ = t_b.send_control(&challenge_a_to_b).await;
let _ = t_a.send_control(&challenge_b_to_a).await;
ui::say(&ui::paint(ui::Tone::Dim, " identity challenge exchanged, waiting for sealed certs..."));
tokio::time::sleep(Duration::from_millis(800)).await;
}
}
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 let Err(why) = crate::pake::words::validate_chosen_password(&words, &display_name()) {
bail!("'{w}' is too weak: {why}");
}
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 peer_identity_cert: Option<identity::DeviceCert> = None;
let mut sent_identity: bool = false;
let mut identity_exchange_window: Option<std::time::Instant> = None;
let mut pake_peer: Option<String> = None;
let caps = pair_v2_caps();
let scope = crate::identity::IntroScope::Device.to_byte();
let mut cer = Ceremony::new(&my_words, &my_nameplate, caps.clone(), scope);
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() {
if identity_exchange_window.map_or(false, |dl| std::time::Instant::now() < dl) && peer_identity_cert.is_none() {
if !sent_identity {
if let Some(local_cert) = local_device_cert() {
if let Some(ref pid2) = pake_peer {
if let Some(k) = cer.k() {
if let Some(l) = conn.link(pid2) {
if let Some((my_fp, their_fp)) = match &l.peer { Some(p) => p.fingerprints().await, None => None } {
let cmv = crate::pake::our_confirm(k, &my_fp, &their_fp, cer.caps_canon(), cer.scope());
let mut cmv_arr = [0u8; 32];
cmv_arr.copy_from_slice(&cmv);
let dpub = local_cert.device_pub;
let scope_b = cer.scope();
let caps_d = crate::identity::caps_digest(cer.caps_canon());
let ch = crate::identity::cert_hash(&local_cert);
let rz = [0u8; 32];
let pmsg = crate::identity::possession_msg(0x01, &cmv_arr, scope_b, &caps_d, &ch, &dpub, &rz);
if let Ok(psig) = crate::overlay::overlay_sign_possession(&pmsg) {
let inner = serde_json::json!({"cert":local_cert.to_json(),"possession_sig":hex::encode(psig),"device_pub":hex::encode(dpub)});
let sk = crate::identity::sealing_key_from_k(k);
if let Ok((n, s)) = crate::identity::seal_plaintext(&sk, inner.to_string().as_bytes()) {
sio.emit("signal", serde_json::json!({"to":pid2,"data":{"type":"identity-expose","v":2,"nonce":hex::encode(n),"sealed":hex::encode(s)}})).await.ok();
sent_identity = true;
}
}
}
}
}
}
}
}
} else {
if peer_identity_cert.is_none() {
let p = devices_path();
if let Ok(raw) = std::fs::read_to_string(&p) {
if let Ok(arr) = serde_json::from_str::<Vec<Value>>(&raw) {
if let Err(e) = identity::check_fail_closed(&arr, &n, None) {
bail!("fail-closed: {}", e);
}
}
}
devices_store_v2(&n, &sec, &caps)?;
} else {
let pcert = peer_identity_cert.as_ref().unwrap();
devices_upsert_atomic(&n, Some(&sec), Some(pcert), Some(&caps), Some(scope), None)
.context("atomic store secret+cert")?;
store_provisional_identity(&n, pcert)
.context("store provisional")?;
}
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();
}
}
}
}
if !sent_identity {
if agreed_secret.is_some() {
if let Some(local_cert) = local_device_cert() {
if let Some(pid2) = pake_peer.clone() {
if let Some(k) = cer.k() {
if let Some(l) = conn.link(&pid2) {
if let Some((my_fp, their_fp)) = match &l.peer { Some(p) => p.fingerprints().await, None => None } {
let confirm_mac_vec = crate::pake::our_confirm(k, &my_fp, &their_fp, cer.caps_canon(), cer.scope());
let mut cmv_arr = [0u8; 32];
cmv_arr.copy_from_slice(&confirm_mac_vec);
let device_pub = local_cert.device_pub;
let scope_byte = cer.scope();
let caps_digest = crate::identity::caps_digest(cer.caps_canon());
let chash = crate::identity::cert_hash(&local_cert);
let receiver_zero = [0u8; 32];
let possession_msg = crate::identity::possession_msg(
0x01, &cmv_arr, scope_byte, &caps_digest, &chash, &device_pub, &receiver_zero
);
if let Ok(possession_sig) = crate::overlay::overlay_sign_possession(&possession_msg) {
let inner = serde_json::json!({
"cert": local_cert.to_json(),
"possession_sig": hex::encode(possession_sig),
"device_pub": hex::encode(device_pub)
});
let sealing_key = crate::identity::sealing_key_from_k(k);
if let Ok((nonce, sealed)) = crate::identity::seal_plaintext(&sealing_key, inner.to_string().as_bytes()) {
let payload = serde_json::json!({
"type": "identity-expose",
"v": 2,
"nonce": hex::encode(nonce),
"sealed": hex::encode(sealed)
});
sio.emit("signal", serde_json::json!({"to": pid2, "data": payload})).await.ok();
sent_identity = true;
}
}
}
}
}
}
}
}
}
}
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());
identity_exchange_window = Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
}
}
PakeInbound::Abort(why) => {
bail!("pairing REFUSED: {why}. Nothing was stored; ask for a FRESH code.");
}
PakeInbound::Ignored => {}
}
continue;
}
if data["type"].as_str() == Some("identity-expose") {
if let Some(k) = cer.k() {
if let Some(nonce_hex) = data.get("nonce").and_then(|v| v.as_str()) {
if let Some(sealed_hex) = data.get("sealed").and_then(|v| v.as_str()) {
if let Ok(nonce_bytes) = hex::decode(nonce_hex) {
if let Ok(sealed_bytes) = hex::decode(sealed_hex) {
if nonce_bytes.len() == 12 {
let mut nonce_arr = [0u8; 12];
nonce_arr.copy_from_slice(&nonce_bytes);
let sealing_key = identity::sealing_key_from_k(k);
if let Ok(plaintext) = identity::open_sealed(&sealing_key, &nonce_arr, &sealed_bytes) {
if let Ok(inner) = serde_json::from_slice::<Value>(&plaintext) {
if let Some(cert_json) = inner.get("cert") {
if let Some(cert) = identity::DeviceCert::from_json(cert_json) {
if cert.verify(identity::now_secs()).is_ok() {
if let Some(sig_hex) = inner.get("possession_sig").and_then(|v| v.as_str()) {
if let Ok(sig_bytes) = hex::decode(sig_hex) {
if sig_bytes.len() == 64 {
let mut sig_arr = [0u8; 64];
sig_arr.copy_from_slice(&sig_bytes);
let fps2 = match conn.link(&from) {
Some(l) => match &l.peer { Some(p) => p.fingerprints().await, None => None },
None => None,
};
if let Some((my_fp2, their_fp2)) = fps2 {
let (lo, hi) = crate::pake::sort_fps(&my_fp2, &their_fp2);
let scope = cer.scope();
let (_send, expect_dir) = crate::pake::confirm_dirs(&my_fp2, lo);
let expected_confirm_vec = crate::pake::confirm_mac(k, expect_dir, lo, hi, cer.caps_canon(), scope);
let mut cmv_arr = [0u8; 32];
cmv_arr.copy_from_slice(&expected_confirm_vec);
let chash = identity::cert_hash(&cert);
let caps_digest = identity::caps_digest(cer.caps_canon());
let sender_pub = cert.device_pub;
let receiver_zero = [0u8; 32];
let possession_msg = identity::possession_msg(
0x01, &cmv_arr, scope, &caps_digest, &chash, &sender_pub, &receiver_zero
);
if identity::verify_possession_sig(&cert.device_pub, &possession_msg, &sig_arr).is_ok() {
match crate::overlay::overlay_pubkey_bytes() {
Ok(own_dpub) if cert.device_pub == own_dpub => {
}
Ok(_) => {
peer_identity_cert = Some(cert);
}
Err(_) => {
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
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>,
identity_device_pub: Option<[u8; 32]>,
identity_user_pub: Option<[u8; 32]>,
identity_binding: crate::capability::BindingStrength,
identity_cert_expires: Option<u64>,
principal_kind: crate::capability::PrincipalKind,
}
impl Link {
fn shown(&self) -> &str {
self.verified_name.as_deref().unwrap_or(&self.name)
}
fn admit_delegated(&mut self, owner_pub: [u8; 32], device_pub: [u8; 32], expires: u64, caps: Vec<String>) {
self.identity_user_pub = Some(owner_pub);
self.identity_device_pub = Some(device_pub);
self.identity_binding = crate::capability::BindingStrength::Proven;
self.identity_cert_expires = Some(expires);
self.principal_kind = crate::capability::PrincipalKind::Delegated { caps };
}
}
#[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>,
buffered_offers: HashMap<String, (Vec<String>, Option<String>)>,
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(),
buffered_offers: 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);
self.buffered_offers.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()),
identity_device_pub: None,
identity_user_pub: None,
identity_binding: crate::capability::BindingStrength::None,
identity_cert_expires: None,
principal_kind: crate::capability::PrincipalKind::OwnerDevice,
},
);
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,
},
);
if let Some((cands, srflx)) = self.buffered_offers.remove(pid) {
ui::debug(&format!("replaying buffered transport-offer from {pid}"));
self.on_transport_offer(pid, cands, srflx);
}
}
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());
}
resolve_peer_identity(existing);
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()),
identity_device_pub: None,
identity_user_pub: None,
identity_binding: crate::capability::BindingStrength::None,
identity_cert_expires: None,
principal_kind: crate::capability::PrincipalKind::OwnerDevice,
},
);
}
}
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 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);
let count = k - 1;
let pid = pid.to_string();
let tx = self.tx.clone();
let primary = primary.clone();
if answerer {
let mut endpoints = Vec::with_capacity(count);
let mut ports = Vec::with_capacity(count);
for _ in 0..count {
if let Ok((ep, port)) = direct::bind_endpoint() {
endpoints.push(ep);
ports.push(port);
}
}
if endpoints.is_empty() {
return;
}
let offer = json!({ "type": "worker-ports", "v": 1, "ports": ports, "for": &pid });
let p = primary.clone();
tokio::spawn(async move {
let _ = p.send_control(&offer).await;
});
tokio::spawn(async move {
let workers = direct::accept_workers(endpoints, tkey, pid.clone(), tx.clone(), count).await;
if !workers.is_empty() {
let _ = tx.send(net::Ev::DirectWorkersReady(pid, workers));
}
});
} else {
let peer_ip = match primary.remote_addr().map(|a| a.ip()) {
Some(ip) => ip,
None => return,
};
let (port_tx, port_rx) = tokio::sync::oneshot::channel();
self.worker_port_tx.insert(pid.clone(), port_tx);
tokio::spawn(async move {
let ports = match tokio::time::timeout(std::time::Duration::from_secs(5), port_rx).await
{
Ok(Ok(p)) => p,
_ => return,
};
let workers = direct::dial_workers(ports, peer_ip, tkey, pid.clone(), tx.clone(), count).await;
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 => {
self.note_progress(pid);
None
}
resilience::Liveness::Establishing => {
let in_repair = self.resil.stall_repairs.get(pid)
.map(|s| s.pending)
.unwrap_or(false);
if !in_repair {
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 dead_on_entry = self.transport_of(pid).map(|t| t.is_dead()).unwrap_or(false);
let st = self.resil.stall_repairs.entry(pid.to_string()).or_default();
st.pending = true;
if dead_on_entry && st.attempts == 0 {
st.attempts = 1;
}
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()),
identity_device_pub: None,
identity_user_pub: None,
identity_binding: crate::capability::BindingStrength::None,
identity_cert_expires: None,
principal_kind: crate::capability::PrincipalKind::OwnerDevice,
},
);
}
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_val) = v["id"].as_str() else { return false };
let pid = pid_val.to_string();
self.roster.remove(&pid);
if !self.links.contains_key(&pid) {
return false;
}
if matches!(self.link(&pid).map(|l| &l.principal_kind), Some(crate::capability::PrincipalKind::Delegated { .. })) {
let name = self.link(&pid).map(|l| l.name.clone()).unwrap_or_default();
self.drop_link(&pid);
ui::debug(&format!("ephemeral device {name} disconnected, enrollment revoked"));
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::Arm { key_id, expiry } => {
crate::ephemeral::arm(key_id.clone(), *expiry);
req.reply(&json!({ "ok": true })).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,
ctl::ReqKind::CapStatus => req.reject("cap-status not handled here").await,
ctl::ReqKind::ListPending => req.reject("list-pending not handled here").await,
ctl::ReqKind::ApproveRequest { .. } => req.reject("approve-request not handled here").await,
ctl::ReqKind::DenyRequest { .. } => req.reject("deny-request 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 first == "help" {
argv.remove(1); argv.push("--help".into());
} else 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 first == "init" {
eprintln!(" did you mean `filament identity init`?");
} else 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);
}
}
}
if argv.get(1).map(String::as_str) == Some("devices")
&& argv.get(2).map(String::as_str) == Some("remove")
{
eprintln!("filament: `devices remove` is not a command");
eprintln!(" did you mean `filament devices forget <name>`?");
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, auth_key } => {
if let Some(ak_path) = auth_key {
enroll_and_send_cmd(&server, ak_path, to, paths, relay, remember).await
} else {
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::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::Identity { action } => {
match action {
IdentityAction::Init => {
match identity::UserKey::load(&crate::platform::PlatformKeyStore)? {
Some(uk) => {
println!("{}", ui::paint(ui::Tone::Dim,
&format!("you already have a user identity: fingerprint {}",
uk.fingerprint())));
println!(" use 'filament identity show' to see it");
}
None => {
let uk = identity::UserKey::generate(&crate::platform::PlatformKeyStore)?;
ensure_self_genesis_header(&crate::settings::config_dir(), &uk);
println!(" {} user identity created: fingerprint {}",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ui::paint(ui::Tone::Bold, &uk.fingerprint()));
println!(" {}", ui::paint(ui::Tone::Dim, "next: certify your devices with 'filament identity certify <device>'"));
}
}
Ok(())
}
IdentityAction::Show => {
match identity::UserKey::load(&crate::platform::PlatformKeyStore)? {
None => {
println!("no user identity yet. Run 'filament identity init' to create one.");
}
Some(uk) => {
let uk_pub = uk.public_key_bytes();
println!(" user fingerprint: {}", ui::paint(ui::Tone::Bold, &uk.fingerprint()));
println!(" public key: {}", ui::paint(ui::Tone::Dim, &uk.public_key_hex()));
let devices = devices_load();
let mut found = 0usize;
for (name, _secret) in &devices {
if let Some(cert) = device_cert_for(name) {
if cert.user_pub == uk_pub {
let exp = if identity::now_secs() >= cert.expires {
"EXPIRED".to_string()
} else {
format!("{}d", cert.expires.saturating_sub(identity::now_secs()) / 86400)
};
println!(" {} {}", ui::paint(ui::Tone::Bold, name), ui::paint(ui::Tone::Dim, &format!("(valid {exp})")));
found += 1;
}
}
}
if found == 0 {
println!(" {}", ui::paint(ui::Tone::Dim, "no certified devices. use 'filament identity certify <device>'"));
}
}
}
Ok(())
}
IdentityAction::Certify { device } => {
let uk = match identity::UserKey::load(&crate::platform::PlatformKeyStore)? {
Some(uk) => uk,
None => bail!("no user identity. Run 'filament identity init' first."),
};
let device_pub = crate::overlay::overlay_pubkey_bytes()?;
let now = identity::now_secs();
let cert = identity::DeviceCert::certify(&uk, device_pub, now, identity::CERT_TTL_SECS)?;
update_device_cert(&device, &uk, &cert)?;
ui::say(&format!(
" {} {} certified as your device (valid {} days)",
ui::paint(ui::Tone::Ok, ui::glyph_ok()),
ui::paint(ui::Tone::Bold, &device),
identity::CERT_TTL_SECS / 86400,
));
Ok(())
}
}
}
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::Reset => reset_cmd(&ui_caps),
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)");
}
Some(DevicesAction::Vouch { a, b }) => {
introduce_cmd(&server, &a, &b, relay).await?;
}
}
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::Shell { peer, ssh, args } => {
if ssh {
l2::ssh_cmd(&server, &peer, &args, relay).await
} else {
l2::pty_cmd(&server, &peer, relay, args).await
}
},
Cmd::Reach { dev_port, socks, port, bind, http_port } => {
if socks {
l2::proxy_cmd(&server, &bind, port, http_port, relay).await
} else if let Some(dp) = dev_port {
let parts: Vec<&str> = dp.splitn(2, ':').collect();
if parts.len() == 2 {
let peer = parts[0].to_string();
let rport: u16 = parts[1].parse().map_err(|_| anyhow!("invalid port in '{}'", dp))?;
l2::netcat_cmd(&server, &peer, rport, relay).await
} else {
bail!("reach requires <device>:<port> format, e.g. `filament reach laptop:5432`");
}
} else {
bail!("reach requires <device>:<port> or --socks. Run `filament reach --help` for usage.");
}
},
Cmd::Forward { lport, peer, rport } => l2::forward_cmd(&server, lport, &peer, rport, relay).await,
Cmd::Expose { port, to, peer, list, off } => {
if off {
if let Some(p) = port {
ui_caps.confirm("unexpose a port")?;
expose::unexpose_cmd(p).await
} else {
bail!("expose --off requires a port number");
}
} else {
expose::expose_cmd(port, to, peer, list).await
}
},
Cmd::Doctor { device, watch, repeat, json } => {
doctor::doctor_cmd(&server, device, watch, repeat, json, relay).await
}
Cmd::Grant { device, capability, tag } => {
let config_dir = crate::settings::config_dir();
let mut store = crate::capability::load_cap_store(&config_dir);
if let Some(ref t) = tag {
let Some(user_key) = load_owner_key() else {
bail!("identity not initialized");
};
let pk = user_key.public_key_bytes();
let target_bytes = crate::capability::make_tag_target(&pk, t);
let ver = crate::capability::hlc_next(0, crate::capability::now_ms());
let mut op = crate::capability::CapOp {
op: crate::capability::CapOpKind::Grant,
grantor: pk,
target_kind: 0x03,
target: target_bytes,
resource: "self".to_string(),
permissions: vec![capability.clone()],
expires: crate::capability::now_secs().saturating_add(90 * 24 * 3600),
issued_at: crate::capability::now_secs(),
version: ver,
sig: [0u8; 64],
};
op.sig = crate::capability::sign_cap_op(&op, user_key.keypair());
store.push(op.to_json());
let _ = crate::capability::save_and_list_revoked(&store, &config_dir)
.context("save cap store")?;
println!("granted '{capability}' to tag '{t}'.");
return Ok(());
}
device_set_cap(&device, &capability, true)?;
if let Ok(Some(user_key)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
let config_dir = crate::settings::config_dir();
let mut store = crate::capability::load_cap_store(&config_dir);
let pk = user_key.public_key_bytes();
let has_header = store.iter().any(|e| {
e.get("type").and_then(|v| v.as_str()) == Some("cap_header")
&& e["resource"].as_str() == Some("self")
});
if !has_header {
let pk = user_key.public_key_bytes();
let nonce = crate::capability::self_resource_nonce();
let resource = crate::capability::self_resource_id(&pk);
let mut hdr = crate::capability::CapHeader {
resource,
epoch: 0,
owner_pub: pk,
nonce,
floors: vec![],
issued_at: crate::capability::now_secs(),
prev_owner_pub: None,
prev_header_hash: None,
sig: [0u8; 64],
};
hdr.sig = crate::capability::sign_cap_header(&hdr, &user_key.keypair());
let mut hdr_json = hdr.to_json();
hdr_json["resource"] = serde_json::json!("self");
store.push(hdr_json);
}
let Some(peer_cert) = device_cert_for(&device) else {
return Err(anyhow!(
"peer identity for '{device}' is not available. Pair with the peer first so their identity can be certified; the grant requires a known user key to target"
));
};
if peer_cert.verify(crate::identity::now_secs()).is_err() {
return Err(anyhow!("peer identity cert for '{device}' is expired; re-pair to refresh it"));
}
let target_arr = peer_cert.user_pub;
let v = crate::capability::hlc_next(0, crate::capability::now_ms());
let mut op = crate::capability::CapOp {
op: crate::capability::CapOpKind::Grant,
grantor: pk,
target_kind: 0x00, target: target_arr,
resource: "self".to_string(),
permissions: vec![capability.clone()],
expires: crate::capability::now_secs().saturating_add(90 * 24 * 3600),
issued_at: crate::capability::now_secs(),
version: v,
sig: [0u8; 64],
};
op.sig = crate::capability::sign_cap_op(&op, &user_key.keypair());
let mut hdr_json = serde_json::json!({
"type": "cap_grant",
});
let mut op_json = op.to_json();
op_json["type"] = serde_json::json!("cap_grant");
store.push(op_json);
crate::capability::update_ratchet(&mut store, &pk, op.issued_at)
.context("capability grant created but ratchet initialization failed; the grant will not be effective. Re-run the grant command")?;
let revoked = crate::capability::save_and_list_revoked(&store, &config_dir)
.context("save cap store")?;
{
let authoritative = crate::capability::cap_authoritative();
let ak_path = sshkeys::authorized_keys_path();
let ak_content = std::fs::read_to_string(&ak_path).unwrap_or_default();
for device in &revoked {
if sshkeys::has_block(&ak_content, device) && !authoritative {
eprintln!("CAP-SHADOW RECONCILE: WOULD remove shell key for '{device}' (cap store denies shell); NOT removing in shadow");
}
}
let new_ak = crate::capability::reconcile_shell_keys(&revoked, &ak_content, authoritative);
if new_ak != ak_content {
if let Err(e) = crate::platform::SecretFile::write_str(&ak_path, &new_ak) {
eprintln!("shell-key reconcile: failed to write authorized_keys: {e}");
}
}
}
}
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 let Ok(Some(user_key)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
let config_dir = crate::settings::config_dir();
let mut store = crate::capability::load_cap_store(&config_dir);
let pk = user_key.public_key_bytes();
let header = store
.iter()
.find(|e| {
e.get("type").and_then(|v| v.as_str()) == Some("cap_header")
&& e["resource"].as_str() == Some("self")
})
.and_then(crate::capability::CapHeader::from_json);
if let (Some(hdr), Some(peer_cert)) = (header, device_cert_for(&device)) {
let target_arr = peer_cert.user_pub;
let existing_ver = store
.iter()
.filter(|e| {
e.get("type").and_then(|v| v.as_str()) == Some("cap_grant")
&& e["grantor"].as_str() == Some(hex::encode(pk).as_str())
&& e["resource"].as_str() == Some("self")
&& e["target"].as_str() == Some(hex::encode(target_arr).as_str())
})
.filter_map(|e| e["version"].as_u64())
.max()
.unwrap_or(0);
let v = crate::capability::hlc_next(existing_ver, crate::capability::now_ms());
let now = crate::capability::now_secs();
let mut op = crate::capability::CapOp {
op: crate::capability::CapOpKind::Revoke,
grantor: pk,
target_kind: 0x00, target: target_arr,
resource: "self".to_string(),
permissions: vec![capability.clone()],
expires: now.saturating_add(90 * 24 * 3600),
issued_at: now,
version: v,
sig: [0u8; 64],
};
op.sig = crate::capability::sign_cap_op(&op, user_key.keypair());
match crate::capability::apply_cap_op(&mut store, &hdr, &op, now) {
Ok(()) => {
let revoked =
crate::capability::save_and_list_revoked(&store, &config_dir)
.unwrap_or_default();
let authoritative = crate::capability::cap_authoritative();
let ak_path = sshkeys::authorized_keys_path();
let ak_content = std::fs::read_to_string(&ak_path).unwrap_or_default();
for d in &revoked {
if sshkeys::has_block(&ak_content, d) && authoritative {
eprintln!("shell-key reconcile (revoke): removing managed key for '{d}' (cap store denies shell)");
}
}
let new_ak = crate::capability::reconcile_shell_keys(
&revoked,
&ak_content,
authoritative,
);
if new_ak != ak_content {
if let Err(e) =
crate::platform::SecretFile::write_str(&ak_path, &new_ak)
{
eprintln!("shell-key reconcile (revoke): failed to write authorized_keys: {e}");
}
}
}
Err(e) => {
eprintln!("revoke: owner-signed cap_op not applied ({e}); legacy revoke still took effect");
}
}
}
}
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, off } => {
if let Some(path) = off {
ui_caps.confirm(&format!("unmount {path}"))?;
mount::unmount_cmd(&path)
} else 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::Requests { action } => requests_cmd(action).await,
Cmd::Ephemeral { action } => ephemeral_cmd(&server, action, relay).await,
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 let Err(why) = crate::pake::words::validate_chosen_password(&words, &display_name()) {
bail!("'{w}' is too weak: {why}");
}
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(), crate::identity::IntroScope::Device.to_byte()))
} 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) {
let transport_dead = conn.transport_of(&active).map(|t| t.is_dead()).unwrap_or(false);
if transport_dead || 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;
}
if conn.direct_pending.contains_key(&from) {
conn.on_transport_offer(&from, cands, srflx);
} else {
let known = crate::devices_load()
.into_iter()
.find(|(n, _)| conn.links.get(&from).map(|l| l.name == *n).unwrap_or(false));
if let Some((name, secret)) = known {
conn.start_direct(&from, &name, &secret).await;
}
if conn.direct_pending.contains_key(&from) {
conn.on_transport_offer(&from, cands, srflx);
} else {
conn.buffered_offers.insert(from.clone(), (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 let Some(sec) = cer.secret() {
if !pake_done {
pake_done = true;
ui::say(&ui::paint(ui::Tone::Dim, " authenticated, sending"));
conn.drop_link(&from);
conn.start_direct(&from, &from, &sec).await;
if conn.active.is_none() {
conn.active = Some(from.clone());
}
}
}
}
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();
ui::trace(&format!("[T:SERVE] worker-ports handler: looking up key={pid}"));
if let Some(tx) = conn.worker_port_tx.remove(pid) {
ui::trace(&format!("[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);
}
}
Some("identity-nonce-challenge") => {
if let Some(t) = conn.transport_of(&pid) {
respond_to_identity_challenge(&t, &v).await;
}
}
Some("identity-auth-key-enroll-challenge") => {
if let Some(t) = conn.transport_of(&pid) {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let verifier_hex = v["verifier_pub"].as_str().unwrap_or_default();
if let (Ok(nonce_bytes), Ok(verifier_bytes)) = (hex::decode(nonce_hex), hex::decode(verifier_hex)) {
if let (Ok(nonce_arr), Ok(verifier_pub)) = (
nonce_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
verifier_bytes.as_slice().try_into().map(|a: &[u8; 32]| *a),
) {
let device_cert = v.get("device_cert").cloned().unwrap_or(serde_json::Value::Null);
if let Some(response) = crate::ephemeral::build_enrollment_response(
&pid, nonce_arr, verifier_pub, &device_cert,
) {
let _ = t.send_control(&json!({
"type": "identity-auth-key-enroll-response",
"auth_key": response["auth_key"],
"device_pub": response["device_pub"],
"enroll_possession_sig": response["enroll_possession_sig"],
"device_possession_sig": response["device_possession_sig"],
})).await;
}
}
}
}
}
_ 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 trace = cfg!(feature = "debug-logs") && std::env::var("FILAMENT_TRACE_THROUGHPUT").is_ok();
let mut f = tokio::fs::File::open(&path).await?;
f.seek(SeekFrom::Start(start)).await?;
let mut pos = start;
let mut buf_a = vec![0u8; chunk];
let mut buf_b = vec![0u8; chunk];
let mut using_buf_a = true;
let mut chunk_idx: u64 = 0;
let first_want = std::cmp::min(chunk as u64, end - pos) as usize;
if first_want == 0 {
return Ok(());
}
let t_first_read = if trace { Some(std::time::Instant::now()) } else { None };
let mut cur_n = f.read(&mut buf_a[..first_want]).await?;
let _first_read_us = t_first_read.map(|t| t.elapsed().as_micros()).unwrap_or(0);
if cur_n == 0 {
return Ok(());
}
while pos < end {
let next_want = std::cmp::min(chunk as u64, end - (pos + cur_n as u64)) as usize;
let next_read = if next_want > 0 {
let mut f2 = tokio::fs::File::open(&path).await?;
f2.seek(SeekFrom::Start(pos + cur_n as u64)).await?;
let alt_buf = if using_buf_a {
std::mem::replace(&mut buf_b, vec![0u8; chunk])
} else {
std::mem::replace(&mut buf_a, vec![0u8; chunk])
};
Some(tokio::spawn(async move {
let mut buf = alt_buf;
let n = f2.read(&mut buf[..next_want]).await?;
Ok::<(Vec<u8>, usize), anyhow::Error>((buf, n))
}))
} else {
None
};
let cur_buf = if using_buf_a { &buf_a[..cur_n] } else { &buf_b[..cur_n] };
let t_send_start = if trace { Some(std::time::Instant::now()) } else { None };
t.send_frame(sid, pos, cur_buf).await?;
let send_us = t_send_start.map(|t| t.elapsed().as_micros()).unwrap_or(0);
pos += cur_n as u64;
let total =
progress.fetch_add(cur_n as u64, std::sync::atomic::Ordering::Relaxed) + cur_n as u64;
chunk_idx += 1;
let should_tick = chunk_idx % 8 == 0 || pos >= end;
let t_tick_start = if trace && should_tick { Some(std::time::Instant::now()) } else { None };
if should_tick {
bar.lock().await.tick(total);
}
let tick_us = t_tick_start.map(|t| t.elapsed().as_micros()).unwrap_or(0);
if trace && (chunk_idx % 10 == 0 || send_us + tick_us > 5000) {
dlog!("[TRACE stream_one] chunk={} offset={} len={} send_frame={}us tick={}us", chunk_idx, pos - cur_n as u64, cur_n, send_us, tick_us);
}
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 })));
}
}
match next_read {
Some(handle) => match handle.await {
Ok(Ok((buf, n))) => {
if using_buf_a { buf_b = buf; } else { buf_a = buf; }
using_buf_a = !using_buf_a;
cur_n = n;
if cur_n == 0 { break; }
}
Ok(Err(e)) => return Err(e.into()),
Err(e) => return Err(anyhow!("read-ahead task panicked: {e}")),
},
None => break,
}
}
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(())
}
fn safe_incoming_name(raw: &str) -> String {
let base = std::path::Path::new(raw)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "file.bin".into());
let cleaned: String = base.chars().filter(|c| !c.is_control()).collect();
if cleaned.is_empty() || cleaned == "." || cleaned == ".." {
"file.bin".to_string()
} else {
cleaned
}
}
#[cfg(unix)]
async fn safe_create_part(path: &std::path::Path) -> std::io::Result<tokio::fs::File> {
#[cfg(target_os = "linux")]
{
let parent = path.parent().unwrap_or(std::path::Path::new("."));
let rel = path.strip_prefix(parent).unwrap_or(path);
crate::mount_proto::safe_open_beneath(parent, rel, (libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY) as i32)
.map_err(|e| std::io::Error::new(e.kind(), format!("safe create .part: {e}")))
.map(|f| tokio::fs::File::from_std(f))
}
#[cfg(not(target_os = "linux"))]
{
use std::os::unix::fs::OpenOptionsExt;
tokio::fs::OpenOptions::new()
.write(true)
.create_new(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.await
}
}
#[cfg(unix)]
async fn safe_resume_part(path: &std::path::Path) -> std::io::Result<tokio::fs::File> {
#[cfg(target_os = "linux")]
{
let parent = path.parent().unwrap_or(std::path::Path::new("."));
let rel = path.strip_prefix(parent).unwrap_or(path);
let file = crate::mount_proto::safe_open_beneath(parent, rel, libc::O_WRONLY | libc::O_NONBLOCK as i32)
.map_err(|e| std::io::Error::new(e.kind(), format!("safe resume .part: {e}")))?;
let meta = file.metadata()?;
if !meta.file_type().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("refusing to resume: .part is not a regular file (type: {:?})", meta.file_type()),
));
}
Ok(tokio::fs::File::from_std(file))
}
#[cfg(not(target_os = "linux"))]
{
use std::os::unix::fs::{OpenOptionsExt, MetadataExt};
let file = tokio::fs::OpenOptions::new()
.write(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.await?;
let meta = file.metadata().await?;
if !meta.file_type().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("refusing to resume: .part is not a regular file (type: {:?})", meta.file_type()),
));
}
Ok(file)
}
}
#[cfg(not(unix))]
async fn safe_create_part(path: &std::path::Path) -> std::io::Result<tokio::fs::File> {
tokio::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.await
}
#[cfg(not(unix))]
async fn safe_resume_part(path: &std::path::Path) -> std::io::Result<tokio::fs::File> {
tokio::fs::OpenOptions::new()
.write(true)
.open(path)
.await
}
#[cfg(not(unix))]
async fn safe_open_part(path: &std::path::Path) -> std::io::Result<tokio::fs::File> {
tokio::fs::OpenOptions::new()
.write(true)
.open(path)
.await
}
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, u64) {
let end = pos + len as u64;
if ranges.is_empty() {
ranges.push((pos, end));
let total = len as u64;
return (total, total);
}
let mut lo = 0usize;
let mut hi = ranges.len();
while lo < hi {
let mid = (lo + hi) / 2;
if ranges[mid].1 < pos {
lo = mid + 1;
} else {
hi = mid;
}
}
let mut idx = lo;
let mut new_s = pos;
let mut new_e = end;
let mut removed_total: u64 = 0;
while idx < ranges.len() && ranges[idx].0 <= new_e {
let (s, e) = ranges[idx];
removed_total += e - s;
new_s = new_s.min(s);
new_e = new_e.max(e);
ranges.remove(idx);
}
let new_len = new_e - new_s;
ranges.insert(idx, (new_s, new_e));
let delta = new_len.saturating_sub(removed_total);
let total: u64 = ranges.iter().map(|(s, e)| e - s).sum();
(delta, total)
}
fn record_range_total(ranges: &mut Vec<(u64, u64)>, pos: u64, len: usize) -> u64 {
let (_delta, total) = record_range(ranges, pos, len);
total
}
fn coverage_complete(ranges: &[(u64, u64)], size: u64) -> bool {
if size == 0 { return true; }
ranges.len() == 1 && ranges[0].0 == 0 && ranges[0].1 == size
}
fn first_gap(ranges: &[(u64, u64)], size: u64) -> Option<u64> {
if size == 0 { return None; }
let mut cursor = 0u64;
for &(s, e) in ranges {
if s > cursor { return Some(cursor); }
cursor = cursor.max(e);
if cursor >= size { return None; }
}
if cursor < size { Some(cursor) } else { None }
}
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,
}
}
fn ensure_self_genesis_header(
config_dir: &std::path::Path,
user_key: &crate::identity::UserKey,
) -> bool {
let mut store = crate::capability::load_cap_store(config_dir);
let pk = user_key.public_key_bytes();
let owner_hex = hex::encode(pk);
let has_header = store.iter().any(|e| {
e.get("type").and_then(|v| v.as_str()) == Some("cap_header")
&& e["resource"].as_str() == Some("self")
});
let has_ratchet = store.iter().any(|e| {
e.get("type").and_then(|v| v.as_str()) == Some("cap_ratchet")
&& e["owner_pub"].as_str() == Some(owner_hex.as_str())
});
if has_header && has_ratchet {
return false;
}
let now = crate::capability::now_secs();
if !has_header {
let nonce = crate::capability::self_resource_nonce();
let resource = crate::capability::self_resource_id(&pk);
let mut hdr = crate::capability::CapHeader {
resource,
epoch: 0,
owner_pub: pk,
nonce,
floors: vec![],
issued_at: now,
prev_owner_pub: None,
prev_header_hash: None,
sig: [0u8; 64],
};
hdr.sig = crate::capability::sign_cap_header(&hdr, &user_key.keypair());
let mut hdr_json = hdr.to_json();
hdr_json["resource"] = serde_json::json!("self");
store.push(hdr_json);
}
if !has_ratchet && crate::capability::update_ratchet(&mut store, &pk, now).is_err() {
return false;
}
crate::capability::save_cap_store(config_dir, &store).is_ok()
}
#[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 {
if let Ok(Some(uk)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
if ensure_self_genesis_header(&crate::settings::config_dir(), &uk) {
ui::debug("seeded owner self genesis capability header");
}
}
}
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 identity_nonces: HashMap<String, ([u8; 32], Instant, [u8; 32])> = HashMap::new();
let pending_proven: Arc<Mutex<HashMap<String, (Arc<dyn Transport>, Instant)>>> =
Arc::new(Mutex::new(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_pending_direct: HashMap<String, (Vec<String>, Option<String>)> = 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 crate::ephemeral::is_armed() {
ui::debug("enrollment armed: ephemeral devices may enroll");
} else {
ui::debug("enrollment closed (no armed keys — mint or arm an auth-key to open)");
}
let chans: Vec<String> = devices.iter().map(|(_, s)| channel_of(s)).collect();
let mut c = chans;
if crate::ephemeral::is_armed() {
if let Ok(Some(uk)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
let ek = crate::ephemeral::enroll_channel(&uk.public_key_bytes());
if !c.contains(&ek) { c.push(ek); }
}
}
sess.emit(&sio, "subscribe", json!({ "channels": c })).await;
sess.channels = c;
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 matches!(&req.kind, ctl::ReqKind::CapStatus) {
let counts = crate::capability::cap_shadow_counts();
let action_counts = crate::capability::cap_action_counts();
req.reply(&json!({
"ok": true,
"counts": {
"la_authorized": counts.la_authorized,
"la_denied": counts.la_denied,
"la_no_header": counts.la_no_header,
"ld_authorized": counts.ld_authorized,
"ld_denied": counts.ld_denied,
"ld_no_header": counts.ld_no_header,
"ceiling_denied": counts.ceiling_denied,
},
"by_action": action_counts,
"flip_ready": counts.flip_ready(),
"summary": counts.summary(),
})).await;
} else if matches!(&req.kind, ctl::ReqKind::ListPending) {
let mut requests = load_requests();
expire_requests(&mut requests);
req.reply(&json!({
"ok": true,
"requests": requests,
})).await;
} else if let ctl::ReqKind::ApproveRequest { id } = &req.kind {
let id = *id;
let mut requests = load_requests();
expire_requests(&mut requests);
if let Some(r) = requests.iter_mut().find(|r| r.id == id && r.status == "pending") {
r.status = "approved".to_string();
r.granted_at = Some(crate::capability::now_secs());
let peer = r.peer.clone();
let cap = r.capability.clone();
save_requests(&requests);
if let Err(e) = device_set_cap(&peer, &cap, true) {
req.reject(&format!("grant failed: {e}")).await;
} else {
req.reply(&json!({ "ok": true, "id": id, "peer": peer, "capability": cap })).await;
}
} else {
req.reject(&format!("request {id} not found or not pending")).await;
}
} else if let ctl::ReqKind::DenyRequest { id } = &req.kind {
let id = *id;
let mut requests = load_requests();
expire_requests(&mut requests);
if let Some(r) = requests.iter_mut().find(|r| r.id == id && r.status == "pending") {
r.status = "denied".to_string();
save_requests(&requests);
req.reply(&json!({ "ok": true, "id": id })).await;
} else {
req.reject(&format!("request {id} not found or not pending")).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 let Ok(Some(uk)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
let ek = crate::ephemeral::enroll_channel(&uk.public_key_bytes());
let armed = crate::ephemeral::is_armed();
let subscribed = sess.channels.contains(&ek);
if armed && !subscribed {
sess.channels.push(ek.clone());
let _ = sio.emit("subscribe", json!({ "channels": [ek] })).await;
} else if !armed && subscribed {
sess.channels.retain(|c| c != &ek);
let _ = sio.emit("channel-goodbye", json!({ "channels": [ek] })).await;
}
}
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) {
let transport_dead = conn.transport_of(&pid).map(|t| t.is_dead()).unwrap_or(false);
if transport_dead || 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()));
}
} else if let Ok(Some(uk)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
let ek = crate::ephemeral::enroll_channel(&uk.public_key_bytes());
if v["channel"].as_str() == Some(&ek) {
let pid = v["id"].as_str().unwrap_or_default().to_string();
if !conn.links.contains_key(&pid) && !conn.direct_pending.contains_key(&pid) {
ui::debug("enrollment peer appeared on channel, dialing");
}
conn.maybe_adopt(&v, true).await?;
}
}
}
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 recv_code_path && !recv_pake_done {
recv_pending_direct.insert(from.clone(), (cands, srflx));
ui::debug(&format!("buffering pre-auth transport-offer from {from}"));
continue;
}
if data["probe"].as_bool() == Some(true) {
conn.answer_upgrade_probe(&from).await;
}
if conn.direct_pending.contains_key(&from) {
conn.on_transport_offer(&from, cands, srflx);
} else {
let known = crate::devices_load()
.into_iter()
.find(|(n, _)| conn.links.get(&from).map(|l| l.name == *n).unwrap_or(false));
if let Some((name, secret)) = known {
conn.start_direct(&from, &name, &secret).await;
}
if conn.direct_pending.contains_key(&from) {
conn.on_transport_offer(&from, cands, srflx);
} else {
conn.buffered_offers.insert(from.clone(), (cands, srflx));
ui::debug(&format!("buffering re-dial transport-offer from {from} (no pending yet)"));
}
}
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(), crate::identity::IntroScope::Device.to_byte()));
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()));
let mut secret_opt: Option<String> = None;
let mut is_abort = false;
let mut abort_why = String::new();
if let Some(cer) = recv_cers.get_mut(&from) {
match cer.on_signal(&data, fp_ref) {
PakeInbound::Consumed => {
secret_opt = cer.secret().cloned();
}
PakeInbound::Abort(why) => {
is_abort = true;
abort_why = why;
}
PakeInbound::Ignored => {}
}
}
if let Some(sec) = secret_opt {
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"));
conn.drop_link(&from);
conn.start_direct(&from, &from, &sec).await;
if conn.active.is_none() {
conn.active = Some(from.clone());
}
if let Some((cands, srflx)) = recv_pending_direct.remove(&from) {
conn.on_transport_offer(&from, cands, srflx);
}
recv_pending_direct.clear();
let buffered = recv_pending_offers.remove(&from);
recv_pending_offers.clear();
if let Some(offer) = buffered {
let _ = tx.send(Ev::Control(from.clone(), offer));
}
continue;
}
if is_abort {
recv_cers.remove(&from);
recv_deadlines.remove(&from);
recv_pending_offers.remove(&from);
recv_pending_direct.remove(&from);
ui::debug(&format!("recv: candidate {from} refused ({abort_why}), dropped"));
}
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);
}
if let Some(l) = conn.link_mut(&pid) {
resolve_peer_identity(l);
}
let needs_proven = if let Some(l) = conn.link(&pid) {
l.identity_device_pub.is_some()
&& l.identity_binding != crate::capability::BindingStrength::Proven
} else {
false
};
if needs_proven && crate::capability::cap_authoritative() {
issue_proven_challenge_and_hold(&conn, &pid, &t, &pending_proven, &mut identity_nonces).await;
ui::say(&format!(" identity challenge sent to {pid}, holding until Proven or timeout"));
let hold_t = t.clone();
let hold_tx = tx.clone();
let hold_pending = pending_proven.clone();
let hold_pid = pid.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(3)).await;
if hold_pending.lock().unwrap().contains_key(&hold_pid) {
hold_pending.lock().unwrap().remove(&hold_pid);
let _ = hold_tx.send(Ev::ChannelReady(hold_pid, hold_t));
}
});
} else {
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(), crate::identity::IntroScope::Device.to_byte()));
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 p = devices_path();
if let Ok(raw) = std::fs::read_to_string(&p) {
if let Ok(arr) = serde_json::from_str::<Vec<Value>>(&raw) {
if let Err(e) = identity::check_overlay_against_pinned_cert(&arr, &who, &ann.pubkey) {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" {}", e)));
clear_provisional_identity(&who);
continue;
}
}
}
}
if let Some(prov_cert) = load_provisional_identity(&who) {
if let Err(e) = identity::provisional_promote_ok(&prov_cert, &ann.pubkey) {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" {} for device {} - clearing provisional, no anchor written", e, who)));
clear_provisional_identity(&who);
continue;
}
{
let p = devices_path();
if let Ok(raw) = std::fs::read_to_string(&p) {
if let Ok(mut arr) = serde_json::from_str::<Vec<Value>>(&raw) {
let scope = crate::identity::IntroScope::Device.to_byte();
match identity::apply_peer_identity(&mut arr, &who, &prov_cert, scope) {
Ok(_) => {
let _ = crate::platform::SecretFile::write_str(&p, &serde_json::to_string_pretty(&arr).unwrap_or_default());
clear_provisional_identity(&who);
}
Err(e) => {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" takeover guard at overlay establishment: {}", e)));
clear_provisional_identity(&who);
continue;
}
}
}
}
}
}
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("identity-nonce-challenge") => {
if let Some(t) = conn.transport_of(&pid) {
respond_to_identity_challenge(&t, &v).await;
}
}
Some("identity-expose") => {
if handle_identity_expose(&mut conn, &pid, &v, &mut identity_nonces) {
if let Some((held_t, _deadline)) = pending_proven.lock().unwrap().remove(&pid) {
let _ = tx.send(Ev::ChannelReady(pid, held_t));
}
}
}
Some("worker-ports") => {
let pid = v["for"].as_str().unwrap_or_default();
ui::trace(&format!("[T:CLI] worker-ports handler: looking up key={pid}"));
if let Some(tx) = conn.worker_port_tx.remove(pid) {
ui::trace(&format!("[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);
}
}
Some("identity-auth-key-enroll-request") => {
respond_to_auth_key_enroll_request(&mut conn, pid.clone(), v.clone()).await;
}
Some("identity-auth-key-enroll-response") => {
handle_auth_key_enroll_response(&mut conn, pid.clone(), v.clone()).await;
}
_ 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 mut l2_deny_reason: Option<String> = None;
let authorized = v["type"].as_str() != Some("l2-open") || {
let legacy_ok = {
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 let Some(l) = conn.link_mut(&pid) {
resolve_peer_identity(l);
}
}
let link = conn.link(&pid);
let idev = link.and_then(|l| l.identity_device_pub.as_ref());
let iusr = link.and_then(|l| l.identity_user_pub.as_ref());
let binding = link.map(|l| l.identity_binding).unwrap_or(crate::capability::BindingStrength::None);
let expires = link.and_then(|l| l.identity_cert_expires);
let ak_caps = link.and_then(|l| l.principal_kind.auth_key_caps());
let outcome = crate::capability::cap_authorize(
&crate::settings::config_dir(),
"self",
"shell",
idev,
iusr,
ak_caps,
);
let reach_port = v["rport"].as_u64().or_else(|| v["port"].as_u64()).unwrap_or(0) as u16;
let scoped_in_bounds = reach_port != 0
&& crate::expose::load().iter().any(|b| b.port == reach_port);
let (own_user, has_grant) = crate::capability::cap_fleet_inputs(
&crate::settings::config_dir(), "self", "shell", idev, iusr, ak_caps,
);
let d = crate::capability::cap_gate_effective(legacy_ok, &outcome, "shell", "self", idev, iusr, binding, expires, ak_caps, own_user.as_ref(), scoped_in_bounds, has_grant);
if let crate::capability::GateDecision::Deny { cap_reason: Some(r) } = &d {
l2_deny_reason = Some(r.clone());
}
d.allowed()
};
if !authorized {
let sid = v["sid"].as_u64().unwrap_or(0) as u32;
let diag = l2_deny_reason.as_deref().unwrap_or("device not granted shell");
ui::say(&format!("l2: refused stream {sid:#x}: {diag}"));
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 };
if crate::capability::cap_authoritative() {
let challenge_in_flight =
pending_proven.lock().unwrap().contains_key(&pid);
let proven = conn
.link(&pid)
.map(|l| {
l.identity_binding
== crate::capability::BindingStrength::Proven
})
.unwrap_or(false);
if challenge_in_flight && !proven {
let rtx = tx.clone();
let rpid = pid.clone();
let rv = v.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(80)).await;
let _ = rtx.send(Ev::Control(rpid, rv));
});
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 legacy_ok = trusted
&& dev
.as_deref()
.map(|n| shell_policy.auto_allows(n) || device_allows(n, "shell"))
.unwrap_or(false);
let granted = {
{
if let Some(l) = conn.link_mut(&pid) {
resolve_peer_identity(l);
}
}
let link = conn.link(&pid);
let idev = link.and_then(|l| l.identity_device_pub.as_ref());
let iusr = link.and_then(|l| l.identity_user_pub.as_ref());
let binding = link.map(|l| l.identity_binding).unwrap_or(crate::capability::BindingStrength::None);
let expires = link.and_then(|l| l.identity_cert_expires);
let ak_caps = link.and_then(|l| l.principal_kind.auth_key_caps());
let outcome = crate::capability::cap_authorize(
&crate::settings::config_dir(),
"self",
"shell",
idev,
iusr,
ak_caps,
);
{
let (own_user, has_grant) = crate::capability::cap_fleet_inputs(
&crate::settings::config_dir(), "self", "shell", idev, iusr, ak_caps,
);
crate::capability::cap_gate_effective(legacy_ok, &outcome, "shell", "self", idev, iusr, binding, expires, ak_caps, own_user.as_ref(), false, has_grant)
}
};
if !granted.allowed() {
let who = dev.as_deref().unwrap_or("<unverified>");
ui::say(&format!("l2: shell bootstrap refused: {who}: {}", granted.deny_reason("no shell cap / untrusted")));
enqueue_if_requestable(who, "shell");
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 legacy_ok = trusted
&& dev
.as_deref()
.map(|n| shell_policy.auto_allows(n) || device_allows(n, "shell"))
.unwrap_or(false);
let granted = {
{
if let Some(l) = conn.link_mut(&pid) {
resolve_peer_identity(l);
}
}
let link = conn.link(&pid);
let idev = link.and_then(|l| l.identity_device_pub.as_ref());
let iusr = link.and_then(|l| l.identity_user_pub.as_ref());
let binding = link.map(|l| l.identity_binding).unwrap_or(crate::capability::BindingStrength::None);
let expires = link.and_then(|l| l.identity_cert_expires);
let ak_caps = link.and_then(|l| l.principal_kind.auth_key_caps());
let outcome = crate::capability::cap_authorize(
&crate::settings::config_dir(),
"self",
"shell",
idev,
iusr,
ak_caps,
);
{
let (own_user, has_grant) = crate::capability::cap_fleet_inputs(
&crate::settings::config_dir(), "self", "shell", idev, iusr, ak_caps,
);
crate::capability::cap_gate_effective(legacy_ok, &outcome, "shell", "self", idev, iusr, binding, expires, ak_caps, own_user.as_ref(), false, has_grant)
}
};
if !granted.allowed() {
let who = dev.as_deref().unwrap_or("<unverified>");
ui::say(&format!("l2: pty refused: {who}: {}", granted.deny_reason("no shell cap / untrusted")));
enqueue_if_requestable(who, "shell");
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);
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 within_share = crate::path_within_canonical(&crate::fleet_share_root(), &root_path);
let (authorized, read_only) = {
{
if let Some(l) = conn.link_mut(&pid) {
resolve_peer_identity(l);
}
}
let link = conn.link(&pid);
let idev = link.and_then(|l| l.identity_device_pub.as_ref());
let iusr = link.and_then(|l| l.identity_user_pub.as_ref());
let binding = link.map(|l| l.identity_binding).unwrap_or(crate::capability::BindingStrength::None);
let expires = link.and_then(|l| l.identity_cert_expires);
let ak_caps = link.and_then(|l| l.principal_kind.auth_key_caps());
let outcome = crate::capability::cap_authorize(
&crate::settings::config_dir(),
"self",
"mount",
idev,
iusr,
ak_caps,
);
let outcome = crate::capability::cap_trust_floor(
&outcome,
trusted,
binding,
crate::capability::cap_authoritative(),
);
let (own_user, has_grant) = crate::capability::cap_fleet_inputs(
&crate::settings::config_dir(), "self", "mount", idev, iusr, ak_caps,
);
let same_owner = match (iusr, own_user.as_ref()) {
(Some(u), Some(o)) => u == o,
_ => false,
};
let _ = within_share;
let mount_scoped_default = false;
let read_only = same_owner
&& binding == crate::capability::BindingStrength::Proven
&& mount_scoped_default
&& !has_grant;
let d = crate::capability::cap_gate_effective(trusted, &outcome, "mount", "self", idev, iusr, binding, expires, ak_caps, own_user.as_ref(), mount_scoped_default, has_grant);
(d, read_only)
};
if !authorized.allowed() {
let who = conn
.link(&pid)
.and_then(|l| l.verified_name.clone())
.unwrap_or_else(|| "<unverified>".into());
ui::say(&format!("mount: refused for '{who}': {}", authorized.deny_reason("not authorized (mount capability required)")));
enqueue_if_requestable(&who, "mount");
let _ = t.send_control(&json!({ "type": "l2-close", "sid": sid, "err": "not authorized: mount capability required" })).await;
continue;
}
let mut caps = mount_proto::mount_caps_for_root(&root_path);
if read_only {
caps.max_write_size = 0;
}
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, read_only);
}
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());
resolve_peer_identity(l);
}
if crate::capability::cap_authoritative() {
let needs_proven = conn.link(&pid).map(|l| {
l.identity_device_pub.is_some()
&& l.identity_binding != crate::capability::BindingStrength::Proven
}).unwrap_or(false);
if needs_proven {
if let Some(t) = conn.transport_of(&pid) {
issue_proven_challenge_and_hold(&conn, &pid, &t, &pending_proven, &mut identity_nonces).await;
ui::debug(&format!(" identity challenge sent to {pid} at pair-proof (universal expose, WebRTC/relay path)"));
let hold_pending = pending_proven.clone();
let hold_pid = pid.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(3)).await;
hold_pending.lock().unwrap().remove(&hold_pid);
});
}
}
}
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
));
if let Ok(own_dpub) = crate::overlay::overlay_pubkey_bytes() {
if let Some(t) = conn.transport_of(&pid) {
use ring::rand::{SecureRandom, SystemRandom};
let rng = SystemRandom::new();
let mut nonce = [0u8; 32];
let _ = rng.fill(&mut nonce);
identity_nonces.insert(iname.clone(), (nonce, Instant::now(), own_dpub));
let challenge = json!({
"type": "identity-nonce-challenge",
"nonce": hex::encode(nonce),
"receiver_device_pub": hex::encode(own_dpub)
});
let _ = t.send_control(&challenge).await;
}
}
} else {
ui::say(&ui::paint(ui::Tone::Warn, &format!(" ignored pair-intro from unverified peer {hub}")));
}
}
Some("identity-nonce-challenge") => {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
let recv_dpub_hex = v["receiver_device_pub"].as_str().unwrap_or_default();
if let (Ok(nonce_bytes), Ok(recv_dpub_bytes)) = (hex::decode(nonce_hex), hex::decode(recv_dpub_hex)) {
if nonce_bytes.len() == 32 && recv_dpub_bytes.len() == 32 {
let mut nonce_arr = [0u8; 32];
nonce_arr.copy_from_slice(&nonce_bytes);
let mut recv_dpub_arr = [0u8; 32];
recv_dpub_arr.copy_from_slice(&recv_dpub_bytes);
if let Some(local_cert) = local_device_cert() {
let scope = crate::identity::IntroScope::User.to_byte(); let caps = "transfer";
let caps_d = crate::identity::caps_digest(caps);
let chash = crate::identity::cert_hash(&local_cert);
let mut sender_dpub = local_cert.device_pub;
let msg = crate::identity::possession_msg(0x02, &nonce_arr, scope, &caps_d, &chash, &sender_dpub, &recv_dpub_arr);
if let Ok(sig) = crate::overlay::overlay_sign_possession(&msg) {
let inner = json!({
"cert": local_cert.to_json(),
"possession_sig": hex::encode(sig)
});
if let Some(t) = conn.transport_of(&pid) {
let payload = json!({
"type": "identity-expose",
"v": 2,
"binding_type": 0x02,
"nonce": hex::encode(nonce_arr),
"cert": local_cert.to_json(),
"possession_sig": hex::encode(sig)
});
let _ = t.send_control(&payload).await;
}
}
}
}
}
}
Some("identity-expose") => {
let nonce_hex = v["nonce"].as_str().unwrap_or_default();
if let Ok(nonce_bytes) = hex::decode(nonce_hex) {
if nonce_bytes.len() == 32 {
let mut nonce_arr = [0u8; 32];
nonce_arr.copy_from_slice(&nonce_bytes);
if let Some((held_nonce, _ts, _held_recv_dpub)) = identity_nonces.get(&pid) {
if held_nonce == &nonce_arr {
if let Some(cert_json) = v.get("cert") {
if let Some(cert) = identity::DeviceCert::from_json(cert_json) {
if cert.verify(identity::now_secs()).is_ok() {
if let Some(sig_hex) = v.get("possession_sig").and_then(|x| x.as_str()) {
if let Ok(sig_bytes) = hex::decode(sig_hex) {
if sig_bytes.len() == 64 {
let mut sig_arr = [0u8; 64];
sig_arr.copy_from_slice(&sig_bytes);
let scope = crate::identity::IntroScope::User.to_byte(); let caps_d = crate::identity::caps_digest("transfer"); let chash = crate::identity::cert_hash(&cert);
if let Ok(own_dpub) = crate::overlay::overlay_pubkey_bytes() {
let sender_dpub = cert.device_pub;
let receiver_dpub = own_dpub; let msg = crate::identity::possession_msg(0x02, &nonce_arr, scope, &caps_d, &chash, &sender_dpub, &receiver_dpub);
if crate::identity::verify_possession_sig(&cert.device_pub, &msg, &sig_arr).is_ok() {
if let Ok(Some(_own_uk)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
if cert.device_pub == own_dpub {
} else {
let _ = store_provisional_identity(&format!("peer-{}", pid), &cert);
if let Some(l) = conn.link_mut(&pid) {
l.identity_device_pub = Some(cert.device_pub);
l.identity_user_pub = Some(cert.user_pub);
l.identity_binding = crate::capability::BindingStrength::Proven;
l.identity_cert_expires = Some(cert.expires);
}
ui::say(&format!(" {} identity verified for peer {}", ui::paint(ui::Tone::Ok, ui::glyph_ok()), pid));
identity_nonces.remove(&pid);
}
} else if let Ok(Some(uk)) = crate::identity::UserKey::load(&crate::platform::PlatformKeyStore) {
let ek = crate::ephemeral::enroll_channel(&uk.public_key_bytes());
if v["channel"].as_str() == Some(&ek) {
let pid = v["id"].as_str().unwrap_or_default().to_string();
if !conn.links.contains_key(&pid) && !conn.direct_pending.contains_key(&pid) {
ui::debug(&format!("enrollment peer appeared on channel, dialing"));
}
conn.maybe_adopt(&v, true).await?;
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
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 = safe_incoming_name(raw);
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"));
}
}
_ => {}
}
}
if crate::capability::cap_authoritative() {
let challenge_in_flight =
pending_proven.lock().unwrap().contains_key(&pid);
let proven = conn
.link(&pid)
.map(|l| {
l.identity_binding
== crate::capability::BindingStrength::Proven
})
.unwrap_or(false);
if challenge_in_flight && !proven {
let rtx = tx.clone();
let rpid = pid.clone();
let rv = v.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(80)).await;
let _ = rtx.send(Ev::Control(rpid, rv));
});
continue;
}
}
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 legacy_ok = if daemon {
link_trusted
} else {
yes || consented || link_trusted || (is_resume && offset > 0)
};
let (ok, xfer_deny_reason, xfer_gate) = {
{
if let Some(l) = conn.link_mut(&pid) {
resolve_peer_identity(l);
}
}
let link = conn.link(&pid);
let idev = link.and_then(|l| l.identity_device_pub.as_ref());
let iusr = link.and_then(|l| l.identity_user_pub.as_ref());
let binding = link.map(|l| l.identity_binding).unwrap_or(crate::capability::BindingStrength::None);
let expires = link.and_then(|l| l.identity_cert_expires);
let ak_caps = link.and_then(|l| l.principal_kind.auth_key_caps());
let outcome = crate::capability::cap_authorize(
&crate::settings::config_dir(),
"self",
"transfer",
idev,
iusr,
ak_caps,
);
let outcome = crate::capability::cap_trust_floor(
&outcome,
link_trusted,
binding,
crate::capability::cap_authoritative(),
);
let landing = dir.join(&name);
let scoped_in_bounds = crate::path_within(&dir, &landing);
let (own_user, has_grant) = crate::capability::cap_fleet_inputs(
&crate::settings::config_dir(), "self", "transfer", idev, iusr, ak_caps,
);
let d = crate::capability::cap_gate_effective(legacy_ok, &outcome, "transfer", "self", idev, iusr, binding, expires, ak_caps, own_user.as_ref(), scoped_in_bounds, has_grant);
let reason = if let crate::capability::GateDecision::Deny { cap_reason } = &d {
cap_reason.clone()
} else {
None
};
(d.allowed(), reason, d)
};
if let Some(reason) = crate::capability::transfer_gate_decision(
&xfer_gate,
crate::capability::cap_authoritative(),
) {
ui::say(&ui::paint(ui::Tone::Dim, &format!(
" declined {name} from {sender_name} ({reason})",
)));
if daemon { enqueue_if_requestable(&sender_name, "transfer"); }
t.send_control(&protocol::decline_msg(&id)).await?;
continue;
}
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 { xfer_deny_reason.as_deref().unwrap_or("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));
match safe_resume_part(&part_path).await {
Ok(f) => f,
Err(e) => { ui::debug(&format!("{name}: cannot open .part to resume, declining: {e}")); continue; }
}
} else {
let _ = std::fs::remove_file(&part_path);
if let Err(e) = (PartMeta { size, head: offer_head, full: effective_full.clone() }.store(&meta_path)) {
ui::debug(&format!("{name}: cannot write .part.meta, declining: {e}"));
continue;
}
match safe_create_part(&part_path).await {
Ok(f) => f,
Err(e) => { ui::debug(&format!("{name}: cannot create .part, declining: {e}")); continue; }
}
};
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 _ = safe_create_part(&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) = safe_resume_part(&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 _ = safe_create_part(&inc.part_path).await;
if let Ok(f) = safe_resume_part(&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 trace = cfg!(feature = "debug-logs") && std::env::var("FILAMENT_TRACE_THROUGHPUT").is_ok();
let t_recv_start = if trace { Some(std::time::Instant::now()) } else { None };
let pos: u64 = match offset {
Some(off) => off,
None => {
dlog!("[recv] REFUSING offsetless chunk sid={sid}: transport must frame offset");
continue;
}
};
inc.inflight.fetch_add(1, Ordering::Relaxed);
let file = Arc::clone(&inc.file);
let inflight = Arc::clone(&inc.inflight);
let end_seen = Arc::clone(&inc.end_seen);
let ranges = Arc::clone(&inc.ranges);
let received = Arc::clone(&inc.received);
let tx = tx.clone();
let pid_c = pid.clone();
let data_len = data.len();
let trace_inner = trace;
tokio::task::spawn_blocking(move || {
let t_pwrite = if trace_inner { Some(std::time::Instant::now()) } else { None };
if let Err(e) = pwrite_at(&file, &data, pos) {
dlog!("[recv] pwrite_at FAILED at pos={pos} len={data_len}: {e}");
} else {
let mut r = ranges.lock().unwrap();
let (_delta, total) = record_range(&mut *r, pos, data_len);
drop(r);
received.fetch_max(total, Ordering::Relaxed);
}
let pwrite_us = t_pwrite.map(|t| t.elapsed().as_micros()).unwrap_or(0);
if trace_inner && pwrite_us > 1000 {
dlog!("[TRACE recv spawn_blocking] pos={} len={} pwrite={}us", pos, data_len, pwrite_us);
}
let prev = inflight.fetch_sub(1, Ordering::Relaxed);
if prev == 1 && end_seen.load(Ordering::Relaxed) {
let _ = tx.send(Ev::MaybeComplete(pid_c, sid));
}
});
let recv_us = t_recv_start.map(|t| t.elapsed().as_micros()).unwrap_or(0);
if trace && recv_us > 500 {
dlog!("[TRACE recv Ev::Chunk] sid={} offset={:?} len={} dispatch={}us", sid, offset, data_len, recv_us);
}
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 r = inc.ranges.lock().unwrap();
if !coverage_complete(&r, inc.size) {
let gap = first_gap(&r, inc.size);
dlog!("[recv] INCOMPLETE at verify: {} ranges, received {}/{}, first gap at {:?}",
r.len(), recvd, inc.size, gap);
drop(r);
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 path_within_bounds_the_share_root() {
let uid = format!("{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-path-test-{uid}"));
let root = tmp.join("filament-share");
let docs = root.join("docs");
let secrets = tmp.join("secrets");
let ssh_dir = tmp.join(".ssh");
std::fs::create_dir_all(&docs).unwrap();
std::fs::create_dir_all(&secrets).unwrap();
std::fs::create_dir_all(&ssh_dir).unwrap();
std::fs::write(docs.join("a.txt"), b"test").unwrap();
assert!(path_within(&root, &root));
assert!(path_within(&root, &docs.join("a.txt")));
assert!(!path_within(&root, &tmp));
assert!(!path_within(&root, &secrets));
assert!(!path_within(&root, std::path::Path::new("/")));
assert!(!path_within(&root, std::path::Path::new("filament-share")));
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn path_within_canonical_refuses_symlink_escape() {
let uid = format!("{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-symlink-test-{uid}"));
let root = tmp.join("share");
let etc = tmp.join("etc");
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&etc).unwrap();
let evil_link = root.join("evil");
#[cfg(unix)]
std::os::unix::fs::symlink(&etc, &evil_link).unwrap();
#[cfg(unix)]
assert!(!path_within_canonical(&root, &evil_link),
"symlink escaping share root must be refused by canonical check");
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(unix)]
#[tokio::test]
async fn transfer_part_refuses_symlink() {
let uid = format!("{}-create-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-xfer-{uid}"));
std::fs::create_dir_all(&tmp).unwrap();
let part_path = tmp.join("evil.tar.part");
std::os::unix::fs::symlink("/etc/passwd", &part_path).unwrap();
let result = safe_create_part(&part_path).await;
assert!(result.is_err(), "must refuse to create through a symlink");
let meta = std::fs::symlink_metadata(&part_path).unwrap();
assert!(meta.file_type().is_symlink(), "symlink must not have been followed");
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(unix)]
#[tokio::test]
async fn transfer_open_part_refuses_symlink() {
let uid = format!("{}-resume-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-xfer-{uid}"));
std::fs::create_dir_all(&tmp).unwrap();
let part_path = tmp.join("data.tar.part");
std::fs::write(&part_path, b"partial data").unwrap();
let result = safe_resume_part(&part_path).await;
assert!(result.is_ok(), "regular file must open normally for resume");
std::fs::remove_file(&part_path).unwrap();
std::os::unix::fs::symlink("/etc/passwd", &part_path).unwrap();
let result = safe_resume_part(&part_path).await;
assert!(result.is_err(), "must refuse to resume through a symlink");
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(unix)]
#[tokio::test]
async fn transfer_resume_refuses_fifo() {
let uid = format!("{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-xfer-fifo-{uid}"));
std::fs::create_dir_all(&tmp).unwrap();
let part_path = tmp.join("data.tar.part");
unsafe { libc::mkfifo(std::ffi::CString::new(part_path.to_str().unwrap()).unwrap().as_ptr(), 0o644); }
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
safe_resume_part(&part_path),
).await;
match result {
Ok(Ok(_)) => panic!("must refuse to resume through a FIFO"),
Ok(Err(_)) => {} Err(_) => panic!("safe_resume_part hung on a FIFO — O_NONBLOCK may be needed"),
}
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(unix)]
#[tokio::test]
async fn transfer_restart_replaces_stale_part() {
let uid = format!("{}-restart-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-xfer-{uid}"));
std::fs::create_dir_all(&tmp).unwrap();
let part_path = tmp.join("data.tar.part");
std::fs::write(&part_path, b"stale partial from a prior interrupted transfer").unwrap();
assert!(
safe_create_part(&part_path).await.is_err(),
"O_EXCL create must refuse a leftover .part"
);
let _ = std::fs::remove_file(&part_path);
let created = safe_create_part(&part_path).await;
assert!(created.is_ok(), "remove-then-create must succeed: {:?}", created.err());
let meta = std::fs::metadata(&part_path).unwrap();
assert_eq!(meta.len(), 0, "restarted .part must start empty");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn transfer_nonexistent_landing_is_in_bounds() {
let uid = format!("{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-xfer-scope-{uid}"));
let drop_dir = tmp.join("inbox");
std::fs::create_dir_all(&drop_dir).unwrap();
let landing = drop_dir.join("photo.jpg");
assert!(!landing.exists(), "landing must not exist yet");
assert!(path_within(&drop_dir, &landing),
"non-existent landing inside drop dir must be in-bounds");
let outside = tmp.join("evil.txt");
assert!(!path_within(&drop_dir, &outside),
"landing outside drop dir must be out-of-bounds");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn mount_root_symlink_refused() {
let uid = format!("{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
let tmp = std::env::temp_dir().join(format!("fil-mount-root-{uid}"));
let share = tmp.join("share");
let etc = tmp.join("etc");
std::fs::create_dir_all(&share).unwrap();
std::fs::create_dir_all(&etc).unwrap();
let evil = share.join("evil");
#[cfg(unix)]
std::os::unix::fs::symlink(&etc, &evil).unwrap();
#[cfg(unix)]
assert!(!path_within_canonical(&share, &evil),
"mount root as symlink escaping share must be refused");
assert!(path_within_canonical(&share, &share),
"real share root must be accepted");
let _ = std::fs::remove_dir_all(&tmp);
}
#[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() {
assert_eq!(safe_incoming_name("../../etc/passwd"), "passwd");
assert_eq!(safe_incoming_name("/absolute/path.bin"), "path.bin");
assert_eq!(safe_incoming_name("plain.bin"), "plain.bin");
assert_eq!(safe_incoming_name("evil\0.bin"), "evil.bin");
assert_eq!(safe_incoming_name("with\ttab\nand\r.bin"), "withtaband.bin");
assert_eq!(safe_incoming_name("\0\0\0"), "file.bin");
assert_eq!(safe_incoming_name(".."), "file.bin");
assert_eq!(safe_incoming_name(""), "file.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");
}
#[test]
fn upsert_peer_record_writes_secret_and_cert_together() {
let mk_cert = |dpub: u8| -> identity::DeviceCert {
identity::DeviceCert::from_json(&serde_json::json!({
"devicePub": hex::encode([dpub; 32]),
"userPub": hex::encode([0x11u8; 32]),
"expires": 9_999_999_999u64,
"issued": 1u64,
"sig": hex::encode([0u8; 64]),
})).unwrap()
};
let cert_a = mk_cert(0xa1);
let cert_b = mk_cert(0xb2);
let mut arr: Vec<Value> = vec![];
upsert_peer_record(&mut arr, "bob", Some("secretA"), Some(&cert_a), None, None, None);
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["secret"].as_str(), Some("secretA"));
let stored_a = identity::DeviceCert::from_json(&arr[0]["deviceCert"]).unwrap();
assert_eq!(stored_a.device_pub, [0xa1u8; 32], "gen A: cert must be certA");
upsert_peer_record(&mut arr, "bob", Some("secretB"), Some(&cert_b), None, None, None);
assert_eq!(arr.len(), 1, "same name updates in place, not duplicated");
assert_eq!(arr[0]["secret"].as_str(), Some("secretB"), "secret must be gen B");
let stored_b = identity::DeviceCert::from_json(&arr[0]["deviceCert"]).unwrap();
assert_eq!(stored_b.device_pub, [0xb2u8; 32], "cert must be gen B — never torn to certA");
assert_ne!(stored_b.device_pub, [0xa1u8; 32], "new secret must not retain the old-gen cert");
}
#[test]
fn consent_add_pending_enqueues() {
let mut reqs = Vec::new();
add_pending_request("alice", "shell", &mut reqs);
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].peer, "alice");
assert_eq!(reqs[0].capability, "shell");
assert_eq!(reqs[0].status, "pending");
add_pending_request("bob", "mount", &mut reqs);
assert_eq!(reqs.len(), 2);
assert_eq!(reqs[1].id, reqs[0].id + 1);
}
#[test]
fn consent_enqueue_skips_unidentified() {
let mut reqs = Vec::new();
add_pending_request("<unverified>", "shell", &mut reqs);
assert_eq!(reqs.len(), 1); }
#[test]
fn consent_enqueue_dedup_same_peer_cap_pending() {
let mut reqs = vec![PendingRequest {
id: 1, peer: "alice".into(), capability: "shell".into(),
timestamp: 0, status: "pending".into(), granted_at: None,
}];
let dup = reqs.iter().any(|r| r.peer == "alice" && r.capability == "shell" && r.status == "pending");
assert!(dup, "existing pending entry must be found as duplicate");
let non_dup = reqs.iter().any(|r| r.peer == "alice" && r.capability == "mount" && r.status == "pending");
assert!(!non_dup, "different cap must not be a duplicate");
let non_dup2 = reqs.iter().any(|r| r.peer == "bob" && r.capability == "shell" && r.status == "pending");
assert!(!non_dup2, "different peer must not be a duplicate");
}
#[test]
fn consent_enqueue_max_evicts_oldest() {
let mut reqs: Vec<PendingRequest> = (0..(MAX_PENDING + 1))
.map(|i| PendingRequest {
id: i as u64, peer: format!("peer{i}"), capability: "shell".into(),
timestamp: 0, status: "pending".into(), granted_at: None,
})
.collect();
add_pending_request("overflow", "shell", &mut reqs);
let has_peer0 = reqs.iter().any(|r| r.peer == "peer0");
assert!(!has_peer0, "oldest pending must be evicted when queue full");
let count_pending = reqs.iter().filter(|r| r.status == "pending").count();
assert!(count_pending <= MAX_PENDING, "queue must not exceed MAX_PENDING");
}
#[test]
fn consent_expiry_is_terminal() {
let old_ts = crate::capability::now_secs().saturating_sub(REQUEST_TTL_SECS + 1);
let mut reqs = vec![PendingRequest {
id: 1, peer: "alice".into(), capability: "shell".into(),
timestamp: old_ts, status: "pending".into(), granted_at: None,
}];
expire_requests(&mut reqs);
assert_eq!(reqs[0].status, "expired", "expired pending must become terminal expired");
reqs[0].status = "expired".to_string();
let snapshot = reqs[0].status.clone();
expire_requests(&mut reqs);
assert_eq!(reqs[0].status, snapshot, "terminal status must not be re-expired");
}
#[test]
fn delegated_principal_never_written_to_device_store() {
let mk_cert = |dpub: u8| -> identity::DeviceCert {
identity::DeviceCert::from_json(&serde_json::json!({
"devicePub": hex::encode([dpub; 32]),
"userPub": hex::encode([0x11u8; 32]),
"expires": 9_999_999_999u64,
"issued": 1u64,
"sig": hex::encode([0u8; 64]),
})).unwrap()
};
let cert = mk_cert(0xdd);
let mut arr: Vec<Value> = vec![];
upsert_peer_record(&mut arr, "delegated-peer", Some("secret123"), Some(&cert), None, None, None);
assert_eq!(arr.len(), 1);
let record = &arr[0];
assert!(!record.get("identity_user_pub").is_some(),
"device store must not contain identity_user_pub — that is a Link-only field");
assert!(!record.get("principal_kind").is_some(),
"device store must not contain principal_kind — that is a Link-only field");
assert!(!record.get("identity_binding").is_some(),
"device store must not contain identity_binding — that is a Link-only field");
let allowed: std::collections::HashSet<&str> = [
"name", "secret", "caps", "deviceCert", "addedAt", "v",
"userKey", "identityScope",
].iter().cloned().collect();
for key in record.as_object().unwrap().keys() {
assert!(allowed.contains(key.as_str()),
"unexpected device store field '{key}' — is a Link field leaking?");
}
}
}