VPS 一键脚本

wg WireGuard VPN

要在 VPS 上搭建 WireGuard,以 root 运行下面的命令。它会安装 wireguard-tools,生成服务端和第一个客户端配置,开启 IP 转发并启动 wg-quick@wg0,最后显示可扫码导入的二维码。

别名 wireguard

命令
$ curl -fsSL xyz.sh/wg | bash
最后更新:
声明支持的系统:Debian 11+, Ubuntu 20.04+, Rocky 9, AlmaLinux 9
别名:wg
需要 root
上游来源:https://www.wireguard.com/install/
SHA256:006030af999441de246d6462b726946e2d9e46d7dbc081e3aad06054a770e075 · 与 curl 拿到的脚本一致,也可访问 /wg.sh.sha256
阅读教程:WireGuard 服务端搭建教程

所有脚本都通过 bash -n 与 ShellCheck 检查,并在 Debian 12/11、Ubuntu 24.04/22.04/20.04/18.04、Rocky 9、AlmaLinux 9、CentOS 7 的官方 Docker 镜像中以 root 运行 curl | bash 测试(结果见各页“容器测试结果”)。容器里没有 systemd、内核模块和真实公网线路,相关部分需要在真实 VPS 上确认。

它会做什么

xyz.sh 自写的简洁脚本:从发行版安装 wireguard-tools(有则加 qrencode),生成 /etc/wireguard/wg0.conf(10.66.66.1/24,默认 UDP 51820,通过 iptables MASQUERADE 在默认网卡做 NAT),在 /etc/sysctl.d/99-xyz-wireguard.conf 开启 IPv4 转发,创建第一个客户端 /etc/wireguard/clients/<名称>.conf 并打印配置与二维码,然后用 systemd 启用 wg-quick@wg0。公网地址优先取默认路由的源地址;若是内网地址,会提示后向 api.ipify.org 查询一次(可用 --endpoint 指定)。UFW / firewalld 已启用时会放行端口。--add-client 增加客户端,--uninstall 删除配置。需要 5.6+ 内核或发行版自带的 WireGuard 模块;不支持 CentOS 7。

支持的系统

  • Debian 11+
  • Ubuntu 20.04+
  • Rocky 9
  • AlmaLinux 9

需要 root

参数与示例

$ curl -fsSL xyz.sh/wireguard | bash -s -- -y
$ curl -fsSL xyz.sh/wireguard | bash -s -- --port 51820 --endpoint vpn.example.com -y
$ curl -fsSL xyz.sh/wireguard | bash -s -- --add-client phone -y
$ curl -fsSL xyz.sh/wireguard | bash -s -- --status
$ curl -fsSL xyz.sh/wireguard | bash -s -- --uninstall -y

上游来源: https://www.wireguard.com/install/

容器测试结果

在各发行版官方 Docker 镜像中以 root、无终端方式运行 curl | bash 的结果。partial 表示容器内无法完整验证(例如没有 systemd、内核模块或测速协议被测试环境的代理拦截),需要在真实 VPS 上确认。 (2026-11-11)

OS结果
debian:12pass
debian:11pass
ubuntu:24.04pass
ubuntu:22.04pass
ubuntu:20.04pass
ubuntu:18.04pass
rockylinux:9pass
almalinux:9pass
centos:7pass

常见问题

这个脚本安全吗?
xyz.sh 不埋点、不偷偷执行未说明的操作,完整源码可在页面上查看,也可以用 curl -fsSL xyz.sh/<name>.sh 下载后先读再运行;每个脚本都有 /<name>.sh.sha256 校验值。把远程脚本管道给 bash 仍有风险,请先阅读源码。私钥只在本机生成并保存在 /etc/wireguard(权限 600),不会上传。若默认路由的源地址是内网地址,会提示后向 api.ipify.org 查询一次公网 IP,可用 --endpoint 避免。
支持哪些系统?
声明支持 Debian 11+、Ubuntu 20.04+、Rocky 9、AlmaLinux 9。脚本会检测发行版,无法识别时会提示。已在这些系统的 Docker 容器里做过语法、--dry-run 与可运行部分的测试(结果见 GitHub 上的 tests/RESULTS.md);依赖 systemd、内核模块或公网的部分需要在真实 VPS 上验证。
需要 root 吗?
需要。请用 root 或 sudo bash 运行;非 root 时脚本会退出并提示。--dry-run 和 --status 可以先看会做什么。
OpenVZ / LXC 能用吗?
WireGuard 需要内核模块。OpenVZ 和多数 LXC 容器无法加载内核模块,wg-quick 会启动失败;请使用 KVM 等完整虚拟化的 VPS。

完整源码

以下就是 curl 实际拿到的内容(公共库 + 脚本正文)。运行前请先读一遍。

xyz.sh/wg.sh
#!/usr/bin/env bash
# Served by xyz.sh/wg
# Human-readable page: https://xyz.sh/wg
# This file is the common library followed by the script body.
# No telemetry is added by xyz.sh.
# =============================================================================
# xyz.sh common library
# Shared helpers for every script served by xyz.sh.
#
# Promises:
#   - No telemetry, analytics, or background "phone-home" calls
#   - No hidden installs or config changes beyond what the script announces
#   - Official upstream URLs are printed before any remote code is executed
#   - Destructive or system-changing steps ask for confirmation (or YES=1 / -y)
# =============================================================================

if [[ -n "${XYZ_COMMON_LOADED:-}" ]]; then
  # When sourced a second time, return. When executed as a file, return fails.
  # shellcheck disable=SC2317
  return 0 2>/dev/null || true
fi
# shellcheck disable=SC2034
XYZ_COMMON_LOADED=1

set -euo pipefail

XYZ_VERSION="1.3.0"
XYZ_BASE="${XYZ_BASE:-https://xyz.sh}"
XYZ_NAME="${XYZ_NAME:-unknown}"

# --- language ---------------------------------------------------------------
# Override with XYZ_LANG=zh or XYZ_LANG=en.
if [[ -z "${XYZ_LANG:-}" ]]; then
  case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
    zh*|ZH*|cmn*|CMN*) XYZ_LANG="zh" ;;
    *) XYZ_LANG="en" ;;
  esac
fi

xyz_t() {
  # xyz_t "English text" "中文文本"
  if [[ "${XYZ_LANG}" == "zh" ]]; then
    printf '%s' "${2:-$1}"
  else
    printf '%s' "$1"
  fi
}

# --- colors (TTY only) ------------------------------------------------------
# XYZ_COLOR=always|never|auto (default auto: TTY and no NO_COLOR); --no-color.
_xyz_color=0
case "${XYZ_COLOR:-auto}" in
  always) _xyz_color=1 ;;
  never) _xyz_color=0 ;;
  *) [[ -t 1 && -z "${NO_COLOR:-}" ]] && _xyz_color=1 ;;
esac
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && _xyz_color=0
if [[ "${_xyz_color}" == "1" ]]; then
  XYZ_C_RESET=$'\033[0m'
  XYZ_C_BOLD=$'\033[1m'
  XYZ_C_DIM=$'\033[2m'
  XYZ_C_RED=$'\033[31m'
  XYZ_C_GREEN=$'\033[32m'
  XYZ_C_YELLOW=$'\033[33m'
  XYZ_C_CYAN=$'\033[36m'
else
  XYZ_C_RESET=""
  XYZ_C_BOLD=""
  XYZ_C_DIM=""
  XYZ_C_RED=""
  XYZ_C_GREEN=""
  XYZ_C_YELLOW=""
  XYZ_C_CYAN=""
fi

xyz_info()  { printf '%s[INFO]%s %s\n'  "${XYZ_C_GREEN}"  "${XYZ_C_RESET}" "$*"; }
xyz_warn()  { printf '%s[WARN]%s %s\n'  "${XYZ_C_YELLOW}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_error() { printf '%s[ERROR]%s %s\n' "${XYZ_C_RED}"    "${XYZ_C_RESET}" "$*" >&2; }
xyz_step()  { printf '%s==>%s %s\n'     "${XYZ_C_CYAN}"   "${XYZ_C_RESET}" "$*"; }

# --- args / init ------------------------------------------------------------
XYZ_ARGS=()
XYZ_YES=0
XYZ_DRY_RUN=0

xyz_parse_global_args() {
  local filtered=()
  local arg
  for arg in "$@"; do
    case "${arg}" in
      -y|--yes)
        XYZ_YES=1
        ;;
      --lang=zh|zh)
        XYZ_LANG="zh"
        ;;
      --lang=en|en)
        XYZ_LANG="en"
        ;;
      --dry-run)
        XYZ_DRY_RUN=1
        ;;
      --no-color)
        XYZ_NO_COLOR=1
        XYZ_C_RESET=""; XYZ_C_BOLD=""; XYZ_C_DIM=""; XYZ_C_RED=""
        XYZ_C_GREEN=""; XYZ_C_YELLOW=""; XYZ_C_CYAN=""
        ;;
      --help|-h)
        XYZ_HELP=1
        filtered+=("${arg}")
        ;;
      *)
        filtered+=("${arg}")
        ;;
    esac
  done
  XYZ_ARGS=("${filtered[@]+"${filtered[@]}"}")
  if [[ "${YES:-}" =~ ^(1|yes|true|YES)$ || "${XYZ_ASSUME_YES:-}" == "1" ]]; then
    XYZ_YES=1
  fi
  # Exported so individual scripts can read flags set here.
  export XYZ_ARGS XYZ_YES XYZ_LANG XYZ_HELP="${XYZ_HELP:-}" XYZ_DRY_RUN="${XYZ_DRY_RUN:-0}"
  export XYZ_NO_COLOR="${XYZ_NO_COLOR:-0}"
  if declare -F xyz_ui_init >/dev/null; then xyz_ui_init; fi
}

xyz_banner() {
  local title="${XYZ_TITLE:-$XYZ_NAME}"
  printf '\n%s%sxyz.sh/%s%s  v%s\n' "${XYZ_C_BOLD}" "${XYZ_C_CYAN}" "${XYZ_NAME}" "${XYZ_C_RESET}" "${XYZ_VERSION}"
  printf '%s\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
  printf '  %s: %s/%s\n' "$(xyz_t "Page" "页面")" "${XYZ_BASE}" "${XYZ_NAME}"
  printf '  %s: %s/%s.sh\n' "$(xyz_t "Raw" "源码")" "${XYZ_BASE}" "${XYZ_NAME}"
  if [[ -n "${XYZ_UPSTREAM:-}" ]]; then
    printf '  %s: %s\n' "$(xyz_t "Upstream" "上游来源")" "${XYZ_UPSTREAM}"
  fi
  printf '  %s\n' "$(xyz_t "No telemetry, no hidden actions." "无遥测、无隐藏操作。")"
  printf '%s\n\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
  if [[ -n "${title}" && "${title}" != "${XYZ_NAME}" ]]; then
    xyz_step "${title}"
  fi
}

xyz_init() {
  xyz_parse_global_args "$@"
  xyz_detect_os
  xyz_banner
}

# --- root / OS --------------------------------------------------------------
xyz_is_root() {
  [[ "${EUID:-$(id -u)}" -eq 0 ]]
}

xyz_require_root() {
  if ! xyz_is_root; then
    xyz_error "$(xyz_t \
      "This script must be run as root. Try: sudo bash" \
      "请使用 root 运行。可尝试:sudo bash")"
    exit 1
  fi
}

xyz_prefer_root() {
  if ! xyz_is_root; then
    xyz_warn "$(xyz_t \
      "Not running as root; some information or tests may be limited." \
      "当前不是 root,部分信息或测试可能不完整。")"
  fi
}

XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY=""   # debian | rhel | unknown
XYZ_OS_PRETTY=""

xyz_detect_os() {
  XYZ_OS_ID=""
  XYZ_OS_LIKE=""
  XYZ_OS_VERSION=""
  XYZ_OS_FAMILY="unknown"
  XYZ_OS_PRETTY="unknown"
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    XYZ_OS_ID="${ID:-}"
    XYZ_OS_LIKE="${ID_LIKE:-}"
    XYZ_OS_VERSION="${VERSION_ID:-}"
    XYZ_OS_PRETTY="${PRETTY_NAME:-$XYZ_OS_ID}"
    export XYZ_OS_ID XYZ_OS_LIKE XYZ_OS_VERSION XYZ_OS_FAMILY XYZ_OS_PRETTY
  fi
  case "${XYZ_OS_ID}" in
    debian|ubuntu)
      XYZ_OS_FAMILY="debian"
      ;;
    centos|rhel|rocky|almalinux|alma|fedora)
      XYZ_OS_FAMILY="rhel"
      ;;
    *)
      case " ${XYZ_OS_ID} ${XYZ_OS_LIKE} " in
        *debian*|*ubuntu*) XYZ_OS_FAMILY="debian" ;;
        *rhel*|*centos*|*fedora*|*rocky*|*alma*) XYZ_OS_FAMILY="rhel" ;;
        *) : ;;
      esac
      ;;
  esac
}

xyz_os_supported() {
  case "${XYZ_OS_ID}" in
    debian|ubuntu|centos|rocky|almalinux) return 0 ;;
    *) : ;;
  esac
  case " ${XYZ_OS_LIKE} " in
    *debian*|*ubuntu*|*rhel*|*centos*|*rocky*|*alma*) return 0 ;;
    *) : ;;
  esac
  return 1
}

xyz_require_supported_os() {
  xyz_detect_os
  if ! xyz_os_supported; then
    xyz_warn "$(xyz_t \
      "Unrecognized distribution: ${XYZ_OS_ID:-unknown} (${XYZ_OS_PRETTY}). Officially supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux." \
      "未能识别的发行版:${XYZ_OS_ID:-unknown}(${XYZ_OS_PRETTY})。官方支持:Debian、Ubuntu、CentOS、Rocky、AlmaLinux。")"
    if ! xyz_confirm "$(xyz_t "Continue anyway?" "仍然继续?")"; then
      exit 1
    fi
  else
    xyz_info "$(xyz_t "Detected OS:" "检测到系统:") ${XYZ_OS_PRETTY} (${XYZ_OS_FAMILY})"
  fi
}

# --- packages / commands ----------------------------------------------------
xyz_need_cmd() {
  command -v "$1" >/dev/null 2>&1
}

xyz_pkg_install() {
  xyz_detect_os
  if (( $# == 0 )); then
    return 0
  fi
  case "${XYZ_OS_FAMILY}" in
    debian)
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -y || xyz_warn "$(xyz_t "apt-get update reported errors (a repository may be unreachable); trying to install anyway." "apt-get update 报错(可能有软件源无法访问),仍尝试安装。")"
      apt-get install -y --no-install-recommends "$@"
      ;;
    rhel)
      if xyz_need_cmd dnf; then
        dnf install -y "$@"
      elif xyz_need_cmd yum; then
        yum install -y "$@"
      else
        xyz_error "$(xyz_t "Neither dnf nor yum is available." "找不到 dnf 或 yum。")"
        exit 1
      fi
      ;;
    *)
      xyz_error "$(xyz_t "Cannot install packages on this OS automatically." "无法在此系统上自动安装软件包。")"
      exit 1
      ;;
  esac
}

xyz_ensure_cmds() {
  local pkg
  local missing=()
  for pkg in "$@"; do
    if ! xyz_need_cmd "${pkg}"; then
      missing+=("${pkg}")
    fi
  done
  if ((${#missing[@]} > 0)); then
    xyz_info "$(xyz_t "Installing missing commands:" "正在安装缺失命令:") ${missing[*]}"
    xyz_pkg_install "${missing[@]}"
  fi
}

# --- confirmation (safe with curl | bash) -----------------------------------
# A readable /dev/tty node is not enough: in containers or curl|bash without a
# controlling terminal, open() fails with ENXIO ("No such device or address").
# [[ -r /dev/tty ]] is true in that case and must not be used as the check.
xyz_open_tty() {
  # Opens /dev/tty into XYZ_TTY_FD if it is a real terminal. Caller must close.
  local fd
  XYZ_TTY_FD=""
  # Brace group so a failed open cannot leak "No such device or address".
  if { exec {fd}<>/dev/tty; } 2>/dev/null && [[ -t "${fd}" ]]; then
    XYZ_TTY_FD="${fd}"
    return 0
  fi
  if [[ -n "${fd:-}" ]]; then
    exec {fd}>&- 2>/dev/null || true
  fi
  return 1
}

xyz_close_tty() {
  if [[ -n "${XYZ_TTY_FD:-}" ]]; then
    exec {XYZ_TTY_FD}>&- 2>/dev/null || true
    XYZ_TTY_FD=""
  fi
}

xyz_confirm() {
  local prompt="$1"
  if [[ "${XYZ_YES}" == "1" ]]; then
    xyz_info "$(xyz_t "Auto-confirmed (-y / YES=1):" "已自动确认(-y / YES=1):") ${prompt}"
    return 0
  fi
  local reply=""
  if xyz_open_tty; then
    printf '%s [y/N] ' "${prompt}" >&"${XYZ_TTY_FD}"
    # shellcheck disable=SC2162
    read -r reply <&"${XYZ_TTY_FD}" || true
    xyz_close_tty
  else
    xyz_warn "$(xyz_t \
      "No TTY available for confirmation. Re-run with -y or YES=1 if you accept." \
      "没有可用的终端来确认。若你已了解并同意,请加 -y 或 YES=1 重试。")"
    return 1
  fi
  case "${reply}" in
    y|Y|yes|YES|Yes) return 0 ;;
    *) return 1 ;;
  esac
}

# --- download helpers -------------------------------------------------------
# Create the temp dir in the main shell. xyz_tmpdir is usually called inside
# $(...), a subshell, so creating it (and its EXIT trap) there would delete the
# directory as soon as the subshell returned.
xyz_init_tmpdir() {
  if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
    XYZ_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/xyzsh.XXXXXX")"
    # shellcheck disable=SC2064
    trap "rm -rf '${XYZ_TMPDIR}'" EXIT
  fi
}

xyz_tmpdir() {
  if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
    xyz_error "internal: temp dir not initialised"
    exit 1
  fi
  printf '%s' "${XYZ_TMPDIR}"
}

xyz_fetch() {
  # xyz_fetch URL DEST
  local url="$1"
  local dest="$2"
  xyz_info "$(xyz_t "Downloading" "正在下载"): ${url}"
  xyz_info "$(xyz_t "Saving to" "保存到"): ${dest}"
  if xyz_need_cmd curl; then
    curl -fsSL --connect-timeout 20 --max-time 180 --retry 2 --retry-delay 1 \
      -o "${dest}" "${url}"
  elif xyz_need_cmd wget; then
    wget -q --timeout=60 -O "${dest}" "${url}"
  else
    xyz_error "$(xyz_t "Need curl or wget to download files." "需要 curl 或 wget 才能下载文件。")"
    exit 1
  fi
  if [[ ! -s "${dest}" ]]; then
    xyz_error "$(xyz_t "Download failed or file is empty:" "下载失败或文件为空:") ${url}"
    exit 1
  fi
  if xyz_need_cmd sha256sum; then
    xyz_info "SHA256: $(sha256sum "${dest}" | awk '{print $1}')"
  elif xyz_need_cmd shasum; then
    xyz_info "SHA256: $(shasum -a 256 "${dest}" | awk '{print $1}')"
  fi
}

xyz_run_upstream() {
  # xyz_run_upstream URL [args...]
  # Downloads an official installer, shows the URL and a short preview, then
  # runs it only after confirmation (unless -y / YES=1).
  local url="$1"
  shift
  local dest
  xyz_init_tmpdir
  dest="$(xyz_tmpdir)/upstream.sh"
  xyz_fetch "${url}" "${dest}"
  chmod +x "${dest}"
  xyz_info "$(xyz_t "Preview (first 20 lines):" "预览(前 20 行):")"
  sed -n '1,20p' "${dest}" || true
  printf '%s\n' "${XYZ_C_DIM}...${XYZ_C_RESET}"
  # Default --dry-run: verify the download and preview, do not execute.
  # Scripts that forward --dry-run to upstream (e.g. docker) set
  # XYZ_FORWARD_DRY_RUN=1 so the official script still runs with that flag.
  if [[ "${XYZ_DRY_RUN:-0}" == "1" && "${XYZ_FORWARD_DRY_RUN:-0}" != "1" ]]; then
    xyz_info "$(xyz_t \
      "Dry-run: official script downloaded and previewed; not executing it." \
      "演练模式:已下载并预览官方脚本,不会执行。")"
    return 0
  fi
  if ! xyz_confirm "$(xyz_t \
    "Run this official upstream script?" \
    "运行这份官方上游脚本?")"; then
    xyz_error "$(xyz_t "Aborted by user." "已取消。")"
    exit 1
  fi
  xyz_step "$(xyz_t "Executing upstream script" "正在执行上游脚本")"
  # Never let the upstream script inherit our stdin: under `curl | bash` that
  # is the rest of this very script. Give it the terminal if there is one,
  # otherwise /dev/null. Interactive installers set XYZ_UPSTREAM_NEEDS_TTY=1.
  if xyz_open_tty; then
    local tty_fd="${XYZ_TTY_FD}"
    XYZ_TTY_FD=""
    bash "${dest}" "$@" <&"${tty_fd}"
    local rc=$?
    exec {tty_fd}>&- 2>/dev/null || true
    return "${rc}"
  fi
  if [[ "${XYZ_UPSTREAM_NEEDS_TTY:-0}" == "1" ]]; then
    xyz_error "$(xyz_t \
      "This official installer is interactive and needs a terminal. Log in with SSH and run: bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})" \
      "这个官方安装脚本需要交互,必须在终端里运行。请用 SSH 登录后执行:bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})")"
    exit 1
  fi
  bash "${dest}" "$@" </dev/null
}

# --- small shared helpers ----------------------------------------------------
xyz_strip_ansi() {
  sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' -e 's/\r//g'
}

# Effective SSH port: sshd -T (what sshd really uses) > config files > env > 22.
xyz_effective_ssh_port() {
  local port=""
  if xyz_need_cmd sshd; then
    port="$(sshd -T 2>/dev/null | awk '$1=="port" {print $2; exit}' || true)"
  fi
  if [[ -z "${port}" && -d /etc/ssh/sshd_config.d ]]; then
    port="$(cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | awk 'tolower($1)=="port" {print $2; exit}' || true)"
  fi
  if [[ -z "${port}" && -f /etc/ssh/sshd_config ]]; then
    port="$(awk 'tolower($1)=="port" {print $2; exit}' /etc/ssh/sshd_config || true)"
  fi
  if [[ -z "${port}" && -n "${SSH_CONNECTION:-}" ]]; then
    port="$(printf '%s' "${SSH_CONNECTION}" | awk '{print $4}')"
  fi
  printf '%s' "${port:-22}"
}

xyz_has_systemd() {
  [[ -d /run/systemd/system ]] && xyz_need_cmd systemctl
}

# Back up a file once per run as FILE.xyz-bak-YYYYmmddHHMMSS and print the path.
xyz_backup_file() {
  local f="$1"
  local bak
  bak="${f}.xyz-bak-$(date +%Y%m%d%H%M%S)"
  if [[ -e "${f}" ]]; then
    cp -a "${f}" "${bak}"
    printf '%s' "${bak}"
  fi
}

# --- misc -------------------------------------------------------------------
xyz_usage_exit() {
  if [[ "${1:-}" != "" ]]; then
    printf '%s\n' "$1"
  fi
  exit 0
}

XYZ_NAME="wg"

# xyz.sh/wireguard — set up a WireGuard VPN server and client configs.
#
# Self-written, small and readable:
#   - installs wireguard-tools (and qrencode when available) from the distro
#   - creates /etc/wireguard/wg0.conf (10.66.66.1/24, UDP 51820 by default)
#     with NAT (iptables MASQUERADE on the default interface) via PostUp/PostDown
#   - enables IPv4 forwarding in /etc/sysctl.d/99-xyz-wireguard.conf
#   - creates the first client in /etc/wireguard/clients/<name>.conf and
#     prints it (plus a QR code if qrencode is installed)
#   - enables wg-quick@wg0 with systemd
# The public endpoint is detected from the default route's source address; if
# that is a private address, api.ipify.org is asked once (announced) unless
# you pass --endpoint.
#
# Usage:
#   curl -fsSL xyz.sh/wireguard | bash -s -- -y
#   curl -fsSL xyz.sh/wireguard | bash -s -- --port 51820 --endpoint vpn.example.com -y
#   curl -fsSL xyz.sh/wireguard | bash -s -- --add-client phone -y
#   curl -fsSL xyz.sh/wireguard | bash -s -- --status
#   curl -fsSL xyz.sh/wireguard | bash -s -- --uninstall -y
#   curl -fsSL xyz.sh/wireguard | bash -s -- --dry-run
#
# Supported: Debian 11+, Ubuntu 20.04+, Rocky / AlmaLinux 9 (kernel 5.6+ or
# a distro WireGuard module). CentOS 7 is not supported. Root required.
# No telemetry is added by this wrapper.

set -euo pipefail

if [[ -z "${XYZ_COMMON_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  if [[ -f "${_xyz_dir}/lib/common.sh" ]]; then
    # shellcheck source=lib/common.sh
    source "${_xyz_dir}/lib/common.sh"
  else
    echo "xyz.sh: missing lib/common.sh" >&2
    exit 1
  fi
fi

XYZ_NAME="${XYZ_NAME:-wireguard}"
case "${XYZ_NAME}" in wireguard|wg) : ;; *) XYZ_NAME="wireguard" ;; esac
XYZ_TITLE="$(xyz_t "WireGuard VPN server" "WireGuard VPN 服务端")"
XYZ_UPSTREAM="https://www.wireguard.com/install/"
WG_DIR="/etc/wireguard"
WG_IF="wg0"
WG_CONF="${WG_DIR}/${WG_IF}.conf"
SYSCTL_FILE="/etc/sysctl.d/99-xyz-wireguard.conf"
xyz_init "$@"

PORT="51820"; ENDPOINT=""; DNS="1.1.1.1, 8.8.8.8"; CLIENT="client1"
DO_STATUS=0; DO_UNINSTALL=0; ADD_CLIENT=""; NO_START=0
i=0
while [[ $i -lt ${#XYZ_ARGS[@]} ]]; do
  arg="${XYZ_ARGS[$i]}"
  case "${arg}" in
    --port) i=$((i + 1)); PORT="${XYZ_ARGS[$i]:-}" ;;
    --endpoint) i=$((i + 1)); ENDPOINT="${XYZ_ARGS[$i]:-}" ;;
    --dns) i=$((i + 1)); DNS="${XYZ_ARGS[$i]:-}" ;;
    --client) i=$((i + 1)); CLIENT="${XYZ_ARGS[$i]:-}" ;;
    --add-client) i=$((i + 1)); ADD_CLIENT="${XYZ_ARGS[$i]:-}" ;;
    --no-start) NO_START=1 ;;
    --status) DO_STATUS=1 ;;
    --uninstall|--remove) DO_UNINSTALL=1 ;;
    --help|-h) XYZ_HELP=1 ;;
    *)
      xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
      exit 1
      ;;
  esac
  i=$((i + 1))
done

if [[ "${XYZ_HELP:-}" == "1" ]]; then
  cat <<HINT
$(xyz_t "Usage:" "用法:")
  curl -fsSL xyz.sh/wireguard | bash -s -- -y
  curl -fsSL xyz.sh/wireguard | bash -s -- --port 51820 --endpoint vpn.example.com -y
  curl -fsSL xyz.sh/wireguard | bash -s -- --add-client phone -y
  curl -fsSL xyz.sh/wireguard | bash -s -- --status
  curl -fsSL xyz.sh/wireguard | bash -s -- --uninstall -y

$(xyz_t "Client configs are saved in ${WG_DIR}/clients/. Allow UDP ${PORT} in your firewall / security group." "客户端配置保存在 ${WG_DIR}/clients/。请在防火墙和安全组放行 UDP ${PORT}。")
HINT
  exit 0
fi

if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then
  xyz_error "--port 1-65535"
  exit 1
fi
for n in "${CLIENT}" "${ADD_CLIENT}"; do
  [[ -z "${n}" || "${n}" =~ ^[A-Za-z0-9_-]{1,32}$ ]] || { xyz_error "$(xyz_t "Client names: letters, digits, _ and -" "客户端名称只能包含字母、数字、_ 和 -")"; exit 1; }
done
DNS_RE='^[0-9A-Fa-f:., ]+$'
[[ "${DNS}" =~ ${DNS_RE} ]] || { xyz_error "--dns takes IP addresses"; exit 1; }

if [[ "${DO_STATUS}" == "1" ]]; then
  if xyz_need_cmd wg; then wg show 2>/dev/null || xyz_info "$(xyz_t "No WireGuard interface is up." "没有运行中的 WireGuard 接口。")"; fi
  if [[ -f "${WG_CONF}" ]]; then
    xyz_info "${WG_CONF} $(xyz_t "exists" "已存在")"
  else
    xyz_info "$(xyz_t "Not configured." "尚未配置。")"
  fi
  for c in "${WG_DIR}"/clients/*.conf; do
    [[ -f "${c}" ]] && printf '  client: %s\n' "$(basename "${c}" .conf)"
  done
  exit 0
fi

xyz_require_root
xyz_require_supported_os
if [[ "${XYZ_OS_ID}" == "centos" && "${XYZ_OS_VERSION%%.*}" == "7" ]]; then
  xyz_error "$(xyz_t "CentOS 7 is not supported (no in-kernel WireGuard). Use Debian 12, Ubuntu 22.04+ or Rocky/Alma 9." "不支持 CentOS 7(内核不含 WireGuard)。请使用 Debian 12、Ubuntu 22.04+ 或 Rocky/Alma 9。")"
  exit 1
fi

svc() {
  # svc ACTION -> systemctl ACTION wg-quick@wg0 if systemd exists
  if xyz_has_systemd; then systemctl "$@" "wg-quick@${WG_IF}"; else return 1; fi
}

if [[ "${DO_UNINSTALL}" == "1" ]]; then
  xyz_info "$(xyz_t "Will stop wg-quick@wg0 and remove" "将停止 wg-quick@wg0 并删除") ${WG_CONF} ${WG_DIR}/clients ${SYSCTL_FILE} $(xyz_t "(packages are kept)" "(保留软件包)")"
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then xyz_info "$(xyz_t "Dry-run: nothing removed." "演练模式:未删除。")"; exit 0; fi
  xyz_confirm "$(xyz_t "Remove the WireGuard server config and all client configs?" "删除 WireGuard 服务端与全部客户端配置?")" || exit 1
  svc disable --now 2>/dev/null || wg-quick down "${WG_IF}" 2>/dev/null || true
  rm -rf "${WG_CONF}" "${WG_DIR}/clients" "${WG_DIR}/server.key" "${WG_DIR}/server.pub" "${SYSCTL_FILE}"
  xyz_info "$(xyz_t "Removed." "已删除。")"
  exit 0
fi

default_if() { ip -4 route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}'; }
default_src() { ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}'; }
is_private() { [[ "$1" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|127\.) ]]; }

detect_endpoint() {
  local ip
  ip="$(default_src || true)"
  if [[ -n "${ip}" ]] && ! is_private "${ip}"; then
    printf '%s' "${ip}"
    return 0
  fi
  xyz_info "$(xyz_t "Default route uses a private address; asking https://api.ipify.org for the public IPv4 (skip with --endpoint)." "默认路由是内网地址;将向 https://api.ipify.org 查询公网 IPv4(可用 --endpoint 跳过)。")" >&2
  curl -4 -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true
}

next_ip() {
  local n=2
  while grep -q "AllowedIPs = 10.66.66.${n}/32" "${WG_CONF}" 2>/dev/null; do n=$((n + 1)); done
  (( n > 254 )) && { xyz_error "subnet full"; exit 1; }
  printf '10.66.66.%s' "${n}"
}

make_client() {
  local name="$1" cip ckey cpub psk spub ep port
  [[ -f "${WG_DIR}/clients/${name}.conf" ]] && { xyz_error "$(xyz_t "Client exists:" "客户端已存在:") ${name}"; exit 1; }
  cip="$(next_ip)"
  ckey="$(wg genkey)"; cpub="$(printf '%s' "${ckey}" | wg pubkey)"; psk="$(wg genpsk)"
  spub="$(cat "${WG_DIR}/server.pub")"
  ep="$(sed -n 's/^# xyz-endpoint = //p' "${WG_CONF}")"
  port="$(awk -F' = ' '$1=="ListenPort"{print $2}' "${WG_CONF}")"
  cat >> "${WG_CONF}" <<PEER

[Peer]
# ${name}
PublicKey = ${cpub}
PresharedKey = ${psk}
AllowedIPs = ${cip}/32
PEER
  mkdir -p "${WG_DIR}/clients"
  umask 077
  cat > "${WG_DIR}/clients/${name}.conf" <<CLIENT
[Interface]
PrivateKey = ${ckey}
Address = ${cip}/24
DNS = ${DNS}

[Peer]
PublicKey = ${spub}
PresharedKey = ${psk}
Endpoint = ${ep}:${port}
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
CLIENT
  chmod 600 "${WG_DIR}/clients/${name}.conf"
  xyz_info "$(xyz_t "Client config:" "客户端配置:") ${WG_DIR}/clients/${name}.conf"
  echo "----- ${name}.conf -----"
  cat "${WG_DIR}/clients/${name}.conf"
  echo "------------------------"
  if xyz_need_cmd qrencode; then qrencode -t ansiutf8 < "${WG_DIR}/clients/${name}.conf" || true; fi
  if wg show "${WG_IF}" >/dev/null 2>&1; then
    wg set "${WG_IF}" peer "${cpub}" preshared-key <(printf '%s' "${psk}") allowed-ips "${cip}/32" || true
  fi
}

install_tools() {
  if xyz_need_cmd wg && xyz_need_cmd wg-quick; then return 0; fi
  case "${XYZ_OS_FAMILY}" in
    debian) xyz_pkg_install wireguard-tools iptables iproute2; xyz_pkg_install qrencode || true ;;
    rhel)
      if ! rpm -q epel-release >/dev/null 2>&1; then xyz_pkg_install epel-release || true; fi
      xyz_pkg_install wireguard-tools iptables iproute; xyz_pkg_install qrencode || true ;;
    *) xyz_error "unsupported OS"; exit 1 ;;
  esac
}

if [[ -n "${ADD_CLIENT}" ]]; then
  [[ -f "${WG_CONF}" ]] || { xyz_error "$(xyz_t "No server config yet; run without --add-client first." "尚无服务端配置,请先不带 --add-client 运行一次。")"; exit 1; }
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then xyz_info "$(xyz_t "Dry-run: would add client" "演练模式:将添加客户端") ${ADD_CLIENT} ($(next_ip))"; exit 0; fi
  xyz_confirm "$(xyz_t "Add client ${ADD_CLIENT}?" "添加客户端 ${ADD_CLIENT}?")" || exit 1
  make_client "${ADD_CLIENT}"
  exit 0
fi

if [[ -f "${WG_CONF}" ]]; then
  xyz_error "$(xyz_t "${WG_CONF} already exists. Use --add-client NAME, --status, or --uninstall." "${WG_CONF} 已存在。可用 --add-client NAME、--status 或 --uninstall。")"
  exit 1
fi

IFACE="$(default_if || true)"
[[ -z "${IFACE}" ]] && IFACE="eth0"
if [[ -z "${ENDPOINT}" && "${XYZ_DRY_RUN:-0}" != "1" ]]; then
  ENDPOINT="$(detect_endpoint)"
fi

xyz_step "$(xyz_t "Planned changes" "计划变更")"
printf '  %s: wireguard-tools iptables (+ qrencode)\n' "$(xyz_t "Packages" "软件包")"
printf '  %s: %s  (10.66.66.1/24, UDP %s, NAT via %s)\n' "$(xyz_t "Server" "服务端")" "${WG_CONF}" "${PORT}" "${IFACE}"
printf '  %s: %s  (net.ipv4.ip_forward=1)\n' "sysctl" "${SYSCTL_FILE}"
printf '  %s: %s/clients/%s.conf  Endpoint=%s:%s  DNS=%s\n' "$(xyz_t "Client" "客户端")" "${WG_DIR}" "${CLIENT}" "${ENDPOINT:-<auto>}" "${PORT}" "${DNS}"
printf '  %s\n' "$(xyz_t "Firewall: UFW / firewalld (if active) will allow UDP ${PORT}. Also open it in your cloud security group." "防火墙:若启用了 UFW / firewalld 会放行 UDP ${PORT}。云安全组也需放行。")"

if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
  xyz_info "$(xyz_t "Dry-run: nothing installed or written." "演练模式:未安装、未写入任何内容。")"
  exit 0
fi
[[ -n "${ENDPOINT}" ]] || { xyz_error "$(xyz_t "Could not detect a public endpoint; pass --endpoint IP_OR_HOST." "无法检测公网地址,请用 --endpoint 指定 IP 或域名。")"; exit 1; }
xyz_confirm "$(xyz_t "Set up the WireGuard server?" "配置 WireGuard 服务端?")" || { xyz_error "$(xyz_t "Aborted by user." "已取消。")"; exit 1; }

install_tools
mkdir -p "${WG_DIR}"
chmod 700 "${WG_DIR}"
umask 077
wg genkey > "${WG_DIR}/server.key"
wg pubkey < "${WG_DIR}/server.key" > "${WG_DIR}/server.pub"
cat > "${WG_CONF}" <<CONF
# Written by xyz.sh/wireguard ${XYZ_VERSION}
# xyz-endpoint = ${ENDPOINT}
[Interface]
Address = 10.66.66.1/24
ListenPort = ${PORT}
PrivateKey = $(cat "${WG_DIR}/server.key")
PostUp = iptables -t nat -A POSTROUTING -s 10.66.66.0/24 -o ${IFACE} -j MASQUERADE; iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.66.66.0/24 -o ${IFACE} -j MASQUERADE; iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT
CONF
chmod 600 "${WG_CONF}"
(umask 022; mkdir -p "$(dirname "${SYSCTL_FILE}")"; printf 'net.ipv4.ip_forward = 1\n' > "${SYSCTL_FILE}")
sysctl -p "${SYSCTL_FILE}" >/dev/null 2>&1 || xyz_warn "$(xyz_t "Could not apply ip_forward now (container?)." "暂时无法应用 ip_forward(可能是容器)。")"

if xyz_need_cmd ufw && ufw status 2>/dev/null | grep -q '^Status: active'; then ufw allow "${PORT}/udp" || true; fi
if xyz_need_cmd firewall-cmd && firewall-cmd --state >/dev/null 2>&1; then
  firewall-cmd --permanent --add-port="${PORT}/udp" && firewall-cmd --permanent --add-masquerade && firewall-cmd --reload || true
fi

make_client "${CLIENT}"

if [[ "${NO_START}" == "1" ]]; then
  xyz_info "$(xyz_t "--no-start: configs written; start later with: systemctl enable --now wg-quick@wg0" "--no-start:配置已写入;稍后用 systemctl enable --now wg-quick@wg0 启动。")"
  exit 0
fi
if svc enable --now; then
  wg show "${WG_IF}" || true
  xyz_info "$(xyz_t "WireGuard is running on UDP ${PORT}." "WireGuard 已在 UDP ${PORT} 运行。")"
elif wg-quick up "${WG_IF}"; then
  xyz_warn "$(xyz_t "Started with wg-quick (no systemd); it will not start on boot." "已用 wg-quick 启动(无 systemd),开机不会自动启动。")"
else
  xyz_error "$(xyz_t "Could not bring up wg0 (kernel module missing or no permission). Configs are kept." "无法启动 wg0(缺少内核模块或权限不足),配置已保留。")"
  exit 1
fi

推荐 VPS

以下为占位展示,尚未接入任何商家或联盟链接,也没有可点击的购买地址。上线推广前会再填入真实套餐与合规的返佣链接。

占位 · 高性价比

商家待定

机房、配置与价格将在接入后填写。当前不是购买入口。

占位 · 大内存 / 大硬盘

商家待定

机房、配置与价格将在接入后填写。当前不是购买入口。