Fix OpenVPN start: CIDR→netmask, dhcp-option DNS, ccd dir, dh none ECDHE

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).
This commit is contained in:
cnbugs
2026-08-09 22:17:29 +08:00
parent 09f6918aeb
commit ae02b60d67
2 changed files with 33 additions and 9 deletions
+3
View File
@@ -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 { if err := os.MkdirAll(filepath.Join(s.Cfg.InstanceDir(in.Name), "logs"), 0o755); err != nil {
return nil, err 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 { if err := s.Ovm.IssueServerCert(in.Name); err != nil {
return nil, fmt.Errorf("issue server cert: %w", err) return nil, fmt.Errorf("issue server cert: %w", err)
+29 -8
View File
@@ -64,12 +64,8 @@ func (m *Manager) EnsureCA() error {
-days 3650 -subj "/CN=OpenVPN-Manager-CA" 2>/dev/null`); err != nil { -days 3650 -subj "/CN=OpenVPN-Manager-CA" 2>/dev/null`); err != nil {
return fmt.Errorf("generate CA: %w", err) return fmt.Errorf("generate CA: %w", err)
} }
// DH 参数(轻量: 1024,生产可改为 2048/4096 // DH 参数:用 "dh none" 走 ECDHE,不再需要 dh.pem 文件。
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)
}
}
// TLS-Auth key // TLS-Auth key
if _, err := os.Stat(filepath.Join(m.pkiDir, "ta.key")); os.IsNotExist(err) { 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 { 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("ca " + filepath.Join(m.pkiDir, "ca.crt") + "\n")
conf.WriteString("cert " + filepath.Join(m.PKIPath(in.Name), "issued", "server.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("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("tls-auth " + filepath.Join(m.pkiDir, "ta.key") + " 0\n")
conf.WriteString("topology subnet\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("ifconfig-pool-persist " + filepath.Join(dir, "ipp.txt") + "\n")
conf.WriteString("keepalive 10 120\n") conf.WriteString("keepalive 10 120\n")
conf.WriteString("persist-key\npersist-tun\n") conf.WriteString("persist-key\npersist-tun\n")
@@ -112,7 +110,14 @@ func (m *Manager) WriteServerConf(in *model.Instance, extraDir string) error {
if line == "" { if line == "" {
continue continue
} }
// 支持 "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") conf.WriteString("push \"" + line + "\"\n")
} else {
for _, ip := range strings.Fields(line) {
conf.WriteString("push \"dhcp-option DNS " + ip + "\"\n")
}
}
} }
} }
if in.PushRoutes != "" { if in.PushRoutes != "" {
@@ -125,6 +130,7 @@ func (m *Manager) WriteServerConf(in *model.Instance, extraDir string) error {
} }
} }
if extraDir != "" { if extraDir != "" {
_ = os.MkdirAll(extraDir, 0o755)
conf.WriteString("client-config-dir " + extraDir + "\n") conf.WriteString("client-config-dir " + extraDir + "\n")
} }
if in.AccessMode == model.AccessWhitelist { if in.AccessMode == model.AccessWhitelist {
@@ -562,6 +568,21 @@ func atoi64(s string) int64 {
return n 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 { func pemTrim(b []byte) string {
return strings.TrimSpace(string(b)) return strings.TrimSpace(string(b))
} }