OCserv 是 OpenConnect 協定的參考伺服器實作,而 OpenConnect 本身則是逆向工程、開放版本的 Cisco AnyConnect SSL VPN 替代品。因為客戶端就是原生的 AnyConnect 或 OpenConnect 應用程式,你可以獲得一個在 iOS、Android、macOS、Windows 和 Linux 上開箱即用的 VPN,客戶端不用安裝任何奇怪的東西,也不用信任第三方 VPN 供應商——伺服器就是一台你自己掌控的 Linux 主機。
本文涵蓋:取得一台伺服器、用快速方式(套件管理器)和彈性方式(原始碼編譯)安裝 ocserv、設定檔與憑證,以及大多數教學會跳過的部分——用 Nginx 把 ocserv 前置,讓它透過基於 SNI 的 TCP 路由和一般網站共用 443 埠,而不需要單獨開放一個埠。
1. 取得一台遠端伺服器
這裡所謂的遠端伺服器,指的是一台 24/7 運行、幾乎都是 Linux 的主機,並且能從你實際想繞過限制的客戶端網路連接到。
1.1 去哪裡取得
Virtual Private Server(虛擬私有伺服器):
含免費方案的雲端運算:
- Oracle Cloud,提供含 2 個運算執行個體的 Always Free 方案。
兩者註冊時都需要 Visa 或 Mastercard 卡片。
1.2 基礎加固
在安裝任何面向公網的服務之前,先做基本的加固:SSH 金鑰認證、非 root 的 sudo 使用者、Fail2Ban 和防火牆。完整流程可參考 Linux 雲伺服器初始化與安全加固。
2. 安裝 OCserv
2.1 快速安裝(套件管理器)
最快的路徑,如果你不需要特定版本或自訂編譯參數,這樣就夠了。
# RHEL 家族 (Rocky Linux, AlmaLinux, ...)
sudo dnf install epel-release -y
sudo dnf install ocserv -y
# Debian/Ubuntu
sudo apt update && sudo apt install ocserv -y
這樣立刻就能拿到一個可用的執行檔和一個 systemd unit;如果這就是你需要的全部,可以直接跳到 2.3 設定檔。
2.2 從原始碼編譯
當你想要比發行版套件更新的最新版本,或者需要套件沒有提供的編譯期選項(seccomp 沙箱、特定的 GnuTLS 版本等等)時,就從原始碼編譯。
安裝編譯所需的依賴套件。
# Debian/Ubuntu
sudo apt update
sudo apt install -y build-essential autoconf automake libtool pkg-config gperf \
libgnutls28-dev libnl-route-3-dev libseccomp-dev libpam0g-dev \
libprotobuf-c-dev protobuf-c-compiler libreadline-dev liblz4-dev
# RHEL 家族
sudo dnf install -y gcc make autoconf automake libtool pkgconfig gperf \
gnutls-devel libnl3-devel libseccomp-devel pam-devel \
protobuf-c-devel readline-devel lz4-devel
如果之後 ./configure 抱怨缺少某個套件,就安裝它提到的那個套件對應的 -dev/-devel 版本,然後重新執行——上面這份清單已經涵蓋常見情況(GnuTLS 用於 TLS/DTLS,libnl3 用於 tun 裝置管理,libseccomp 用於 worker 沙箱,PAM 用於身分驗證,protobuf-c 用於內部 main/worker 的 IPC,readline 用於 occtl 的互動式 shell)。
**下載並編譯。**先到發布清單確認目前的版本,不要寫死一個舊版本號:
OCSERV_VERSION=1.3.0 # 換成目前的發布版本
wget "https://ocserv.gitlab.io/www/download/ocserv-${OCSERV_VERSION}.tar.xz"
tar -xf "ocserv-${OCSERV_VERSION}.tar.xz"
cd "ocserv-${OCSERV_VERSION}"
./configure \
--prefix=/usr \
--sysconfdir=/etc \
--localstatedir=/var \
--with-systemd-unitdir=/usr/lib/systemd/system
make -j"$(nproc)"
sudo make install
使用 --prefix=/usr --sysconfdir=/etc --localstatedir=/var 可以讓安裝的檔案結構和發行版套件一致(設定檔在 /etc/ocserv 下,unit 檔透過 --with-systemd-unitdir 安裝),因此不管你走哪條安裝路徑,本文後面的內容都適用,不需要調整。
建立服務帳號與目錄(套件安裝會自動幫你做這件事;從原始碼安裝的話,你需要自己執行一次):
sudo groupadd -r ocserv 2>/dev/null || true
sudo useradd -r -M -s /usr/sbin/nologin -g ocserv ocserv 2>/dev/null || true
sudo mkdir -p /etc/ocserv/ssl /var/lib/ocserv
sudo chown -R ocserv:ocserv /var/lib/ocserv
sudo systemctl daemon-reload
/var/lib/ocserv 是 worker 行程被關進去的 chroot-dir(見下面的設定)——它必須存在,而且 ocserv 使用者必須有寫入權限,服務才能正常啟動。
檢查安裝是否成功:
ocserv --version
occtl --version
2.3 設定檔
把下面內容存成 /etc/ocserv/ocserv.conf。不管你是用套件安裝還是從原始碼安裝,設定檔的結構都一樣:
ocserv.conf
# --- 監聽設定 ---
tcp-port = 9001
# udp-port 未設定時預設等於 tcp-port。只有在你要用 Nginx 前置 TCP 埠時
# (見第 3 節)才需要單獨設定它——那種情況下這裡會變成 udp-port = 443,
# 而 tcp-port 則保持為僅供內部使用的埠。
# --- 行程 / 沙箱 ---
run-as-user = ocserv
run-as-group = ocserv
socket-file = ocserv.sock
chroot-dir = /var/lib/ocserv
isolate-workers = true
pid-file = /var/run/ocserv.pid
# --- 限制與流量限制 ---
max-clients = 16
max-same-clients = 4
rate-limit-ms = 100
server-stats-reset-time = 604800
max-ban-score = 80
ban-reset-time = 1200
# --- 連線階段 / 保持連線計時器 ---
keepalive = 32400
dpd = 90
mobile-dpd = 1800
switch-to-tcp-timeout = 25
try-mtu-discovery = false
auth-timeout = 240
min-reauth-time = 300
cookie-timeout = 300
rekey-time = 172800
rekey-method = ssl
deny-roaming = false
# --- TLS / 相容性 ---
tls-priorities = "NORMAL:%SERVER_PRECEDENCE"
compression = true
cisco-client-compat = true
dtls-legacy = true
# --- 身分驗證 ---
# 用 ocpasswd 管理的 htpasswd 風格檔案做密碼驗證。
# 如果想改用客戶端憑證驗證,把 auth 改成 "certificate",
# 這種情況下下面的 cert-user-oid/cert-group-oid 才會有意義。
auth = "plain[passwd=/etc/ocserv/ocpasswd]"
cert-user-oid = 0.9.2342.19200300.100.1.1
cert-group-oid = 2.5.4.11
# --- 伺服器憑證 ---
ca-cert = /etc/ocserv/ssl/ca-cert.pem
server-cert = /etc/ocserv/ssl/server-cert.pem
server-key = /etc/ocserv/ssl/server-key.pem
# --- 網路 ---
device = vpns
predictable-ips = true
default-domain = example.com
ipv4-network = 10.10.11.0/24
dns = 1.1.1.1
dns = 8.8.4.4
ping-leases = false
use-occtl = true
# 從隧道中排除的路由,例如避免 VPN 自己的子網段或本機網路
# 又被繞回隧道本身:
no-route = 10.10.10.0/24
no-route = 192.168.192.0/24
這份設定和原本的範例是同一組指令,只是把先前版本中重複、彼此矛盾的 cert-user-oid 那一行移除了,並把一些指令分組整理,讓它從頭到尾讀起來比較清楚,而不是一份沒有分類的清單。完整的指令參考可查閱官方手冊。
2.4 TLS 憑證
ocserv 需要自己的 CA 和伺服器憑證——這和你網頁伺服器所用的憑證是分開的兩件事,因為 ocserv 自己終結它自己的 TLS/DTLS。
自簽憑證(個人使用沒問題;客戶端第一次連線時需要接受/釘選它一次):
sudo mkdir -p /etc/ocserv/ssl && cd /etc/ocserv/ssl
# CA
sudo certtool --generate-privkey --outfile ca-key.pem
sudo certtool --generate-self-signed --load-privkey ca-key.pem \
--template <(printf 'cn = "ocserv-ca"\norganization = "homelab"\nserial = 1\nexpiration_days = 3650\nca\nsigning_key\ncert_signing_key\ncrl_signing_key\n') \
--outfile ca-cert.pem
# 伺服器憑證,由上面的 CA 簽署
sudo certtool --generate-privkey --outfile server-key.pem
sudo certtool --generate-certificate --load-privkey server-key.pem \
--load-ca-certificate ca-cert.pem --load-ca-privkey ca-key.pem \
--template <(printf 'cn = "vpn.example.com"\norganization = "homelab"\nexpiration_days = 3650\nsigning_key\nencryption_key\ntls_www_server\n') \
--outfile server-cert.pem
公開受信任的憑證——如果你打算用 Nginx 把 ocserv 前置在一個真實網域上(第 3 節),建議使用這種方式,這樣客戶端完全不用接受自簽憑證。用一般方式(例如透過 Certbot/ACME)為 vpn.example.com 申請一張憑證,然後把 ocserv.conf 中的 server-cert/server-key 指向那張憑證的 fullchain.pem/privkey.pem,而不是自簽的那一對,並拿掉 ca-cert(或者把它指向公開的 CA 套件),因為客戶端本來就已經信任該憑證的簽發 CA 了。
2.5 建立 VPN 使用者並啟動服務
sudo ocpasswd -c /etc/ocserv/ocpasswd # 會提示輸入使用者名稱與密碼
sudo systemctl enable --now ocserv
sudo systemctl status ocserv
確認它確實在監聽,並檢查有沒有錯誤:
sudo ss -lntup | grep -E ':9001'
sudo journalctl -u ocserv -f
sudo occtl show status
2.6 防火牆與網路調校
啟用 IP 轉發並開啟 BBR 壅塞控制,因為 VPN 的吞吐量同時受這兩者制約:
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/60-ocserv.conf
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.d/60-ocserv.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.d/60-ocserv.conf
sudo sysctl -p /etc/sysctl.d/60-ocserv.conf
開放埠口,並對離開 VPN 子網段的流量做 NAT:
sudo firewall-cmd --add-port=9001/tcp --zone=public --permanent
sudo firewall-cmd --add-port=9001/udp --zone=public --permanent
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.10.11.0/24" masquerade'
sudo firewall-cmd --reload
(如果你的發行版不是用 firewalld,對應的 ufw/iptables 寫法:ufw allow 9001/tcp、ufw allow 9001/udp,再加上在你的對外介面上為 10.10.11.0/24 設定一條 MASQUERADE 規則。)
2.7 雲端供應商層級的防火牆
有些 VPS 供應商(包括 Vultr)在作業系統本身的防火牆之上,還有另外一層獨立的防火牆。記得也要在供應商的控制台裡開放同一個埠(上面範例中的 9001),否則作業系統層級的規則就算開了也沒用。
到這一步,你已經有一個可以透過 your_server_ip:9001 連上的 VPN 了。下一節是選擇性的,但如果這台伺服器同時也在跑一個一般網站,就很值得做:它能讓 VPN 共用 443 埠,而不需要單獨開放一個埠。
3. 用 Nginx 前置 OCserv:TCP 層級的 SNI 路由
3.1 為什麼要這麼做
把 ocserv 跑在自己的埠上(上面的 9001)是可行的,但這是一個明顯、容易被指紋辨識出來的 VPN 端點,而且還多開了一個要顧防火牆的埠。如果同一台伺服器已經在用 Nginx 跑一個 443 埠的網站,你可以讓 Nginx 去看 TLS ClientHello 的 SNI 欄位——完全不需要解密任何東西——把 vpn.example.com 的連線直接路由到 ocserv,其他所有流量則導向網站,全部都走同一個 443 埠。
這之所以可行,是因為 Nginx 的 stream 模組可以在任何 TLS 交握真正發生之前,先偷看一眼 SNI 主機名稱(ssl_preread),然後把原始的 TCP 位元組轉送到匹配的後端——Nginx 從來不會去終結 ocserv 的 TLS,那仍然是 ocserv 自己在做的事。
3.2 前提條件:ssl_preread
確認你的 Nginx 有編譯進這個模組:
nginx -V 2>&1 | grep -o with-stream_ssl_preread_module
大多數發行版套件(Debian/Ubuntu、Fedora、官方 nginx.org 倉庫)都已經內建這個模組。如果你是自己從原始碼編譯 Nginx,就在 ./configure 加上 --with-stream_ssl_preread 然後重新編譯。
3.3 拆分 ocserv 的 TCP 和 UDP 埠
這是讓整個設定真正能用在 VPN 上、而不只是一個普通 HTTPS 網站的關鍵技巧:TCP 和 UDP 在作業系統層級是各自獨立的埠空間,所以 Nginx 綁定 TCP/443 並不會和 ocserv 直接綁定 UDP/443 發生衝突。在 ocserv.conf 中設定:
tcp-port = 9001 # 僅供內部使用;Nginx 會把匹配到的 SNI 轉送到這裡
udp-port = 443 # ocserv 直接綁定這個埠,完全繞過 Nginx
綁定 443 埠需要 root 權限或 CAP_NET_BIND_SERVICE——不管是套件安裝還是第 2.2 節從原始碼編譯安裝的 systemd unit,都已經讓 ocserv 帶有它所需要的這個能力,不用額外處理。改完設定後重新啟動 ocserv:
sudo systemctl restart ocserv
3.4 Nginx 的 stream {} 區塊
stream {} 是一個最上層的 context,和 http {} 是平行關係,不是巢狀在它裡面——把下面內容加到 /etc/nginx/nginx.conf 或它 include 的某個檔案裡:
stream {
map $ssl_preread_server_name $vpn_upstream {
vpn.example.com ocserv_backend;
default website_backend;
}
upstream ocserv_backend {
server 127.0.0.1:9001;
}
upstream website_backend {
server 127.0.0.1:8443; # 你網站真正的 TLS 監聽埠,已經從 443 移開
}
server {
listen 443;
listen [::]:443;
proxy_pass $vpn_upstream;
ssl_preread on;
}
}
加上這個監聽器之後,網站自己在 http {} 裡原本的 server { listen 443 ssl; ... } 區塊,就必須搬到一個僅供內部使用的埠(上面例子中的 8443),這樣才能讓 stream {} 是唯一真正綁定在公開的 443 TCP socket 上的東西;map 裡的 website_backend 分支則會把所有不是 vpn.example.com 的流量原封不動地轉送到那個內部埠——網站自己的 TLS 處理完全不需要改變,只是它監聽的埠不一樣了。
驗證並重新載入:
sudo nginx -t
sudo systemctl reload nginx
3.5 更新防火牆
有了 Nginx 前置 TCP 這一側之後,ocserv 的 TCP 埠就不再需要從外部連得到——只有同一台主機上的 Nginx 會去連它。相對地,UDP 埠仍然需要直接開放,因為 Nginx 完全不會碰它:
sudo firewall-cmd --remove-port=9001/tcp --zone=public --permanent # 不再需要對外開放
sudo firewall-cmd --add-port=443/udp --zone=public --permanent # ocserv 直接使用的 UDP 路徑
sudo firewall-cmd --reload
443/tcp 應該早就為了網站而開放了,這部分不需要變動。
3.6 一個限制:這個做法只涵蓋 TCP
ssl_preread/SNI 路由是一個只適用於 TCP 的技巧——它靠的是在 TCP 連線一開始就讀取明文的 ClientHello。OpenConnect 的資料通道為了吞吐量會優先使用 UDP 上的 DTLS,而 Nginx 在 UDP 上並沒有等價的「偷看主機名稱」機制,所以那部分流量沒辦法用同樣的方式做 SNI 路由。3.3 節的埠拆分是繞過這個限制,而不是真正解決它:TCP(443)透過 Nginx 轉送到 9001 上的 ocserv,UDP(443)則直接進 ocserv,完全不經過 Nginx。如果直連的 UDP 路徑哪天被擋掉了(比如上游防火牆更嚴格,或客戶端網路只允許對外連 443/TCP),第 2.3 節裡 ocserv 的 switch-to-tcp-timeout 會讓客戶端自動退回成只用 TCP——速度會變慢,但走的仍然是同一條 Nginx 前置的路徑,所以連線不會直接斷掉。
4. 客戶端連線
如果你用了第 3 節的 Nginx 設定,直接連 vpn.example.com,不用加埠號(預設就是 443)。否則就連 your_server_ip:9001。
4.1 iOS/Android
AnyConnect,即使在中國大陸,App Store 裡也找得到。
- 伺服器位址:
vpn.example.com(如果沒有做 Nginx 設定,則是your_server_ip:9001)。 - 使用者名稱與密碼:就是你用
ocpasswd設定的那組。
4.2 Mac/Linux/Windows
AnyConnect 在 macOS 上也有,只是安裝檔要花點功夫才找得到。連線方式和 iOS 上一樣。
另一個選擇,也是 Linux 上最簡單的方式,是 OpenConnect——在 macOS 上用 brew install openconnect 安裝,在 Linux 上用你發行版的套件管理器安裝,然後這樣連線:
sudo openconnect vpn.example.com
現在你就有了自己的 VPN,如果照著第 3 節設定好,從外面看起來和一個普通的 HTTPS 網站沒有兩樣。