da8f3a4fef
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>
61 lines
1.5 KiB
Go
61 lines
1.5 KiB
Go
package agentd
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestEnsureSelfSignedCert(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cert := filepath.Join(dir, "hy2.crt")
|
|
key := filepath.Join(dir, "hy2.key")
|
|
|
|
if err := ensureSelfSignedCert(cert, key, "www.apple.com"); err != nil {
|
|
t.Fatalf("generate: %v", err)
|
|
}
|
|
|
|
// 必须是 sing-box(crypto/tls)能加载的有效 keypair。
|
|
pair, err := tls.LoadX509KeyPair(cert, key)
|
|
if err != nil {
|
|
t.Fatalf("LoadX509KeyPair: %v", err)
|
|
}
|
|
leaf, err := x509.ParseCertificate(pair.Certificate[0])
|
|
if err != nil {
|
|
t.Fatalf("parse cert: %v", err)
|
|
}
|
|
if leaf.Subject.CommonName != "pangolin-hy2" {
|
|
t.Errorf("CN = %q, want pangolin-hy2", leaf.Subject.CommonName)
|
|
}
|
|
if len(leaf.DNSNames) != 1 || leaf.DNSNames[0] != "www.apple.com" {
|
|
t.Errorf("SAN = %v, want [www.apple.com]", leaf.DNSNames)
|
|
}
|
|
|
|
// key 文件权限 0600。
|
|
info, err := os.Stat(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if perm := info.Mode().Perm(); perm != 0o600 {
|
|
t.Errorf("key perm = %o, want 600", perm)
|
|
}
|
|
|
|
// 幂等:第二次调用不应重写(内容不变)。
|
|
before, _ := os.ReadFile(cert)
|
|
if err := ensureSelfSignedCert(cert, key, "www.apple.com"); err != nil {
|
|
t.Fatalf("second call: %v", err)
|
|
}
|
|
after, _ := os.ReadFile(cert)
|
|
if string(before) != string(after) {
|
|
t.Error("cert regenerated on second call; expected idempotent")
|
|
}
|
|
|
|
// cert 是 PEM CERTIFICATE 块。
|
|
if blk, _ := pem.Decode(after); blk == nil || blk.Type != "CERTIFICATE" {
|
|
t.Error("cert is not a PEM CERTIFICATE block")
|
|
}
|
|
}
|