From ae02b60d670c81344c83cd040b37b3a78b176fa1 Mon Sep 17 00:00:00 2001 From: cnbugs Date: Sun, 9 Aug 2026 22:17:29 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20OpenVPN=20start:=20CIDR=E2=86=92netmask,?= =?UTF-8?q?=20dhcp-option=20DNS,=20ccd=20dir,=20dh=20none=20ECDHE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four bugs prevented OpenVPN from actually listening on its UDP port: 1. server.conf line 'server 10.8.0.0/24' — OpenVPN 2.5 rejects CIDR. Fix: cidrToServerDirective() converts CIDR to 'NETWORK NETMASK' (e.g. '10.8.0.0 255.255.255.0') using net.ParseCIDR. 2. push_dns '1.1.1.1 8.8.8.8' was emitted as a single push directive instead of multiple 'push dhcp-option DNS x' lines. Fix: split space-separated IPs into individual push directives; pass through if already 'dhcp-option ...' form. 3. client-config-dir referenced a ccd/ directory that was never created. Fix: CreateInstance now MkdirAll(ccd); WriteServerConf also defensively MkdirAll(extraDir) before writing the directive. 4. dh.pem generated with 1024-bit DH params → OpenSSL 3.0 refuses with 'dh key too small'. Fix: use 'dh none' in server.conf so OpenVPN 2.4+ uses ECDHE key exchange — no DH params needed at all. Removed the slow openssl dhparam generation from EnsureCA. Verified: instance starts, OpenVPN parses config successfully, now fails only at TUNSETIFF (expected: requires root/CAP_NET_ADMIN). --- backend/internal/service/service.go | 3 +++ backend/pkg/openvpn/manager.go | 39 ++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go index 565b08c..9161b44 100644 --- a/backend/internal/service/service.go +++ b/backend/internal/service/service.go @@ -117,6 +117,9 @@ func (s *Service) CreateInstance(in model.Instance) (*model.Instance, error) { if err := os.MkdirAll(filepath.Join(s.Cfg.InstanceDir(in.Name), "logs"), 0o755); err != nil { return nil, err } + if err := os.MkdirAll(filepath.Join(s.Cfg.InstanceDir(in.Name), "ccd"), 0o755); err != nil { + return nil, err + } // 签发服务端证书 if err := s.Ovm.IssueServerCert(in.Name); err != nil { return nil, fmt.Errorf("issue server cert: %w", err) diff --git a/backend/pkg/openvpn/manager.go b/backend/pkg/openvpn/manager.go index 7942d91..e1de918 100644 --- a/backend/pkg/openvpn/manager.go +++ b/backend/pkg/openvpn/manager.go @@ -64,12 +64,8 @@ func (m *Manager) EnsureCA() error { -days 3650 -subj "/CN=OpenVPN-Manager-CA" 2>/dev/null`); err != nil { return fmt.Errorf("generate CA: %w", err) } - // DH 参数(轻量: 1024,生产可改为 2048/4096) - if _, err := os.Stat(filepath.Join(m.pkiDir, "dh.pem")); os.IsNotExist(err) { - if err := runShell(`openssl dhparam -out "` + m.pkiDir + `/dh.pem" 1024 2>/dev/null`); err != nil { - return fmt.Errorf("generate DH: %w", err) - } - } + // DH 参数:用 "dh none" 走 ECDHE,不再需要 dh.pem 文件。 + // (保留目录以兼容旧配置) // TLS-Auth key if _, err := os.Stat(filepath.Join(m.pkiDir, "ta.key")); os.IsNotExist(err) { if err := runShell(`openvpn --genkey secret "` + m.pkiDir + `/ta.key"`); err != nil { @@ -93,10 +89,12 @@ func (m *Manager) WriteServerConf(in *model.Instance, extraDir string) error { conf.WriteString("ca " + filepath.Join(m.pkiDir, "ca.crt") + "\n") conf.WriteString("cert " + filepath.Join(m.PKIPath(in.Name), "issued", "server.crt") + "\n") conf.WriteString("key " + filepath.Join(m.PKIPath(in.Name), "private", "server.key") + "\n") - conf.WriteString("dh " + filepath.Join(m.pkiDir, "dh.pem") + "\n") + // 用 "dh none" 让 OpenVPN 走 ECDHE 密钥交换(OpenVPN 2.4+), + // 不需要 DH 参数文件,既安全又避免 openssl dhparam 2048 的长等待。 + conf.WriteString("dh none\n") conf.WriteString("tls-auth " + filepath.Join(m.pkiDir, "ta.key") + " 0\n") conf.WriteString("topology subnet\n") - conf.WriteString("server " + in.Subnet + "\n") + conf.WriteString("server " + cidrToServerDirective(in.Subnet) + "\n") conf.WriteString("ifconfig-pool-persist " + filepath.Join(dir, "ipp.txt") + "\n") conf.WriteString("keepalive 10 120\n") conf.WriteString("persist-key\npersist-tun\n") @@ -112,7 +110,14 @@ func (m *Manager) WriteServerConf(in *model.Instance, extraDir string) error { if line == "" { continue } - conf.WriteString("push \"" + line + "\"\n") + // 支持 "1.1.1.1 8.8.8.8" 或 "dhcp-option DNS 1.1.1.1" 两种写法 + if strings.HasPrefix(line, "dhcp-option") { + conf.WriteString("push \"" + line + "\"\n") + } else { + for _, ip := range strings.Fields(line) { + conf.WriteString("push \"dhcp-option DNS " + ip + "\"\n") + } + } } } if in.PushRoutes != "" { @@ -125,6 +130,7 @@ func (m *Manager) WriteServerConf(in *model.Instance, extraDir string) error { } } if extraDir != "" { + _ = os.MkdirAll(extraDir, 0o755) conf.WriteString("client-config-dir " + extraDir + "\n") } if in.AccessMode == model.AccessWhitelist { @@ -562,6 +568,21 @@ func atoi64(s string) int64 { return n } +// cidrToServerDirective 把 "10.8.0.0/24" 转成 OpenVPN server 指令需要的 +// "10.8.0.0 255.255.255.0" 格式(网络地址 + 点分掩码)。 +// 如果输入不含 "/"(已经是 "ip mask" 形式),原样返回。 +func cidrToServerDirective(subnet string) string { + subnet = strings.TrimSpace(subnet) + if !strings.Contains(subnet, "/") { + return subnet + } + _, ipnet, err := net.ParseCIDR(subnet) + if err != nil { + return subnet // 让 OpenVPN 报错,总比生成错误格式好 + } + return ipnet.IP.String() + " " + net.IP(ipnet.Mask).String() +} + func pemTrim(b []byte) string { return strings.TrimSpace(string(b)) } \ No newline at end of file