- backend: 新增 GRE 隧道实现(ip tunnel 点对点互联),binmgr 支持 iproute2 - backend: api/identity 调整,各协议 service(gre/ikev2/ipsec/l2tp/openvpn)统一改动 - backend: OpenVPN 认证辅助与用户口令同步逻辑完善 - frontend: 协议图标/向导/编辑器支持 GRE,VPN 用户页重构(234 行) - frontend: 新增 RouteListInput 路由列表输入组件,Dashboard/Layout 优化 - i18n: 中英文文案同步更新 - test: 新增 binmgr/common/gre 单测,openvpn 测试补充 - docs: README 补充 GRE 说明,默认监听端口 8080 → 18080 - chore: .gitignore 增加 opsx-vpn.previous(构建备份二进制)
175 lines
5.6 KiB
Go
175 lines
5.6 KiB
Go
package openvpn
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
|
|
"vpn-platform/backend/internal/model"
|
|
)
|
|
|
|
func TestValidateOpenVPNModes(t *testing.T) {
|
|
cfg, err := New().Validate(map[string]interface{}{"auth_mode": "password", "subnet": "10.8.0.0 255.255.255.0"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg["proto"] != "tcp-server" || cfg["port"] != 1194 || cfg["auth_mode"] != "password" {
|
|
t.Fatalf("defaults not applied: %#v", cfg)
|
|
}
|
|
if cfg["enable_nat"] != true {
|
|
t.Fatalf("NAT should default to enabled: %#v", cfg)
|
|
}
|
|
if _, err := New().Validate(map[string]interface{}{"auth_mode": "unknown"}); err == nil {
|
|
t.Fatal("expected invalid auth mode error")
|
|
}
|
|
// 前端历史值 tcp 应归一为 tcp-server
|
|
cfg2, err := New().Validate(map[string]interface{}{"proto": "tcp", "subnet": "10.8.0.0 255.255.255.0"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg2["proto"] != "tcp-server" {
|
|
t.Fatalf("tcp not normalized: %#v", cfg2)
|
|
}
|
|
cfg3, err := New().Validate(map[string]interface{}{
|
|
"subnet": "10.8.0.0 255.255.255.0", "push_routes": "# VPC\n192.168.1.0/24\n# staging\n10.0.0.10",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg3["redirect_gateway"] != false {
|
|
t.Fatalf("push_routes should default redirect_gateway off: %#v", cfg3)
|
|
}
|
|
if cfg3["push_routes"] != "# VPC\n192.168.1.0/24\n# staging\n10.0.0.10/32" {
|
|
t.Fatalf("push_routes not normalized: %#v", cfg3)
|
|
}
|
|
if _, err := New().Validate(map[string]interface{}{"subnet": "10.8.0.0 255.255.255.0", "push_routes": "10.0.0.999"}); err == nil {
|
|
t.Fatal("expected invalid route to fail")
|
|
}
|
|
// MFA 仅允许密码相关模式
|
|
if _, err := New().Validate(map[string]interface{}{
|
|
"auth_mode": "certificate", "mfa_mode": "required", "subnet": "10.8.0.0 255.255.255.0",
|
|
}); err == nil {
|
|
t.Fatal("expected MFA + certificate-only to fail")
|
|
}
|
|
cfg4, err := New().Validate(map[string]interface{}{
|
|
"auth_mode": "password", "mfa_mode": "required", "subnet": "10.8.0.0 255.255.255.0", "proto": "tcp",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg4["proto"] != "tcp-server" || cfg4["mfa_mode"] != "required" {
|
|
t.Fatalf("mfa/tcp normalize: %#v", cfg4)
|
|
}
|
|
}
|
|
|
|
func TestSplitPasswordAndOTP(t *testing.T) {
|
|
pw, otp := splitPasswordAndOTP("secretpass123456", "")
|
|
if pw != "secretpass" || otp != "123456" {
|
|
t.Fatalf("suffix: %q %q", pw, otp)
|
|
}
|
|
scrv := "SCRV1:" + base64.StdEncoding.EncodeToString([]byte("mypass")) + ":" + base64.StdEncoding.EncodeToString([]byte("654321"))
|
|
pw, otp = splitPasswordAndOTP(scrv, "")
|
|
if pw != "mypass" || otp != "654321" {
|
|
t.Fatalf("scrv1: %q %q", pw, otp)
|
|
}
|
|
pw, otp = splitPasswordAndOTP("onlypass", "111222")
|
|
if pw != "onlypass" || otp != "111222" {
|
|
t.Fatalf("challenge line: %q %q", pw, otp)
|
|
}
|
|
}
|
|
|
|
func TestClientProto(t *testing.T) {
|
|
if clientProto("tcp-server") != "tcp-client" || clientProto("udp") != "udp" || clientProto("tcp") != "tcp-client" {
|
|
t.Fatal("clientProto mapping failed")
|
|
}
|
|
}
|
|
|
|
func TestServerConfigIncludesProductionTunings(t *testing.T) {
|
|
cfg, err := New().Validate(map[string]interface{}{
|
|
"auth_mode": "password", "proto": "tcp", "subnet": "10.8.0.0 255.255.240.0",
|
|
"max_clients": 1000, "mfa_mode": "off",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg["proto"] != "tcp-server" {
|
|
t.Fatalf("proto: %#v", cfg["proto"])
|
|
}
|
|
tunn := &model.Tunnel{ID: 1, Protocol: "openvpn"}
|
|
raw := New().serverConfig(tunn, cfg)
|
|
for _, want := range []string{
|
|
"proto tcp-server",
|
|
"tun-mtu 1500",
|
|
"mssfix 1450",
|
|
"keepalive 5 30",
|
|
"reneg-sec 0",
|
|
"verify-client-cert none",
|
|
"username-as-common-name",
|
|
"max-clients 1000",
|
|
"duplicate-cn",
|
|
"via-file",
|
|
"data-ciphers-fallback AES-256-GCM",
|
|
} {
|
|
if !strings.Contains(raw, want) {
|
|
t.Fatalf("server.conf missing %q\n%s", want, raw)
|
|
}
|
|
}
|
|
if strings.Contains(raw, "via-env") {
|
|
t.Fatal("should use via-file not via-env")
|
|
}
|
|
for _, obsolete := range []string{"persist-key", "ifconfig-pool-persist"} {
|
|
if strings.Contains(raw, obsolete) {
|
|
t.Fatalf("server.conf should not contain obsolete %q\n%s", obsolete, raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateTOTPSkew(t *testing.T) {
|
|
// 使用固定 secret 生成当前码并校验
|
|
secret := "JBSWY3DPEHPK3PXP" // "Hello!" base32, common test secret
|
|
code, err := totp.GenerateCode(secret, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ok, counter := validateTOTPSkew(code, secret, 1)
|
|
if !ok || counter == 0 {
|
|
t.Fatalf("expected valid totp, ok=%v counter=%d", ok, counter)
|
|
}
|
|
if ok2, _ := validateTOTPSkew("000000", secret, 1); ok2 {
|
|
t.Fatal("expected invalid code")
|
|
}
|
|
}
|
|
|
|
func TestCertificateCNIsSafeAndStable(t *testing.T) {
|
|
cn := certificateCN("张三 / alice@example.com")
|
|
if !certificateName.MatchString(cn) || !strings.HasPrefix(cn, "u-") {
|
|
t.Fatalf("unsafe CN: %q", cn)
|
|
}
|
|
}
|
|
|
|
func TestPKIScriptUses99YearCertificates(t *testing.T) {
|
|
script := pkiScript(&model.Tunnel{ID: 1, Protocol: "openvpn"})
|
|
for _, want := range []string{"EASYRSA_CA_EXPIRE=36135", "EASYRSA_CERT_EXPIRE=36135"} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("PKI script missing %q\n%s", want, script)
|
|
}
|
|
}
|
|
if !strings.Contains(script, "EASYRSA_REQ_CN=OPSX-VPN-CA") {
|
|
t.Fatalf("PKI script should set a CA name: %s", script)
|
|
}
|
|
if strings.Contains(script, "export EASYRSA_BATCH=1 EASYRSA_REQ_CN") {
|
|
t.Fatalf("PKI script must not export the CA CN to server/client certificate commands: %s", script)
|
|
}
|
|
}
|
|
|
|
func TestSplitStatusFieldsSupportsOpenVPNStatusV3(t *testing.T) {
|
|
line := "CLIENT_LIST\tadmin\ttcp4-server:203.0.113.1:1194\t10.8.0.2\t\t7161451\t8489433\t2026-07-25 23:32:58\t1784993578\tadmin"
|
|
fields := splitStatusFields(line)
|
|
if len(fields) != 10 || fields[5] != "7161451" || fields[9] != "admin" {
|
|
t.Fatalf("status-version 3 fields parsed incorrectly: %#v", fields)
|
|
}
|
|
}
|