This commit is contained in:
netadmin committed 2026-10-25 22:34:13 +03:00
1 parent 5d01dfb63a
commit b0d63c9f0f
20 files changed
+1857 -1417

No files matched your search

+188 -18
View File
@@ -13,12 +13,12 @@ use std::{
net::{IpAddr, Ipv4Addr},
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tokio::sync::{Mutex, Semaphore};
use tokio::sync::{Mutex, Notify, Semaphore};
// ───────────────────────── Лимиты ─────────────────────────
pub const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // один файл
@@ -32,6 +32,9 @@ pub const MAX_TRANSFERS: usize = 20;
pub const MAX_INCOMING: usize = 4;
pub const MAX_MEDIA_LIST: usize = 500;
pub const DISCOVERY_PORT: u16 = 5354;
pub const MIN_PING_SEC: u64 = 10;
pub const MAX_PING_SEC: u64 = 30;
pub const DEFAULT_PING_SEC: u64 = 15;
// ───────────────────────── Ошибки ─────────────────────────
pub struct AppError(pub StatusCode, pub String);
@@ -82,7 +85,10 @@ pub struct PeerDevice {
pub fingerprint: String,
/// Код сверки (SAS): зависит от ОБОИХ сертификатов, на двух устройствах одинаков.
pub pin_code: String,
/// Мы сверили код и подтвердили это устройство (наше решение; по нему принимаем/отправляем).
pub verified: bool,
/// Собеседник подтвердил сверку с НАШИМ устройством (узнаём по mTLS-каналу из /p2p/info).
pub trusts_me: bool,
pub online: bool,
pub last_seen: u64,
}
@@ -97,6 +103,7 @@ impl PeerDevice {
fingerprint: fp.to_string(),
pin_code: sas_code(my_fp, fp),
verified,
trusts_me: false,
online: false,
last_seen: 0,
}
@@ -162,10 +169,16 @@ pub struct UiScanRangeRequest {
pub port: Option<u16>,
}
#[derive(Deserialize)]
pub struct UiSettingsRequest {
pub ping_interval_sec: u64,
}
// ───────────────────────── Состояние ─────────────────────────
pub struct AppState {
pub device_name: String,
pub my_ip: String,
pub my_ip: RwLock<String>,
pub fixed_ip: bool,
pub p2p_port: u16,
pub my_fingerprint: String,
pub cert_der: Vec<u8>,
@@ -182,10 +195,37 @@ pub struct AppState {
pub incoming_slots: Arc<Semaphore>,
pub ping_lock: Mutex<()>,
pub trust_dirty: AtomicBool,
pub probe_cooldown: PlMutex<HashMap<IpAddr, Instant>>,
pub probe_cooldown: PlMutex<HashMap<(IpAddr, u16), Instant>>,
/// Интервал опроса известных пиров (сек), меняется на лету
pub ping_interval_sec: AtomicU64,
pub ping_notify: Notify,
/// true в режиме `serve`: журнал дублируется в stdout (в CLI-командах stdout занят результатом)
pub log_stdout: bool,
/// true в `serve --no-ui`: входящий текст печатается в терминал
pub headless: bool,
/// mtime trusted.json, чтобы подхватывать правки из CLI (trust/untrust) работающим узлом
pub trust_mtime: PlMutex<Option<SystemTime>>,
}
impl AppState {
pub fn current_ip(&self) -> String {
self.my_ip.read().clone()
}
pub fn update_my_ip(&self, ip: Ipv4Addr) -> bool {
if self.fixed_ip {
return false;
}
let s = ip.to_string();
let mut cur = self.my_ip.write();
if *cur != s {
*cur = s;
true
} else {
false
}
}
pub fn add_log(&self, msg: impl Into<String>) {
// защита от log-injection: управляющие символы в именах и т.п.
let text: String = msg
@@ -193,7 +233,9 @@ impl AppState {
.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.collect();
println!("[LOG] {}", text);
if self.log_stdout {
println!("[LOG] {}", text);
}
let mut logs = self.logs.write();
if logs.len() >= MAX_LOGS {
logs.pop_back();
@@ -260,10 +302,16 @@ impl AppState {
name: p.name.clone(),
ips: p.ips.clone(),
port: p.port,
trusts_me: p.trusts_me,
})
.collect();
if let Err(e) = storage::save_trusted(&self.data_dir, &entries) {
self.add_log(format!("Не удалось сохранить список доверенных: {}", e));
match storage::save_trusted(&self.data_dir, &entries) {
Ok(()) => {
if let Ok(mt) = std::fs::metadata(self.data_dir.join("trusted.json")).and_then(|m| m.modified()) {
*self.trust_mtime.lock() = Some(mt);
}
}
Err(e) => self.add_log(format!("Не удалось сохранить список доверенных: {}", e)),
}
}
@@ -314,7 +362,7 @@ impl AppState {
match peers.entry(fp.to_string()) {
Entry::Occupied(mut e) => {
let p = e.get_mut();
let changed = p.ips.first() != Some(&ip_s) || p.port != port;
let changed = p.ips.first() != Some(&ip_s) || p.port != port || p.name != name;
p.name = name.clone();
p.port = port;
p.online = true;
@@ -351,32 +399,141 @@ impl AppState {
created
}
/// Пир уже недавно виден по этому адресу — повторный probe по beacon не нужен.
pub fn is_fresh(&self, fp: &str, ip: IpAddr) -> bool {
/// Обновляет активный IP и время активности уже известного пира (при входящем/исходящем обмене).
pub fn touch_peer_ip(&self, fp: &str, ip: IpAddr) {
if ip.is_unspecified() || ip.is_loopback() || ip.is_multicast() {
return;
}
let ip_s = ip.to_string();
let mut dirty = false;
if let Some(p) = self.peers.write().get_mut(fp) {
let changed = p.ips.first() != Some(&ip_s);
p.online = true;
p.last_seen = now_sec();
if changed {
p.ips.retain(|x| x != &ip_s);
p.ips.insert(0, ip_s);
p.ips.truncate(MAX_PEER_IPS);
if p.verified {
dirty = true;
}
}
}
if dirty {
self.trust_dirty.store(true, Ordering::Relaxed);
}
}
/// Пир уже недавно виден по этому адресу и порту — повторный probe по beacon не нужен.
pub fn is_fresh(&self, fp: &str, ip: IpAddr, port: u16) -> bool {
let ip_s = ip.to_string();
self.peers.read().get(fp).map_or(false, |p| {
p.online
&& p.port == port
&& p.ips.first().map(String::as_str) == Some(ip_s.as_str())
&& now_sec().saturating_sub(p.last_seen) < 10
})
}
pub fn probe_allowed(&self, ip: IpAddr) -> bool {
pub fn probe_allowed(&self, ip: IpAddr, port: u16) -> bool {
let mut m = self.probe_cooldown.lock();
let now = Instant::now();
if m.len() > 512 {
m.retain(|_, t| now.duration_since(*t) < Duration::from_secs(30));
m.retain(|_, t| now.duration_since(*t) < Duration::from_secs(15));
}
match m.get(&ip) {
Some(t) if now.duration_since(*t) < Duration::from_secs(10) => false,
match m.get(&(ip, port)) {
Some(t) if now.duration_since(*t) < Duration::from_secs(4) => false,
_ => {
m.insert(ip, now);
m.insert((ip, port), now);
true
}
}
}
/// Меняет интервал опроса: применяется сразу и сохраняется в config.json.
pub fn set_ping_interval(&self, sec: u64) -> Result<(), String> {
if !(MIN_PING_SEC..=MAX_PING_SEC).contains(&sec) {
return Err(format!("Интервал должен быть от {} до {} секунд", MIN_PING_SEC, MAX_PING_SEC));
}
self.ping_interval_sec.store(sec, Ordering::Relaxed);
self.ping_notify.notify_one(); // будим цикл опроса, чтобы он взял новое значение
if let Err(e) = storage::update_config(&self.data_dir, |c| c.ping_interval_sec = Some(sec)) {
self.add_log(format!("Не удалось сохранить config.json: {}", e));
}
self.add_log(format!("Интервал опроса устройств: {} с", sec));
Ok(())
}
/// Бит «собеседник подтвердил сверку с нами». Источник — только аутентифицированный (mTLS)
/// канал: заголовок/ответ /p2p/info от владельца отпечатка.
pub fn set_remote_trust(&self, fp: &str, v: bool) {
let (msg, hint) = {
let mut peers = self.peers.write();
let Some(p) = peers.get_mut(fp) else { return };
if p.trusts_me == v {
return;
}
p.trusts_me = v;
if v && !p.verified {
(
format!(
"«{}» запрашивает взаимную сверку. Код сверки: {} — сравните его на экране устройства и подтвердите",
p.name, p.pin_code
),
Some(short_id(fp)),
)
} else if v {
(format!("«{}» подтвердил сверку — доверие взаимное", p.name), None)
} else {
(format!("«{}» отозвал доверие к вашему устройству", p.name), None)
}
};
self.trust_dirty.store(true, Ordering::Relaxed);
self.add_log(msg);
if let (true, Some(id)) = (self.headless, hint) {
println!(" → подтвердить: ramshare_ui trust {}", id);
}
}
/// Подхватывает изменения trusted.json, сделанные другим процессом (CLI: trust/untrust).
/// Повреждённый файл игнорируется, а не «разжалует» всех пиров.
pub fn sync_trust_from_disk(&self) {
let path = self.data_dir.join("trusted.json");
let Ok(mt) = std::fs::metadata(&path).and_then(|m| m.modified()) else { return };
{
let mut last = self.trust_mtime.lock();
if *last == Some(mt) {
return;
}
*last = Some(mt);
}
let Some(entries) = storage::read_trusted_strict(&self.data_dir) else { return };
let mut peers = self.peers.write();
for p in peers.values_mut() {
p.verified = entries.iter().any(|e| e.fp == p.id);
}
for e in entries {
if !is_valid_fp(&e.fp) || e.fp == self.my_fingerprint || peers.contains_key(&e.fp) {
continue;
}
if peers.len() >= MAX_PEERS {
break;
}
let ips: Vec<String> = e
.ips
.into_iter()
.filter(|s| s.parse::<IpAddr>().is_ok())
.take(MAX_PEER_IPS)
.collect();
let port = if e.port == 0 { 8443 } else { e.port };
let mut np = PeerDevice::new(&e.fp, clean_label(&e.name, 48), ips, port, &self.my_fingerprint, true);
np.trusts_me = e.trusts_me;
peers.insert(e.fp.clone(), np);
}
}
pub fn maintenance(&self) {
self.sync_trust_from_disk();
let now = now_sec();
self.peers
.write()
@@ -460,6 +617,13 @@ pub fn clean_label(s: &str, max_chars: usize) -> String {
.collect()
}
/// Для печати чужого текста в терминал: убирает управляющие/escape-символы (кроме \n и \t).
pub fn strip_ctrl(s: &str) -> String {
s.chars()
.map(|c| if c.is_control() && c != '\n' && c != '\t' { '\u{fffd}' } else { c })
.collect()
}
pub fn pct_decode(s: &str) -> String {
percent_decode_str(s).decode_utf8_lossy().into_owned()
}
@@ -472,8 +636,10 @@ pub fn hdr<'a>(h: &'a HeaderMap, name: &str) -> Option<&'a str> {
h.get(name).and_then(|v| v.to_str().ok())
}
/// Локальные IPv4: RFC 1918 (10/8, 172.16/12, 192.168/16), Link-Local (169.254/16) и CGNAT/VPN (100.64/10).
pub fn is_lan_v4(ip: Ipv4Addr) -> bool {
ip.is_private() || ip.is_link_local()
let o = ip.octets();
ip.is_private() || ip.is_link_local() || (o[0] == 100 && (o[1] & 0b1100_0000) == 0b0100_0000)
}
/// Принимает уже раскодированное имя. Ничего не декодирует.
@@ -505,11 +671,15 @@ pub fn sanitize_filename(name: &str) -> String {
}
/// Диапазоны: "all", "192.168.1.1-254", "192.168.1.10-192.168.1.80", "192.168.1.0/24" (маски /22–/32),
/// одиночные адреса и списки через запятую. Только локальные (private/link-local) IPv4.
/// одиночные адреса и списки через запятую. Только локальные IPv4.
pub fn parse_ip_range(input: &str, my_ip: &str) -> Result<(String, Vec<Ipv4Addr>), String> {
const MAX_TOTAL: usize = 1024;
let raw = input.trim();
let mine: Option<Ipv4Addr> = my_ip.parse().ok();
let mine: Option<Ipv4Addr> = my_ip
.parse::<Ipv4Addr>()
.ok()
.filter(|ip| is_lan_v4(*ip))
.or_else(crate::net::detect_lan_ip);
let effective = if raw.is_empty() || raw.eq_ignore_ascii_case("all") || raw.eq_ignore_ascii_case("full") {
let base = mine.ok_or("Не удалось определить подсеть автоматически. Введите диапазон вручную.")?;