精简文档并完善跨平台发布、安装与备份测试

This commit is contained in:
imengying committed 2026-09-04 17:41:47 +08:00
1 parent 6e3819dedf
commit a9b4106966
45 files changed
+1217 -658

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
[build.env]
passthrough = ["NODEFLARE_VERSION"]
[target.aarch64-unknown-linux-musl]
runner = "qemu-user"
# cross 0.2.5 自带的 FreeBSD 镜像 sysroot 漏了 libgeom。上游 main 镜像已
# 补齐完整 /lib 及共享库软链,因此为 FreeBSD Agent 固定使用该镜像摘要。
# 该镜像以 FreeBSD 13 为 sysroot,所以产物最低支持 FreeBSD 13。
[target.x86_64-unknown-freebsd]
image = "ghcr.io/cross-rs/x86_64-unknown-freebsd@sha256:d1ecbeb055803f5316607acce26c8af2b8609a04e38a06ba7aed84f9df80042e"
+38 -3
View File
@@ -36,6 +36,30 @@ fail() {
exit 1
}
detect_glibc_version() {
[ -x "$glibc_loader" ] || return
LC_ALL=C "$glibc_loader" --version 2>/dev/null | awk '
tolower($0) ~ /glibc|gnu libc|gnu c library/ {
sub(/\.$/, "")
if (match($0, /[0-9]+\.[0-9]+(\.[0-9]+)?$/)) {
print substr($0, RSTART, RLENGTH)
exit
}
}
'
}
glibc_is_supported() {
glibc_major=${1%%.*}
glibc_minor=${1#*.}
glibc_minor=${glibc_minor%%.*}
case "$glibc_major:$glibc_minor" in
*[!0-9:]*|:*) return 1 ;;
esac
[ "$glibc_major" -gt 2 ] \
|| { [ "$glibc_major" -eq 2 ] && [ "$glibc_minor" -ge 28 ]; }
}
safe_value() {
case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac
}
@@ -113,7 +137,9 @@ install_agent() {
[ -n "$token" ] && [ -n "$endpoint" ] || { usage; exit 1; }
endpoint=${endpoint%/}
[ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || fail "安装参数长度超出限制"
safe_value "$token" && safe_value "$endpoint" || fail "服务地址或 Agent Token 格式无效"
if ! safe_value "$token" || ! safe_value "$endpoint"; then
fail "服务地址或 Agent Token 格式无效"
fi
case "$endpoint" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
@@ -132,7 +158,16 @@ install_agent() {
esac
case "$mirror" in *@*) fail "下载加速前缀不能包含用户信息" ;; esac
fi
case "$(uname -m)" in x86_64|amd64) arch="x64" ;; aarch64|arm64) arch="aarch64" ;; *) fail "暂不支持当前 CPU 架构:$(uname -m)" ;; esac
case "$(uname -m)" in
x86_64|amd64) arch="x64"; glibc_loader=/lib64/ld-linux-x86-64.so.2 ;;
aarch64|arm64) arch="aarch64"; glibc_loader=/lib/ld-linux-aarch64.so.1 ;;
*) fail "暂不支持当前 CPU 架构:$(uname -m)" ;;
esac
libc=musl
glibc_version=$(detect_glibc_version || true)
if [ -n "$glibc_version" ] && glibc_is_supported "$glibc_version"; then
libc=glibc
fi
init_system=$(detect_init_system)
[ "$init_system" != "unknown" ] || fail "未检测到正在运行的 systemd 或 OpenRC"
@@ -142,7 +177,7 @@ install_agent() {
chmod 750 "$STATE_DIR"
temporary="$INSTALL_DIR/.agent.$$.download"
trap 'rm -f "$temporary"' EXIT HUP INT TERM
artifact="agent-linux-$arch"
artifact="agent-linux-$arch-$libc"
release_api="https://api.github.com/repos/imengying/NodeFlare/releases/latest"
log "正在获取 GitHub 最新正式版本($artifact)"
release_json=$(curl --fail --location --silent --show-error --max-time 30 \
+8 -5
View File
@@ -27,7 +27,7 @@ NodeFlare Agent FreeBSD 安装脚本
install-freebsd.sh --status
install-freebsd.sh --uninstall
仅支持 FreeBSD amd64。Agent Token 请勿泄露。
支持 FreeBSD amd64 和 arm64。Agent Token 请勿泄露。
-m 为可选的 GitHub 下载加速前缀(形如 https://ghproxy.net),
仅作用于 Release 下载,摘要校验不受影响。
EOF
@@ -87,8 +87,9 @@ fi
[ "$(id -u)" -eq 0 ] || fail "请使用 root 权限运行安装"
[ "$(uname -s)" = "FreeBSD" ] || fail "此脚本仅支持 FreeBSD"
case "$(uname -m)" in
amd64|x86_64) ;;
*) fail "仅支持 FreeBSD amd64" ;;
amd64|x86_64) arch=x64 ;;
arm64|aarch64) arch=aarch64 ;;
*) fail "仅支持 FreeBSD amd64 和 arm64" ;;
esac
command -v fetch >/dev/null 2>&1 || command -v curl >/dev/null 2>&1 || fail "缺少 fetch 或 curl"
command -v sha256 >/dev/null 2>&1 || fail "缺少 sha256,无法校验下载文件"
@@ -121,7 +122,9 @@ done
[ -n "$token" ] && [ -n "$endpoint" ] || { usage; exit 1; }
endpoint=${endpoint%/}
[ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || fail "安装参数长度超出限制"
safe_value "$token" && safe_value "$endpoint" || fail "服务地址或 Agent Token 格式无效"
if ! safe_value "$token" || ! safe_value "$endpoint"; then
fail "服务地址或 Agent Token 格式无效"
fi
case "$endpoint" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
@@ -147,7 +150,7 @@ chmod 750 "$STATE_ROOT"
chmod 750 "$STATE_DIR"
temporary="$INSTALL_DIR/.agent.$$.download"
trap 'rm -f "$temporary"' EXIT HUP INT TERM
artifact="agent-freebsd-x64"
artifact="agent-freebsd-$arch"
release_api="https://api.github.com/repos/imengying/NodeFlare/releases/latest"
log "正在获取 GitHub 最新正式版本($artifact)"
release_json=$(download_stdout "$release_api")
+3 -1
View File
@@ -89,7 +89,9 @@ done
[ -n "$token" ] && [ -n "$endpoint" ] || { usage; exit 1; }
endpoint=${endpoint%/}
[ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || fail "安装参数长度超出限制"
safe_value "$token" && safe_value "$endpoint" || fail "服务地址或 Agent Token 格式无效"
if ! safe_value "$token" || ! safe_value "$endpoint"; then
fail "服务地址或 Agent Token 格式无效"
fi
case "$endpoint" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
-1
View File
@@ -5,7 +5,6 @@ use super::Report;
pub(crate) const MAX_LIVE_BATCH_BYTES: usize = 768 * 1024;
pub(crate) fn batch_len(queue: &VecDeque<Report>) -> usize {
// Allow room for the JSON envelope, commas, and future small protocol fields.
let mut encoded_bytes = 64_usize;
let mut count = 0_usize;
for report in queue {
+14 -20
View File
@@ -60,8 +60,6 @@ const LIVE_RECONNECT_DELAY: Duration = Duration::from_secs(3);
const LIVE_QUEUE_CAPACITY: usize = 720;
const MAX_PENDING_SPOOL_BYTES: u64 = 64 * 1024 * 1024;
const MAX_PENDING_SPOOL_LINE_BYTES: usize = 1024 * 1024;
// Database persistence can take a few hundred milliseconds; allow the ACK to arrive
// before continuing with the configured live interval.
const LIVE_ACK_READ_TIMEOUT: Duration = Duration::from_secs(5);
const LIVE_HINT_READ_TIMEOUT: Duration = Duration::from_millis(10);
const LIVE_REPORT_DIVISOR: u64 = 15;
@@ -250,8 +248,6 @@ impl ClockCalibration {
type SharedClock = Arc<Mutex<ClockCalibration>>;
/// 双栈公网地址探测:分别请求按协议族锁死的 icanhazip 端点(Cloudflare 运营,
/// 纯文本返回调用方出口 IP)。短暂失败保留最近结果,持续失败则清空过期地址。
#[derive(Default)]
struct PublicIpValue {
address: Option<IpAddr>,
@@ -286,7 +282,6 @@ struct PublicIpProbe {
}
impl PublicIpProbe {
/// 距上次尝试超过刷新间隔且没有探测在进行时,后台起一轮探测(不阻塞采样)。
fn refresh(&self) {
let due = self
.last_attempt
@@ -329,7 +324,6 @@ impl PublicIpProbe {
.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 {
@@ -358,7 +352,6 @@ fn probe_public_ip(url: &str, ipv6: bool) -> Option<IpAddr> {
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) {
@@ -1912,9 +1905,7 @@ fn receive_remote_output(
match receiver.recv_timeout(REMOTE_OUTPUT_DRAIN_TIMEOUT) {
Ok(output) => output,
Err(_) => {
// A background descendant can keep the inherited pipe open after
// the shell exits. End the task's process group so result
// collection itself cannot hang the Agent.
// Descendants may keep the pipe open after the shell exits.
terminate_remote_process_tree(child);
receiver
.recv_timeout(REMOTE_OUTPUT_DRAIN_TIMEOUT)
@@ -2785,12 +2776,7 @@ fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWorker) {
}
drop_socket = true;
}
Ok(LiveRead::RemoteTask(_task)) => {
// 在 wait_for_live_ack 中已处理,这里不应该出现
}
Ok(LiveRead::TaskResultAck(_)) => {
// 在 wait_for_live_ack 中已处理,这里不应该出现
}
Ok(LiveRead::RemoteTask(_) | LiveRead::TaskResultAck(_)) => {}
Ok(LiveRead::Pending) => {
eprintln!("live ACK timed out");
drop_socket = true;
@@ -3030,11 +3016,20 @@ fn version_triplet(value: &str) -> Option<(u64, u64, u64)> {
fn agent_artifact_name() -> Option<&'static str> {
match (env::consts::OS, env::consts::ARCH) {
("linux", "x86_64") => Some("agent-linux-x64"),
("linux", "aarch64") => Some("agent-linux-aarch64"),
("linux", "x86_64") => Some(if cfg!(target_env = "musl") {
"agent-linux-x64-musl"
} else {
"agent-linux-x64-glibc"
}),
("linux", "aarch64") => Some(if cfg!(target_env = "musl") {
"agent-linux-aarch64-musl"
} else {
"agent-linux-aarch64-glibc"
}),
("windows", "x86_64") => Some("agent-windows-x64.exe"),
("macos", "aarch64") => Some("agent-macos-aarch64"),
("freebsd", "x86_64") => Some("agent-freebsd-x64"),
("freebsd", "aarch64") => Some("agent-freebsd-aarch64"),
_ => None,
}
}
@@ -3490,7 +3485,6 @@ mod tests {
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);
@@ -3575,7 +3569,7 @@ mod tests {
fn validates_release_asset_digests() {
let hash = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
let mut asset = GithubReleaseAsset {
name: "agent-linux-x64".to_string(),
name: "agent-linux-x64-glibc".to_string(),
browser_download_url: "https://example.com/agent".to_string(),
digest: Some(format!("sha256:{hash}")),
};