feat: 新增 GRE 隧道协议,管理端口调整为 18080
- 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(构建备份二进制)
This commit is contained in:
1 parent
c7f59f2cbf
commit
ef7eb378d2
28 files changed
+1363
-439
No files matched your search
@@ -40,6 +40,7 @@ const (
|
||||
otpPeriod = 30
|
||||
otpDigits = 6
|
||||
otpDefaultSlop = 90
|
||||
pkiExpireDays = 99 * 365
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
@@ -75,7 +76,7 @@ func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{},
|
||||
return nil, fmt.Errorf("port 必须是 1-65535 的整数")
|
||||
}
|
||||
if service.Str(out, "proto") == "" {
|
||||
out["proto"] = "udp"
|
||||
out["proto"] = "tcp-server"
|
||||
}
|
||||
// 前端/API 可用 udp | tcp | tcp-server;服务端统一写成 udp 或 tcp-server。
|
||||
switch strings.ToLower(service.Str(out, "proto")) {
|
||||
@@ -105,7 +106,7 @@ func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{},
|
||||
return nil, fmt.Errorf("dns 必须是 IP 地址")
|
||||
}
|
||||
if service.Str(out, "auth_mode") == "" {
|
||||
out["auth_mode"] = "certificate"
|
||||
out["auth_mode"] = "password"
|
||||
}
|
||||
switch service.Str(out, "auth_mode") {
|
||||
case "certificate", "password", "certificate+password":
|
||||
@@ -151,25 +152,23 @@ func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{},
|
||||
// 默认 password_is_cr=1:期望 static-challenge / SCRV1 响应格式。
|
||||
out["otp_password_is_cr"] = true
|
||||
}
|
||||
if service.Str(out, "push_routes") != "" {
|
||||
for _, r := range strings.Split(service.Str(out, "push_routes"), ",") {
|
||||
r = strings.TrimSpace(r)
|
||||
if r == "" {
|
||||
continue
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(r); err != nil {
|
||||
return nil, fmt.Errorf("push_routes 含非法 CIDR: %s", r)
|
||||
}
|
||||
}
|
||||
if normalized, err := normalizePushRoutes(service.Str(out, "push_routes")); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
out["push_routes"] = normalized
|
||||
}
|
||||
if _, ok := out["redirect_gateway"]; !ok {
|
||||
// 未填 push_routes 时默认全流量;有 push_routes 时默认仅推内网。
|
||||
out["redirect_gateway"] = service.Str(out, "push_routes") == ""
|
||||
// 未填有效路由时默认全流量;只有注释不应改变默认出口。
|
||||
routes, _ := parsePushRoutes(service.Str(out, "push_routes"))
|
||||
out["redirect_gateway"] = len(routes) == 0
|
||||
}
|
||||
if _, ok := out["pki_auto_init"]; !ok {
|
||||
out["pki_auto_init"] = true
|
||||
}
|
||||
if service.Bool(out, "enable_nat", false) && service.Str(out, "egress_interface") == "" {
|
||||
if _, ok := out["enable_nat"]; !ok {
|
||||
out["enable_nat"] = true
|
||||
}
|
||||
if service.Bool(out, "enable_nat", true) && service.Str(out, "egress_interface") == "" {
|
||||
if iface, err := service.DefaultRouteInterface(); err == nil {
|
||||
out["egress_interface"] = iface
|
||||
}
|
||||
@@ -235,6 +234,66 @@ func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{},
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func normalizePushRoutes(raw string) (string, error) {
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
comment, routePart := "", trimmed
|
||||
if idx := strings.Index(routePart, "#"); idx >= 0 {
|
||||
comment, routePart = strings.TrimSpace(routePart[idx:]), strings.TrimSpace(routePart[:idx])
|
||||
}
|
||||
routes, err := parsePushRoutes(routePart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(routes) == 0 && comment != "" {
|
||||
lines = append(lines, comment)
|
||||
continue
|
||||
}
|
||||
if len(routes) > 0 {
|
||||
line = strings.Join(routes, ", ")
|
||||
if comment != "" {
|
||||
line += " " + comment
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func parsePushRoutes(raw string) ([]string, error) {
|
||||
routes := make([]string, 0)
|
||||
for _, line := range strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") {
|
||||
if idx := strings.Index(line, "#"); idx >= 0 {
|
||||
line = line[:idx]
|
||||
}
|
||||
for _, route := range strings.Split(line, ",") {
|
||||
route = strings.TrimSpace(route)
|
||||
if route == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(route, "/") {
|
||||
ip := net.ParseIP(route).To4()
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("push_routes 含非法 IP 或 CIDR: %s", route)
|
||||
}
|
||||
route = ip.String() + "/32"
|
||||
}
|
||||
ip, network, err := net.ParseCIDR(route)
|
||||
if err != nil || ip.To4() == nil {
|
||||
return nil, fmt.Errorf("push_routes 含非法 IPv4 CIDR: %s", route)
|
||||
}
|
||||
routes = append(routes, ip.Mask(network.Mask).String()+"/"+strconv.Itoa(maskSize(network.Mask)))
|
||||
}
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func maskSize(mask net.IPMask) int { size, _ := mask.Size(); return size }
|
||||
|
||||
func parseSubnet(v string) (net.IP, net.IPMask, error) {
|
||||
f := strings.Fields(v)
|
||||
if len(f) != 2 {
|
||||
@@ -296,17 +355,14 @@ func (s *Service) serverConfig(t *model.Tunnel, cfg map[string]interface{}) stri
|
||||
crl := filepath.Join(dir, "pki", "crl.pem")
|
||||
fmt.Fprintf(&b, "ca %q\ncert %q\nkey %q\ndh %q\ntls-crypt %q\ncrl-verify %q\n", ca, cert, key, dh, ta, crl)
|
||||
// 地址池与拓扑
|
||||
fmt.Fprintf(&b, "topology subnet\nserver %s\nifconfig-pool-persist %q\n", service.StrOr(cfg, "subnet", "10.10.0.0 255.255.255.0"), filepath.Join(dir, "ipp.txt"))
|
||||
fmt.Fprintf(&b, "topology subnet\nserver %s\n", service.StrOr(cfg, "subnet", "10.10.0.0 255.255.255.0"))
|
||||
// push:DNS / 全流量 / 内网路由
|
||||
fmt.Fprintf(&b, "push \"dhcp-option DNS %s\"\n", service.StrOr(cfg, "dns", service.DefaultDNS()))
|
||||
if service.Bool(cfg, "redirect_gateway", true) {
|
||||
b.WriteString("push \"redirect-gateway def1 bypass-dhcp\"\n")
|
||||
}
|
||||
for _, r := range strings.Split(service.Str(cfg, "push_routes"), ",") {
|
||||
r = strings.TrimSpace(r)
|
||||
if r == "" {
|
||||
continue
|
||||
}
|
||||
routes, _ := parsePushRoutes(service.Str(cfg, "push_routes"))
|
||||
for _, r := range routes {
|
||||
ip, network, err := net.ParseCIDR(r)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -316,7 +372,7 @@ func (s *Service) serverConfig(t *model.Tunnel, cfg map[string]interface{}) stri
|
||||
}
|
||||
// 会话保活(生产 keepalive 5 30)
|
||||
fmt.Fprintf(&b, "keepalive %d %d\n", int(service.Num(cfg, "keepalive_interval", 5)), int(service.Num(cfg, "keepalive_timeout", 30)))
|
||||
fmt.Fprintf(&b, "data-ciphers %s:AES-128-GCM\npersist-key\npersist-tun\n", service.StrOr(cfg, "cipher", "AES-256-GCM"))
|
||||
fmt.Fprintf(&b, "data-ciphers %s:AES-128-GCM\ndata-ciphers-fallback %s\npersist-tun\n", service.StrOr(cfg, "cipher", "AES-256-GCM"), service.StrOr(cfg, "cipher", "AES-256-GCM"))
|
||||
// reneg-sec:生产/MFA 常 0,避免周期性 TLS 重协商
|
||||
fmt.Fprintf(&b, "reneg-sec %d\n", int(service.Num(cfg, "reneg_sec", 0)))
|
||||
if service.Bool(cfg, "duplicate_cn", false) {
|
||||
@@ -372,6 +428,10 @@ func (s *Service) InitializePKI(t *model.Tunnel) error {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
// Any PKI that failed the readiness check cannot start a server safely.
|
||||
if err := os.RemoveAll(filepath.Join(dir, "pki")); err != nil {
|
||||
return fmt.Errorf("清理不完整的 OpenVPN PKI 失败: %w", err)
|
||||
}
|
||||
if err := runEasyRSA(dir, easy, "init-pki"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -403,9 +463,16 @@ func easyRSAPath() string {
|
||||
if p, ok := service.LookBin("easyrsa"); ok {
|
||||
return p
|
||||
}
|
||||
for _, p := range []string{"/usr/share/easy-rsa/easyrsa", "/usr/local/share/easy-rsa/easyrsa"} {
|
||||
if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() {
|
||||
return p
|
||||
for _, pattern := range []string{
|
||||
"/usr/share/easy-rsa/easyrsa",
|
||||
"/usr/share/easy-rsa/*/easyrsa",
|
||||
"/usr/local/share/easy-rsa/easyrsa",
|
||||
} {
|
||||
paths, _ := filepath.Glob(pattern)
|
||||
for _, p := range paths {
|
||||
if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -413,7 +480,17 @@ func easyRSAPath() string {
|
||||
func runEasyRSA(dir, binary string, args ...string) error {
|
||||
cmd := exec.Command(binary, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "EASYRSA_BATCH=1", "EASYRSA_REQ_CN=OPSX-VPN-CA")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"EASYRSA_BATCH=1",
|
||||
fmt.Sprintf("EASYRSA_CA_EXPIRE=%d", pkiExpireDays),
|
||||
fmt.Sprintf("EASYRSA_CERT_EXPIRE=%d", pkiExpireDays),
|
||||
)
|
||||
// Easy-RSA 3.1 rejects EASYRSA_REQ_CN for build-server/client-full because
|
||||
// those commands derive the common name from their positional filename.
|
||||
// It is only needed when the CA is created.
|
||||
if len(args) >= 2 && args[len(args)-2] == "build-ca" {
|
||||
cmd.Env = append(cmd.Env, "EASYRSA_REQ_CN=OPSX-VPN-CA")
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("easy-rsa %s 失败: %v: %s", strings.Join(args, " "), err, out)
|
||||
@@ -430,7 +507,8 @@ func pkiReady(t *model.Tunnel) error {
|
||||
}
|
||||
func pkiScript(t *model.Tunnel) string {
|
||||
easy := shellQuote(easyRSAPathOrDefault())
|
||||
return "#!/bin/sh\nset -eu\ncd " + shellQuote(service.InstanceDir(t)) + "\nexport EASYRSA_BATCH=1 EASYRSA_REQ_CN=OPSX-VPN-CA\n" + easy + " init-pki\n" + easy + " --batch build-ca nopass\n" + easy + " --batch build-server-full server nopass\n" + easy + " gen-dh\n" + easy + " gen-crl\nopenvpn --genkey secret ta.key\necho 'OpenVPN PKI initialized'\n"
|
||||
expires := "EASYRSA_BATCH=1 EASYRSA_CA_EXPIRE=" + strconv.Itoa(pkiExpireDays) + " EASYRSA_CERT_EXPIRE=" + strconv.Itoa(pkiExpireDays)
|
||||
return "#!/bin/sh\nset -eu\ncd " + shellQuote(service.InstanceDir(t)) + "\n" + easy + " init-pki\nEASYRSA_REQ_CN=OPSX-VPN-CA " + expires + " " + easy + " --batch build-ca nopass\n" + expires + " " + easy + " --batch build-server-full server nopass\n" + expires + " " + easy + " gen-dh\n" + expires + " " + easy + " gen-crl\nopenvpn --genkey secret ta.key\necho 'OpenVPN PKI initialized'\n"
|
||||
}
|
||||
func easyRSAPathOrDefault() string {
|
||||
if p := easyRSAPath(); p != "" {
|
||||
@@ -560,7 +638,8 @@ func (s *Service) GenerateUserConfig(t *model.Tunnel, user *model.VPNUser) (stri
|
||||
}
|
||||
}
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "client\ndev tun\nproto %s\nremote %s %d\nresolv-retry infinite\nnobind\npersist-key\npersist-tun\nremote-cert-tls server\ndata-ciphers %s:AES-128-GCM\nverb 3\n", clientProto(service.StrOr(cfg, "proto", "udp")), service.StrOr(cfg, "public_endpoint", "your-server-ip"), int(service.Num(cfg, "port", 1194)), service.StrOr(cfg, "cipher", "AES-256-GCM"))
|
||||
cipher := service.StrOr(cfg, "cipher", "AES-256-GCM")
|
||||
fmt.Fprintf(&b, "client\ndev tun\nproto %s\nremote %s %d\nresolv-retry infinite\nnobind\npersist-tun\nremote-cert-tls server\ndata-ciphers %s:AES-128-GCM\ndata-ciphers-fallback %s\nverb 3\n", clientProto(service.StrOr(cfg, "proto", "udp")), service.StrOr(cfg, "public_endpoint", "your-server-ip"), int(service.Num(cfg, "port", 1194)), cipher, cipher)
|
||||
if mode == "password" || mode == "certificate+password" {
|
||||
b.WriteString("auth-user-pass\n")
|
||||
b.WriteString("auth-nocache\n")
|
||||
@@ -596,6 +675,11 @@ func (s *Service) GenerateClientConfig(t *model.Tunnel) (string, error) {
|
||||
}
|
||||
username := service.Str(cfg, "client_username")
|
||||
if username == "" {
|
||||
// Password-only authentication has no per-user certificate. Generate a
|
||||
// reusable profile so operators can distribute it before creating users.
|
||||
if service.StrOr(cfg, "auth_mode", "certificate") == "password" {
|
||||
return s.GenerateUserConfig(t, &model.VPNUser{})
|
||||
}
|
||||
return "", fmt.Errorf("请指定 client_username 或通过拨号用户下载其专属 .ovpn 配置")
|
||||
}
|
||||
var user model.VPNUser
|
||||
@@ -609,8 +693,17 @@ func (s *Service) Start(t *model.Tunnel) error {
|
||||
if _, ok := findOpenVPNBin(); !ok {
|
||||
return fmt.Errorf("无法启动 OpenVPN:未检测到 openvpn")
|
||||
}
|
||||
cfg, err := store.TunnelConfig(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pkiReady(t); err != nil {
|
||||
return fmt.Errorf("无法启动 OpenVPN:PKI 尚未就绪:%w", err)
|
||||
if !service.Bool(cfg, "pki_auto_init", true) {
|
||||
return fmt.Errorf("无法启动 OpenVPN:PKI 尚未就绪:%w", err)
|
||||
}
|
||||
if initErr := s.InitializePKI(t); initErr != nil {
|
||||
return fmt.Errorf("无法启动 OpenVPN:自动初始化 PKI 失败: %w", initErr)
|
||||
}
|
||||
}
|
||||
if err := service.RequireRoot("OpenVPN"); err != nil {
|
||||
return err
|
||||
@@ -618,10 +711,6 @@ func (s *Service) Start(t *model.Tunnel) error {
|
||||
if err := service.RequireIPv4Forwarding("OpenVPN"); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := store.TunnelConfig(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port := int(service.Num(cfg, "port", 1194))
|
||||
if service.StrOr(cfg, "proto", "udp") == "udp" {
|
||||
if err := service.RequireUDPPortAvailable(port, "OpenVPN"); err != nil {
|
||||
@@ -630,7 +719,7 @@ func (s *Service) Start(t *model.Tunnel) error {
|
||||
} else if err := service.RequireTCPPortAvailable(port, "OpenVPN"); err != nil {
|
||||
return err
|
||||
}
|
||||
if service.Bool(cfg, "enable_nat", false) {
|
||||
if service.Bool(cfg, "enable_nat", true) {
|
||||
if err := s.installNAT(t, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -647,7 +736,7 @@ func (s *Service) Stop(t *model.Tunnel) error {
|
||||
if err := service.KillPid(t); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
if cfg, err := store.TunnelConfig(t); err == nil && service.Bool(cfg, "enable_nat", false) {
|
||||
if cfg, err := store.TunnelConfig(t); err == nil && service.Bool(cfg, "enable_nat", true) {
|
||||
if err := s.removeNAT(t, cfg); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
@@ -677,8 +766,8 @@ func (s *Service) installNAT(t *model.Tunnel, cfg map[string]interface{}) error
|
||||
comment := fmt.Sprintf("opsx-vpn-openvpn-%d", t.ID)
|
||||
rules := [][]string{
|
||||
{"-t", "nat", "-A", "POSTROUTING", "-s", subnet, "-o", egress, "-m", "comment", "--comment", comment, "-j", "MASQUERADE"},
|
||||
{"-A", "FORWARD", "-s", subnet, "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
{"-A", "FORWARD", "-d", subnet, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
{"-A", "FORWARD", "-i", "tun+", "-o", egress, "-s", subnet, "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
{"-A", "FORWARD", "-i", egress, "-o", "tun+", "-d", subnet, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
}
|
||||
for _, rule := range rules {
|
||||
check := append([]string{}, rule...)
|
||||
@@ -718,6 +807,9 @@ func (s *Service) removeNAT(t *model.Tunnel, cfg map[string]interface{}) error {
|
||||
comment := fmt.Sprintf("opsx-vpn-openvpn-%d", t.ID)
|
||||
candidates := [][]string{
|
||||
{"-t", "nat", "-D", "POSTROUTING", "-s", subnet, "-o", egress, "-m", "comment", "--comment", comment, "-j", "MASQUERADE"},
|
||||
{"-D", "FORWARD", "-i", "tun+", "-o", egress, "-s", subnet, "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
{"-D", "FORWARD", "-i", egress, "-o", "tun+", "-d", subnet, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
// Rules produced by releases before interface-scoped forwarding.
|
||||
{"-D", "FORWARD", "-s", subnet, "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
{"-D", "FORWARD", "-d", subnet, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-m", "comment", "--comment", comment, "-j", "ACCEPT"},
|
||||
}
|
||||
@@ -775,7 +867,7 @@ func (s *Service) collectTraffic(t *model.Tunnel) (int64, int64) {
|
||||
// 收集在线客户端信息(供 sessionmgr 同步会话)
|
||||
var onlineClients []sessionmgr.ClientInfo
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
f := strings.Split(line, ",")
|
||||
f := splitStatusFields(line)
|
||||
if len(f) < 7 || f[0] != "CLIENT_LIST" {
|
||||
continue
|
||||
}
|
||||
@@ -821,6 +913,16 @@ func (s *Service) collectTraffic(t *model.Tunnel) (int64, int64) {
|
||||
sessionmgr.SyncSessions(t.ID, "openvpn", onlineClients)
|
||||
return totalRx, totalTx
|
||||
}
|
||||
|
||||
// splitStatusFields supports OpenVPN status-version 3 (tab-separated) and
|
||||
// legacy comma-separated status files.
|
||||
func splitStatusFields(line string) []string {
|
||||
if strings.Contains(line, "\t") {
|
||||
return strings.Split(line, "\t")
|
||||
}
|
||||
return strings.Split(line, ",")
|
||||
}
|
||||
|
||||
func (s *Service) Delete(t *model.Tunnel) error { return s.Stop(t) }
|
||||
|
||||
// RunAuthHelper is called by the same opsx-vpn binary from OpenVPN's
|
||||
|
||||
@@ -16,9 +16,12 @@ func TestValidateOpenVPNModes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg["proto"] != "udp" || cfg["port"] != 1194 {
|
||||
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")
|
||||
}
|
||||
@@ -31,7 +34,7 @@ func TestValidateOpenVPNModes(t *testing.T) {
|
||||
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": "192.168.1.0/24",
|
||||
"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)
|
||||
@@ -39,6 +42,12 @@ func TestValidateOpenVPNModes(t *testing.T) {
|
||||
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",
|
||||
@@ -102,6 +111,7 @@ func TestServerConfigIncludesProductionTunings(t *testing.T) {
|
||||
"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)
|
||||
@@ -110,6 +120,11 @@ func TestServerConfigIncludesProductionTunings(t *testing.T) {
|
||||
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) {
|
||||
@@ -134,3 +149,26 @@ func TestCertificateCNIsSafeAndStable(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user