Frps+MCSM最详细搭建指北
写在开头
这是我长期折腾计算机以来,首次围绕数据安全、网络安全、服务需求及稳定性四大维度,系统性从 0 搭建的 Ubuntu Server(GNU/Linux)服务器。整个过程就是 “边搭建边优化” 的迭代:最初采用 “开放所有必要端口 + HTTP 连接” 的基础方案,意识到安全隐患后,又调整为 “指定个别端口 + Nginx 反向代理 + HTTPS 加密” 的加固架构;存储层面也从单盘转向保障数据冗余的 RAID 1 阵列。
为确保实体机部署顺利,我先在虚拟机环境中完成了 2 次全流程演练,后续又在实体机上进行了 2 次(第二次是为了组RAID)实际安装调试,才最终达成预期效果。此外,考虑到计算机行业 “文档化运维” 的传统 —— 便于后期排障、迭代与复用,因此整理此篇文档,记录整个搭建过程与关键决策。
安装Ubuntu Server
安装时组建RAID1
-
在存储配置的页面,选择自定义
-
进入到此界面
-
清除已有分区
-
可以看到,此 VPS 上有两块虚拟硬盘,大小均为 20GB,其中“/dev/vda”已经设置了几个分区,而“/dev/vdb”还未使用。
-
选中“/dev/vda”并按回车,然后进入“Reformat”选项。此时会提示你,此操作会清除所有数据,确认即可。如果你的数据盘也有数据,也按此方式清除掉所有分区。
-
清除完成后,你会看到此界面。
-
-
在硬盘上创建分区
-
再次选中“/dev/vda”,选择“Use As Boot Device”并确认。此操作是为了在系统盘上创建引导分区。
-
创建完成后,剩余空间是没法直接使用的,需要创建分区。选择可用空间,然后进入“Add GPT Partition”创建分区,空间留空(默认使用全部剩余空间),“Format”选择“Leave unformatted”。
-
然后,在数据盘上也创建一个分区,空间留空,“Format”同样选择“Leave unformatted”。
-
-
创建 RAID 并分区
-
在两个硬盘上创建好分区以后,回到分区界面,选择“Create software RAID (md)”,然后在弹出的界面中选中刚才在两块硬盘上创建的分区,类型选择 RAID 1,名称默认是“md0”,并确认。
-
此时你会在“AVAILABLE DEVICES”中看到刚才创建的 RAID 磁盘。选中并进入“Add GPT Partition”创建分区,“Format”选择“ext4”,“Mount”设置为“/”,即根目录。
-
-
完成后,可以看到如下界面
启用ufw(Uncomplicated Firewall)
-
启动 ufw 防火墙
1
sudo ufw enable
-
查看当前 ufw 状态
1
sudo ufw status
配置RAID 1失败邮件告知脚本
-
安装相关软件
1
sudo apt update && sudo apt install smartmontools msmtp msmtp-mta -y
-
配置脚本并设置权限
1 2
sudo nano /usr/local/bin/raid1_monitor.sh sudo chmod +x /usr/local/bin/raid1_monitor.sh
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
#!/bin/bash # /usr/local/bin/raid1_monitor.sh # mdadm PROGRAM style: $1 = EVENT, $2 = RAID_DEV, $3 = component (may be empty) # If run with no args -> TEST mode (send a test email) set -eu # configuration (adjust if needed) EMAIL="example@examle.com" FROM_ADDR="example-sender@example.com" ACCOUNT_NAME="qq" # msmtp account name (not strictly required if /etc/msmtprc default) STATE_DIR="/var/lib/raid_monitor" LOCKFILE="/var/run/raid_monitor.lock" DEBOUNCE_SEC=10 DEDUP_SEC=$((15*60)) PROGRESS_THRESHOLDS=(25 50 75 100) PROGRESS_STATE_SUFFIX="_rebuild_progress" LOG_PREFIX="RAID 事件" mkdir -p "$STATE_DIR" touch "$STATE_DIR/.dummy" >/dev/null 2>&1 || true # safe arg handling: if no args -> test mode if [ $# -eq 0 ]; then echo "No arguments provided. Running in TEST mode: will send a test email." EVENT="Test" RAID_DEV="${RAID_DEV:-/dev/md0}" DRIVE="${DRIVE:-/dev/sda1}" else EVENT="$1" RAID_DEV="${2:-}" DRIVE="${3:-}" fi # locking to avoid concurrent runs exec 9>"$LOCKFILE" if ! flock -n 9; then logger -p info "$LOG_PREFIX: another instance is running, exiting" exit 0 fi log() { logger -p info "$LOG_PREFIX: $*"; } send_mail() { local subject="$1" local body="$2" { echo "From: Zephyr BD <$FROM_ADDR>" echo "To: $EMAIL" echo "Subject: $subject" echo "Content-Type: text/plain; charset=UTF-8" echo echo -e "$body" } | msmtp --from="$FROM_ADDR" -t } # mdadm helpers get_failed_count() { sudo mdadm --detail "$RAID_DEV" 2>/dev/null | awk -F: '/Failed Devices/ {gsub(/ /,"",$2); print $2; exit}' } is_degraded() { sudo mdadm --detail "$RAID_DEV" 2>/dev/null | grep -qiE 'degraded' && return 0 || return 1 } is_resyncing() { grep -q "$(basename "$RAID_DEV")" /proc/mdstat 2>/dev/null && grep -qE 'resync|recovery|rebuild' /proc/mdstat 2>/dev/null && return 0 || return 1 } # SMART helper check_smart_one() { local disk="$1" [ -b "$disk" ] || { echo "NOTPRESENT"; return; } if sudo smartctl -i "$disk" 2>/dev/null | grep -qi 'SMART support is:.*Unavailable'; then echo "NOSMART"; return fi if sudo smartctl -H "$disk" 2>/dev/null | grep -qi 'PASSED'; then echo "PASSED" else echo "FAILED" fi } # dedupe helpers state_file="${STATE_DIR}/$(basename "$RAID_DEV")_state" last_event_file="${STATE_DIR}/$(basename "$RAID_DEV")_last_event" progress_state_file="${STATE_DIR}/$(basename "$RAID_DEV")${PROGRESS_STATE_SUFFIX}" now_ts=$(date +%s) should_send_dedup_ok() { local ev="$1" if [ -f "$last_event_file" ]; then read -r last_ev last_ts < <(awk '{print $1" "$2}' "$last_event_file" 2>/dev/null || true) if [ "$last_ev" = "$ev" ]; then if [ -n "$last_ts" ] && [ $((now_ts - last_ts)) -lt "$DEDUP_SEC" ]; then return 1 fi fi fi return 0 } record_last_event() { local ev="$1" printf "%s %s\n" "$ev" "$now_ts" > "$last_event_file" } # If Test mode -> send a sample email and exit if [ "$EVENT" = "Test" ]; then SUBJECT_FINAL="RAID 监控测试邮件" BODY="这是一封来自 RAID 监控脚本的测试邮件。\n主机: $(hostname)\n时间: $(date -u +"%Y-%m-%d %H:%M:%SZ")\n示例事件: Test\n" if send_mail "$SUBJECT_FINAL" "$BODY"; then log "Test email sent." exit 0 else log "Failed to send test email." exit 2 fi fi # handle rebuild progress parsing rebuild_progress_percent="" if echo "$EVENT" | grep -qE '^Rebuild[0-9]+'; then rebuild_progress_percent=$(echo "$EVENT" | sed -E 's/[^0-9]*([0-9]+).*/\1/') norm_event="RebuildProgress" else norm_event="$EVENT" fi SENDMAIL=false case "$norm_event" in Fail) SENDMAIL=true ;; DegradedArray) if should_send_dedup_ok "DegradedArray"; then SENDMAIL=true else log "DegradedArray suppressed by dedupe" SENDMAIL=false fi ;; RebuildStarted) if should_send_dedup_ok "RebuildStarted"; then SENDMAIL=true else SENDMAIL=false fi echo "0" > "$progress_state_file" 2>/dev/null || true ;; RebuildFinished) SENDMAIL=true echo "100" > "$progress_state_file" 2>/dev/null || true ;; RebuildProgress) # threshold-based progress notifications last_notified=0 if [ -f "$progress_state_file" ]; then read -r last_notified < "$progress_state_file" 2>/dev/null || last_notified=0 last_notified=${last_notified:-0} fi curr_p="${rebuild_progress_percent:-0}" new_notified=$last_notified for t in "${PROGRESS_THRESHOLDS[@]}"; do if [ "$curr_p" -ge "$t" ] && [ "$t" -gt "$last_notified" ]; then new_notified="$t" fi done if [ "$new_notified" -gt "$last_notified" ]; then SENDMAIL=true progress_to_record="$new_notified" else SENDMAIL=false fi ;; SpareActive|DeviceDisappeared|NewArray) # DeviceDisappeared/NewArray can be transient — debounce log "$EVENT on $RAID_DEV detected for $DRIVE — waiting ${DEBOUNCE_SEC}s to confirm" sleep "$DEBOUNCE_SEC" if is_degraded || [ "$(get_failed_count || echo 0)" -gt 0 ] || is_resyncing; then if should_send_dedup_ok "DegradedArray"; then SENDMAIL=true norm_event="DegradedArray" else log "After debounce, DegradedArray confirmed but dedupe suppressed mail" SENDMAIL=false fi else log "Transient $EVENT on $RAID_DEV resolved; not sending mail" SENDMAIL=false fi ;; *) SENDMAIL=false ;; esac # build message body MESSAGE="" if [[ "$norm_event" == "Fail" || "$norm_event" == "DegradedArray" || "$norm_event" == "RebuildStarted" || "$norm_event" == "RebuildFinished" ]]; then MESSAGE="事件: $EVENT\n阵列: $RAID_DEV\n组件: $DRIVE\n时间: $(date -u +"%Y-%m-%d %H:%M:%SZ")\n" fi if [[ "$norm_event" == "Fail" || "$norm_event" == "DegradedArray" ]]; then MESSAGE+="\nSMART 检查结果:\n" for disk in /dev/sd[a-z]; do res=$(check_smart_one "$disk") case "$res" in PASSED) MESSAGE+="✅ $disk: SMART PASSED\n";; FAILED) MESSAGE+="❌ $disk: SMART FAILED (请检查)\n" if sudo smartctl -l error "$disk" >/dev/null 2>&1; then MESSAGE+="---- 最近 Error Log (前 5 行) ----\n$(sudo smartctl -l error "$disk" 2>/dev/null | sed -n '1,5p')\n" fi ;; NOSMART) MESSAGE+="ℹ️ $disk: 不支持 SMART,已跳过\n";; NOTPRESENT) MESSAGE+="ℹ️ $disk: 设备不存在\n";; esac done fi # progress mail handling if [ "$SENDMAIL" = true ] && [ "$norm_event" = "RebuildProgress" ]; then SUBJECT_FINAL="RAID 重建进度: ${RAID_DEV} - ${rebuild_progress_percent}%" BODY="🔄 阵列 ${RAID_DEV} 重建进度: ${rebuild_progress_percent}%\n组件: ${DRIVE}\n时间: $(date -u +"%Y-%m-%d %H:%M:%SZ")\n\n(已触发阈值通知)" if send_mail "$SUBJECT_FINAL" "$BODY"; then log "Rebuild progress ${rebuild_progress_percent}% notification sent for ${RAID_DEV}" if [ -n "${progress_to_record:-}" ]; then echo "$progress_to_record" > "$progress_state_file" 2>/dev/null || true else echo "$rebuild_progress_percent" > "$progress_state_file" 2>/dev/null || true fi record_last_event "RebuildProgress_${rebuild_progress_percent}" else log "Failed to send rebuild progress mail for ${RAID_DEV} ${rebuild_progress_percent}%" fi exit 0 fi # generic send for other events if [ "$SENDMAIL" = true ]; then SUBJECT_FINAL="服务器 RAID 事件通知: $RAID_DEV - $norm_event" BODY="$MESSAGE" if send_mail "$SUBJECT_FINAL" "$BODY"; then log "邮件已发送: $SUBJECT_FINAL" record_last_event "$norm_event" if [ "$norm_event" = "RebuildStarted" ]; then echo "0" > "$progress_state_file" 2>/dev/null || true fi if [ "$norm_event" = "RebuildFinished" ]; then echo "100" > "$progress_state_file" 2>/dev/null || true fi else log "邮件发送失败: $SUBJECT_FINAL" fi else log "$EVENT detected on $RAID_DEV ($DRIVE). No email (informational or suppressed)." fi exit 0
-
修改msmtp配置
1 2
sudo nano /etc/msmtprc sudo chmod 600 /etc/msmtprc
内容如下(QQ邮箱):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# /etc/msmtprc defaults auth on tls on tls_starttls off tls_trust_file /etc/ssl/certs/ca-certificates.crt logfile /var/log/msmtp.log account qq host smtp.qq.com port 465 from example-sender@example.com user example-sender@example.com password example-password account default : qq
-
测试脚本
1
sudo /usr/local/bin/raid1_monitor.sh
-
编辑 mdadm 配置,添加通知脚本
1 2
sudo nano /etc/mdadm/mdadm.conf sudo systemctl restart mdmonitor
添加以下内容:
1
PROGRAM /usr/local/bin/raid1_monitor.sh
-
最后测试
1 2 3 4 5 6 7 8 9
# 假设 RAID 是 /dev/md127,某块盘是 /dev/sdb1 sudo mdadm --manage /dev/md127 --fail /dev/sdb1 # 移除失败标记 sudo mdadm --manage /dev/md127 --remove /dev/sdb1 # 再重新加回去,触发 rebuild sudo mdadm --manage /dev/md127 --add /dev/sdb1 # 查看重建状态 cat /proc/mdstat
配置OpenSSH
公私钥可通过XSheel生成
-
上传公钥到服务器
1
nano ~/.ssh/authorized_keys
粘贴公钥即可
-
打开配置文件
1
sudo nano /etc/ssh/sshd_config
更改设置如下:
1 2 3 4 5 6
Port 1022 AddressFamily any ListenAddress 0.0.0.0 PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 PasswordAuthentication no
如果配置没有生效,修改
1
sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf
-
放行1022端口
1
sudo ufw allow 1022/tcp && sudo ufw reload
-
重启 SSH 服务
1 2 3
sudo systemctl daemon-reload # 重新加载 systemd 配置 sudo systemctl restart ssh.socket # 重启 SSH socket 单元(负责监听端口) sudo systemctl restart sshd # 重启 SSH 服务
-
验证是否生效
1
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no zephyrbd@192.168.31.159 -p 1022
安装实用软件和环境
-
安装zsh和neofetch
1
sudo apt update && sudo apt install zsh neofetch
设置zsh为默认Shell
1
chsh -s $(which zsh)
-
安装SMART信息软件(在上面安装过就跳过)
1
sudo apt install smartmontools -y
确认硬盘设备名并查看 SMART 信息
1 2
lsblk sudo smartctl -a /dev/sda
-
安装java
1 2
# 安装 OpenJDK 17 和 21 sudo apt update && sudo apt install openjdk-17-jre openjdk-21-jre -y
交互式切换默认版本
1
sudo update-alternatives --config java
安装NVIDIA GPU驱动
-
查看推荐版本
1
ubuntu-drivers devices
-
自动安装推荐版本
1
sudo ubuntu-drivers autoinstall
-
重启并验证
1 2
sudo reboot nvidia-smi
安装并配置 Frp 服务端(Frps)
-
打开 FRP 官方 Releases 页面,找到最新版本(如
v0.52.3
),复制对应架构的下载链接(例如frp_0.52.3_linux_amd64.tar.gz
)。在服务器上用
wget
下载(替换链接为实际版本):1
wget https://github.com/fatedier/frp/releases/download/v0.52.3/frp_0.52.3_linux_amd64.tar.gz
-
解压压缩包并进入文件夹
1 2
tar -zxvf frp_0.52.3_linux_amd64.tar.gz cd frp_0.52.3_linux_amd64
-
查看核心文件
frps
:服务端程序(我们需要的)frps.toml
:服务端配置文件frpc
/frpc.toml
:客户端程序和配置(暂时不用)
-
移动到系统目录
1 2
sudo mkdir -p /etc/frp sudo cp frps frps.toml /etc/frp/
-
编辑配置文件
1
sudo nano /etc/frp/frps.toml
具体如下:
1 2 3 4 5 6 7 8
bindPort = 7000 auth.method = "token" auth.token = "example-token" webServer.addr = "0.0.0.0" webServer.port = 7001 webServer.user = "admin" webServer.password = "example"
-
创建系统服务
1
sudo nano /etc/systemd/system/frps.service
服务配置如下:
1 2 3 4 5 6 7 8 9 10 11 12
[Unit] Description=FRP Server Service After=network.target [Service] Type=simple ExecStart=/etc/frp/frps -c /etc/frp/frps.toml Restart=always User=root [Install] WantedBy=multi-user.target
-
重载系统服务、启动 FRP 服务、设置开机自启
1 2 3
sudo systemctl daemon-reload sudo systemctl start frps sudo systemctl enable frps
-
放行
7000
端口1
sudo ufw allow 7000
安装并配置MCSM(MCSMANAGER)面板服务
-
执行自动安装脚本(只支持 Ubuntu/Centos/Debian/Arch 等主流 x86_64/ARM 架构的操作系统)
1
sudo su -c "wget -qO- https://script.mcsmanager.com/setup_cn.sh | bash"
-
修改Web和Daemon的端口(可选)
1 2
sudo nano /opt/mcsmanager/web/data/SystemConfig/config.json sudo nano /opt/mcsmanager/daemon/data/Config/global.json
这里我的Web改成了
22223
,Daemon改成了23334
。 -
放行
25565
、19132
端口1 2
sudo ufw allow 25565/tcp sudo ufw allow 19132/udp
修改时区和设置时间同步
-
设置为北京时间
1
sudo timedatectl set-timezone Asia/Shanghai
-
验证设置
1
date
此时应显示类似 Wed Aug 20 12:30:00 CST 2025 的时间(CST 即中国标准时间)。
-
使用
chrony
同步系统时间1
sudo apt install chrony
-
设置开机启动
1
sudo systemctl enable chrony.service
设置定时关机(Option One)
-
编辑 crontab 配置
1
sudo crontab -e
-
添加定时关机任务
1
30 0 * * * /sbin/shutdown -h now
- 含义解释:
30 0 * * *
表示每天 0 点 30 分 /sbin/shutdown -h now
是立即关机的命令
- 含义解释:
-
验证任务是否添加成功
1
sudo crontab -l
列出所有定时任务,确认刚才添加的关机任务是否存在。
配置阿里云备份存档(Option Two)
特别感谢tickstep/aliyunpan项目
-
安装
aliyunpan:
1
sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg > /dev/null && echo "deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg arch=amd64,arm64] http://file.tickstep.com/apt aliyunpan main" | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list > /dev/null && sudo apt-get update && sudo apt-get install -y aliyunpan
zip包:
1
sudo apt update && sudo apt install zip
-
登录
1
aliyunpan login
-
配置脚本
1
nano ~/mc_backup.sh
-
不带清除多余备份和关机版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#!/bin/bash SOURCE_DIR="/opt/mcsmanager/daemon/data/InstanceData/bd4fab342c5b4d1aaed443c7f7c1a081" TMP_DIR="/tmp/mc_backup" PAN_DIR="/Buckups/minecraft_backups" mkdir -p "$TMP_DIR" aliyunpan mkdir -p "$PAN_DIR" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") ZIP_NAME="mc_worlds_$TIMESTAMP.zip" # 打包 zip -r "$TMP_DIR/$ZIP_NAME" \ "$SOURCE_DIR/world" \ "$SOURCE_DIR/world_nether" \ "$SOURCE_DIR/world_the_end" # 上传 aliyunpan upload "$TMP_DIR/$ZIP_NAME" "$PAN_DIR" # 删除临时文件 rm -f "$TMP_DIR/$ZIP_NAME"
-
带清除多余备份和关机版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#!/bin/bash SOURCE_DIR="/opt/mcsmanager/daemon/data/InstanceData/bd4fab342c5b4d1aaed443c7f7c1a081" PAN_DIR="/Buckups/minecraft_backups" mkdir -p "$TMP_DIR" aliyunpan mkdir -p "$PAN_DIR" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") ZIP_NAME="mc_worlds_$TIMESTAMP.zip" # 打包上传 zip -r "$TMP_DIR/$ZIP_NAME" \ "$SOURCE_DIR/world" \ "$SOURCE_DIR/world_nether" \ "$SOURCE_DIR/world_the_end" aliyunpan upload "$TMP_DIR/$ZIP_NAME" "$PAN_DIR" rm -f "$TMP_DIR/$ZIP_NAME" # 清理云盘1天前的备份 DATE_THRESHOLD=$(date -d "1 day ago" +"%Y%m%d") aliyunpan ls "$PAN_DIR" | tail -n +5 | awk '{print $NF}' | while read file_info; do if [[ "$file_info" =~ ^mc_worlds_([0-9]{8})_.*\.zip$ ]]; then FILE_DATE="${BASH_REMATCH[1]}" if [[ "$FILE_DATE" -lt "$DATE_THRESHOLD" ]]; then aliyunpan rm "$PAN_DIR/$file_info" fi fi done # 关机 /sbin/shutdown -h now
-
-
赋予执行权限
1 2
chmod +x ~/mc_backup_1505.sh chmod +x ~/mc_backup_0025.sh
-
配置定时任务
1
crontab -e
填入:
1 2
5 15 * * * /home/zephyrbd/mc_backup_1505.sh >> /home/zephyrbd/mc_backup.log 2>&1 25 0 * * * /home/zephyrbd/mc_backup_0025.sh >> /home/zephyrbd/mc_backup.log 2>&1
-
测试
1
~/mc_backup_1505.sh
使用Nginx进行Https代理
生成自签名证书(适合个人使用)
|
|
获取ZeroSSL证书(通过CA机构认证)
-
创建DuckDNS自动更新ip脚本
1 2
sudo nano /usr/local/bin/duckdns-update.sh sudo chmod +x /usr/local/bin/duckdns-update.sh
填入以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
#!/usr/bin/env bash set -euo pipefail # ====== 必填 ====== TOKEN="example-token" # 支持多个子域,用逗号分隔,如 "foo,bar,baz" DOMAINS="example" # ====== 可选:配置 ====== # 留空表示“让 DuckDNS 自动检测本机公网 IP” # 你也可以手动指定:IPV4="1.2.3.4" IPV6="2001:db8::1" IPV4="" IPV6="" # 日志与状态缓存 STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/duckdns" LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/duckdns" mkdir -p "$STATE_DIR" "$LOG_DIR" LAST_FILE="$STATE_DIR/last.txt" LOG_FILE="$LOG_DIR/update.log" # 重试设置 MAX_RETRY=3 SLEEP_SECS=5 timestamp() { date +"%Y-%m-%d %H:%M:%S%z"; } # 构造请求 URL(DuckDNS: https://www.duckdns.org/update?domains=...&token=...&ip=...&ipv6=...) build_url() { local base="https://www.duckdns.org/update?domains=${DOMAINS}&token=${TOKEN}" # 只有当设置了IP时才带上参数;否则让 DuckDNS 自行判断 [[ -n "${IPV4}" ]] && base="${base}&ip=${IPV4}" [[ -n "${IPV6}" ]] && base="${base}&ipv6=${IPV6}" printf '%s' "$base" } # 简单状态去重:如果上次请求URL相同,就不重复调用 need_update() { local url="$1" if [[ -f "$LAST_FILE" ]]; then local last last="$(cat "$LAST_FILE" || true)" [[ "$last" == "$url" ]] && return 1 fi return 0 } do_update() { local url="$1" local attempt=1 while (( attempt <= MAX_RETRY )); do # -s 静默,--connect-timeout 防止挂起,-f 失败即非0退出 if resp="$(curl -s echo "$(timestamp) domains=${DOMAINS} resp=${resp}" | tee -a "$LOG_FILE" # DuckDNS 约定返回 "OK" 或 "KO" if [[ "$resp" =~ ^OK ]]; then printf '%s' "$url" > "$LAST_FILE" return 0 fi # 其他视为失败,进入重试 fi echo "$(timestamp) update failed (attempt ${attempt}/${MAX_RETRY}), retrying in ${SLEEP_SECS}s..." | tee -a "$LOG_FILE" sleep "$SLEEP_SECS" ((attempt++)) done echo "$(timestamp) update failed after ${MAX_RETRY} attempts." | tee -a "$LOG_FILE" return 1 } main() { # 基于“自动检测公网IP”的默认行为,通常无需自己探测 IP。 local url url="$(build_url)" if need_update "$url"; then do_update "$url" else echo "$(timestamp) skip: no change in request (likely IP unchanged)" | tee -a "$LOG_FILE" fi } main "$@"
-
创建ZeroSSL证书更新脚本
1 2
sudo nano /usr/local/bin/duckdns-cert.sh sudo chmod +x /usr/local/bin/duckdns-cert.sh
填入以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#!/usr/bin/env bash set -euo pipefail DOMAIN="example-ddns.com" ACME_HOME="/home/zephyrbd/.acme.sh" export DuckDNS_Token="example-token" CERT_DIR="/home/zephyrbd/mycerts/$DOMAIN" mkdir -p "$CERT_DIR" # 申请或更新证书(忽略“Domains not changed”退出码) "$ACME_HOME"/acme.sh --issue --dns dns_duckdns -d "$DOMAIN" || true "$ACME_HOME"/acme.sh --install-cert -d "$DOMAIN" \ --key-file "$CERT_DIR/privkey.pem" \ --fullchain-file "$CERT_DIR/fullchain.pem" \ --reloadcmd "systemctl reload nginx" || true # 强制 systemd 返回成功 exit 0
-
配置服务和定时器
-
DuckDNS更新服务和Timer
1 2
sudo nano /etc/systemd/system/duckdns.service sudo nano /etc/systemd/system/duckdns.timer
Service
1 2 3 4 5 6 7 8 9 10 11
[Unit] Description=DuckDNS IP updater After=network-online.target Wants=network-online.target [Service] Type=oneshot Environment="HOME=/root" ExecStart=/usr/local/bin/duckdns-update.sh StandardOutput=journal StandardError=journal
Timer
1 2 3 4 5 6 7 8 9 10 11
[Unit] Description=Run DuckDNS updater every 5 minutes [Timer] OnBootSec=30s OnUnitActiveSec=5min AccuracySec=30s Unit=duckdns.service [Install] WantedBy=timers.target
-
SLL证书更新服务和Timer
-
安装 acme.sh
1
curl https://get.acme.sh | sh
-
注册账户
1
~/.acme.sh/acme.sh --register-account -m example@examle.com
-
创建服务和定时器
1 2
sudo nano /etc/systemd/system/duckdns-cert.service sudo nano /etc/systemd/system/duckdns-cert.timer
Service:
1 2 3 4 5 6 7 8 9 10 11 12
[Unit] Description=DuckDNS ZeroSSL Certificate Updater After=network-online.target Wants=network-online.target [Service] Type=oneshot User=root Environment="HOME=/home/zephyrbd" ExecStart=/usr/local/bin/duckdns-cert.sh StandardOutput=journal StandardError=journal
Timer:
1 2 3 4 5 6 7 8 9 10 11
[Unit] Description=Run DuckDNS Certificate Updater Daily [Timer] OnBootSec=2min OnUnitActiveSec=1d AccuracySec=1h Unit=duckdns-cert.service [Install] WantedBy=timers.target
-
-
刷新服务
1 2 3 4 5
sudo systemctl daemon-reload sudo systemctl enable --now duckdns.timer sudo systemctl start duckdns.service sudo systemctl enable --now duckdns-cert.timer sudo systemctl start duckdns-cert.service
-
安装并配置 Nginx 反向代理
-
安装 Nginx
1 2
sudo apt install nginx -y sudo systemctl enable --now nginx # 启动并设置开机自启启
-
创建 Nginx 配置文件
1
sudo nano /etc/nginx/conf.d/https-proxy.conf
填入以下配置(根据实际服务端口修改,包括证书):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
# WebSocket 协议映射 map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 4444 ssl; # HTTPS端口 server_name _; # 匹配所有IP访问 # SSL证书 # ssl_certificate /etc/ssl/private/myserver/server.crt; # ssl_certificate_key /etc/ssl/private/myserver/server.key; ssl_certificate /home/zephyrbd/mycerts/mczs.duckdns.org/fullchain.pem; ssl_certificate_key /home/zephyrbd/mycerts/mczs.duckdns.org/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; # 拒绝直接用 IP 访问 if ($host ~* "^[0-9\.]+$") { return 403 "禁止直接使用 IP 访问,请使用域名"; } # -------------------------- # 1. FRP 面板代理 # -------------------------- location /frp/ { proxy_pass http://127.0.0.1:7001/; proxy_redirect http://127.0.0.1:7001/ /frp/; proxy_redirect / /frp/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } # -------------------------- # 2. MCS Web面板代理 # -------------------------- location /mcs/ { proxy_pass http://127.0.0.1:22223/; proxy_redirect http://127.0.0.1:22223/ /mcs/; proxy_set_header X-Forwarded-Prefix /mcs; proxy_set_header Host $host; proxy_set_header X-Real-Ip $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; client_max_body_size 0; proxy_request_buffering off; proxy_buffering off; proxy_cookie_flags ~ secure; } # -------------------------- # 3. 多服务 API 分流 # -------------------------- location /api/ { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; # 来自 MCS 页面 if ($http_referer ~* /mcs/) { proxy_pass http://127.0.0.1:22223; break; } # 来自 Other 服务 if ($http_referer ~* /other/) { proxy_pass http://127.0.0.1:4444; break; } # 其它来源返回 404 return 404; } # -------------------------- # 4. MCS 守护进程 WebSocket # -------------------------- location /mcs-ws/ { proxy_pass http://127.0.0.1:23334/; proxy_set_header Host $host; proxy_set_header X-Real-Ip $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; client_max_body_size 0; proxy_request_buffering off; proxy_buffering off; } # -------------------------- # 5. 根路径静态页面 # -------------------------- location / { root /var/www/html; index index.html; } # -------------------------- # 6. 404 页面 # -------------------------- error_page 404 /404.html; location = /404.html { root /var/www/html; } }
-
放行4444端口
1
sudo ufw allow 4444/tcp
修改mcsm-web的config
-
将
"reverseProxyMode"
改为true
1
sudo nano /opt/mcsmanager/web/data/SystemConfig/config.json
-
重启
mcsm-web
服务1
sudo systemctl restart mcsm-web.service
修改MCS面板中的节点设置(针对本地)
-
远程节点 IP 地址:
wss://example-ddns.com
(即浏览器现连接服务器需要的地址)。 -
端口填写Nginx监听端口,这里是
4444
。 -
路径前缀填写为MCS守护进程的反代名称,这里是
/mcs-ws/
。
网络安全进阶
fail2ban
-
安装
1
sudo apt update && sudo apt install fail2ban -y
-
配置
sshd
封禁1
sudo nano /etc/fail2ban/jail.local
输入以下内容:
1 2 3 4 5 6 7
[sshd] enabled = true port = 1022 # 改为你的SSH端口 filter = sshd logpath = /var/log/auth.log maxretry = 5 # 5次失败尝试后封禁 bantime = 68400 # 封禁24小时(单位:秒)
-
启动并设置开机自启
1 2
sudo systemctl start fail2ban sudo systemctl enable fail2ban
-
配置Frps+MCSM面板封禁
1.创建过滤规则(filter)
1
sudo nano /etc/fail2ban/filter.d/nginx-frp-mcsm.conf
2.填入以下内容:
1 2 3 4
[Definition] failregex = ^<HOST> .* "GET /frp/ HTTP/.*" 401 .*$ ^<HOST> .* "POST /mcs/api/auth/login HTTP/.*" 500 .*$ ignoreregex =
-
创建 jail 配置(启用监控)
1
sudo nano /etc/fail2ban/jail.d/nginx-frp-mcsm.conf
输入以下内容:
1 2 3 4 5 6 7 8 9 10
[nginx-frp-mcsm] enabled = true port = 4444 filter = nginx-frp-mcsm logpath = /var/log/nginx/access.log maxretry = 5 bantime = 86400 findtime = 600 action = ufw[actiontype=deny] backend = auto
-
重启 fail2ban 使配置生效
1
sudo systemctl restart fail2ban
-
查看规则是否已加载
1 2
sudo fail2ban-client status nginx-frp-mcsm sudo fail2ban-client status sshd
-
尝试后解封IP的命令
1
sudo fail2ban-client set nginx-frp-mcsm unbanip 192.168.XX.XXX
Basic Auth
-
安装apache2-utils
1
sudo apt update && sudo apt install apache2-utils -y
-
新建密码文件
首次
1
sudo htpasswd -c -B /etc/nginx/.htpasswd admin
追加
1
sudo htpasswd -B /etc/nginx/.htpasswd user2
执行后按提示输入密码(输入时不显示,确认密码后回车)
-
修改Nginx配置
添加:
1 2 3 4 5 6 7 8 9 10 11 12
server { ··· # Basic Auth 核心配置(与加密方式无关) auth_basic "Admin Area - 请输入用户名密码"; # 登录弹窗提示语 auth_basic_user_file /etc/nginx/.htpasswd; # 指向 bcrypt 加密的密码文件 # 站点其他配置 root /var/www/html; index index.html; ··· }
-
取消MCS Demon和api的密码认证
1 2 3 4 5 6 7 8 9 10 11 12
server { ··· location /api/ { auth_basic off;# 覆盖父级认证配置 ··· } location /mcs-ws/ { auth_basic off;# 覆盖父级认证配置 ··· } ··· }
-
验证并重启Nginx
1 2
sudo nginx -t sudo systemctl restart nginx