新增双栈节点上报
This commit is contained in:
1 parent
0ecd3128c1
commit
7c76c8cee2
10 files changed
+192
-24
No files matched your search
+128
-2
@@ -14,7 +14,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
|
||||
use std::sync::{mpsc, Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
@@ -62,6 +62,11 @@ const LIVE_HINT_READ_TIMEOUT: Duration = Duration::from_millis(10);
|
||||
const LIVE_REPORT_DIVISOR: u64 = 15;
|
||||
const BASIC_INFO_REFRESH_INTERVAL: Duration = Duration::from_secs(5 * 60);
|
||||
const SLOW_METRICS_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const PUBLIC_IP_REFRESH_INTERVAL: Duration = Duration::from_secs(10 * 60);
|
||||
const PUBLIC_IP_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const PUBLIC_IP_WAIT_BUDGET: Duration = Duration::from_secs(8);
|
||||
const PUBLIC_IP_V4_URL: &str = "https://ipv4.icanhazip.com/";
|
||||
const PUBLIC_IP_V6_URL: &str = "https://ipv6.icanhazip.com/";
|
||||
const RUNTIME_STATS_INTERVAL: Duration = Duration::from_secs(5 * 60);
|
||||
const CLOCK_CALIBRATION_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
const CLOCK_CALIBRATION_MIN_CHANGE_MS: i64 = 20_000;
|
||||
@@ -235,6 +240,96 @@ impl ClockCalibration {
|
||||
|
||||
type SharedClock = Arc<Mutex<ClockCalibration>>;
|
||||
|
||||
/// 双栈公网地址探测:分别请求按协议族锁死的 icanhazip 端点(Cloudflare 运营,
|
||||
/// 纯文本返回调用方出口 IP),记录最近一次成功的结果。主连接只用一个协议族,
|
||||
/// 这里补齐另一个;探测失败保留旧值,从未成功则为空。
|
||||
#[derive(Default)]
|
||||
struct PublicIps {
|
||||
v4: Option<IpAddr>,
|
||||
v6: Option<IpAddr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct PublicIpProbe {
|
||||
state: Arc<Mutex<PublicIps>>,
|
||||
probing: Arc<AtomicBool>,
|
||||
last_attempt: Arc<Mutex<Option<Instant>>>,
|
||||
}
|
||||
|
||||
impl PublicIpProbe {
|
||||
/// 距上次尝试超过刷新间隔且没有探测在进行时,后台起一轮探测(不阻塞采样)。
|
||||
fn refresh(&self) {
|
||||
let due = self
|
||||
.last_attempt
|
||||
.lock()
|
||||
.unwrap()
|
||||
.is_none_or(|at| at.elapsed() >= PUBLIC_IP_REFRESH_INTERVAL);
|
||||
if !due || self.probing.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
*self.last_attempt.lock().unwrap() = Some(Instant::now());
|
||||
let state = Arc::clone(&self.state);
|
||||
let probing = Arc::clone(&self.probing);
|
||||
thread::spawn(move || {
|
||||
let v4 = probe_public_ip(PUBLIC_IP_V4_URL, false);
|
||||
let v6 = probe_public_ip(PUBLIC_IP_V6_URL, true);
|
||||
let mut state = state.lock().unwrap();
|
||||
if let Some(ip) = v4 {
|
||||
state.v4 = Some(ip);
|
||||
}
|
||||
if let Some(ip) = v6 {
|
||||
state.v6 = Some(ip);
|
||||
}
|
||||
probing.store(false, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
fn v4(&self) -> Option<String> {
|
||||
self.state.lock().unwrap().v4.map(|ip| ip.to_string())
|
||||
}
|
||||
|
||||
fn v6(&self) -> Option<String> {
|
||||
self.state.lock().unwrap().v6.map(|ip| ip.to_string())
|
||||
}
|
||||
|
||||
/// 一次性模式没有后续采样,等首轮探测落地(或预算耗尽)再出报告。
|
||||
fn wait_initial(&self) {
|
||||
let deadline = Instant::now() + PUBLIC_IP_WAIT_BUDGET;
|
||||
while self.probing.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
thread::sleep(Duration::from_millis(150));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn probe_public_ip(url: &str, ipv6: bool) -> Option<IpAddr> {
|
||||
let probe_agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(PUBLIC_IP_PROBE_TIMEOUT))
|
||||
.build()
|
||||
.into();
|
||||
let mut response = probe_agent
|
||||
.get(url)
|
||||
.header("User-Agent", format!("nodeflare-agent/{VERSION}"))
|
||||
.call()
|
||||
.ok()?;
|
||||
let mut body = String::new();
|
||||
response
|
||||
.into_body()
|
||||
.into_reader()
|
||||
.take(128)
|
||||
.read_to_string(&mut body)
|
||||
.ok()?;
|
||||
parse_public_ip(&body, ipv6)
|
||||
}
|
||||
|
||||
/// 端点返回的协议族必须与请求的锁族一致,防止代理或劫持回一个错族地址。
|
||||
fn parse_public_ip(text: &str, ipv6: bool) -> Option<IpAddr> {
|
||||
let ip: IpAddr = text.trim().parse().ok()?;
|
||||
match (ipv6, ip) {
|
||||
(true, IpAddr::V6(_)) | (false, IpAddr::V4(_)) => Some(ip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveSender {
|
||||
pending: Arc<(Mutex<VecDeque<Report>>, Condvar)>,
|
||||
send_interval: Arc<Mutex<Duration>>,
|
||||
@@ -389,6 +484,10 @@ struct Report {
|
||||
gpu_usage: f64,
|
||||
gpu_model: String,
|
||||
agent_version: String,
|
||||
#[serde(default)]
|
||||
ip_v4: String,
|
||||
#[serde(default)]
|
||||
ip_v6: String,
|
||||
disk_read_bps: f64,
|
||||
disk_write_bps: f64,
|
||||
disk_read_iops: f64,
|
||||
@@ -1193,6 +1292,7 @@ struct Collector {
|
||||
basic_at: Instant,
|
||||
slow: SlowMetrics,
|
||||
slow_at: Instant,
|
||||
public_ip: PublicIpProbe,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -1207,6 +1307,7 @@ impl Collector {
|
||||
basic_at: Instant::now(),
|
||||
slow: SlowMetrics::default(),
|
||||
slow_at: Instant::now(),
|
||||
public_ip: PublicIpProbe::default(),
|
||||
};
|
||||
collector.refresh_basic();
|
||||
collector.refresh_slow();
|
||||
@@ -1295,6 +1396,7 @@ impl Collector {
|
||||
if self.slow_at.elapsed() >= SLOW_METRICS_REFRESH_INTERVAL {
|
||||
self.refresh_slow();
|
||||
}
|
||||
self.public_ip.refresh();
|
||||
|
||||
let mem = text("/proc/meminfo");
|
||||
let mem_total = mem_value(&mem, "MemTotal:");
|
||||
@@ -1355,6 +1457,8 @@ impl Collector {
|
||||
gpu_usage: self.basic.gpu_usage,
|
||||
gpu_model: self.basic.gpu_model.clone(),
|
||||
agent_version: VERSION.to_string(),
|
||||
ip_v4: self.public_ip.v4().unwrap_or_default(),
|
||||
ip_v6: self.public_ip.v6().unwrap_or_default(),
|
||||
disk_read_bps: per_second(
|
||||
io_now
|
||||
.read_sectors
|
||||
@@ -1399,6 +1503,7 @@ struct Collector {
|
||||
basic_at: Instant,
|
||||
slow: SlowMetrics,
|
||||
slow_at: Instant,
|
||||
public_ip: PublicIpProbe,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "freebsd"))]
|
||||
@@ -1413,6 +1518,7 @@ impl Collector {
|
||||
basic_at: Instant::now(),
|
||||
slow: SlowMetrics::default(),
|
||||
slow_at: Instant::now(),
|
||||
public_ip: PublicIpProbe::default(),
|
||||
};
|
||||
collector.refresh_basic();
|
||||
collector.refresh_slow();
|
||||
@@ -1505,6 +1611,7 @@ impl Collector {
|
||||
if self.slow_at.elapsed() >= SLOW_METRICS_REFRESH_INTERVAL {
|
||||
self.refresh_slow();
|
||||
}
|
||||
self.public_ip.refresh();
|
||||
|
||||
let disk_metrics = self.slow.disks.clone();
|
||||
let tcp_connections = self.slow.tcp_connections;
|
||||
@@ -1553,6 +1660,8 @@ impl Collector {
|
||||
gpu_usage: self.basic.gpu_usage,
|
||||
gpu_model: self.basic.gpu_model.clone(),
|
||||
agent_version: VERSION.to_string(),
|
||||
ip_v4: self.public_ip.v4().unwrap_or_default(),
|
||||
ip_v6: self.public_ip.v6().unwrap_or_default(),
|
||||
disk_read_bps: 0.0,
|
||||
disk_write_bps: 0.0,
|
||||
disk_read_iops: 0.0,
|
||||
@@ -2394,6 +2503,10 @@ fn run(options: &CliOptions, once: bool, print_only: bool) -> Result<()> {
|
||||
.build()
|
||||
.into();
|
||||
let mut collector = Collector::new(&config);
|
||||
collector.public_ip.refresh();
|
||||
if once || print_only {
|
||||
collector.public_ip.wait_initial();
|
||||
}
|
||||
let mut next_collect = Instant::now() + Duration::from_secs(config.collect_interval);
|
||||
let mut next_latency: HashMap<String, Instant> = HashMap::new();
|
||||
let stats = Arc::new(runtime_stats::RuntimeStats::default());
|
||||
@@ -2622,7 +2735,7 @@ mod tests {
|
||||
ack_d1_interval, ack_wss_interval, advance_deadline, clock_offset_from_http_date,
|
||||
corrected_timestamp, gpu_name_from_uevent, is_public_probe_ip, live_endpoint,
|
||||
live_update_payload, monotonic_report_timestamp, normalized_version, parse_lspci_gpu_names,
|
||||
parse_pciconf_gpu_names, parse_probe_target, parse_system_profiler_gpu_names,
|
||||
parse_pciconf_gpu_names, parse_probe_target, parse_public_ip, parse_system_profiler_gpu_names,
|
||||
ping_latencies, ping_latency, prune_report_samples, release_asset_sha256,
|
||||
sanitize_latency_tasks, update_check_jitter, valid_endpoint, version_triplet, CliOptions,
|
||||
ClockCalibration, GithubReleaseAsset, LatencyResult, LatencyTask, LiveAck, Report,
|
||||
@@ -2635,6 +2748,19 @@ mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::disk_device;
|
||||
|
||||
#[test]
|
||||
fn parses_family_matched_public_ips() {
|
||||
let v4: std::net::IpAddr = "203.0.113.7".parse().unwrap();
|
||||
let v6: std::net::IpAddr = "2001:db8::1".parse().unwrap();
|
||||
assert_eq!(parse_public_ip("203.0.113.7\n", false), Some(v4));
|
||||
assert_eq!(parse_public_ip("2001:db8::1", true), Some(v6));
|
||||
// 协议族不匹配或无法解析时一律视为探测失败。
|
||||
assert_eq!(parse_public_ip("2001:db8::1", false), None);
|
||||
assert_eq!(parse_public_ip("203.0.113.7", true), None);
|
||||
assert_eq!(parse_public_ip("not an ip", false), None);
|
||||
assert_eq!(parse_public_ip("", false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_platform_gpu_names() {
|
||||
let lspci = "00:02.0 VGA compatible controller: Intel Corporation CometLake-S GT2 [UHD Graphics 630] (rev 05)\n\
|
||||
|
||||
@@ -136,18 +136,24 @@ function ipFamily(ip: string): "v4" | "v6" | null {
|
||||
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) ? "v4" : null;
|
||||
}
|
||||
|
||||
function ServerIpMeta({ ip, agentVersion, onCopy }: { ip: string; agentVersion: string | null; onCopy: (ip: string) => void }) {
|
||||
const family = ipFamily(ip);
|
||||
function ServerIpMeta({ server, agentVersion, onCopy }: { server: AdminServer; agentVersion: string | null; onCopy: (ip: string) => void }) {
|
||||
// 双栈探测上报的地址优先;旧版 agent 还没上报时回落到连接出口地址(last_ip)。
|
||||
const family = ipFamily(server.last_ip);
|
||||
const v4 = server.ip_v4 || (family === "v4" ? server.last_ip : "");
|
||||
const v6 = server.ip_v6 || (family === "v6" ? server.last_ip : "");
|
||||
const entries = [
|
||||
{ family: "v4" as const, ip: v4 },
|
||||
{ family: "v6" as const, ip: v6 },
|
||||
].filter((entry) => entry.ip);
|
||||
return (
|
||||
<div className="server-name-meta">
|
||||
{family ? (
|
||||
<>
|
||||
<span className={`ip-badge ${family}`}>{family === "v6" ? "IPv6" : "IPv4"}</span>
|
||||
<button type="button" className="ip-value" title={`点击复制:${ip}`} onClick={() => onCopy(ip)}>{ip}</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="meta-item">{ip || "尚未上报 IP"}</span>
|
||||
)}
|
||||
{entries.map((entry) => (
|
||||
<span className="server-ip-entry" key={entry.family}>
|
||||
<span className={`ip-badge ${entry.family}`}>{entry.family === "v6" ? "IPv6" : "IPv4"}</span>
|
||||
<button type="button" className="ip-value" title={`点击复制:${entry.ip}`} onClick={() => onCopy(entry.ip)}>{entry.ip}</button>
|
||||
</span>
|
||||
))}
|
||||
{!entries.length ? <span className="meta-item">{server.last_ip || "尚未上报 IP"}</span> : null}
|
||||
<span className="meta-dot">·</span>
|
||||
<span className="meta-item">{agentVersion ? `Agent v${agentVersion}` : "Agent 未上报版本"}</span>
|
||||
</div>
|
||||
@@ -653,7 +659,7 @@ export function AdminPanel({
|
||||
<button type="button" className="drag-handle" draggable onDragStart={(event) => startDrag(event, server.id)} onDragEnd={() => setDraggingId("")} title={`拖动排序:${server.name}`}><GripVertical size={15} /></button>
|
||||
<Checkbox checked={selectedIds.includes(server.id)} onChange={() => toggleSelected(server.id)} ariaLabel={`选择 ${server.name}`} />
|
||||
<span className={`status-dot ${settings && isOnline(server, settings.offline_threshold_seconds) ? "online" : ""}`} />
|
||||
<div className="server-name"><div className="server-name-main"><Flag region={server.region} size={17} /><strong>{server.name}</strong></div><ServerIpMeta ip={server.last_ip} agentVersion={server.agent_version} onCopy={(value) => void copyServerIp(value)} /></div>
|
||||
<div className="server-name"><div className="server-name-main"><Flag region={server.region} size={17} /><strong>{server.name}</strong></div><ServerIpMeta server={server} agentVersion={server.agent_version} onCopy={(value) => void copyServerIp(value)} /></div>
|
||||
<div className="row-actions"><button className="icon-btn" disabled={index === 0} onClick={() => void move(index, -1)} title="上移"><ChevronUp size={15} /></button><button className="icon-btn" disabled={index === servers.length - 1} onClick={() => void move(index, 1)} title="下移"><ChevronDown size={15} /></button><button className="icon-btn" disabled={busy} onClick={() => void showInstallCommand(server)} title="显示安装命令"><Download size={15} /></button><button className="icon-btn" onClick={() => openEditor(server)} title="编辑节点"><Pencil size={15} /></button><button className="icon-btn danger" onClick={() => void remove(server)} title="删除节点"><Trash2 size={15} /></button></div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -310,7 +310,8 @@
|
||||
.server-name-main { display: flex; min-width: 0; align-items: center; gap: 7px; }
|
||||
.server-name-main .region-flag { flex: 0 0 auto; font-size: 16px; line-height: 1; }
|
||||
.server-name-main strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.server-name-meta { display: flex; min-width: 0; align-items: center; gap: 7px; color: var(--admin-muted); font-size: 12px; }
|
||||
.server-name-meta { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 3px 7px; color: var(--admin-muted); font-size: 12px; }
|
||||
.server-ip-entry { display: inline-flex; min-width: 0; align-items: center; gap: 5px; }
|
||||
.ip-badge { flex: 0 0 auto; padding: 0 6px; border: 1px solid var(--admin-border); border-radius: 999px; font-size: 10px; font-weight: 600; line-height: 1.5; letter-spacing: .03em; }
|
||||
.ip-badge.v6 { color: var(--admin-accent); border-color: color-mix(in srgb, var(--admin-accent) 40%, transparent); }
|
||||
.ip-value { overflow: hidden; min-width: 0; max-width: 30ch; padding: 0; border: 0; background: none; color: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: copy; }
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
--chart-grid: rgb(83 104 116 / 0.15);
|
||||
--shadow: 0 10px 32px rgb(31 48 58 / 0.09), inset 0 1px 0 rgb(255 255 255 / 0.58);
|
||||
--node-offline-bg: rgb(255 255 255 / 0.2);
|
||||
/* Card geometry lives here so the grid, skeletons and the latency panels
|
||||
cannot drift apart and make rows jump as data arrives. */
|
||||
--node-card-min-height: 336px;
|
||||
--node-card-carrier-min-height: 416px;
|
||||
/* Card geometry lives here so the grid and the latency panels cannot drift
|
||||
apart and make rows jump as data arrives. */
|
||||
--node-card-min-height: 320px;
|
||||
--node-card-carrier-min-height: 400px;
|
||||
--quality-panel-height: 44px;
|
||||
--carrier-panel-height: 124px;
|
||||
color: var(--foreground);
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
.os-icon, .os-icon-fallback { width: 16px; height: 16px; flex: 0 0 auto; object-fit: contain; }
|
||||
.os-icon-fallback { color: var(--muted-foreground); }
|
||||
.region-flag { display: inline-flex; min-width: 18px; flex: 0 0 auto; align-items: center; justify-content: center; line-height: 1; }
|
||||
.node-body { position: relative; display: flex; min-height: 0; flex: 1; flex-direction: column; justify-content: space-between; gap: 14px; padding: 0 16px 16px; }
|
||||
.node-body { position: relative; display: flex; min-height: 0; flex: 1; flex-direction: column; justify-content: space-between; gap: 8px; padding: 0 16px 16px; }
|
||||
.node-chips { display: flex; height: 19px; flex: 0 0 auto; align-items: center; gap: 6px; overflow: hidden; margin-top: -4px; }
|
||||
.node-chips span { min-width: 0; overflow: hidden; border-radius: 999px; padding: 2px 8px; color: var(--muted-foreground); background: color-mix(in srgb, var(--muted) 82%, transparent); font-size: 12px; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.node-chips span + span { margin-left: auto; text-align: right; }
|
||||
.metric-grid { display: grid; gap: 12px 16px; grid-template-columns: 1fr 1fr; }
|
||||
.metric-grid { display: grid; gap: 10px 16px; grid-template-columns: 1fr 1fr; }
|
||||
.metric { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.metric.muted { opacity: .55; }
|
||||
.metric > div { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; }
|
||||
|
||||
@@ -206,6 +206,8 @@ export interface Server extends ServerSummary {
|
||||
export interface AdminServer extends ServerSummary {
|
||||
hidden: boolean;
|
||||
last_ip: string;
|
||||
ip_v4: string | null;
|
||||
ip_v6: string | null;
|
||||
network_interface: string;
|
||||
report_interval: number;
|
||||
collect_interval: number;
|
||||
|
||||
@@ -115,6 +115,8 @@ SELECT
|
||||
CAST(json_extract(m.state, '$.report.gpu_usage') AS REAL) AS gpu_usage,
|
||||
json_extract(m.state, '$.report.gpu_model') AS gpu_model,
|
||||
json_extract(m.state, '$.report.agent_version') AS agent_version,
|
||||
json_extract(m.state, '$.report.ip_v4') AS ip_v4,
|
||||
json_extract(m.state, '$.report.ip_v6') AS ip_v6,
|
||||
CAST(json_extract(m.state, '$.report.disk_read_bps') AS REAL) AS disk_read_bps,
|
||||
CAST(json_extract(m.state, '$.report.disk_write_bps') AS REAL) AS disk_write_bps,
|
||||
CAST(json_extract(m.state, '$.report.disk_read_iops') AS REAL) AS disk_read_iops,
|
||||
|
||||
+23
-3
@@ -12,7 +12,7 @@ mod theme;
|
||||
mod turnstile;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
@@ -812,6 +812,11 @@ pub(crate) fn validate_report(report: &AgentReport) -> Option<&'static str> {
|
||||
if report.agent_version.chars().count() > 80 {
|
||||
return Some("Agent 版本字段过长");
|
||||
}
|
||||
for value in [report.ip_v4.as_str(), report.ip_v6.as_str()] {
|
||||
if value.chars().count() > 45 || (!value.is_empty() && value.parse::<IpAddr>().is_err()) {
|
||||
return Some("公网地址字段无效");
|
||||
}
|
||||
}
|
||||
if report.disks.len() > 64
|
||||
|| report.gpus.len() > 32
|
||||
|| report.disks.iter().any(|disk| {
|
||||
@@ -1310,11 +1315,26 @@ mod tests {
|
||||
rewrite_remote_theme_index, same_origin, submitted_secret, valid_agent_mirror,
|
||||
valid_background_urls, valid_cloudflare_account_id, valid_cloudflare_api_token,
|
||||
valid_password_derived, valid_ping_target, valid_public_asset_url, validate_latency_task,
|
||||
validate_server,
|
||||
validate_report, validate_server,
|
||||
};
|
||||
use crate::{db::SECRET_MASK, models::LatencyTaskInput, models::ServerInput};
|
||||
use crate::{db::SECRET_MASK, models::AgentReport, models::LatencyTaskInput, models::ServerInput};
|
||||
use worker::{Method, Url};
|
||||
|
||||
#[test]
|
||||
fn validates_report_public_ips() {
|
||||
let mut report = AgentReport::default();
|
||||
assert_eq!(validate_report(&report), None);
|
||||
report.ip_v4 = "203.0.113.7".into();
|
||||
report.ip_v6 = "2001:db8::1".into();
|
||||
assert_eq!(validate_report(&report), None);
|
||||
// 非空字段必须是合法 IP,空串表示该协议族不可用、放行。
|
||||
report.ip_v4 = "not-an-ip".into();
|
||||
assert_eq!(validate_report(&report), Some("公网地址字段无效"));
|
||||
report.ip_v4 = String::new();
|
||||
report.ip_v6 = "2001:db8::1 extra".into();
|
||||
assert_eq!(validate_report(&report), Some("公网地址字段无效"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigns_rate_limits_by_route() {
|
||||
assert_eq!(
|
||||
|
||||
+5
-1
@@ -41,7 +41,7 @@ const DASHBOARD_HUB_NAME: &str = "dashboard";
|
||||
/// 代价:agent 升级后这些值要等下一次 bootstrap 轮询才刷新。
|
||||
/// 只影响展示,不影响任何判定 —— 告警和持久化读的是 `AgentReport` 本身,
|
||||
/// 剥离只发生在发往浏览器的副本上。
|
||||
const BROWSER_OMITTED_REPORT_FIELDS: [&str; 7] = [
|
||||
const BROWSER_OMITTED_REPORT_FIELDS: [&str; 9] = [
|
||||
"cpu_model",
|
||||
"os",
|
||||
"kernel",
|
||||
@@ -49,6 +49,8 @@ const BROWSER_OMITTED_REPORT_FIELDS: [&str; 7] = [
|
||||
"virtualization",
|
||||
"gpu_model",
|
||||
"agent_version",
|
||||
"ip_v4",
|
||||
"ip_v6",
|
||||
];
|
||||
|
||||
fn server_hub_name(server_id: &str) -> String {
|
||||
@@ -1747,6 +1749,8 @@ mod tests {
|
||||
gpu_usage: 0.0,
|
||||
gpu_model: String::new(),
|
||||
agent_version: "test".to_string(),
|
||||
ip_v4: "203.0.113.7".to_string(),
|
||||
ip_v6: String::new(),
|
||||
disk_read_bps: 0.0,
|
||||
disk_write_bps: 0.0,
|
||||
disk_read_iops: 0.0,
|
||||
|
||||
@@ -215,6 +215,11 @@ pub struct AgentReport {
|
||||
pub gpu_usage: f64,
|
||||
pub gpu_model: String,
|
||||
pub agent_version: String,
|
||||
/// 双栈公网地址,agent 双栈探测的结果;空串表示该协议族不可用或未上报。
|
||||
#[serde(default)]
|
||||
pub ip_v4: String,
|
||||
#[serde(default)]
|
||||
pub ip_v6: String,
|
||||
pub disk_read_bps: f64,
|
||||
pub disk_write_bps: f64,
|
||||
pub disk_read_iops: f64,
|
||||
@@ -244,6 +249,8 @@ pub struct ServerView {
|
||||
#[serde(serialize_with = "serialize_sqlite_bool")]
|
||||
pub auto_renewal: i64,
|
||||
pub last_ip: String,
|
||||
pub ip_v4: Option<String>,
|
||||
pub ip_v6: Option<String>,
|
||||
pub network_interface: String,
|
||||
pub reset_day: i64,
|
||||
pub report_interval: i64,
|
||||
|
||||
Reference in new issue
Block a user