From e23a338194c02656c3ff233f675d08c7c0c5b776 Mon Sep 17 00:00:00 2001 From: HA Date: Mon, 27 Apr 2026 10:48:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0realm=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm/README.md | 98 +++++++++ realm/realm.sh | 525 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 623 insertions(+) create mode 100644 realm/README.md create mode 100644 realm/realm.sh diff --git a/realm/README.md b/realm/README.md new file mode 100644 index 0000000..505d77e --- /dev/null +++ b/realm/README.md @@ -0,0 +1,98 @@ +# realm.sh + +[realm](https://github.com/zhboner/realm) 的交互式安装与管理脚本。安装时会自动写入 systemd 单元 (`realm.service`) 作为守护进程。 + +## 系统要求 + +- Linux x86_64 / aarch64 / armv7 +- 使用 systemd 的发行版(Debian 10+ / Ubuntu 20.04+ / CentOS 8+ / Rocky / AlmaLinux 等) +- root 权限 + +## 依赖 + +| 依赖 | 用途 | +| --- | --- | +| `curl` | 下载二进制、查询 GitHub API | +| `tar` | 解压发行包 | +| `systemd` | 服务管理 | + +Debian / Ubuntu: + +```bash +apt update && apt install -y curl tar +``` + +CentOS / RHEL / Rocky / Alma: + +```bash +dnf install -y curl tar +``` + +## 一键执行 + +```bash +bash <(curl -fsSL https://git.suhang.me/suhang/scripts/raw/branch/release/realm/realm.sh) +``` + +## 菜单 + +| 选项 | 说明 | +| --- | --- | +| 1 | 安装 realm(询问安装目录与 GitHub 代理,下载最新版二进制 + 写入 systemd 单元) | +| 2 | 添加一条转发规则(监听端口 → 远端地址:端口) | +| 3 | 删除指定编号的转发规则 | +| 4 | 列出当前所有转发规则 | +| 5 / 6 / 7 | 启动 / 停止 / 重启服务 | +| 8 | 查看 `systemctl status realm` | +| 9 | `journalctl -u realm -f` 实时日志 | +| 10 | 更新 realm 内核到 GitHub 最新版 | +| 11 | 更新本脚本(从 release 分支拉取) | +| 12 | 卸载(删除二进制、配置目录、systemd 单元、日志) | + +## 路径与文件 + +安装时会询问安装目录,默认 `/opt/realm`。所有自身文件都集中在该目录下,systemd 单元固定在 `/etc/systemd/system/`。 + +| 路径 | 说明 | +| --- | --- | +| `/realm` | 可执行二进制(默认 `/opt/realm/realm`) | +| `/config.toml` | 配置文件(TOML) | +| `/realm.log` | realm 自身日志 | +| `/etc/systemd/system/realm.service` | systemd 单元(`ExecStart` 即所选目录) | + +后续运行脚本时,会从 `realm.service` 的 `ExecStart` 反查出原安装目录,无需再次输入。 + +## GitHub 代理 + +安装时会提示输入 GitHub 代理域名(默认 `ghproxy.com`,输入 `none` 表示直连)。脚本会把代理拼接到 GitHub API 与 release 下载地址前,例如: + +``` +https://ghproxy.com/https://github.com/zhboner/realm/releases/download/... +https://ghproxy.com/https://api.github.com/repos/zhboner/realm/releases/latest +``` + +选择会保存到 `/.proxy`,更新内核(菜单 `10`)时会沿用。如需更换代理,可直接编辑该文件后再执行更新。 + +## 配置示例 + +脚本生成的 `config.toml` 大致如下,每条规则对应一个 `[[endpoints]]` 块: + +```toml +[log] +level = "warn" +output = "/opt/realm/realm.log" + +[network] +no_tcp = false +use_udp = true + +[[endpoints]] +listen = "0.0.0.0:5000" +remote = "example.com:443" +``` + +如需手动编辑,可直接修改该文件后执行 `systemctl restart realm`。 + +## 卸载 + +进入菜单选择 `12` 即可移除二进制、配置目录、systemd 单元和日志文件。 diff --git a/realm/realm.sh b/realm/realm.sh new file mode 100644 index 0000000..3ba0af1 --- /dev/null +++ b/realm/realm.sh @@ -0,0 +1,525 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +YELLOW='\033[0;33m' +NC='\033[0m' + +DEFAULT_INSTALL_DIR="/opt/realm" +INSTALL_DIR="" +REALM_BIN="" +REALM_CONF="" +REALM_LOG="" +REALM_SERVICE="/etc/systemd/system/realm.service" +SCRIPT_RAW_URL="https://git.suhang.me/suhang/scripts/raw/branch/release/realm/realm.sh" +GITHUB_API="https://api.github.com/repos/zhboner/realm/releases/latest" +DEFAULT_PROXY="ghproxy.com" +GITHUB_PROXY="" + +function set_paths() { + INSTALL_DIR="$1" + REALM_BIN="${INSTALL_DIR}/realm" + REALM_CONF="${INSTALL_DIR}/config.toml" + REALM_LOG="${INSTALL_DIR}/realm.log" +} + +function detect_install_dir() { + local dir="" + if [[ -f "$REALM_SERVICE" ]]; then + dir=$(grep -oE 'ExecStart=[^ ]+' "$REALM_SERVICE" | head -n1 | sed -E 's|ExecStart=(.+)/realm$|\1|') + fi + [[ -z "$dir" ]] && dir="$DEFAULT_INSTALL_DIR" + set_paths "$dir" + load_proxy +} + +function gh_url() { + local url="$1" + if [[ -n "$GITHUB_PROXY" ]]; then + echo "https://${GITHUB_PROXY}/${url}" + else + echo "$url" + fi +} + +function load_proxy() { + if [[ -n "$INSTALL_DIR" && -f "${INSTALL_DIR}/.proxy" ]]; then + GITHUB_PROXY=$(head -n1 "${INSTALL_DIR}/.proxy" 2>/dev/null | tr -d '[:space:]') + fi +} + +function prompt_proxy() { + local p + read -p "请输入 GitHub 代理域名 [默认 ${DEFAULT_PROXY},输入 none 不使用代理]: " p + p="${p:-$DEFAULT_PROXY}" + if [[ "$p" == "none" || "$p" == "NONE" ]]; then + GITHUB_PROXY="" + echo "GitHub proxy: disabled" + else + p="${p#http://}" + p="${p#https://}" + p="${p%/}" + GITHUB_PROXY="$p" + echo "GitHub proxy: ${GITHUB_PROXY}" + fi + mkdir -p "$INSTALL_DIR" + printf '%s\n' "$GITHUB_PROXY" > "${INSTALL_DIR}/.proxy" +} + +function prompt_install_dir() { + local dir + read -p "请输入安装目录 [默认 ${DEFAULT_INSTALL_DIR}]: " dir + dir="${dir:-$DEFAULT_INSTALL_DIR}" + if [[ "$dir" != /* ]]; then + echo -e "${RED}必须是绝对路径。${NC}" + return 1 + fi + set_paths "$dir" + mkdir -p "$INSTALL_DIR" || return 1 + echo "Install directory: ${INSTALL_DIR}" +} + +function check_root() { + if [[ $EUID -ne 0 ]]; then + echo -e "${RED}This script must be run as root.${NC}" + exit 1 + fi +} + +function check_dependencies() { + local deps=(curl tar systemctl) + local missing=() + for d in "${deps[@]}"; do + if ! command -v "$d" >/dev/null 2>&1; then + missing+=("$d") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + echo -e "${RED}Missing dependencies: ${missing[*]}${NC}" + echo -e "${YELLOW}Please install them and re-run the script.${NC}" + exit 1 + fi +} + +function detect_arch() { + local m + m=$(uname -m) + case "$m" in + x86_64|amd64) echo "x86_64-unknown-linux-gnu" ;; + aarch64|arm64) echo "aarch64-unknown-linux-gnu" ;; + armv7l|armv7) echo "armv7-unknown-linux-gnueabihf" ;; + *) + echo -e "${RED}Unsupported architecture: $m${NC}" >&2 + return 1 + ;; + esac +} + +function get_latest_version() { + local api_url v + api_url=$(gh_url "$GITHUB_API") + v=$(curl -fsSL "$api_url" 2>/dev/null | grep -oE '"tag_name":\s*"[^"]+"' | head -n1 | sed -E 's/.*"([^"]+)"$/\1/') + if [[ -z "$v" ]]; then + echo -e "${RED}Failed to fetch latest realm version from GitHub (proxy: ${GITHUB_PROXY:-none}).${NC}" >&2 + return 1 + fi + echo "$v" +} + +function download_realm() { + local version="$1" + local arch + arch=$(detect_arch) || return 1 + + local url + url=$(gh_url "https://github.com/zhboner/realm/releases/download/${version}/realm-${arch}.tar.gz") + local tmp + tmp=$(mktemp -d) + + echo "Downloading realm ${version} (${arch}) via ${GITHUB_PROXY:-direct}..." + if ! curl -fsSL "$url" -o "${tmp}/realm.tar.gz"; then + echo -e "${RED}Download failed: $url${NC}" + rm -rf "$tmp" + return 1 + fi + + if ! tar -xzf "${tmp}/realm.tar.gz" -C "$tmp"; then + echo -e "${RED}Failed to extract realm archive.${NC}" + rm -rf "$tmp" + return 1 + fi + + if [[ ! -f "${tmp}/realm" ]]; then + echo -e "${RED}realm binary not found in archive.${NC}" + rm -rf "$tmp" + return 1 + fi + + install -m 0755 "${tmp}/realm" "$REALM_BIN" + rm -rf "$tmp" + echo -e "${GREEN}realm installed to ${REALM_BIN}${NC}" +} + +function write_default_config() { + mkdir -p "$INSTALL_DIR" + if [[ -f "$REALM_CONF" ]]; then + return 0 + fi + cat > "$REALM_CONF" < "$REALM_SERVICE" </dev/null 2>&1 + echo "Systemd unit written to ${REALM_SERVICE}" +} + +function open_firewall_port() { + local port="$1" + [[ -z "$port" ]] && return 0 + + if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then + ufw allow "${port}/tcp" >/dev/null 2>&1 + ufw allow "${port}/udp" >/dev/null 2>&1 + echo "ufw: opened ${port}/tcp,udp" + return 0 + fi + + if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -q running; then + firewall-cmd --zone=public --add-port="${port}/tcp" --permanent >/dev/null 2>&1 + firewall-cmd --zone=public --add-port="${port}/udp" --permanent >/dev/null 2>&1 + firewall-cmd --reload >/dev/null 2>&1 + echo "firewalld: opened ${port}/tcp,udp" + return 0 + fi + + if command -v iptables >/dev/null 2>&1; then + iptables -C INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null || iptables -A INPUT -p tcp --dport "$port" -j ACCEPT + iptables -C INPUT -p udp --dport "$port" -j ACCEPT 2>/dev/null || iptables -A INPUT -p udp --dport "$port" -j ACCEPT + if command -v ip6tables >/dev/null 2>&1; then + ip6tables -C INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null || ip6tables -A INPUT -p tcp --dport "$port" -j ACCEPT + ip6tables -C INPUT -p udp --dport "$port" -j ACCEPT 2>/dev/null || ip6tables -A INPUT -p udp --dport "$port" -j ACCEPT + fi + if [[ -e /etc/iptables/rules.v4 ]]; then + iptables-save > /etc/iptables/rules.v4 + [[ -e /etc/iptables/rules.v6 ]] && ip6tables-save > /etc/iptables/rules.v6 + elif [[ -e /etc/sysconfig/iptables ]]; then + iptables-save > /etc/sysconfig/iptables + [[ -e /etc/sysconfig/ip6tables ]] && ip6tables-save > /etc/sysconfig/ip6tables + fi + echo "iptables: opened ${port}/tcp,udp" + return 0 + fi + + echo "No active firewall detected, skipping." +} + +function install_realm() { + if [[ -f "$REALM_SERVICE" ]] || [[ -x "$REALM_BIN" ]]; then + echo -e "${YELLOW}realm 已安装,如需重新安装请先卸载。${NC}" + return 0 + fi + check_dependencies + prompt_install_dir || return 1 + prompt_proxy + local version + version=$(get_latest_version) || return 1 + download_realm "$version" || return 1 + write_default_config + write_systemd_unit + systemctl restart realm + sleep 1 + if systemctl is-active --quiet realm; then + echo -e "${GREEN}realm 安装完成并已启动。${NC}" + else + echo -e "${YELLOW}realm 已安装,但服务未启动 —— 当前没有任何转发规则。请通过菜单 [2] 添加规则。${NC}" + fi +} + +function ensure_installed() { + if [[ ! -x "$REALM_BIN" ]]; then + echo -e "${RED}realm 尚未安装,请先选择 [1] 安装。${NC}" + return 1 + fi + return 0 +} + +function list_rules() { + ensure_installed || return 1 + if [[ ! -f "$REALM_CONF" ]]; then + echo -e "${YELLOW}配置文件不存在。${NC}" + return 0 + fi + local idx=0 + local listen="" + local remote="" + echo "------------------------------------------------------------" + printf " %-4s %-30s %-30s\n" "编号" "本地监听" "转发目标" + echo "------------------------------------------------------------" + while IFS= read -r line; do + line="${line%%#*}" + if [[ "$line" =~ ^[[:space:]]*\[\[endpoints\]\] ]]; then + if [[ -n "$listen" || -n "$remote" ]]; then + idx=$((idx+1)) + printf " %-4s %-30s %-30s\n" "$idx" "$listen" "$remote" + fi + listen="" + remote="" + elif [[ "$line" =~ ^[[:space:]]*listen[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then + listen="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^[[:space:]]*remote[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then + remote="${BASH_REMATCH[1]}" + fi + done < "$REALM_CONF" + if [[ -n "$listen" || -n "$remote" ]]; then + idx=$((idx+1)) + printf " %-4s %-30s %-30s\n" "$idx" "$listen" "$remote" + fi + echo "------------------------------------------------------------" + if [[ $idx -eq 0 ]]; then + echo -e "${YELLOW}当前没有任何转发规则。${NC}" + else + echo "共 $idx 条规则。" + fi +} + +function add_rule() { + ensure_installed || return 1 + write_default_config + + local listen_port listen_addr remote_host remote_port + while true; do + read -p "请输入本地监听端口 (1-65535): " listen_port + if [[ "$listen_port" =~ ^[0-9]+$ ]] && (( listen_port >= 1 && listen_port <= 65535 )); then + break + fi + echo -e "${RED}端口无效。${NC}" + done + + read -p "请输入本地监听地址 [默认 0.0.0.0]: " listen_addr + listen_addr="${listen_addr:-0.0.0.0}" + + while true; do + read -p "请输入转发目标地址 (域名或 IP): " remote_host + [[ -n "$remote_host" ]] && break + echo -e "${RED}目标地址不能为空。${NC}" + done + + while true; do + read -p "请输入转发目标端口 (1-65535): " remote_port + if [[ "$remote_port" =~ ^[0-9]+$ ]] && (( remote_port >= 1 && remote_port <= 65535 )); then + break + fi + echo -e "${RED}端口无效。${NC}" + done + + local listen="${listen_addr}:${listen_port}" + local remote + if [[ "$remote_host" =~ : && ! "$remote_host" =~ ^\[ ]]; then + remote="[${remote_host}]:${remote_port}" + else + remote="${remote_host}:${remote_port}" + fi + + cat >> "$REALM_CONF" < ${remote}${NC}" + else + echo -e "${RED}规则已写入,但服务启动失败,请使用 [9] 查看日志排查。${NC}" + fi +} + +function delete_rule() { + ensure_installed || return 1 + list_rules + [[ ! -f "$REALM_CONF" ]] && return 0 + + local total + total=$(grep -cE '^\[\[endpoints\]\]' "$REALM_CONF") + if (( total == 0 )); then + return 0 + fi + + local idx + read -p "请输入要删除的规则编号 (回车取消): " idx + [[ -z "$idx" ]] && return 0 + if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > total )); then + echo -e "${RED}编号无效。${NC}" + return 1 + fi + + awk -v target="$idx" ' + BEGIN { count = 0; skip = 0 } + /^\[\[endpoints\]\]/ { + count++ + if (count == target) { skip = 1; next } + else { skip = 0 } + } + /^\[/ && !/^\[\[endpoints\]\]/ { skip = 0 } + { if (!skip) print } + ' "$REALM_CONF" > "${REALM_CONF}.new" + + awk 'BEGIN{blank=0} /^[[:space:]]*$/{blank++; if(blank<=1) print; next} {blank=0; print}' "${REALM_CONF}.new" > "${REALM_CONF}.tmp" + mv "${REALM_CONF}.tmp" "$REALM_CONF" + rm -f "${REALM_CONF}.new" + + systemctl restart realm + echo -e "${GREEN}已删除规则 #${idx}${NC}" +} + +function service_action() { + ensure_installed || return 1 + local action="$1" + systemctl "$action" realm + sleep 1 + systemctl status realm --no-pager -l | head -n 15 +} + +function view_logs() { + ensure_installed || return 1 + echo -e "${CYAN}按 Ctrl+C 退出日志查看。${NC}" + journalctl -u realm -n 100 -f --no-pager +} + +function update_realm() { + ensure_installed || return 1 + local version current + version=$(get_latest_version) || return 1 + current=$("$REALM_BIN" --version 2>/dev/null | awk '{print $NF}') + echo "Current: ${current:-unknown} | Latest: ${version}" + + if [[ -n "$current" && "${version#v}" == "${current#v}" ]]; then + read -p "已是最新版本,是否仍要重新下载? [y/N]: " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && return 0 + fi + + systemctl stop realm + download_realm "$version" || { systemctl start realm; return 1; } + systemctl start realm + echo -e "${GREEN}realm 已更新到 ${version}${NC}" +} + +function update_script() { + local self="$0" + local tmp + tmp=$(mktemp) + if curl -fsSL "$SCRIPT_RAW_URL" -o "$tmp"; then + if [[ -s "$tmp" ]] && head -n1 "$tmp" | grep -q '^#!/bin/bash'; then + install -m 0755 "$tmp" "$self" + rm -f "$tmp" + echo -e "${GREEN}脚本已更新,请重新运行。${NC}" + exit 0 + fi + fi + rm -f "$tmp" + echo -e "${RED}脚本更新失败。${NC}" +} + +function uninstall_realm() { + read -p "确认卸载 realm 并删除全部配置? [y/N]: " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && return 0 + + systemctl stop realm 2>/dev/null + systemctl disable realm 2>/dev/null + rm -f "$REALM_SERVICE" + systemctl daemon-reload + rm -rf "$INSTALL_DIR" + echo -e "${GREEN}realm 已卸载(已删除 ${INSTALL_DIR})。${NC}" +} + +function show_status() { + if [[ ! -f "$REALM_SERVICE" ]] || [[ ! -x "$REALM_BIN" ]]; then + echo -e "状态: ${YELLOW}未安装${NC}" + return + fi + local ver + ver=$("$REALM_BIN" --version 2>/dev/null | awk '{print $NF}') + local active + if systemctl is-active --quiet realm; then + active="${GREEN}running${NC}" + else + active="${RED}stopped${NC}" + fi + local rules + rules=$(grep -cE '^\[\[endpoints\]\]' "$REALM_CONF" 2>/dev/null || echo 0) + echo -e "状态: 已安装 (${ver:-unknown}) | 目录: ${INSTALL_DIR} | 服务: ${active} | 规则数: ${rules}" +} + +function main_menu() { + clear + echo "╔════════════════════════════════════════════════════════════════════════╗" + echo -e "║ ${CYAN}realm 管理脚本${NC} ║" + echo -e "║ ${CYAN}项目地址${NC}: https://github.com/zhboner/realm ║" + echo "╠════════════════════════════════════════════════════════════════════════╣" + show_status + echo "╠════════════════════════════════════════════════════════════════════════╣" + echo -e "║${CYAN} [1]${NC} 安装 realm ${CYAN} [2]${NC} 添加转发规则 ║" + echo -e "║${CYAN} [3]${NC} 删除转发规则 ${CYAN} [4]${NC} 查看转发规则 ║" + echo -e "║${CYAN} [5]${NC} 启动服务 ${CYAN} [6]${NC} 停止服务 ║" + echo -e "║${CYAN} [7]${NC} 重启服务 ${CYAN} [8]${NC} 查看服务状态 ║" + echo -e "║${CYAN} [9]${NC} 查看日志 ${CYAN} [10]${NC} 更新 realm 内核 ║" + echo -e "║${CYAN} [11]${NC} 更新脚本 ${CYAN} [12]${NC} 卸载 ║" + echo -e "║${CYAN} [0]${NC} 退出 ║" + echo "╚════════════════════════════════════════════════════════════════════════╝" + + local choice + read -p "请选择 [0-12]: " choice + case "$choice" in + 1) install_realm ;; + 2) add_rule ;; + 3) delete_rule ;; + 4) list_rules ;; + 5) service_action start ;; + 6) service_action stop ;; + 7) service_action restart ;; + 8) ensure_installed && systemctl status realm --no-pager -l ;; + 9) view_logs ;; + 10) update_realm ;; + 11) update_script ;; + 12) uninstall_realm ;; + 0) exit 0 ;; + *) echo -e "${RED}无效的选择。${NC}" ;; + esac +} + +check_root +detect_install_dir +main_menu