Scert/backend/internal/dnsprovider/aliyun_scope.go

139 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dnsprovider
import (
"context"
"errors"
"fmt"
"strings"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"github.com/go-acme/lego/v5/challenge/dns01"
)
const (
protectedAliyunDomain = "pcux.cn"
aliyunZoneResolutionTimeout = 2 * time.Second
)
// AliyunZoneResolver 只读识别用户域名当前的权威 DNS 托管区。
// 它绝不能枚举 RAM 账号域名,也不能触发任何阿里云写操作。
type AliyunZoneResolver interface {
Resolve(context.Context, string) (string, error)
}
type aliyunZoneResolverFunc func(context.Context, string) (string, error)
func (f aliyunZoneResolverFunc) Resolve(ctx context.Context, requested string) (string, error) {
return f(ctx, requested)
}
type authoritativeAliyunZoneResolver struct {
findZone func(context.Context, string) (string, error)
timeout time.Duration
}
func newDefaultAliyunZoneResolver() AliyunZoneResolver {
return authoritativeAliyunZoneResolver{
findZone: func(ctx context.Context, fqdn string) (string, error) {
client := dns01.NewClient(&dns01.Options{Timeout: aliyunZoneResolutionTimeout})
return client.FindZoneByFqdn(ctx, fqdn)
},
timeout: aliyunZoneResolutionTimeout,
}
}
func (r authoritativeAliyunZoneResolver) Resolve(ctx context.Context, requested string) (string, error) {
base, err := validateAliyunRequestedDomain(requested)
if err != nil {
return "", err
}
if r.findZone == nil {
return "", errors.New("阿里云权威 DNS 托管区识别器未初始化")
}
if ctx == nil {
ctx = context.Background()
}
timeout := r.timeout
if timeout <= 0 {
timeout = aliyunZoneResolutionTimeout
}
resolveCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
zone, err := r.findZone(resolveCtx, base+".")
if err != nil {
return "", fmt.Errorf("无法识别域名 %s 的权威 DNS 托管区,已停止验证:%w", requested, err)
}
return normalizeAuthorizationDomain(zone), nil
}
// ResolveAliyunTargetDomain 根据公开 DNS 的 SOA 结果识别唯一权威托管区。
// SOA 查询可能读取父标签以定位权威区,但整个过程从不向候选父域写入 TXT 记录。
func ResolveAliyunTargetDomain(requested string) (string, error) {
return resolveAliyunTargetDomain(context.Background(), requested, newDefaultAliyunZoneResolver())
}
func resolveAliyunTargetDomain(ctx context.Context, requested string, resolver AliyunZoneResolver) (string, error) {
base, err := validateAliyunRequestedDomain(requested)
if err != nil {
return "", err
}
if resolver == nil {
return "", errors.New("阿里云权威 DNS 托管区识别器未初始化")
}
if ctx == nil {
ctx = context.Background()
}
zone, err := resolver.Resolve(ctx, base)
if err != nil {
return "", err
}
zone = normalizeAuthorizationDomain(zone)
if !domain.IsValidName(zone) || strings.HasPrefix(zone, "*.") {
return "", fmt.Errorf("权威 DNS 托管区识别结果无效:%s", zone)
}
if isProtectedAliyunDomain(zone) {
return "", fmt.Errorf("域名 %s 的权威 DNS 托管区 %s 受本机保护策略限制", requested, zone)
}
if base != zone && !strings.HasSuffix(base, "."+zone) {
return "", fmt.Errorf("权威 DNS 托管区 %s 不属于待操作域名 %s,已停止验证", zone, requested)
}
return zone, nil
}
// validateAliyunRequestedDomain 仅做不访问网络的本地格式和保护策略检查。
// 这使受保护域在读取凭据、构造客户端或进行 SOA 查询前被拒绝。
func validateAliyunRequestedDomain(requested string) (string, error) {
requested = normalizeAuthorizationDomain(requested)
base := strings.TrimPrefix(requested, "*.")
if !domain.IsValidName(requested) || base == "" {
return "", errors.New("待操作域名格式无效")
}
if isProtectedAliyunDomain(base) {
return "", fmt.Errorf("域名 %s 受本机保护策略限制,未构造阿里云客户端或调用云 API", requested)
}
return base, nil
}
func isProtectedAliyunDomain(name string) bool {
name = normalizeAuthorizationDomain(strings.TrimPrefix(name, "*."))
return name == protectedAliyunDomain || strings.HasSuffix(name, "."+protectedAliyunDomain)
}
// ValidateAliyunTargetDomains 在任何凭据读取、客户端构造或云 API 调用前完成整单本地校验。
// SAN 中只要有一个保护域,整单都会被拒绝。
func ValidateAliyunTargetDomains(providerID string, domains []string) error {
if !strings.EqualFold(strings.TrimSpace(providerID), Aliyun) {
return nil
}
if len(domains) == 0 {
return errors.New("至少需要一个待操作域名")
}
for _, name := range domains {
if _, err := validateAliyunRequestedDomain(name); err != nil {
return err
}
}
return nil
}