FreeBSD 15 下 Nginx 前置 Gost MWSS 生产级配置

在 FreeBSD 15 上从域名、证书、Nginx 伪装站点、低权限 Gost v3 后端到 sing-box 客户端接入,完整部署 MWSS TCP 节点。

这篇记录 FreeBSD 15 上 Nginx 前置 Gost v3 MWSS 的完整部署流程,目标是做一条可长期运行、伪装自然、客户端容易接入的 TCP 代理节点。

先说结论:在新的 FreeBSD 15 VPS 上,Gost MWSS + Nginx 的表现比 Xray Reality TCP 更适合作为这台机器的 TCP 生产候选。实测同一台服务器、同一个客户端下,Xray Reality 大文件下载多在 6-9 MB/s,Gost MWSS + Nginx 多轮在 8-18 MB/s 间波动,最终选择 Gost 作为这台 FreeBSD 的 TCP 主方案。

这个结果也说明 FreeBSD TCP 代理性能和 VPS 商家、虚拟化环境、线路关系很大。不要只看延迟,部署完一定要做本文最后的大文件吞吐测试。

整体结构如下:

sing-box -> 127.0.0.1:1080 -> 客户端 Gost
        -> mwss://proxy.example.com:443/secret-gost-path
        -> FreeBSD Nginx 443
        -> 127.0.0.1:18080 -> 服务端 Gost v3 MWS

这里有一个关键点:普通用户不能监听 443 这种低端口,但本方案监听 443/tcp 的是 Nginx。Nginx master 以 root 启动绑定低端口,worker 用 www 用户处理请求;Gost 只监听本机 127.0.0.1:18080,所以可以安全地用专用低权限用户运行。

示例信息

本文统一使用公版示例值,部署时替换成自己的真实信息:

项目示例值
域名proxy.example.com
服务器公网 IP203.0.113.10
WebSocket 路径/secret-gost-path
Gost 用户名GOST_USER
Gost 密码GOST_PASS
Nginx 入口443/tcp
Gost 后端127.0.0.1:18080
客户端本地 SOCKS127.0.0.1:1080

本文命令默认使用 root 执行。

支持系统

本文面向 FreeBSD 15 amd64。Gost v3 官方 release 也提供 FreeBSD arm64 包,如果你的机器是 ARM64,把下载文件名里的 freebsd_amd64 改成 freebsd_arm64

FreeBSD 和 Linux 的主要差异:

  • FreeBSD 使用 rc.d 管理服务,不使用 systemd。
  • Nginx 配置路径通常是 /usr/local/etc/nginx/
  • 网站目录建议放在 /usr/local/www/
  • certbot 证书路径通常是 /usr/local/etc/letsencrypt/live/<domain>/
  • sysctl 持久配置写入 /etc/sysctl.conf

1. 准备域名

先注册一个域名,例如:

example.com

建议不要直接用根域名做节点入口,而是使用一个看起来像普通业务服务的子域名,例如:

proxy.example.com
cloud.example.com
files.example.com
drive.example.com

本文后续统一以 proxy.example.com 为例。

在 DNS 后台添加一条 A 记录:

类型主机记录记录值
Aproxy203.0.113.10

如果服务器没有 IPv6,不要添加 AAAA 记录,避免客户端优先走 IPv6 后连接失败。

在服务器上检查解析:

drill proxy.example.com

正常应该能看到:

proxy.example.com.  300  IN  A  203.0.113.10

没有 drill 时也可以用:

host proxy.example.com

签证书前必须确认域名已经解析到当前服务器公网 IP。

同时确认防火墙或云安全组已经放行:

协议端口用途
TCP22SSH 管理
TCP80Let’s Encrypt HTTP 验证和 HTTP 跳转
TCP443Nginx HTTPS 入口和 MWSS 入口

本方案不需要对公网放行 18080,这个端口只给本机 Nginx 访问。

2. 安装基础组件

更新包索引并安装 Nginx、certbot 和常用工具:

pkg update -f
pkg install -y nginx py311-certbot ca_root_nss curl

启用 Nginx:

sysrc nginx_enable="YES"

先不要急着启动 HTTPS,后面要先用 HTTP 站点签证书。

3. 安装 Gost v3

以 amd64 为例:

GOST_VERSION="3.2.6"

fetch -o /tmp/gost_freebsd_amd64.tar.gz \
  "https://github.com/go-gost/gost/releases/download/v${GOST_VERSION}/gost_${GOST_VERSION}_freebsd_amd64.tar.gz"

mkdir -p /tmp/gost-v3
tar -xzf /tmp/gost_freebsd_amd64.tar.gz -C /tmp/gost-v3
install -m 0755 /tmp/gost-v3/gost /usr/local/bin/gost

/usr/local/bin/gost -V

正常会看到类似:

gost v3.2.6 (go1.25.4 freebsd/amd64)

如果你的服务器是 ARM64,把下载文件名改成:

gost_${GOST_VERSION}_freebsd_arm64.tar.gz

4. 创建伪装站点

创建站点目录:

mkdir -p /usr/local/www/proxy.example.com

写入一个英文网盘登录页:

ee /usr/local/www/proxy.example.com/index.html

内容如下:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex,nofollow">
  <title>Cloud Drive</title>
  <style>
    :root {
      color-scheme: light;
      --bg: #f6f8fb;
      --panel: #ffffff;
      --panel-soft: #f9fbfd;
      --text: #172033;
      --muted: #637083;
      --line: #dce3ec;
      --accent: #2368c4;
      --accent-dark: #174f99;
      --focus: rgba(35, 104, 196, .18);
      --shadow: 0 24px 70px rgba(23, 32, 51, .12);
    }

    * { box-sizing: border-box; }

    html, body { min-height: 100%; }

    body {
      margin: 0;
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      color: var(--text);
      background:
        linear-gradient(135deg, rgba(35, 104, 196, .08), transparent 34%),
        linear-gradient(315deg, rgba(42, 157, 143, .08), transparent 36%),
        var(--bg);
      display: grid;
      place-items: center;
      padding: 28px;
    }

    .shell {
      width: min(980px, 100%);
      min-height: 620px;
      display: grid;
      grid-template-columns: minmax(0, 1.08fr) minmax(360px, .92fr);
      overflow: hidden;
      border: 1px solid rgba(220, 227, 236, .9);
      border-radius: 6px;
      background: var(--panel);
      box-shadow: var(--shadow);
    }

    .overview {
      position: relative;
      padding: 56px;
      background: linear-gradient(180deg, #ffffff 0%, #f4f8fc 100%);
      border-right: 1px solid var(--line);
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      gap: 48px;
    }

    .brand {
      display: flex;
      align-items: center;
      gap: 12px;
      font-size: 15px;
      font-weight: 700;
      letter-spacing: 0;
    }

    .logo {
      width: 36px;
      height: 36px;
      border-radius: 6px;
      background: var(--accent);
      display: grid;
      place-items: center;
      color: #fff;
    }

    .hero h1 {
      max-width: 520px;
      margin: 0 0 18px;
      font-size: clamp(36px, 5vw, 58px);
      line-height: 1.02;
      letter-spacing: 0;
    }

    .hero p {
      max-width: 490px;
      margin: 0;
      color: var(--muted);
      font-size: 17px;
      line-height: 1.7;
    }

    .status-grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 12px;
    }

    .status-item {
      min-height: 92px;
      padding: 18px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: rgba(255, 255, 255, .72);
    }

    .status-label {
      color: var(--muted);
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0;
    }

    .status-value {
      margin-top: 10px;
      font-size: 20px;
      font-weight: 750;
    }

    .login {
      padding: 56px 48px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      background: var(--panel);
    }

    .login h2 {
      margin: 0 0 10px;
      font-size: 28px;
      letter-spacing: 0;
    }

    .login .hint {
      margin: 0 0 32px;
      color: var(--muted);
      line-height: 1.6;
    }

    form {
      display: grid;
      gap: 18px;
    }

    label {
      display: grid;
      gap: 8px;
      color: #2d3848;
      font-size: 14px;
      font-weight: 650;
    }

    input[type="email"],
    input[type="password"] {
      width: 100%;
      height: 48px;
      border: 1px solid var(--line);
      border-radius: 6px;
      padding: 0 14px;
      color: var(--text);
      background: var(--panel-soft);
      font: inherit;
      outline: none;
      transition: border-color .18s ease, box-shadow .18s ease, background .18s ease;
    }

    input:focus {
      border-color: var(--accent);
      box-shadow: 0 0 0 4px var(--focus);
      background: #fff;
    }

    .row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 14px;
      margin-top: 2px;
      color: var(--muted);
      font-size: 14px;
    }

    .remember {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      font-weight: 500;
      color: var(--muted);
    }

    input[type="checkbox"] {
      width: 16px;
      height: 16px;
      accent-color: var(--accent);
    }

    a {
      color: var(--accent);
      text-decoration: none;
      font-weight: 650;
    }

    button {
      height: 50px;
      border: 0;
      border-radius: 6px;
      background: var(--accent);
      color: #fff;
      font: inherit;
      font-weight: 750;
      cursor: pointer;
      transition: background .18s ease, transform .18s ease;
    }

    button:hover { background: var(--accent-dark); }
    button:active { transform: translateY(1px); }

    .note {
      margin-top: 28px;
      padding: 16px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--panel-soft);
      color: var(--muted);
      font-size: 14px;
      line-height: 1.6;
    }

    .footer {
      margin-top: 28px;
      color: #7d8898;
      font-size: 13px;
      line-height: 1.6;
    }

    @media (max-width: 820px) {
      body { padding: 18px; align-items: start; }
      .shell { grid-template-columns: 1fr; min-height: auto; }
      .overview { padding: 34px; border-right: 0; border-bottom: 1px solid var(--line); gap: 34px; }
      .login { padding: 34px; }
      .status-grid { grid-template-columns: 1fr; }
      .hero h1 { font-size: 38px; }
    }

    @media (max-width: 460px) {
      body { padding: 0; background: var(--panel); }
      .shell { border: 0; border-radius: 0; box-shadow: none; }
      .overview, .login { padding: 28px 20px; }
      .row { align-items: flex-start; flex-direction: column; }
    }
  </style>
</head>
<body>
  <main class="shell" aria-label="Cloud Drive sign in">
    <section class="overview" aria-label="Service overview">
      <div class="brand">
        <div class="logo" aria-hidden="true">
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" role="img">
            <path d="M7.4 18.5h9.1a4.1 4.1 0 0 0 .6-8.15 5.35 5.35 0 0 0-10.18-1.9A5.05 5.05 0 0 0 7.4 18.5Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
          </svg>
        </div>
        <span>Cloud Drive</span>
      </div>

      <div class="hero">
        <h1>Secure file access for your workspace.</h1>
        <p>Sign in to manage shared files, private folders, transfer history, and device sessions from one protected workspace.</p>
      </div>

      <div class="status-grid" aria-label="System status">
        <div class="status-item">
          <div class="status-label">Storage</div>
          <div class="status-value">Encrypted</div>
        </div>
        <div class="status-item">
          <div class="status-label">Access</div>
          <div class="status-value">Private</div>
        </div>
        <div class="status-item">
          <div class="status-label">Sync</div>
          <div class="status-value">Online</div>
        </div>
      </div>
    </section>

    <section class="login" aria-label="Sign in form">
      <h2>Sign in</h2>
      <p class="hint">Use your workspace account to continue.</p>

      <form action="/" method="post" autocomplete="on">
        <label>
          Email address
          <input type="email" name="email" autocomplete="email" inputmode="email" required>
        </label>

        <label>
          Password
          <input type="password" name="password" autocomplete="current-password" required>
        </label>

        <div class="row">
          <label class="remember">
            <input type="checkbox" name="remember">
            Remember this device
          </label>
          <a href="/">Forgot password?</a>
        </div>

        <button type="submit">Sign in</button>
      </form>

      <div class="note">Need access? Contact your workspace administrator to request an account or recover permissions.</div>
      <div class="footer">Protected workspace access. Unauthorized use is prohibited.</div>
    </section>
  </main>
</body>
</html>

5. 配置 Nginx 主配置

备份默认配置:

cp /usr/local/etc/nginx/nginx.conf /usr/local/etc/nginx/nginx.conf.bak

编辑主配置:

ee /usr/local/etc/nginx/nginx.conf

写成下面这样:

worker_processes auto;
worker_rlimit_nofile 1048576;

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
    worker_connections 65535;
    multi_accept on;
}

http {
    include mime.types;
    default_type application/octet-stream;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    server_tokens off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 1h;

    gzip on;

    include /usr/local/etc/nginx/conf.d/*.conf;
}

创建站点配置目录:

mkdir -p /usr/local/etc/nginx/conf.d

检查 Nginx 优化是否生效

先检查配置语法:

nginx -t

再查看 Nginx 实际加载后的完整配置:

nginx -T | egrep 'worker_processes|worker_rlimit_nofile|worker_connections|multi_accept|tcp_nopush|tcp_nodelay|server_tokens|ssl_session_cache'

正常能看到类似这些关键项:

worker_processes auto;
worker_rlimit_nofile 1048576;
worker_connections 65535;
multi_accept on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
ssl_session_cache shared:SSL:20m;

启动 Nginx 后检查 master 和 worker:

service nginx start
service nginx status
ps -axo user,pid,command | grep '[n]ginx'

生产状态一般是:

root  nginx: master process /usr/local/sbin/nginx
www   nginx: worker process

这说明 root 只负责绑定 80/443 低端口,实际请求由 www worker 处理。

最后确认端口监听:

sockstat -4 -6 -l | egrep ':(80|443)'

能看到 Nginx 监听 80/tcp443/tcp 即可。后端 18080 不应该由 Nginx 监听,后面启动 Gost 后才会出现。

6. 先配置 HTTP 站点

第一次签 Let’s Encrypt 证书,需要先让 HTTP 站点能访问。

ee /usr/local/etc/nginx/conf.d/gost-mwss.conf

写入临时 HTTP 配置:

server {
    listen 80;
    listen [::]:80;
    server_name proxy.example.com;

    root /usr/local/www/proxy.example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

检查并启动 Nginx:

nginx -t
service nginx start

从浏览器访问:

http://proxy.example.com/

如果能看到英文登录页,说明 DNS、80 端口和 Nginx root 都正确。

7. 签发 Let’s Encrypt 证书

使用 certbot 的 webroot 模式签证书:

certbot certonly --webroot \
  -w /usr/local/www/proxy.example.com \
  -d proxy.example.com \
  --agree-tos \
  --register-unsafely-without-email \
  --non-interactive

成功后证书路径是:

/usr/local/etc/letsencrypt/live/proxy.example.com/fullchain.pem
/usr/local/etc/letsencrypt/live/proxy.example.com/privkey.pem

启用 FreeBSD 的 certbot 周期续期:

sysrc -f /etc/periodic.conf weekly_certbot_enable="YES"

添加续期后 reload Nginx 的 hook:

mkdir -p /usr/local/etc/letsencrypt/renewal-hooks/deploy

cat <<'EOF' > /usr/local/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
service nginx reload
EOF

chmod +x /usr/local/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

先不急着 dry-run,等 HTTPS 配置完成后再统一测试。

8. 配置 HTTPS 和 MWSS 反代

重新编辑站点配置:

ee /usr/local/etc/nginx/conf.d/gost-mwss.conf

写入完整 HTTPS 和 MWSS 反代配置:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    http2 on;
    server_name _;

    ssl_certificate /usr/local/etc/letsencrypt/live/proxy.example.com/fullchain.pem;
    ssl_certificate_key /usr/local/etc/letsencrypt/live/proxy.example.com/privkey.pem;
    return 444;
}

server {
    listen 80;
    listen [::]:80;
    server_name proxy.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name proxy.example.com;

    ssl_certificate /usr/local/etc/letsencrypt/live/proxy.example.com/fullchain.pem;
    ssl_certificate_key /usr/local/etc/letsencrypt/live/proxy.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    root /usr/local/www/proxy.example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /secret-gost-path {
        access_log off;
        proxy_pass http://127.0.0.1:18080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_socket_keepalive on;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

这里故意没有写成 try_files $uri $uri/ /index.html;。正常访问 / 时,Nginx 会按 index index.html 显示英文网盘登录页;访问不存在的路径时直接返回 404;访问未绑定的 Host 时由 default server 返回 444。这样比所有路径都显示同一个页面更像正常站点,也能少暴露一点扫描特征。

说明:

  • 前两个 default_server 是兜底配置,非目标域名访问直接断开。
  • 普通浏览器访问 / 会看到英文网盘登录页。
  • 只有 /secret-gost-path 会被转发到本机 Gost 后端。
  • Gost 后端只监听 127.0.0.1:18080,不暴露公网。

检查配置:

nginx -t
service nginx restart

确认 HTTPS 伪装页可用:

curl -I https://proxy.example.com/

正常应返回 HTTP/2 200

9. FreeBSD 网络参数优化

追加 TCP 和 socket 缓冲优化:

cat <<'EOF' >> /etc/sysctl.conf

# Gost MWSS TCP tuning
kern.ipc.maxsockbuf=33554432
net.inet.tcp.sendbuf_max=16777216
net.inet.tcp.recvbuf_max=16777216
net.inet.tcp.sendspace=1048576
net.inet.tcp.recvspace=1048576
net.inet.tcp.mssdflt=1460
net.inet.tcp.cc.algorithm=cubic
net.inet.tcp.fast_finwait2_recycle=1
net.inet.tcp.syncookies=1
kern.ipc.somaxconn=65535
EOF

sysctl -f /etc/sysctl.conf

查看当前值:

sysctl kern.ipc.maxsockbuf kern.ipc.somaxconn
sysctl net.inet.tcp.sendbuf_max net.inet.tcp.recvbuf_max
sysctl net.inet.tcp.sendspace net.inet.tcp.recvspace
sysctl net.inet.tcp.cc.algorithm

这些参数用于提升 TCP 缓冲、监听队列和高延迟链路表现。FreeBSD 15 默认 TCP 栈已经比较稳,不要照搬 Linux 的 BBR 命令。

10. 创建低权限 Gost 用户

创建专用用户和组:

pw groupshow gost >/dev/null 2>&1 || pw groupadd gost
pw usershow gost >/dev/null 2>&1 || pw useradd gost -g gost -d /usr/local/var/empty/gost -s /bin/sh -c "Gost service user"

mkdir -p /usr/local/var/empty/gost
chown gost:gost /usr/local/var/empty/gost
chmod 0755 /usr/local/var/empty/gost

如果用户已经存在,可以跳过创建,检查一下即可:

id gost

11. 写入 Gost 后端配置

创建 root-only 配置目录:

install -d -m 0750 -o root -g wheel /usr/local/etc/gost

写入后端监听和认证信息:

cat <<'EOF' > /usr/local/etc/gost/gost_mwss.conf
gost_mwss_backend="mws://GOST_USER:GOST_PASS@127.0.0.1:18080?path=/secret-gost-path"
EOF

chmod 600 /usr/local/etc/gost/gost_mwss.conf
chown root:wheel /usr/local/etc/gost/gost_mwss.conf

GOST_USERGOST_PASS 改成自己的用户名和强密码。

注意这里是 mws://,因为 TLS 已经由 Nginx 负责,Gost 后端只处理本机明文 WebSocket。

12. 创建 Gost runner

runner 由 root 执行,读取 root-only 配置文件,然后用 su -m gost 把真正的 Gost 子进程降权到 gost 用户。

cat <<'EOF' > /usr/local/sbin/gost_mwss_runner
#!/bin/sh
set -eu
CONFIG="/usr/local/etc/gost/gost_mwss.conf"
if [ -r "$CONFIG" ]; then
    . "$CONFIG"
fi
: ${gost_mwss_run_user:="gost"}
: ${gost_mwss_backend:="mws://GOST_USER:GOST_PASS@127.0.0.1:18080?path=/secret-gost-path"}
exec /usr/bin/su -m "$gost_mwss_run_user" -c "exec /usr/local/bin/gost -L \"$gost_mwss_backend\""
EOF

chmod 700 /usr/local/sbin/gost_mwss_runner
chown root:wheel /usr/local/sbin/gost_mwss_runner

这样密码不会出现在 rc.conf 里,也不会放在普通可读脚本里。

13. 创建 rc.d 服务

创建服务脚本:

cat <<'EOF' > /usr/local/etc/rc.d/gost_mwss
#!/bin/sh

# PROVIDE: gost_mwss
# REQUIRE: NETWORKING nginx
# KEYWORD: shutdown

. /etc/rc.subr

name="gost_mwss"
rcvar="gost_mwss_enable"

load_rc_config $name

: ${gost_mwss_enable:="NO"}
: ${gost_mwss_run_user:="gost"}
: ${gost_mwss_log:="/var/log/gost-mwss.log"}
: ${gost_mwss_restart_delay:="3"}
: ${gost_mwss_pid_dir:="/var/run/gost_mwss"}

pidfile="${gost_mwss_pid_dir}/${name}.pid"
command="/usr/sbin/daemon"
start_precmd="gost_mwss_precmd"
stop_cmd="gost_mwss_stop"
status_cmd="gost_mwss_status"
command_args="-f -r -R ${gost_mwss_restart_delay} -p ${pidfile} -o ${gost_mwss_log} /usr/local/sbin/gost_mwss_runner"

gost_mwss_precmd()
{
    install -d -o root -g wheel -m 0755 ${gost_mwss_pid_dir}
    touch ${gost_mwss_log}
    chown ${gost_mwss_run_user}:${gost_mwss_run_user} ${gost_mwss_log}
    chmod 0640 ${gost_mwss_log}
}

gost_mwss_status()
{
    pgrep -u ${gost_mwss_run_user} -f "/usr/local/bin/gost -L" >/dev/null && echo "${name} is running." || echo "${name} is not running."
}

gost_mwss_stop()
{
    pkill -u ${gost_mwss_run_user} -f "/usr/local/bin/gost -L" 2>/dev/null || true
    pkill -f "/usr/local/sbin/gost_mwss_runner" 2>/dev/null || true
    rm -f ${pidfile}
}

run_rc_command "$1"
EOF

chmod 755 /usr/local/etc/rc.d/gost_mwss
chown root:wheel /usr/local/etc/rc.d/gost_mwss

启用服务:

sysrc gost_mwss_enable="YES"
sysrc gost_mwss_run_user="gost"

启动:

service gost_mwss start
service gost_mwss status

确认进程用户和监听地址:

ps -axo user,pid,command | egrep '[g]ost_mwss_runner|[s]u -m gost|[g]ost -L mws'
sockstat -4 -6 -l | grep ':18080'

正常状态应该类似:

root  daemon: /usr/local/sbin/gost_mwss_runner
root  /usr/bin/su -m gost -c exec /usr/local/bin/gost ...
gost  /usr/local/bin/gost -L mws://...
gost  gost  tcp4  127.0.0.1:18080

重点是最后真正的 Gost 子进程必须是 gost 用户,并且监听 127.0.0.1:18080

14. 配置日志轮转

创建日志文件:

touch /var/log/gost-mwss.log
chown gost:gost /var/log/gost-mwss.log
chmod 640 /var/log/gost-mwss.log

创建 newsyslog 配置目录:

mkdir -p /usr/local/etc/newsyslog.conf.d

写入轮转配置:

cat <<'EOF' > /usr/local/etc/newsyslog.conf.d/gost-mwss.conf
/var/log/gost-mwss.log  gost:gost  640  7  1024  *  JC
EOF

含义是:日志超过 1024KB 后压缩轮转,保留 7 份。

15. 完整服务检查

检查 Nginx:

nginx -t
service nginx status

检查 Gost:

service gost_mwss status
tail -f /var/log/gost-mwss.log

检查端口:

sockstat -4 -6 -l | egrep ':(80|443|18080|22)'

理想状态:

nginx  *:80
nginx  *:443
gost   127.0.0.1:18080
sshd   *:22

如果还有其他服务占用 443/tcp,需要先迁移。占用 443/udp 的服务不影响本方案,因为 Nginx 用的是 TCP。

16. 证书续期演练

执行 dry-run:

certbot renew --dry-run --no-random-sleep-on-renew

正常输出应包含:

Congratulations, all simulated renewals succeeded

如果不加 --no-random-sleep-on-renew,certbot 可能随机等待几分钟,这是正常行为,但手动测试时比较烦。

17. 客户端 Gost 测试

在客户端安装 Gost v3 后,前台测试:

gost -L socks5://127.0.0.1:1080 \
  -F "mwss://GOST_USER:GOST_PASS@proxy.example.com:443?path=/secret-gost-path"

另开一个终端测试:

curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me

如果输出是服务端公网 IP,说明链路已经打通。

Linux 客户端可以写成 systemd 服务:

[Unit]
Description=Gost MWSS client
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/gost -L "socks5://127.0.0.1:1080" -F "mwss://GOST_USER:GOST_PASS@proxy.example.com:443?path=/secret-gost-path"
Restart=always
RestartSec=3
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

保存为:

/etc/systemd/system/gost-mwss.service

启用:

systemctl daemon-reload
systemctl enable --now gost-mwss
systemctl status gost-mwss

18. sing-box 客户端接入

sing-box 不需要直接支持 MWSS,只需要把流量交给本机 Gost 提供的 SOCKS5。

分片配置示例

04_outbounds.json 的 selector 里加入新节点 tag:

{
  "tag": "Proxy",
  "type": "selector",
  "outbounds": [
    "auto",
    "node-a",
    "node-b",
    "gost-mwss"
  ],
  "interrupt_exist_connections": true,
  "default": "auto"
}

如果有 urltest,也把它加进去:

{
  "tag": "auto",
  "type": "urltest",
  "outbounds": [
    "node-a",
    "node-b",
    "gost-mwss"
  ],
  "interval": "2m0s"
}

outbounds 数组靠后位置加入 SOCKS 出站:

{
  "type": "socks",
  "tag": "gost-mwss",
  "server": "127.0.0.1",
  "server_port": 1080,
  "version": "5",
  "udp_over_tcp": false
}

路由直连规则

05_route.jsonroute.rules 最前面加入节点域名和节点 IP 直连:

{
  "domain": [
    "proxy.example.com"
  ],
  "ip_cidr": [
    "203.0.113.10/32"
  ],
  "outbound": "direct"
}

带上下文示例:

{
  "route": {
    "final": "Proxy",
    "rules": [
      {
        "domain": [
          "proxy.example.com"
        ],
        "ip_cidr": [
          "203.0.113.10/32"
        ],
        "outbound": "direct"
      },
      {
        "ip_is_private": true,
        "outbound": "direct"
      },
      {
        "rule_set": "geosite-geolocation-!cn",
        "outbound": "Proxy"
      }
    ]
  }
}

这条直连规则是为了避免代理回环。但要注意,sing-box 的 route.rules 只影响进入 sing-box 的流量,运行在本机的 Gost 客户端是独立进程,它自己会走系统 DNS 解析 proxy.example.com

fake-ip DNS 环境必须固定 Gost 节点解析

如果运行 Gost 客户端的机器同时跑了 sing-box/mosdns,并且系统 DNS 指向本机 fake-ip DNS,Gost 可能把节点域名解析成 28.0.0.0/8 这类虚拟 IP。结果就是 Gost 去连接 fake-ip,而不是真实服务器 IP。

这个问题很容易误判成节点被墙或服务端故障:

  • sing-box 面板里 Gost 节点测速失败,或者延迟一直测不出来。
  • curl --socks5-hostname 127.0.0.1:1080 https://www.gstatic.com/generate_204 卡住直到超时。
  • Gost 客户端日志里上游目标是 fake-ip,例如 remote="28.0.14.153:443"
  • 服务端 Nginx/Gost 看不到请求,或者只看到 WebSocket 异常断开。

先在运行 Gost 客户端的机器上检查解析结果:

getent hosts proxy.example.com

如果返回 fake-ip,不要把 hosts 写到 FreeBSD 服务端上,而是写到运行 Gost 客户端的机器上。也就是说,写在执行下面这条命令的那台机器上:

gost -L "socks5://127.0.0.1:1080" -F "mwss://GOST_USER:GOST_PASS@proxy.example.com:443?path=/secret-gost-path"

固定 hosts:

echo "203.0.113.10 proxy.example.com" >> /etc/hosts

如果这台客户端是 PVE 里的 Debian/Ubuntu 云镜像,或者 /etc/hosts 顶部提示由 cloud-init 管理,还要同步写入模板,避免重启后被覆盖:

echo "203.0.113.10 proxy.example.com" >> /etc/cloud/templates/hosts.debian.tmpl

然后重启 Gost 客户端并验证:

systemctl restart gost-mwss
getent hosts proxy.example.com
curl --socks5-hostname 127.0.0.1:1080 -m 15 https://www.gstatic.com/generate_204

正常时,getent hosts 应该返回你写入的真实服务器 IP,例如:

203.0.113.10    proxy.example.com

这就说明系统解析已经绕过 fake-ip,Gost 客户端会连接真实节点地址。如果仍然返回类似下面这种虚拟地址,就说明 hosts 没生效:

28.0.14.153     proxy.example.com

curl 正常返回或没有继续卡到超时,说明 Gost 本地 SOCKS 已经能通过真实节点访问外网。如果继续失败,再看 Gost 客户端日志,确认上游 dstremote 已经是真实服务器 IP,而不是 fake-ip。

最后检查并重启 sing-box:

sing-box check -C /etc/sing-box/conf/
systemctl restart sing-box

如果是单文件配置,用同样思路:outbounds 加 SOCKS,route.rules 开头加节点直连。

19. 验收与测速

服务端验收:

nginx -t
service nginx status
service gost_mwss status
sockstat -4 -6 -l | egrep ':(80|443|18080|22)'
certbot renew --dry-run --no-random-sleep-on-renew
curl -I https://proxy.example.com/

客户端验收:

systemctl status gost-mwss
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me

如果客户端接入 sing-box:

sing-box check -C /etc/sing-box/conf/
systemctl is-active sing-box

建议再做一次大文件吞吐测试,避免只看延迟:

curl --socks5-hostname 127.0.0.1:1080 -L -o /dev/null \
  -w 'code=%{http_code} speed=%{speed_download}Bps total=%{time_total}s size=%{size_download}\n' \
  -m 35 http://cachefly.cachefly.net/100mb.test

如果延迟能测出来、小网页能打开,但大文件只有几十 KB/s 或几百 KB/s,这就不是配置没通,而是当前 FreeBSD TCP 代理转发链路不适合做高速节点。可以换 VPS 商家、换虚拟化环境继续复现,也可以把 FreeBSD 留给 Hysteria2 UDP,把 TCP 节点放到 Linux 上。

常见问题

  • curl -I https://proxy.example.com/ 返回 200:正常,说明伪装站点工作。
  • 访问普通路径看到 Cloud Drive 登录页:正常。
  • WebSocket 路径返回 502:通常是 Gost 没启动,或 127.0.0.1:18080 没监听。
  • Gost 进程是 root:检查是否直接用 daemon 启动了 Gost,生产建议使用本文 runner 方式让 Gost 子进程降到 gost 用户。
  • certbot webroot 签发失败:检查 Nginx 是否真的 include 了 /usr/local/etc/nginx/conf.d/*.conf,以及 HTTP 根目录是否是 /usr/local/www/proxy.example.com
  • sing-box 加了节点但没速度:检查 proxy.example.com203.0.113.10/32 是否在路由最前面走 direct,并确认 /etc/hosts 没写错。
  • 客户端 curl --socks5-hostname 返回的不是服务端 IP:检查 Gost 客户端连接的域名、路径、用户名和密码是否与服务端一致。

到这里,FreeBSD 15 上的 Nginx 前置 Gost MWSS 节点就完成了:公网只暴露标准 HTTPS,伪装站点正常,Gost 后端低权限运行并藏在本机端口,客户端通过本地 SOCKS5 接入,再交给 sing-box 分流。是否达到你的生产标准,最终以你所在 VPS 环境的大文件吞吐测试为准。