极空间 NAS:SSH 高阶玩法实践

以极空间 Z4Pro+ 性能版为例,其 CPU 为 N355,配备 16GB DDR5 内存。若仅作为 NAS 存储使用,性能略显冗余,因此是时候尝试开启 SSH,以探索更多玩法。

一、服务器面板

面板选取

极空间的 ZOS 系统是基于 Linux 深度定制的操作系统,本质上可以视作一台服务器。为了更高效地管理该系统,使用服务器面板无疑是最佳选择——毕竟在具备 GUI 条件的情况下仍坚持命令行操作的行过于愚蠢。

至于为何选择 1Panel 而非宝塔,主要基于以下两点考虑:

  • 隐私安全:宝塔面板使用时强制要求注册宝塔账户并上传服务器配置信息,此举存在潜在的隐私风险。

  • 系统侵入性:宝塔面板采用原生方式安装,对系统结构改动较大。而极空间作为一款 NAS 设备,稳定性应为首要考量。相比之下,1Panel 基于 Docker 部署,对底层系统影响更小,更符合轻量化与可控性的需求。

安装 1Panel

参考飞致云文档,安装脚本如下:

bash -c "$(curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)"
 ██╗    ██████╗  █████╗ ███╗   ██╗███████╗██╗
███║ ██╔══██╗██╔══██╗████╗ ██║██╔════╝██║
╚██║ ██████╔╝███████║██╔██╗ ██║█████╗ ██║
██║ ██╔═══╝ ██╔══██║██║╚██╗██║██╔══╝ ██║
██║ ██║ ██║ ██║██║ ╚████║███████╗███████╗
╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
[1Panel Log]: ======================= 开始安装 =======================
设置 1Panel 安装目录 (默认为 /opt): /data_n001/data/udata/real/szyink
是否要配置镜像加速 [y/n]: y
[1Panel Log]: 配置文件已存在,我们将备份现有的配置文件到: /etc/docker/daemon.json.1panel_bak.
[1Panel Log]: 创建新配置文件 /etc/docker/daemon.json...

[1Panel Log]: 已添加镜像加速配置。
[1Panel Log]: 正在重启 Docker 服务...
[1Panel Log]: Docker 服务已成功重启。

闪存容量

极空间设备的闪存容量相对有限。以 Z4S 为例,其总闪存空间为 14.6GB,其中仅有 6.5GB 分配给根目录;而在 Z4Pro+ 上,总闪存空间为 29.1GB,分配给根目录的仅有 7GB。由此可见,在安装 1Panel 面板时,务必修改默认安装位置,以避免根目录空间耗尽。

➜ lsblk -o NAME,SIZE,TYPE,MOUNTPOINT | grep mmcblk
mmcblk0 14.6G disk
├─mmcblk0p1 285M part /boot/efi
├─mmcblk0p2 6.5G part /
└─mmcblk0p3 7.7G part /zspace
➜ lsblk -o NAME,SIZE,TYPE,MOUNTPOINT | grep mmcblk
mmcblk0 29.1G disk
├─mmcblk0p1 12M part
├─mmcblk0p2 512M part /boot
├─mmcblk0p3 2G part /rom
├─mmcblk0p4 2G part
├─mmcblk0p5 7G part /overlay
├─mmcblk0p6 12G part /zspace
├─mmcblk0p7 3G part /zspace/applications/logs
├─mmcblk0p8 300M part
└─mmcblk0p9 2G part
mmcblk0boot0 4M disk
mmcblk0boot1 4M disk
df -h /
Filesystem Size Used Avail Use% Mounted on
overlay 6.8G 1.2G 5.3G 19% /

安装位置首选 NVMe 硬盘。在极空间设备中,这类硬盘通常以 /data_n001/data_n002 等路径挂载到根目录下。参考默认的硬盘路径划分,建议将安装目录设定为:

/data_n001/data/udata/real/szyink

由于目录层级较深,为了兼顾使用习惯,可以软链接该目录到 /opt

mv /opt /opt_back
ln -s /data_n001/data/udata/real/szyink /opt

容器配置

在安装 1Panel 的过程中,其安装脚本提示覆盖 Docker 的 daemon.json 文件,并自动将原始内容备份到同目录下的 daemon.json.1panel_bak 文件。此操作导致极空间中原先已安装的容器丢失。为解决该问题,可以将备份文件恢复并重启 Docker 引擎:

cp /etc/docker/daemon.json.1panel_bak /etc/docker/daemon.json
➜ systemctl restart docker

二、桌面端样式修改

极空间桌面端目录位于:

/zspace/applications/services/pcweb/home/

极空间样式修饰

手动插入 CSS 链接,注入个性化样式修订。

修改列表:登录页样式跳转、全部应用页样式调整、最大化窗口时覆盖 Dock 和导航栏等。将样式代码写入进 custom.min.css 并保存到 index.html 同目录中,并添加引用:

<link href='/home/custom.min.css?v=1' rel='stylesheet'>
custome.css
@charset "UTF-8";.copyright, .loginFooter > .tc, .login > .titleText div:not(:first-child) {display: none;}body.normal #app .contain:has(.login) {background-image: url("https://inkss.cn/img/wallhaven/wallhaven-w58mv7.png");position: relative;display: flex;justify-content: center;align-items: center;}body.normal #app .contain:has(.login) .login {background: #d5d7ec00;box-shadow: 0 25px 45px #0000001a;transition: 0.5s;border-radius: 2px;}body.normal #app .contain:has(.login) .login:hover, body.normal #app .contain:has(.login) .login:has(input:focus) {border-radius: 30px;backdrop-filter: blur(10px);}body.normal #app .contain:has(.login) .login .titleText::before {content: "";position: relative;display: block;bottom: -40px;width: 0;height: 3px;background: white;transition: 0.5s;border-radius: 2px;}body.normal #app .contain:has(.login) .login .titleText:hover::before {width: 60px;}body.normal #app .contain:has(.login) .login .el-button {width: inherit;background-color: #f4d2d280;transition: 0.5s;}body.normal #app .contain:has(.login) .login .el-button:hover {transform: scale(0.98);}body.normal #app .contain:has(.login) .login .loginFooter .el-checkbox {transition: 0.5s;}body.normal #app .contain:has(.login) .login .loginFooter .el-checkbox:hover {transform: scale(1.1);}body.normal #app .contain:has(.login) .login .loginFooter .el-checkbox .el-checkbox__inner {border: 1px solid #feffff59;background-color: #feffff59 !important;}body.normal #app .contain:has(.login) .login .el-input::after {content: "";width: 0;transition: 0.5s;}body.normal #app .contain:has(.login) .login .el-input:hover::after, body.normal #app .contain:has(.login) .login .el-input:has(input:focus)::after {width: 450px;}body.normal #app .contain:has(.login) .login .el-input input::placeholder {color: #fbf1f1;}body.normal #app .home .windows .devName.bold {width: 40px;overflow: hidden;}body.normal #app .home .windows .devName.bold img {display: none;}body.normal #app .home .desktop .widget .actIcon {opacity: 0;transition: opacity 0.5s;}body.normal #app .home .desktop .widget:hover .actIcon {opacity: 1;}body.normal #app:has(.maximum.active):not(:has(.noframeViewerTheme.active)) .winOperateArea, body.normal #app:has(.maximum.active):not(:has(.noframeViewerTheme.active)) .rightArea > *:not(.insteadOfWinOperateIcon) {display: none;}body.normal #app:has(.maximum.active):not(:has(.noframeViewerTheme.active)) .main {top: 0;z-index: 10;}body.normal #app:has(.maximum.active):not(:has(.noframeViewerTheme.active)) .main .siderBar {display: none;}body.normal .setup-popper .userInfo .nickname {display: flex;flex-direction: column;}body.normal .setup-popper .userInfo .nickname::after {content: '言语道断,非物所拘。';font-weight: 500;font-size: 13px;line-height: 15px;color: #666;white-space: nowrap;margin-top: 4px;}body.normal .setup-popper .userInfo .username, body.normal .setup-popper .userInfo .userInfoBottomItem:has(.nasName) {display: none;}
原始未压缩版本
custom.scss
// SCSS TO CSS: https://www.dute.org/sass-to-css
@charset "UTF-8";

// 隐藏元素
.copyright,
.loginFooter > .tc,
.login > .titleText div:not(:first-child) {
display: none;
}

// 颜色变量
$input-placeholder-color: #fbf1f1;
$box-shadow-color: #0000001a;
$border-color: #feffff59;
$login-bg-color: #d5d7ec00;
$btn-bg-color: #f4d2d280;
$checkbox-bg-color: #feffff59;

// 通用过渡效果
$transition: 0.5s;

// 正常模式样式
body.normal {
#app {
// 登录组件样式
.contain:has(.login) {
background-image: url('https://inkss.cn/img/wallhaven/wallhaven-w58mv7.png');
position: relative;
display: flex;
justify-content: center;
align-items: center;

.login {
background: $login-bg-color;
box-shadow: 0 25px 45px $box-shadow-color;
transition: $transition;
border-radius: 2px;

&:hover,
&:has(input:focus) {
border-radius: 30px;
backdrop-filter: blur(10px);
}

.titleText {
&::before {
content: "";
position: relative;
display: block;
bottom: -40px;
width: 0;
height: 3px;
background: white;
transition: $transition;
border-radius: 2px;
}

&:hover::before {
width: 60px;
}
}

.el-button {
width: inherit;
background-color: $btn-bg-color;
transition: $transition;

&:hover {
transform: scale(0.98);
}
}

.loginFooter .el-checkbox {
transition: $transition;

&:hover {
transform: scale(1.1);
}

.el-checkbox__inner {
border: 1px solid $border-color;
background-color: $checkbox-bg-color !important;
}
}

.el-input {
&::after {
content: "";
width: 0;
transition: $transition;
}

&:hover::after,
&:has(input:focus)::after {
width: 450px;
}

input::placeholder {
color: $input-placeholder-color;
}
}
}
}

// 主页组件样式
.home {
.windows .devName.bold {
width: 40px;
overflow: hidden;

img {
display: none;
}
}

.desktop .widget {
.actIcon {
opacity: 0;
transition: opacity $transition;
}

&:hover .actIcon {
opacity: 1;
}
}
}

// 最大化窗口模式样式
&:has(.maximum.active):not(:has(.noframeViewerTheme.active)) {
.winOperateArea,
.rightArea > *:not(.insteadOfWinOperateIcon) {
display: none;
}

.main {
top: 0;
z-index: 10;

.siderBar {
display: none;
}
}
}
}

// 设置弹出框样式
.setup-popper .userInfo {
.nickname {
display: flex;
flex-direction: column;

&::after {
content: '言语道断,非物所拘。';
font-weight: 500;
font-size: 13px;
line-height: 15px;
color: #666;
white-space: nowrap;
margin-top: 4px;
}
}

.username,
.userInfoBottomItem:has(.nasName) {
display: none;
}
}
}

菜单页

自动化部署脚本

极空间在每次系统更新时会重置该文件,可通过编写脚本并配置至 1Panel 的定时任务中自动恢复修改。

  1. 替换标题并引用 CSS:检测 index.html 原标题,替换为新标题且添加自定义 CSS 引用;

  2. 复制覆盖资源:将指定的自定义 CSS 和网站图标,强制复制到目标目录覆盖旧文件;

  3. 前置环境检查:验证目标文件 / 目录可写性、依赖工具是否存在等。

文件目录结构(部分)
/zspace/applications/services/pcweb/
├── home
│ └── index.html
└── template
├── custom.min.css
├── custom.scss
└── favicon.ico
check_index.sh
check_index.sh
#!/bin/bash

# 配置项
CONFIG=(
"file_path=/zspace/applications/services/pcweb/home/index.html"
"title_search=<title>极空间 - 私有云</title>"
"title_replace=<title>柚子屋</title><link href='/home/custom.min.css?v=2' rel='stylesheet'>"
"css_source=/zspace/applications/services/pcweb/szyink/custom.min.css"
"favicon_source=/zspace/applications/services/pcweb/szyink/favicon.ico"
"target_dir=/zspace/applications/services/pcweb/home/"
)
backup_path="${CONFIG[0]#*=}.bak"

# 日志函数
log() {
local level="$1"
shift
local message="[$level] $*"
echo "$message" >&2
}

# 检查文件和目录
check_prerequisites() {
local file_path="${CONFIG[0]#*=}"
local target_dir="${CONFIG[5]#*=}"
if [ ! -f "$file_path" ] || [ ! -w "$file_path" ]; then
log "ERROR" "文件 $file_path 不存在或不可写。"
exit 1
fi
if [ ! -d "$target_dir" ] || [ ! -w "$target_dir" ]; then
log "ERROR" "目标目录 $target_dir 不存在或不可写。"
exit 1
fi
if ! command -v perl >/dev/null; then
log "ERROR" "Perl 未安装,请安装 perl。"
exit 1
fi
log "INFO" "检查通过:文件和目录存在且可写,Perl 可用。"
}

# 备份文件
backup_file() {
local file_path="${CONFIG[0]#*=}"
if [ -f "$backup_path" ]; then
log "INFO" "备份文件 $backup_path 已存在,跳过备份。"
return 0
fi
if cp "$file_path" "$backup_path" && cmp -s "$file_path" "$backup_path"; then
log "INFO" "备份成功:文件已备份到 $backup_path。"
else
log "ERROR" "备份文件 $file_path 失败。"
exit 1
fi
}

# 替换内容
replace_content() {
local file_path="${CONFIG[0]#*=}"
local title_search="${CONFIG[1]#*=}"
local title_replace="${CONFIG[2]#*=}"

# 检测标题是否需要替换
grep -q "$title_search" "$file_path" && title_needs_replace=1 || title_needs_replace=0

if [ "$title_needs_replace" -eq 0 ]; then
log "INFO" "文件已符合目标状态,无需修改。"
return 0
fi

# 备份并替换
backup_file
cp "$file_path" "$file_path.tmp" || { log "ERROR" "创建临时文件失败。"; exit 1; }
if [ "$title_needs_replace" -eq 1 ]; then
if sed "s|$title_search|$title_replace|" "$file_path.tmp" > "$file_path.tmp2" && mv "$file_path.tmp2" "$file_path.tmp"; then
log "INFO" "替换成功:已更新标题。"
else
mv "$file_path.tmp" "$file_path"
log "ERROR" "标题替换失败,已回滚。"
exit 1
fi
fi

# 更新文件
mv "$file_path.tmp" "$file_path" || { log "ERROR" "更新文件失败。"; exit 1; }
}

# 复制自定义文件
copy_custom_files() {
local target_dir="${CONFIG[5]#*=}"
for src in "${CONFIG[3]#*=}" "${CONFIG[4]#*=}"; do
if [ ! -f "$src" ]; then
log "ERROR" "源文件 $src 不存在。"
exit 1
fi
if ! cp -f "$src" "$target_dir"; then
log "ERROR" "无法复制 $src$target_dir。"
exit 1
fi
done
log "INFO" "复制成功:已更新 custom.min.css 和 favicon.ico。"
}

# 主逻辑
main() {
log "INFO" "开始执行脚本..."
check_prerequisites
replace_content
copy_custom_files
log "INFO" "脚本执行完成。"
}

# 执行主逻辑
main

配置检验

三、存储池之间镜像同步

极空间备份中心虽然支持不同存储池之间的镜像备份,但即使是管理员用户也无法为其他用户设置备份,除非登录对方账户手动配置。然而,由于这些备份项目并不在自身账户下,即使备份失败也无法获知。为解决这一问题,可以手动编写一个 Rsync 脚本,直接对存储目录进行备份,并通过 1Panel 的定时任务功能定期执行。

终端下安装 rsync
➜ apt install rsync

极空间用户数据存储布局示意:

极空间存储目录结构
/tmp/zfsv3/存储硬盘类型/用户手机号/data/
rsync 脚本
根据个人情况自行替换脚本内容
#!/bin/bash

# 定义需要同步的目录对
declare -a directories=(
"/tmp/zfsv3/nvme12/XXXX/data/相册存储:/tmp/zfsv3/sata1/XXXX/data/备份中心/相册存储"
"/tmp/zfsv3/nvme12/XXXX/data/文档同步/手机备份:/tmp/zfsv3/sata1/XXXX/data/备份中心/手机备份"
# 原始目录:备份目录
# more...
)

# 获取当前时间并格式化
log() {
echo "$(date "+[%Y-%m-%d %H:%M:%S]") $1"
}

# 记录开始时间
log "同步开始"

# 同步函数
sync_directories() {
local source_dir="$1"
local target_dir="$2"

# 检查源目录是否存在
if [ ! -d "$source_dir" ]; then
log "源目录 $source_dir 不存在。"
exit 1
fi

# 检查目标目录是否存在,如果不存在则创建
if [ ! -d "$target_dir" ]; then
log "警告:目标目录 $target_dir 不存在,正在创建..."
mkdir -p "$target_dir"
if [ $? -ne 0 ]; then
log "创建目标目录 $target_dir 失败。"
exit 1
fi
fi

log "同步 $source$target..."
# 使用 rsync 进行同步,并记录详细日志
rsync -av --delete "$source_dir/" "$target_dir/" | while read line; do
log "$line"
done

# 检查 rsync 命令是否成功
if [ $? -eq 0 ]; then
log "同步成功!已将 $source_dir 下的文件同步到 $target_dir。"
else
log "同步失败!请检查问题并重新尝试。"
exit 1
fi
}

# 遍历数组并调用同步函数
for pair in "${directories[@]}"; do
IFS=":" read -r source target <<< "$pair"
sync_directories "$source" "$target"
done

log "同步完成"

镜像同步

四、OneDrive 云端备份

极空间备份中心虽支持云端备份,但不知从何时起,OneDrive 备份功能在上传文件过程中会修改文件时间属性,导致图片类文件丢失原始元数据而无法正常使用。

丢失时间属性

为规避该问题,可使用 rclone 工具自行实现 OneDrive 备份。

sudo -v ; curl https://rclone.org/install.sh | sudo bash
  • 参考该教程进行 Onedrive 的连接配置:Microsoft OneDrive

  • rclone 命令参考:

    同步文件
    rclone sync /local/file <远端名称>:/remote/file
    配置密码
    rclone config password

最后我们编写个脚本,方便在计划任务中调用:

rclone 脚本
根据个人情况自行替换脚本内容
#!/bin/bash

# rclone 密码
export RCLONE_CONFIG_PASS="XXXXXX"

# 定义本地和远程目录数组
declare -A directories=(
["/tmp/zfsv3/sata1/XXXX/data/备份中心/相册存储"]="<远端名称>:/备份/异地云备/相册存储"
["/tmp/zfsv3/sata1/XXXX/data/备份中心/手机备份"]="<远端名称>:/备份/异地云备/手机备份"
# 添加更多的目录对
)

# 获取当前时间并格式化
current_time() {
date "+[%Y-%m-%d %H:%M:%S]"
}

# 记录开始时间
echo "$(current_time) 备份开始"

# 循环同步每个目录对
for local_dir in "${!directories[@]}"; do
remote_dir=${directories[$local_dir]}
echo "$(current_time) 同步 $local_dir$remote_dir..."

# 同步操作,并捕获错误
if rclone sync "$local_dir" "$remote_dir" --progress; then
echo "$(current_time) 完成 $local_dir 同步"
else
echo "$(current_time) 失败 $local_dir 同步"
exit 1
fi
done

# 输出备份完成消息并记录结束时间
echo "$(current_time) 所有备份已完成!"
echo "$(current_time) 备份结束"

# 取消设置环境变量
unset RCLONE_CONFIG_PASS

云备

五、极影视合集时间

在极影视中按时间排序时,合集会被排到最后,因为数据库中的 release_yearrelease_date 为空。要避免这种情况,可以在数据库中填补这两个字段的值。

zvideo

极影视的数据库文件位于 /zspace/zsrp/sqlite/zvideo/zvideo.db,修改前请记得备份该文件。

update_zvideo.sql
-- 开始事务以确保数据更新的原子性
BEGIN;

-- 确保临时表不存在
DROP TABLE IF EXISTS temp_zvideo;

-- 步骤1:创建临时表
CREATE TEMPORARY TABLE temp_zvideo (
id INTEGER,
title VARCHAR(250),
auto_series_id VARCHAR(64),
user_name VARCHAR(50),
release_year VARCHAR(16),
release_date INTEGER,
release TEXT
);

-- 步骤1.1:插入数据到临时表
INSERT INTO temp_zvideo
SELECT DISTINCT zc.id, zc.title, zc.auto_series_id, zc.user_name,
COALESCE(zc2.release_year, zc.release_year) AS release_year,
COALESCE(zc2.release_date, zc.release_date) AS release_date,
COALESCE(zc2.release, zc.release) AS release
FROM zvideo_collection zc
LEFT JOIN (
SELECT auto_series_id, user_name, release_year, release_date, release
FROM zvideo_collection zc2
WHERE extend_type != 7
AND release_date = (
SELECT MAX(release_date)
FROM zvideo_collection zc3
WHERE zc3.auto_series_id = zc2.auto_series_id
AND zc3.user_name = zc2.user_name
AND zc3.extend_type != 7
AND zc3.release_date IS NOT NULL
)
) zc2 ON zc.auto_series_id = zc2.auto_series_id AND zc.user_name = zc2.user_name
WHERE zc.extend_type = 7;

-- 步骤2:更新原表
UPDATE zvideo_collection
SET
release_year = COALESCE((
SELECT release_year
FROM temp_zvideo
WHERE temp_zvideo.id = zvideo_collection.id
), release_year),
release_date = COALESCE((
SELECT release_date
FROM temp_zvideo
WHERE temp_zvideo.id = zvideo_collection.id
), release_date),
release = COALESCE((
SELECT release
FROM temp_zvideo
WHERE temp_zvideo.id = zvideo_collection.id
), release)
WHERE id IN (SELECT id FROM temp_zvideo);

-- 步骤3:删除临时表
DROP TABLE temp_zvideo;

-- 提交事务
COMMIT;

-- 检查更新行数
SELECT changes() AS rows_updated;
update_zvideo.sh
#!/bin/bash

# 定义常量
DB_PATH="/zspace/zsrp/sqlite/zvideo/zvideo.db" # SQLite 数据库文件路径
SQL_FILE="/opt/credentials/sql/update_zvideo.sql" # SQL 脚本文件路径
MAX_RETRIES=10 # 最大重试次数
RETRY_INTERVAL=5 # 重试间隔时间(秒)

# 日志函数,记录带时间戳的消息
log() {
echo "$(date "+[%Y-%m-%d %H:%M:%S]") $1"
}

# 检查 sqlite3 命令是否可用
if ! command -v sqlite3 &> /dev/null; then
log "错误:sqlite3 未安装,请先安装 sqlite3。"
exit 1
fi

# 检查文件是否存在,提升脚本鲁棒性
if [ ! -f "$DB_PATH" ]; then
log "错误:数据库文件 $DB_PATH 不存在。"
exit 1
fi
if [ ! -f "$SQL_FILE" ]; then
log "错误:SQL 文件 $SQL_FILE 不存在。"
exit 1
fi

log "开始更新数据库..."

# 初始化重试计数器
retry_count=0

# 使用循环处理数据库锁定情况
while [ $retry_count -lt $MAX_RETRIES ]; do
# 执行 SQL 脚本,捕获输出和状态码
output=$(sqlite3 "$DB_PATH" < "$SQL_FILE" 2>&1)
status=$?

# 检查执行结果
if [ $status -eq 0 ]; then
log "数据库更新成功完成。更新行数:$output"
break # 成功后退出循环
else
# 检查是否因数据库锁定失败
if echo "$output" | grep -q "database is locked"; then
retry_count=$((retry_count + 1))
log "数据库被锁定,等待 $RETRY_INTERVAL 秒后重试... (尝试 $retry_count/$MAX_RETRIES)"
sleep $RETRY_INTERVAL
else
log "数据库更新失败,错误信息如下:"
log "$output" # 直接打印错误信息
exit 1 # 非锁定错误,直接退出
fi
fi
done

# 检查是否达到最大重试次数
if [ $retry_count -eq $MAX_RETRIES ]; then
log "错误:达到最大重试次数 $MAX_RETRIES,数据库仍被锁定,更新失败。"
exit 1
fi

log "数据库更新过程结束。"

合集更新

六、SSH 配置文件自动恢复

check_sshdconfig.sh
#!/bin/bash

# 定义常量
SSHD_CONFIG="/etc/ssh/sshd_config"
BACKUP_SSHD_CONFIG="${SSHD_CONFIG}.bak"
LOG_FILE="/var/log/ssh_config_script.log"
restart_needed=false

# 日志函数,输出到终端和文件
log() {
local level="$1"
shift
local message="[$level] $*"
echo "$message" >&2
echo "$message" >> "$LOG_FILE"
}

# 备份配置文件
backup_config() {
log INFO "正在备份配置文件..."
if ! sudo cp "$SSHD_CONFIG" "$BACKUP_SSHD_CONFIG"; then
log ERROR "备份配置文件失败"
exit 1
fi
log INFO "已备份到 $BACKUP_SSHD_CONFIG"
}

# 检查和更新监听地址
update_listen_address() {
log INFO "正在检查监听地址..."
if grep -Fx "ListenAddress 0.0.0.0" "$SSHD_CONFIG" >/dev/null && \
grep -Fx "ListenAddress ::" "$SSHD_CONFIG" >/dev/null; then
log INFO "监听地址未改动,无需修改。"
return 0
fi

log INFO "监听地址需要更新"
backup_config

# 使用临时文件确保修改的原子性
local temp_config
temp_config=$(mktemp)
if ! sed '/^ListenAddress/d' "$SSHD_CONFIG" > "$temp_config"; then
log ERROR "无法处理配置文件。"
rm -f "$temp_config"
exit 1
fi
if ! echo -e "ListenAddress 0.0.0.0\nListenAddress ::" >> "$temp_config"; then
log ERROR "无法写入新监听地址。"
rm -f "$temp_config"
exit 1
fi
if ! sudo mv "$temp_config" "$SSHD_CONFIG"; then
log ERROR "无法更新配置文件。"
rm -f "$temp_config"
exit 1
fi
log INFO "已更新监听地址为 0.0.0.0 和 ::"
restart_needed=true
}

# 验证配置文件语法
check_config() {
log INFO "正在检查 SSH 配置文件语法..."
if ! sudo sshd -t >/dev/null 2>&1; then
log ERROR "SSH 配置文件语法错误,请检查 $SSHD_CONFIG"
# 恢复备份
if [[ -f "$BACKUP_SSHD_CONFIG" ]]; then
log INFO "恢复备份配置文件"
sudo mv "$BACKUP_SSHD_CONFIG" "$SSHD_CONFIG"
fi
exit 1
fi
log INFO "SSH 配置文件语法检查通过"
}

# 重新加载 SSH 服务
reload_service() {
log INFO "正在重新加载 SSH 服务..."
if ! systemctl is-active --quiet sshd; then
log ERROR "SSH 服务未运行,请检查服务状态"
exit 1
fi
if ! sudo systemctl reload sshd; then
log ERROR "重新加载 SSH 服务失败,恢复备份"
if [[ -f "$BACKUP_SSHD_CONFIG" ]]; then
sudo mv "$BACKUP_SSHD_CONFIG" "$SSHD_CONFIG"
fi
exit 1
fi
log INFO "已重新加载 SSH 服务。"
}

# 主逻辑
main() {
# 检查 root 权限
if [[ $EUID -ne 0 ]]; then
log ERROR "此脚本需要以 root 权限运行,请使用 sudo 或切换到 root 用户。"
exit 1
fi

# 检查配置文件存在
if [[ ! -f "$SSHD_CONFIG" ]]; then
log ERROR "SSH 配置文件 $SSHD_CONFIG 不存在。"
exit 1
fi

log INFO "开始执行脚本..."

# 检查和更新监听地址
update_listen_address

# 如果需要重启,验证配置并重新加载服务
if $restart_needed; then
check_config
reload_service
fi

log INFO "脚本执行完成。"
}

# 执行主函数
main

七、IPV6 地址老化监听

ipv6_check.sh
#!/bin/bash

# 日志函数(仅输出级别和消息,时间由外部程序添加)
log() {
local level="$1"
shift
echo "[$level] $*" >&2
}

# 配置参数
TARGET_SUFFIX="::43b" # 目标地址固定后缀
INTERFACE="bond0" # 检测接口(按需修改)
TEST_URL="https://ipv6.icanhazip.com" # 外部IPv6测试服务
TIMEOUT=10 # 单次检测超时时间(秒)
RESTART_WAIT=15 # 重启网络后等待恢复的时间(秒)
CONNECTION="netplan-bond0" # 网络连接名称
RETRY_COUNT=1 # 检测失败后的重试次数


# 查找目标IPv6地址(后缀为::cbe)
find_target_ip() {
log "INFO" "开始查找${INTERFACE}接口中后缀为${TARGET_SUFFIX}的IPv6地址..."
local ip=$(ip -6 addr show dev "$INTERFACE" 2>/dev/null | grep -oP "([0-9a-fA-F:]+)$TARGET_SUFFIX/[0-9]+" | cut -d '/' -f1 | head -n1)
if [ -z "$ip" ]; then
log "ERROR" "未找到${INTERFACE}接口中后缀为${TARGET_SUFFIX}的IPv6地址,请检查网络配置!"
exit 1
fi
log "INFO" "找到目标IPv6地址:$ip"
echo "$ip"
}


# 主动检测:目标地址能否访问外网(精简重试日志)
check_outbound() {
local ip="$1"
log "INFO" "检测$ip能否访问外网(目标:$TEST_URL)..."

local retry=$RETRY_COUNT
local final_output=""
while [ $retry -ge 0 ]; do
local output=$(curl --interface "$ip" -s --connect-timeout "$TIMEOUT" "$TEST_URL" 2>&1)
local exit_code=$?
if [ $exit_code -eq 0 ]; then
log "INFO" "✅ 外网访问成功:$ip 可正常访问$TEST_URL"
return 0
fi
final_output="$output" # 记录最后一次错误
retry=$((retry - 1))
done

log "ERROR" "❌ 外网访问失败:$ip 无法访问$TEST_URL(错误:$final_output)"
return 1
}


# 被动检测:目标地址能否被外网识别(精简重试日志)
check_inbound() {
local ip="$1"
log "INFO" "检测$ip能否被外网识别(验证外部服务返回IP)..."

local retry=$RETRY_COUNT
while [ $retry -ge 0 ]; do
local remote_ip=$(curl -s --connect-timeout "$TIMEOUT" "$TEST_URL" 2>/dev/null | tr -d '\n')
if [ -n "$remote_ip" ]; then
# 忽略大小写比对
if [ "$(echo "$remote_ip" | tr 'A-Z' 'a-z')" = "$(echo "$ip" | tr 'A-Z' 'a-z')" ]; then
log "INFO" "✅ 外网识别成功:外部服务识别IP为$remote_ip(与目标一致)"
return 0
else
log "WARN" "⚠️ 外网识别不一致:外部服务识别IP为$remote_ip(与目标$ip不符)"
return 1 # 识别不一致,不触发重启
fi
fi
retry=$((retry - 1))
done

log "ERROR" "❌ 外网识别检测失败:无法获取外部服务返回的客户端IP(可能网络不通)"
return 1 # 网络不通,触发重启
}


# 重启网络连接(精简状态日志)
restart_network() {
log "INFO" "检测到网络异常,重启${CONNECTION}网络连接..."

# 断开连接
local down_output=$(sudo nmcli connection down "$CONNECTION" 2>&1)
if [ $? -ne 0 ]; then
log "ERROR" "❌ 断开${CONNECTION}失败:$down_output"
return 1
fi

# 启用连接
local up_output=$(sudo nmcli connection up "$CONNECTION" 2>&1)
if [ $? -ne 0 ]; then
log "ERROR" "❌ 启用${CONNECTION}失败:$up_output"
return 1
fi

log "INFO" "网络连接重启成功,等待${RESTART_WAIT}秒恢复..."
sleep "$RESTART_WAIT"
return 0
}


# 主逻辑
main() {
# 首次检测
local target_ip=$(find_target_ip)
local outbound_success=0 # 0=成功,1=失败
local inbound_success=0 # 0=成功,1=失败(网络不通)

check_outbound "$target_ip" || outbound_success=1
check_inbound "$target_ip" || inbound_success=1

# 异常时重启并二次检测
if [ $outbound_success -eq 1 ] || [ $inbound_success -eq 1 ]; then
if restart_network; then
local new_ip=$(find_target_ip)
log "INFO" "重启后再次检测..."
check_outbound "$new_ip"
check_inbound "$new_ip"
else
log "ERROR" "网络重启失败,检测终止"
fi
fi

log "INFO" "所有检测完成"
}

# 执行主逻辑
main

评论