一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的 Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、 访问控制、DDNS、文件共享、Docker 管理等。 核心设计原则:不重新实现任何协议 PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。 Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。 面板删掉后,系统仍是一台配置完好的 Debian 路由器。 安全默认值 · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务 · 所有改网络的动作都需要用户在面板上亲手点「应用」 · 避开 5900 端口,不触碰 VNC / X 会话 · 配置写入前先做语法预检,失败则回滚 架构 · backend/ Python 3 标准库,零第三方依赖(9 个模块) · web/ 原生 JS/CSS,无构建步骤 · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess · 鉴权走 X-Token 请求头 打包 · packaging/build-deb.sh → drouter_1.0.0_all.deb(含 6 个 systemd 单元) · scripts/make-offline-deps.sh → 236 个依赖 .deb + 本地仓库索引 · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装 · packaging/build-docker.sh → 容器镜像(体验用) 测试 40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配), 全部挂在 devtools/sync.sh 的预检链里。 许可:MIT
425 lines
18 KiB
Bash
425 lines
18 KiB
Bash
#!/bin/bash
|
||
# =============================================================================
|
||
# drouter 一键运维脚本 —— 安装 / 卸载 / 修复 / 更新 / 状态
|
||
#
|
||
# drouter-ctl.sh install 交互式安装(询问网卡、LAN IP/网段、网关、DNS、管理员账号)
|
||
# drouter-ctl.sh uninstall 卸载(保留配置与快照;加 --purge 彻底清除)
|
||
# drouter-ctl.sh repair 修复(重部署文件 + 修权限 + 重建单元 + 校验)
|
||
# drouter-ctl.sh update 更新(更新代码,保留配置与主题)
|
||
# drouter-ctl.sh status 状态(服务/端口/版本/配置摘要/自检)
|
||
#
|
||
# 安全红线(任何时候都不得违反):
|
||
# 1. 绝不启动/启用 dnsmasq、radvd、miniupnpd 等会抢端口或发 DHCP 的服务
|
||
# 2. 绝不触碰 RealVNC(5900)
|
||
# 3. /etc/drouter/BUILD_MODE 存在时(构建保护模式),任何写网络动作一律中止
|
||
# 4. 卸载默认保留 /etc/drouter 配置与快照,避免用户配置被误删
|
||
# =============================================================================
|
||
set -u
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
|
||
OPT=/opt/drouter
|
||
SRC_DEFAULT=/tmp/drouter-src
|
||
DB=$OPT/data/drouter.db
|
||
UNIT_WEB=drouter-web
|
||
UNIT_SNAP=drouter-snapshot.timer
|
||
UNIT_LOGD=drouter-logd.timer
|
||
UNIT_RESCUE=drouter-rescue
|
||
UNIT_SHELLD=drouter-shelld
|
||
UNIT_HELPD=drouter-helpd
|
||
|
||
C_RESET=$'\033[0m'; C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_DIM=$'\033[2m'
|
||
say() { printf '%s\n' "$*"; }
|
||
ok() { printf '%s✔%s %s\n' "$C_GRN" "$C_RESET" "$*"; }
|
||
warn() { printf '%s⚠%s %s\n' "$C_YEL" "$C_RESET" "$*"; }
|
||
err() { printf '%s✘%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
|
||
step() { printf '\n%s==>%s %s\n' "$C_DIM" "$C_RESET" "$*"; }
|
||
die() { err "$*"; exit 1; }
|
||
|
||
# ---------------------------------------------------------------- 前置检查
|
||
need_root() {
|
||
[ "$(id -u)" = 0 ] || die "请以 root 运行(sudo bash $0 $1)"
|
||
}
|
||
# 构建保护模式:存在该文件时禁止一切可能改网络的动作
|
||
guard_build_mode() {
|
||
if [ -f /etc/drouter/BUILD_MODE ]; then
|
||
die "检测到构建保护模式 /etc/drouter/BUILD_MODE,已中止。
|
||
如需继续,请先确认当前不在构建阶段,然后:rm -f /etc/drouter/BUILD_MODE"
|
||
fi
|
||
}
|
||
# 安全红线自检:本脚本绝不去动这些服务/端口
|
||
assert_no_network_daemons() {
|
||
local bad=""
|
||
for s in dnsmasq radvd miniupnpd dhcpcd isc-dhcp-server; do
|
||
if systemctl is-enabled "$s" 2>/dev/null | grep -qx enabled; then bad="$bad $s"; fi
|
||
done
|
||
[ -z "$bad" ] || warn "以下服务当前是 enabled 状态:$bad(本脚本不会启用它们,请自行确认)"
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 输入校验
|
||
valid_ipv4() {
|
||
python3 - "$1" <<'PY'
|
||
import sys, ipaddress
|
||
try:
|
||
ipaddress.IPv4Address(sys.argv[1]); print('yes')
|
||
except Exception:
|
||
print('no')
|
||
PY
|
||
}
|
||
valid_cidr() {
|
||
python3 - "$1" <<'PY'
|
||
import sys, ipaddress
|
||
try:
|
||
ipaddress.IPv4Network(sys.argv[1], strict=False); print('yes')
|
||
except Exception:
|
||
print('no')
|
||
PY
|
||
}
|
||
# 由网段推导:网络地址、掩码、可用主机范围
|
||
net_info() {
|
||
python3 - "$1" <<'PY'
|
||
import sys, ipaddress
|
||
n = ipaddress.IPv4Network(sys.argv[1], strict=False)
|
||
hosts = list(n.hosts())
|
||
gw = str(hosts[0]) if hosts else str(n.network_address)
|
||
p1 = str(hosts[min(10, len(hosts)-1)]) if hosts else ''
|
||
p2 = str(hosts[-1]) if hosts else ''
|
||
print(' '.join([str(n.network_address), str(n.netmask), str(n.broadcast_address), gw, p1, p2]))
|
||
PY
|
||
}
|
||
ask() { # ask 变量名 提示 默认值
|
||
local var="$1" prompt="$2" def="$3" val
|
||
if [ -n "${NONINTERACTIVE:-}" ]; then val="$def"
|
||
else
|
||
read -r -p "$(printf '%s%s%s [%s]: ' "$C_DIM" "$prompt" "$C_RESET" "$def")" val
|
||
val="${val:-$def}"
|
||
fi
|
||
printf -v "$var" '%s' "$val"
|
||
}
|
||
ask_secret() { # 密码类:不回显
|
||
local var="$1" prompt="$2" val
|
||
if [ -n "${NONINTERACTIVE:-}" ]; then val="${3:-}"; printf -v "$var" '%s' "$val"; return; fi
|
||
read -r -s -p "$(printf '%s%s%s: ' "$C_DIM" "$prompt" "$C_RESET")" val; echo
|
||
printf -v "$var" '%s' "$val"
|
||
}
|
||
|
||
# =============================================================================
|
||
cmd_install() {
|
||
need_root install
|
||
guard_build_mode
|
||
local SRC="${1:-$SRC_DEFAULT}"
|
||
[ -d "$SRC/backend" ] || die "源码目录不存在:$SRC(请先同步源码到该目录)"
|
||
|
||
step "1/7 收集安装参数"
|
||
# 自动探测候选网卡(排除 lo / 虚拟口)
|
||
local ifaces
|
||
ifaces=$(ls /sys/class/net 2>/dev/null | grep -vE '^(lo|docker|br-|veth|virbr|ppp)' | tr '\n' ' ')
|
||
say " 可用网卡:$ifaces"
|
||
ask LAN_IFACE "LAN 网卡(内网)" "$(echo "$ifaces" | awk '{print $1}')"
|
||
ask WAN_IFACE "WAN 网卡(外网,可留空稍后在界面选)" ""
|
||
ask LAN_CIDR "LAN 地址(IP/掩码位数)" "192.168.7.3/24"
|
||
valid_cidr "$LAN_CIDR" | grep -qx yes || die "LAN 地址格式不对:$LAN_CIDR(示例 192.168.7.3/24)"
|
||
|
||
read -r _NET _MASK _BC _GW _P1 _P2 <<<"$(net_info "$LAN_CIDR")"
|
||
say " ${C_DIM}网段 ${_NET} 掩码 ${_MASK} 可用 ${_P1} ~ ${_P2}${C_RESET}"
|
||
ask GATEWAY "本机 LAN 网关(路由器自身地址,通常就是 LAN IP)" "${LAN_CIDR%%/*}"
|
||
valid_ipv4 "$GATEWAY" | grep -qx yes || die "网关地址不合法:$GATEWAY"
|
||
ask DNS_LIST "DNS(逗号分隔)" "223.5.5.5,119.29.29.29"
|
||
ask DHCP_ON "是否开启 DHCP 地址池?(y/N)" "N"
|
||
POOL_START=""; POOL_END=""
|
||
if [ "${DHCP_ON}" = "y" ] || [ "${DHCP_ON}" = "Y" ]; then
|
||
ask POOL_START "地址池起始" "${_P1:-192.168.7.100}"
|
||
ask POOL_END "地址池结束" "${_P2:-192.168.7.200}"
|
||
valid_ipv4 "$POOL_START" | grep -qx yes || die "地址池起始不合法"
|
||
valid_ipv4 "$POOL_END" | grep -qx yes || die "地址池结束不合法"
|
||
fi
|
||
ask WEB_PORT "Web 管理端口" "8443"
|
||
ask ADM_USER "Web 管理员用户名" "admin"
|
||
ask_secret ADM_PASS "Web 管理员密码(至少 6 位,留空则沿用 admin123)" ""
|
||
if [ -n "$ADM_PASS" ] && [ "${#ADM_PASS}" -lt 6 ]; then die "密码至少 6 位"; fi
|
||
ask PPP_USER "PPPoE 宽带账号(可留空)" ""
|
||
PPP_PASS=""
|
||
if [ -n "$PPP_USER" ]; then ask_secret PPP_PASS "PPPoE 宽带密码" ""; fi
|
||
|
||
step "2/7 安装系统依赖(进入构建保护模式,不会启动任何服务)"
|
||
bash "$SRC/scripts/install-base.sh" 2>&1 | tail -12
|
||
|
||
step "3/7 部署程序文件"
|
||
DROUTER_SRC="$SRC" bash "$SRC/scripts/deploy.sh" 2>&1 | tail -20
|
||
|
||
step "4/7 启动管理后台(只启动 drouter-web,绝不启动 DHCP/DNS)"
|
||
# 自定义端口必须写进 /etc/drouter/web-port 才会生效(后端启动时读取)
|
||
if [ "$WEB_PORT" != "8443" ]; then
|
||
mkdir -p /etc/drouter
|
||
printf '%s\n' "$WEB_PORT" > /etc/drouter/web-port
|
||
ok "Web 端口已设为 $WEB_PORT"
|
||
fi
|
||
systemctl daemon-reload
|
||
systemctl enable "$UNIT_WEB" >/dev/null 2>&1 || true
|
||
systemctl restart "$UNIT_WEB"
|
||
# 等待端口就绪
|
||
local i=0
|
||
while [ $i -lt 30 ]; do
|
||
if ss -lnt 2>/dev/null | grep -q ":${WEB_PORT}\b"; then break; fi
|
||
sleep 1; i=$((i+1))
|
||
done
|
||
ss -lnt 2>/dev/null | grep -q ":${WEB_PORT}\b" \
|
||
&& ok "管理后台已监听 :${WEB_PORT}" \
|
||
|| { systemctl status "$UNIT_WEB" --no-pager -n 20; die "管理后台未能启动"; }
|
||
|
||
step "5/7 写入预置配置(走官方 API,保证与界面同一套校验)"
|
||
local pw="${ADM_PASS:-admin123}"
|
||
LOGIN_USER="$ADM_USER" LOGIN_PASS="$pw" PORT="$WEB_PORT" \
|
||
LAN_IFACE="$LAN_IFACE" WAN_IFACE="$WAN_IFACE" LAN_CIDR="$LAN_CIDR" \
|
||
GATEWAY="$GATEWAY" DNS_LIST="$DNS_LIST" POOL_START="$POOL_START" POOL_END="$POOL_END" \
|
||
PPP_USER="$PPP_USER" PPP_PASS="$PPP_PASS" \
|
||
python3 - <<'PY'
|
||
import json, os, ssl, urllib.request, urllib.error, sys
|
||
|
||
CTX = ssl._create_unverified_context() # 自签证书,仅本机回环调用
|
||
# 优先 HTTPS(自定义端口),失败回退 HTTP 8080(后端同时监听两者)
|
||
CANDIDATES = ['https://127.0.0.1:%s' % os.environ['PORT'], 'http://127.0.0.1:8080']
|
||
BASE = None
|
||
|
||
def call(path, payload=None, token=None):
|
||
global BASE
|
||
bases = [BASE] if BASE else CANDIDATES
|
||
last = {'ok': False, 'msg_cn': '无可用地址'}
|
||
for b in bases:
|
||
data = json.dumps(payload or {}).encode('utf-8')
|
||
req = urllib.request.Request(b + path, data=data, method='POST')
|
||
req.add_header('Content-Type', 'application/json')
|
||
if token: req.add_header('X-Token', token)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=20, context=CTX) as r:
|
||
BASE = b
|
||
return json.loads(r.read().decode('utf-8'))
|
||
except urllib.error.HTTPError as e:
|
||
try:
|
||
BASE = b
|
||
return json.loads(e.read().decode('utf-8'))
|
||
except Exception:
|
||
last = {'ok': False, 'msg_cn': 'HTTP %s' % e.code}
|
||
except Exception as e:
|
||
last = {'ok': False, 'msg_cn': str(e)}
|
||
return last
|
||
|
||
r = call('/api/login', {'username': os.environ['LOGIN_USER'],
|
||
'password': os.environ['LOGIN_PASS']})
|
||
if not r.get('ok'):
|
||
print(' 登录失败:%s' % r.get('msg_cn')); sys.exit(1)
|
||
tok = (r.get('data') or {}).get('token')
|
||
print(' 已登录(%s)' % BASE)
|
||
|
||
lan = os.environ['LAN_IFACE']; wan = os.environ['WAN_IFACE']
|
||
patch = {
|
||
'system': {'lan_iface': lan, 'lan_address': os.environ['LAN_CIDR'],
|
||
'gateway': os.environ['GATEWAY'], 'wan_iface': wan,
|
||
'wan_present': bool(wan)},
|
||
'nft_v4': {'lan_ifaces': [lan], 'wan_iface': wan or 'ppp0'},
|
||
'nft_v6': {'lan_ifaces': [lan], 'wan_iface': wan or 'ppp0'},
|
||
'radvd': {'iface': lan},
|
||
'dhcpv6': {'lan_iface': lan},
|
||
'dnsmasq': {'option_gateway': os.environ['GATEWAY'],
|
||
'option_dns': os.environ['GATEWAY'],
|
||
'dns_custom': os.environ['DNS_LIST'],
|
||
'dhcp_enabled': bool(os.environ.get('POOL_START'))},
|
||
}
|
||
if os.environ.get('POOL_START'):
|
||
patch['dnsmasq'].update({'pool_start': os.environ['POOL_START'],
|
||
'pool_end': os.environ['POOL_END'],
|
||
'pool_netmask': '255.255.255.0'})
|
||
if os.environ.get('PPP_USER'):
|
||
patch['pppoe'] = {'username': os.environ['PPP_USER'],
|
||
'password': os.environ.get('PPP_PASS', ''),
|
||
'iface': wan or '', 'saved_only': True}
|
||
for mod, data in patch.items():
|
||
rr = call('/api/config', {'module': mod, 'data': data}, tok)
|
||
print(' %-8s %s' % (mod, '已保存' if rr.get('ok') else '失败:%s' % rr.get('msg_cn')))
|
||
PY
|
||
|
||
step "6/7 安全红线复核"
|
||
assert_no_network_daemons
|
||
if ss -lnt 2>/dev/null | grep -q ':5900\b'; then ok "RealVNC 5900 仍在监听(未被影响)"
|
||
else warn "5900 未监听 —— 请确认 RealVNC 状态(本脚本未做任何操作)"; fi
|
||
[ -f /etc/drouter/BUILD_MODE ] && rm -f /etc/drouter/BUILD_MODE && ok "已退出构建保护模式"
|
||
|
||
step "7/7 完成"
|
||
local ip="${LAN_CIDR%%/*}"
|
||
ok "安装完成"
|
||
say " 管理地址:https://${ip}:${WEB_PORT}/"
|
||
say " 管理员:${ADM_USER} / ${ADM_PASS:+(已设置)}${ADM_PASS:-admin123(请尽快修改)}"
|
||
say ""
|
||
say " ${C_DIM}提示:以上只写入了配置,尚未应用到系统。${C_RESET}"
|
||
say " ${C_DIM}请在界面的「应用配置」里确认后再下发,避免影响现有网络。${C_RESET}"
|
||
}
|
||
|
||
# =============================================================================
|
||
cmd_uninstall() {
|
||
need_root uninstall
|
||
guard_build_mode
|
||
local PURGE=0
|
||
[ "${1:-}" = "--purge" ] && PURGE=1
|
||
|
||
step "停止并禁用服务"
|
||
for u in $UNIT_RESCUE $UNIT_WEB $UNIT_SNAP $UNIT_LOGD $UNIT_SHELLD $UNIT_HELPD; do
|
||
systemctl stop "$u" 2>/dev/null || true
|
||
systemctl disable "$u" 2>/dev/null || true
|
||
done
|
||
systemctl daemon-reload
|
||
|
||
step "移除 systemd 单元与系统配置"
|
||
rm -f /etc/systemd/system/drouter-*.service /etc/systemd/system/drouter-*.timer
|
||
rm -f /etc/sudoers.d/drouter /etc/logrotate.d/drouter
|
||
systemctl daemon-reload
|
||
|
||
step "移除程序目录"
|
||
rm -rf "$OPT"
|
||
|
||
if [ "$PURGE" = 1 ]; then
|
||
warn "--purge:同时删除配置、快照与日志"
|
||
rm -rf /etc/drouter /var/log/drouter /var/lib/drouter /run/drouter-helper
|
||
rm -f /etc/drouter/generated/theme.css
|
||
ok "已彻底清除(含用户配置与快照)"
|
||
else
|
||
ok "已卸载;保留 /etc/drouter(配置与主题)、/var/log/drouter(日志)"
|
||
say " ${C_DIM}如需彻底清除:bash $0 uninstall --purge${C_RESET}"
|
||
fi
|
||
# 顺带处理 apt 装的包?不动 —— 卸载程序不应卸载用户可能共用的系统包
|
||
say " ${C_DIM}注:系统软件包(dnsmasq/nftables 等)未被移除,避免影响其它服务。${C_RESET}"
|
||
}
|
||
|
||
# =============================================================================
|
||
cmd_repair() {
|
||
need_root repair
|
||
guard_build_mode
|
||
local SRC="${1:-$SRC_DEFAULT}"
|
||
[ -d "$SRC/backend" ] || die "源码目录不存在:$SRC"
|
||
|
||
step "1/5 重新部署文件(覆盖,不影响已有配置)"
|
||
DROUTER_SRC="$SRC" bash "$SRC/scripts/deploy.sh" 2>&1 | tail -12
|
||
|
||
step "2/5 修复权限与属主"
|
||
id drouter >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin -d "$OPT" -M drouter
|
||
chown -R root:root "$OPT/backend" "$OPT/web"
|
||
chown -R drouter:drouter "$OPT/data" "$OPT/snapshots" "$OPT/certs" 2>/dev/null || true
|
||
chmod 750 "$OPT/data" "$OPT/snapshots" "$OPT/certs" 2>/dev/null || true
|
||
chmod 700 /run/drouter-helper 2>/dev/null || true
|
||
chmod 0440 /etc/sudoers.d/drouter
|
||
visudo -c -f /etc/sudoers.d/drouter >/dev/null && ok "sudoers 语法正确"
|
||
|
||
step "3/5 重建服务单元并重启管理后台"
|
||
systemctl daemon-reload
|
||
systemctl enable $UNIT_WEB >/dev/null 2>&1 || true
|
||
systemctl restart $UNIT_WEB
|
||
systemctl enable $UNIT_SNAP >/dev/null 2>&1 || true
|
||
|
||
step "4/5 校验关键文件"
|
||
local bad=0
|
||
for f in render.py drouter-helper.py drouter-web.py drouter-logd.py \
|
||
drouter-snapshotd.py drouter-rescue.py drouter-shelld.py \
|
||
drouter-helpd.py theme.py; do
|
||
[ -f "$OPT/backend/$f" ] || { err "缺少 $f"; bad=1; }
|
||
done
|
||
[ -d /etc/drouter/themes ] || { mkdir -p /etc/drouter/themes; ok "已补建主题目录"; }
|
||
[ "$bad" = 0 ] && ok "8 个后端模块齐全"
|
||
|
||
step "5/5 复核安全红线"
|
||
assert_no_network_daemons
|
||
ok "修复完成"
|
||
}
|
||
|
||
# =============================================================================
|
||
cmd_update() {
|
||
need_root update
|
||
guard_build_mode
|
||
local SRC="${1:-$SRC_DEFAULT}"
|
||
[ -d "$SRC/backend" ] || die "源码目录不存在:$SRC"
|
||
|
||
step "1/4 更新前自动快照(便于回滚)"
|
||
if [ -f "$OPT/backend/drouter-snapshotd.py" ]; then
|
||
python3 "$OPT/backend/drouter-snapshotd.py" >/dev/null 2>&1 \
|
||
&& ok "快照已生成" || warn "快照失败(继续更新)"
|
||
fi
|
||
|
||
step "2/4 备份当前版本"
|
||
local ts; ts=$(date +%Y%m%d-%H%M%S)
|
||
local BAK="/opt/drouter-backup-$ts"
|
||
mkdir -p "$BAK"
|
||
cp -a "$OPT/backend" "$BAK/" 2>/dev/null || true
|
||
cp -a "$OPT/web" "$BAK/" 2>/dev/null || true
|
||
ok "已备份到 $BAK"
|
||
|
||
step "3/4 部署新版本(配置与主题不受影响)"
|
||
DROUTER_SRC="$SRC" bash "$SRC/scripts/deploy.sh" 2>&1 | tail -12
|
||
|
||
step "4/4 重启并校验"
|
||
systemctl daemon-reload
|
||
systemctl restart $UNIT_WEB
|
||
sleep 2
|
||
systemctl is-active $UNIT_WEB | grep -qx active \
|
||
&& ok "管理后台已重启" || { systemctl status $UNIT_WEB --no-pager -n 15; die "启动失败,可回滚:$BAK"; }
|
||
ok "更新完成(回滚:把 $BAK 里的目录拷回 $OPT 后 restart)"
|
||
}
|
||
|
||
# =============================================================================
|
||
cmd_status() {
|
||
step "服务"
|
||
printf ' %-24s %-10s %s\n' "单元" "活动" "开机自启"
|
||
for u in $UNIT_WEB $UNIT_SNAP $UNIT_LOGD $UNIT_RESCUE $UNIT_SHELLD $UNIT_HELPD; do
|
||
printf ' %-24s %-10s %s\n' "$u" \
|
||
"$(systemctl is-active "$u" 2>/dev/null)" "$(systemctl is-enabled "$u" 2>/dev/null)"
|
||
done
|
||
|
||
step "安装"
|
||
if [ -d "$OPT" ]; then
|
||
ok "程序目录:$OPT"
|
||
local n=0
|
||
for f in "$OPT"/backend/*.py; do [ -f "$f" ] && n=$((n+1)); done
|
||
say " 后端模块:$n 个"
|
||
[ -f "$DB" ] && say " 配置库:$(du -h "$DB" | cut -f1)" || warn "配置库不存在(可能尚未启动过)"
|
||
else
|
||
warn "未安装($OPT 不存在)"
|
||
fi
|
||
|
||
step "端口"
|
||
ss -lntup 2>/dev/null | grep -E ':(8443|53|67|546|5900)\b' | sed 's/^/ /' || say " (无相关端口)"
|
||
|
||
step "主题之家"
|
||
if [ -f /etc/drouter/active-theme ]; then
|
||
say " 当前主题:$(cat /etc/drouter/active-theme)"
|
||
say " 自定义主题数:$(ls -1 /etc/drouter/themes 2>/dev/null | wc -l)"
|
||
else warn " 主题状态文件不存在"; fi
|
||
|
||
step "安全红线"
|
||
if [ -f /etc/drouter/BUILD_MODE ]; then warn "构建保护模式:开启(写网络操作被禁止)"
|
||
else say " 构建保护模式:关闭"; fi
|
||
ss -lnt 2>/dev/null | grep -q ':5900\b' && say " RealVNC 5900:正常监听" || say " RealVNC 5900:未监听"
|
||
}
|
||
|
||
# =============================================================================
|
||
usage() {
|
||
cat <<'USAGE'
|
||
drouter 一键运维脚本
|
||
|
||
用法:drouter-ctl.sh <命令> [参数]
|
||
|
||
install [源码目录] 交互式安装(询问网卡 / LAN IP 网段 / 网关 / DNS / 管理员账号)
|
||
非交互可用环境变量 NONINTERACTIVE=1 并配合默认值
|
||
uninstall [--purge] 卸载。默认保留 /etc/drouter 配置与日志;--purge 彻底清除
|
||
repair [源码目录] 修复:重部署 + 修权限 + 重建单元 + 校验
|
||
update [源码目录] 更新:先快照备份,再部署新版本,配置与主题保留
|
||
status 状态:服务 / 端口 / 版本 / 主题 / 安全红线
|
||
|
||
安全约束:本脚本绝不启用 dnsmasq/radvd 等会抢端口的服务,绝不触碰 RealVNC 5900。
|
||
USAGE
|
||
}
|
||
|
||
case "${1:-}" in
|
||
install) shift; cmd_install "$@" ;;
|
||
uninstall) shift; cmd_uninstall "$@" ;;
|
||
repair) shift; cmd_repair "$@" ;;
|
||
update) shift; cmd_update "$@" ;;
|
||
status) shift; cmd_status "$@" ;;
|
||
*) usage; exit 1 ;;
|
||
esac
|