This commit is contained in:
netadmin committed 2026-10-23 21:34:20 +03:00
1 parent e88dc07696
commit 5d01dfb63a
12 files changed
+2438 -1891

No files matched your search

+264
View File
@@ -0,0 +1,264 @@
use crate::models::*;
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use socket2::{Domain, Protocol, Socket, Type};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket as StdUdpSocket},
sync::Arc,
time::Duration,
};
use tokio::{net::UdpSocket, sync::Semaphore};
const MAX_PROBE_BODY: usize = 8 * 1024;
pub struct ProbeInfo {
pub fp: String,
pub name: String,
}
// ───────────────────────── Интерфейсы ─────────────────────────
/// (ip, broadcast) всех локальных (private/link-local) IPv4-интерфейсов.
pub fn lan_interfaces() -> Vec<(Ipv4Addr, Option<Ipv4Addr>)> {
if_addrs::get_if_addrs()
.unwrap_or_default()
.into_iter()
.filter_map(|i| match &i.addr {
if_addrs::IfAddr::V4(v4) if !i.is_loopback() && is_lan_v4(v4.ip) => Some((v4.ip, v4.broadcast)),
_ => None,
})
.collect()
}
/// Адрес интерфейса маршрута по умолчанию (UDP connect не отправляет пакетов и не требует интернета).
pub fn detect_lan_ip() -> Option<Ipv4Addr> {
if let Ok(s) = StdUdpSocket::bind("0.0.0.0:0") {
if s.connect("192.0.2.1:9").is_ok() {
if let Ok(SocketAddr::V4(a)) = s.local_addr() {
if is_lan_v4(*a.ip()) {
return Some(*a.ip());
}
}
}
}
let all = lan_interfaces();
all.iter()
.map(|(ip, _)| *ip)
.find(|ip| !(ip.octets()[0] == 172 && ip.octets()[1] == 17)) // не docker0
.or_else(|| all.first().map(|(ip, _)| *ip))
}
// ───────────────────────── Probe ─────────────────────────
/// Рукопожатие с узлом. Отпечаток берём из РЕАЛЬНО предъявленного сертификата.
pub async fn probe(state: &AppState, ip: IpAddr, port: u16) -> Result<ProbeInfo, String> {
let url = format!("https://{}:{}/p2p/info", ip, port);
let mut resp = state
.probe_client
.get(&url)
.header("x-node-name", pct_encode(&state.device_name))
.header("x-node-port", state.p2p_port.to_string())
.send()
.await
.map_err(|_| "узел недоступен".to_string())?;
if !resp.status().is_success() {
return Err(format!("узел вернул код {}", resp.status()));
}
let fp = resp
.extensions()
.get::<reqwest::tls::TlsInfo>()
.and_then(|t| t.peer_certificate())
.map(fingerprint)
.ok_or_else(|| "не удалось получить сертификат узла".to_string())?;
if resp.content_length().map_or(false, |l| l as usize > MAX_PROBE_BODY) {
return Err("слишком большой ответ".into());
}
let mut buf: Vec<u8> = Vec::new();
while let Some(chunk) = resp.chunk().await.map_err(|_| "ошибка чтения ответа".to_string())? {
buf.extend_from_slice(&chunk);
if buf.len() > MAX_PROBE_BODY {
return Err("слишком большой ответ".into());
}
}
let v: serde_json::Value = serde_json::from_slice(&buf).map_err(|_| "некорректный ответ".to_string())?;
Ok(ProbeInfo {
fp,
name: clean_label(v["name"].as_str().unwrap_or("Node"), 48),
})
}
pub async fn probe_and_register(state: &AppState, ip: IpAddr, port: u16) -> Result<ProbeInfo, String> {
let info = probe(state, ip, port).await?;
if info.fp == state.my_fingerprint {
return Err("это ваше собственное устройство".into());
}
state.upsert_peer(&info.name, ip, port, &info.fp);
Ok(info)
}
// ───────────────────────── Ping ─────────────────────────
pub async fn ping_all_peers(state: &Arc<AppState>) {
let Ok(_guard) = state.ping_lock.try_lock() else {
return; // опрос уже идёт
};
let targets: Vec<(String, Vec<String>, u16)> = state
.peers
.read()
.values()
.filter(|p| !p.ips.is_empty())
.map(|p| (p.id.clone(), p.ips.clone(), p.port))
.collect();
futures_util::stream::iter(targets)
.for_each_concurrent(8, |(id, ips, port)| async move {
ping_one(state, &id, ips, port).await;
})
.await;
}
async fn ping_one(state: &AppState, id: &str, ips: Vec<String>, port: u16) {
for ip_s in ips {
let Ok(ip) = ip_s.parse::<IpAddr>() else { continue };
if let Ok(info) = probe(state, ip, port).await {
if info.fp == id {
state.upsert_peer(&info.name, ip, port, id);
return;
}
// по этому адресу теперь другое устройство — игнорируем
}
}
let mut peers = state.peers.write();
if let Some(p) = peers.get_mut(id) {
if now_sec().saturating_sub(p.last_seen) > 15 {
p.online = false;
}
}
}
// ───────────────────────── Сканер ─────────────────────────
pub async fn run_scan(state: Arc<AppState>, ips: Vec<Ipv4Addr>, port: u16) {
let st = &state;
futures_util::stream::iter(ips)
.for_each_concurrent(48, |ip| async move {
let addr = SocketAddr::new(IpAddr::V4(ip), port);
let open = matches!(
tokio::time::timeout(Duration::from_millis(500), tokio::net::TcpStream::connect(addr)).await,
Ok(Ok(_))
);
let found = open && probe_and_register(st, IpAddr::V4(ip), port).await.is_ok();
let mut s = st.scan_status.write();
s.scanned += 1;
if found {
s.found += 1;
}
})
.await;
}
// ───────────────────────── UDP-обнаружение (без mDNS) ─────────────────────────
/// Beacon — лишь подсказка «я здесь». Ему НЕ доверяют: пир регистрируется только
/// после настоящего TLS-рукопожатия (probe), поэтому подделка UDP не отравляет список адресов.
#[derive(Serialize, Deserialize)]
struct Beacon {
v: u8,
port: u16,
fp: String,
}
fn bind_discovery_socket() -> std::io::Result<UdpSocket> {
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
socket.set_reuse_address(true)?;
socket.set_broadcast(true)?;
socket.set_nonblocking(true)?;
let addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DISCOVERY_PORT);
socket.bind(&addr.into())?;
UdpSocket::from_std(socket.into())
}
pub fn start_discovery(state: Arc<AppState>) {
// отправка
let tx = state.clone();
tokio::spawn(async move {
let sock = match UdpSocket::bind("0.0.0.0:0").await {
Ok(s) => s,
Err(e) => {
tx.add_log(format!("Ошибка UDP-сокета отправки: {}", e));
return;
}
};
let _ = sock.set_broadcast(true);
let pkt = serde_json::to_vec(&Beacon {
v: 1,
port: tx.p2p_port,
fp: tx.my_fingerprint.clone(),
})
.unwrap_or_default();
loop {
// интерфейсы пересчитываются каждый цикл — смена сети подхватится сама
let mut targets: Vec<Ipv4Addr> = lan_interfaces()
.into_iter()
.map(|(ip, bc)| {
bc.unwrap_or_else(|| {
let o = ip.octets();
Ipv4Addr::new(o[0], o[1], o[2], 255)
})
})
.collect();
targets.push(Ipv4Addr::BROADCAST);
targets.sort();
targets.dedup();
for t in targets {
let _ = sock
.send_to(&pkt, SocketAddr::new(IpAddr::V4(t), DISCOVERY_PORT))
.await;
}
tokio::time::sleep(Duration::from_secs(3)).await;
}
});
// приём
tokio::spawn(async move {
let socket = match bind_discovery_socket() {
Ok(s) => s,
Err(e) => {
state.add_log(format!(
"UDP-порт {} недоступен ({}). Автообнаружение отключено, используйте сканер.",
DISCOVERY_PORT, e
));
return;
}
};
let slots = Arc::new(Semaphore::new(8));
let mut buf = [0u8; 512];
loop {
let (len, remote) = match socket.recv_from(&mut buf).await {
Ok(v) => v,
Err(_) => {
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
};
let Ok(b) = serde_json::from_slice::<Beacon>(&buf[..len]) else { continue };
if b.v != 1 || b.port == 0 || !is_valid_fp(&b.fp) || b.fp == state.my_fingerprint {
continue;
}
// адрес — только реальный адрес источника пакета
let IpAddr::V4(ip4) = remote.ip() else { continue };
if !is_lan_v4(ip4) {
continue;
}
let ip = IpAddr::V4(ip4);
if state.is_fresh(&b.fp, ip) || !state.probe_allowed(ip) {
continue;
}
let Ok(permit) = slots.clone().try_acquire_owned() else { continue };
let st = state.clone();
let port = b.port;
tokio::spawn(async move {
let _permit = permit;
let _ = probe_and_register(&st, ip, port).await;
});
}
});
}