Files
pangolin/server/internal/agentd/hy2cert.go
T
wangjia da8f3a4fef feat(server): Hy2 自签 TLS 证书(方案①)— 补全 hysteria2 入站可用
Hy2 跑在 QUIC 上、强制 TLS,需服务端证书(不像 REALITY 借大站证书)。此前
handler_grpc 只发 ListenPort、cert 路径为空 → hysteria2 入站起不来。改为节点自签:

- agentd/hy2cert.go: ensureSelfSignedCert 生成 ECDSA P-256 自签证书到
  /etc/sing-box/hy2.{crt,key}(幂等,缺失才生成;key 0600;SAN=reality SNI)
- singbox.go: writeAndRestart 渲染前对启用 hy2 的节点 ensure 证书并把
  CertPath/KeyPath 写回 hy2 配置
- httpapi/clientconfig.go: hy2 出站 TLS 加 insecure:true + server_name,收自签
  (两端自有,服务端鉴权靠 per-user 派生的 hy2 密码)

验证:单元测试(证书可被 crypto/tls 加载/幂等/0600)+ sing-box check 接受自签 hy2 入站。
注:节点实际启用 hy2 还需配 Hy2Port + 放行 UDP(单独步骤)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:12:35 +08:00

77 lines
2.4 KiB
Go

package agentd
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"time"
)
// ensureSelfSignedCert 保证 hysteria2 入站用的自签 TLS 证书存在(方案①)。
//
// 为什么自签:Hy2 跑在 QUIC 上,QUIC 强制 TLS 1.3 → 服务端必须出示证书。它不像
// REALITY 借真实大站证书,得自备一张。两端都自有,客户端用 tls.insecure 收自签即可
// (真正的服务端鉴权靠 per-user 派生的 hy2 密码)。
//
// 幂等:两个文件都在则直接返回;否则生成一张 ECDSA P-256、10 年有效的自签证书,
// cert 0644 / key 0600 写盘。sni 写入 SAN(客户端 insecure 时仅作 ClientHello 用)。
func ensureSelfSignedCert(certPath, keyPath, sni string) error {
if fileExists(certPath) && fileExists(keyPath) {
return nil
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("agentd: hy2 cert keygen: %w", err)
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return fmt.Errorf("agentd: hy2 cert serial: %w", err)
}
tmpl := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "pangolin-hy2"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
if sni != "" {
tmpl.DNSNames = []string{sni}
}
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
if err != nil {
return fmt.Errorf("agentd: hy2 cert create: %w", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
if err := os.WriteFile(certPath, certPEM, 0o644); err != nil {
return fmt.Errorf("agentd: write hy2 cert: %w", err)
}
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return fmt.Errorf("agentd: marshal hy2 key: %w", err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
return fmt.Errorf("agentd: write hy2 key: %w", err)
}
return nil
}
func fileExists(p string) bool {
info, err := os.Stat(p)
return err == nil && !info.IsDir()
}