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:
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user