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>
This commit is contained in:
@@ -314,19 +314,51 @@ func (s *SingBox) RenderConfig() ([]byte, error) {
|
||||
|
||||
// writeAndRestart renders, writes the config file and restarts sing-box.
|
||||
func (s *SingBox) writeAndRestart(ctx context.Context) error {
|
||||
if err := os.MkdirAll(filepath.Dir(s.cfg.SingboxConfigPath), 0o755); err != nil {
|
||||
return fmt.Errorf("agentd: mkdir singbox cfg: %w", err)
|
||||
}
|
||||
// Hy2 启用时:渲染前确保自签 TLS 证书就位,并把路径写回 hy2 配置(方案①)。
|
||||
if err := s.ensureHy2Cert(); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := s.RenderConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(s.cfg.SingboxConfigPath), 0o755); err != nil {
|
||||
return fmt.Errorf("agentd: mkdir singbox cfg: %w", err)
|
||||
}
|
||||
if err := atomicWrite(s.cfg.SingboxConfigPath, data, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.restarter.Restart(ctx)
|
||||
}
|
||||
|
||||
// ensureHy2Cert 为启用 hy2 的节点准备自签证书(幂等),并把 CertPath/KeyPath 写回当前
|
||||
// hy2 配置,供 RenderConfig 渲染 hysteria2 入站的 certificate_path/key_path。
|
||||
func (s *SingBox) ensureHy2Cert() error {
|
||||
s.mu.Lock()
|
||||
enabled := s.hy2 != nil
|
||||
var sni string
|
||||
if s.reality != nil {
|
||||
sni = s.reality.ServerName
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
dir := filepath.Dir(s.cfg.SingboxConfigPath)
|
||||
certPath := filepath.Join(dir, "hy2.crt")
|
||||
keyPath := filepath.Join(dir, "hy2.key")
|
||||
if err := ensureSelfSignedCert(certPath, keyPath, sni); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.hy2 != nil {
|
||||
s.hy2.CertPath = certPath
|
||||
s.hy2.KeyPath = keyPath
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// markDirty signals the debounce loop that a render is pending (non-blocking).
|
||||
func (s *SingBox) markDirty() {
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user