- handler/static.go: add sync.RWMutex to StaticHandler; protect Handle with RLock and all setters with Lock to prevent data races - http2/server.go: delete empty connection slice keys from pool map to prevent memory leak under high client churn - loadbalance/slow_start.go: recreate stopCh in Start() to support Start-Stop-Start cycles; guard double-close in Stop() - resolver/resolver.go: recreate stopCh in Start() to support restart - logging/logging.go: save *os.File handles from getOutput so Close() actually closes log files; exclude os.Stdout/os.Stderr from closing - ssl/session_tickets.go: protect started/rotateTimer access in scheduleRotation with mu; support Start-Stop-Start cycles - ssl/ssl.go: cache parsed default certificate to avoid re-parsing on every TLS handshake for OCSP stapling
477 lines
11 KiB
Go
477 lines
11 KiB
Go
// Package resolver 提供 DNS 解析功能,支持缓存和后台刷新。
|
||
//
|
||
// 该包实现了带缓存的 DNS 解析器,用于动态解析后端服务域名。
|
||
// 支持 UDP DNS 查询、TTL 缓存、后台刷新等特性。
|
||
//
|
||
// 主要用途:
|
||
//
|
||
// 用于代理模块动态解析 upstream 域名,支持域名变更自动感知
|
||
//
|
||
// 注意事项:
|
||
// - 解析器使用 sync.Map 实现并发安全的缓存
|
||
// - 后台刷新协程需要调用 Start() 启动
|
||
// - 停止使用时应调用 Stop() 释放资源
|
||
//
|
||
// 作者:xfy
|
||
package resolver
|
||
|
||
import (
|
||
"container/list"
|
||
"context"
|
||
"fmt"
|
||
"net"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
)
|
||
|
||
// Resolver DNS 解析器接口。
|
||
type Resolver interface {
|
||
// LookupHost 解析主机名,返回 IP 地址列表
|
||
LookupHost(ctx context.Context, host string) ([]string, error)
|
||
|
||
// LookupHostWithCache 带缓存的解析,优先返回缓存结果
|
||
LookupHostWithCache(ctx context.Context, host string) ([]string, error)
|
||
|
||
// Refresh 刷新指定主机的缓存
|
||
Refresh(host string) error
|
||
|
||
// Start 启动后台刷新协程
|
||
Start() error
|
||
|
||
// Stop 停止解析器
|
||
Stop() error
|
||
|
||
// Stats 返回统计信息
|
||
Stats() Stats
|
||
}
|
||
|
||
// Stats 解析器统计信息。
|
||
type Stats struct {
|
||
CacheHits int64 // 缓存命中次数
|
||
CacheMisses int64 // 缓存未命中次数
|
||
CacheEntries int // 当前缓存条目数
|
||
ResolveErrors int64 // 解析错误次数
|
||
AverageLatency time.Duration // 平均解析延迟
|
||
}
|
||
|
||
// storeCache 存入缓存(带 LRU 淘汰)。
|
||
func (r *DNSResolver) storeCache(host string, entry *DNSCacheEntry) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
if elem, ok := r.lruIndex[host]; ok {
|
||
r.cache[host] = entry
|
||
r.lruList.MoveToFront(elem)
|
||
return
|
||
}
|
||
|
||
if r.config.CacheSize > 0 && len(r.cache) >= r.config.CacheSize {
|
||
r.evictLRULocked()
|
||
}
|
||
|
||
r.cache[host] = entry
|
||
elem := r.lruList.PushFront(host)
|
||
r.lruIndex[host] = elem
|
||
}
|
||
|
||
// evictLRULocked 淘汰最久未使用的条目(需持有锁)。
|
||
func (r *DNSResolver) evictLRULocked() {
|
||
oldest := r.lruList.Back()
|
||
if oldest == nil {
|
||
return
|
||
}
|
||
host, _ := oldest.Value.(string)
|
||
delete(r.cache, host)
|
||
delete(r.lruIndex, host)
|
||
r.lruList.Remove(oldest)
|
||
}
|
||
|
||
// DNSResolver 实现 Resolver 接口的 DNS 解析器。
|
||
type DNSResolver struct {
|
||
config *config.ResolverConfig
|
||
stopCh chan struct{}
|
||
refreshHosts map[string]struct{}
|
||
cache map[string]*DNSCacheEntry
|
||
lruList *list.List
|
||
lruIndex map[string]*list.Element
|
||
hits atomic.Int64
|
||
misses atomic.Int64
|
||
errors atomic.Int64
|
||
latencyNs atomic.Int64
|
||
count atomic.Int64
|
||
mu sync.RWMutex
|
||
serverIdx atomic.Uint32
|
||
started atomic.Bool
|
||
}
|
||
|
||
// DNSCacheEntry DNS 缓存条目。
|
||
type DNSCacheEntry struct {
|
||
ExpiresAt time.Time
|
||
LastLookup time.Time
|
||
Error error
|
||
IPs []string
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// New 创建新的 DNS 解析器。
|
||
//
|
||
// 该函数根据配置创建一个 DNSResolver 实例。如果配置中
|
||
// Enabled 为 false,则返回空操作的 noopResolver。
|
||
// 对于启用的解析器,会自动设置合理的默认值:
|
||
// - Valid: 30 秒(缓存有效期)
|
||
// - Timeout: 5 秒(查询超时)
|
||
// - IPv4: 默认启用(除非显式禁用)
|
||
//
|
||
// 参数:
|
||
// - cfg: 解析器配置,包含 DNS 服务器地址、缓存大小等
|
||
//
|
||
// 返回值:
|
||
// - Resolver: DNS 解析器接口实现,禁用时返回 noopResolver
|
||
func New(cfg *config.ResolverConfig) Resolver {
|
||
if cfg == nil || !cfg.Enabled {
|
||
return &noopResolver{}
|
||
}
|
||
|
||
// 设置默认值
|
||
valid := cfg.Valid
|
||
if valid == 0 {
|
||
valid = 30 * time.Second
|
||
}
|
||
timeout := cfg.Timeout
|
||
if timeout == 0 {
|
||
timeout = 5 * time.Second
|
||
}
|
||
|
||
// 创建新配置副本,应用默认值
|
||
configCopy := *cfg
|
||
configCopy.Valid = valid
|
||
configCopy.Timeout = timeout
|
||
if !configCopy.IPv4 && !configCopy.IPv6 {
|
||
configCopy.IPv4 = true
|
||
}
|
||
|
||
return &DNSResolver{
|
||
config: &configCopy,
|
||
stopCh: make(chan struct{}),
|
||
refreshHosts: make(map[string]struct{}),
|
||
cache: make(map[string]*DNSCacheEntry),
|
||
lruList: list.New(),
|
||
lruIndex: make(map[string]*list.Element),
|
||
}
|
||
}
|
||
|
||
// LookupHost 解析主机名,返回 IP 地址列表。
|
||
func (r *DNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
|
||
return r.lookup(ctx, host, false)
|
||
}
|
||
|
||
// LookupHostWithCache 带缓存的解析,优先返回缓存结果。
|
||
func (r *DNSResolver) LookupHostWithCache(ctx context.Context, host string) ([]string, error) {
|
||
return r.lookup(ctx, host, true)
|
||
}
|
||
|
||
// lookup 内部解析方法。
|
||
func (r *DNSResolver) lookup(ctx context.Context, host string, useCache bool) ([]string, error) {
|
||
// 如果 host 已经是 IP 地址,直接返回
|
||
if ip := net.ParseIP(host); ip != nil {
|
||
return []string{host}, nil
|
||
}
|
||
|
||
// 尝试从缓存获取
|
||
if useCache {
|
||
r.mu.RLock()
|
||
entry, ok := r.cache[host]
|
||
r.mu.RUnlock()
|
||
|
||
if ok {
|
||
entry.mu.RLock()
|
||
ips := entry.IPs
|
||
expiresAt := entry.ExpiresAt
|
||
cacheErr := entry.Error
|
||
entry.mu.RUnlock()
|
||
|
||
// 缓存未过期,返回缓存结果
|
||
if time.Now().Before(expiresAt) {
|
||
r.hits.Add(1)
|
||
if cacheErr != nil {
|
||
return nil, cacheErr
|
||
}
|
||
return ips, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
r.misses.Add(1)
|
||
|
||
// 执行 DNS 查询
|
||
start := time.Now()
|
||
ips, err := r.queryDNS(ctx, host)
|
||
latency := time.Since(start)
|
||
|
||
r.latencyNs.Add(latency.Nanoseconds())
|
||
r.count.Add(1)
|
||
|
||
if err != nil {
|
||
r.errors.Add(1)
|
||
}
|
||
|
||
// 更新缓存
|
||
entry := &DNSCacheEntry{
|
||
IPs: ips,
|
||
ExpiresAt: time.Now().Add(r.config.TTL()),
|
||
LastLookup: time.Now(),
|
||
Error: err,
|
||
}
|
||
r.storeCache(host, entry)
|
||
|
||
// 添加到刷新列表
|
||
r.mu.Lock()
|
||
r.refreshHosts[host] = struct{}{}
|
||
r.mu.Unlock()
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ips, nil
|
||
}
|
||
|
||
// queryDNS 执行实际的 DNS 查询。
|
||
func (r *DNSResolver) queryDNS(ctx context.Context, host string) ([]string, error) {
|
||
if len(r.config.Addresses) == 0 {
|
||
// 使用系统默认 DNS
|
||
return r.queryWithResolver(ctx, host, "")
|
||
}
|
||
|
||
// 轮询选择 DNS 服务器
|
||
idx := r.serverIdx.Add(1) % uint32(len(r.config.Addresses))
|
||
dnsServer := r.config.Addresses[idx]
|
||
|
||
return r.queryWithResolver(ctx, host, dnsServer)
|
||
}
|
||
|
||
// queryWithResolver 使用指定的 DNS 服务器查询。
|
||
func (r *DNSResolver) queryWithResolver(ctx context.Context, host, server string) ([]string, error) {
|
||
var ips []string
|
||
|
||
// 创建带超时的 context
|
||
if r.config.Timeout > 0 {
|
||
var cancel context.CancelFunc
|
||
ctx, cancel = context.WithTimeout(ctx, r.config.Timeout)
|
||
defer cancel()
|
||
}
|
||
|
||
// 创建自定义 resolver
|
||
var resolver *net.Resolver
|
||
if server != "" {
|
||
resolver = &net.Resolver{
|
||
PreferGo: true,
|
||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||
d := net.Dialer{}
|
||
return d.DialContext(ctx, "udp", server)
|
||
},
|
||
}
|
||
}
|
||
|
||
// 查询 IPv4
|
||
if r.config.IPv4 {
|
||
var ipAddrs []net.IPAddr
|
||
var err error
|
||
if resolver != nil {
|
||
ipAddrs, err = resolver.LookupIPAddr(ctx, host)
|
||
} else {
|
||
ipList, lookupErr := net.LookupIP(host)
|
||
if lookupErr != nil {
|
||
err = lookupErr
|
||
} else {
|
||
ipAddrs = make([]net.IPAddr, len(ipList))
|
||
for i, ip := range ipList {
|
||
ipAddrs[i] = net.IPAddr{IP: ip}
|
||
}
|
||
}
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("DNS lookup failed for %s: %w", host, err)
|
||
}
|
||
|
||
for _, addr := range ipAddrs {
|
||
if ip4 := addr.IP.To4(); ip4 != nil {
|
||
ips = append(ips, ip4.String())
|
||
}
|
||
}
|
||
}
|
||
|
||
// 查询 IPv6
|
||
if r.config.IPv6 {
|
||
var ipAddrs []net.IPAddr
|
||
var err error
|
||
if resolver != nil {
|
||
ipAddrs, err = resolver.LookupIPAddr(ctx, host)
|
||
} else {
|
||
ipList, lookupErr := net.LookupIP(host)
|
||
if lookupErr != nil {
|
||
err = lookupErr
|
||
} else {
|
||
ipAddrs = make([]net.IPAddr, len(ipList))
|
||
for i, ip := range ipList {
|
||
ipAddrs[i] = net.IPAddr{IP: ip}
|
||
}
|
||
}
|
||
}
|
||
if err != nil {
|
||
// IPv6 查询失败不返回错误,继续使用 IPv4 结果
|
||
_ = err
|
||
} else {
|
||
for _, addr := range ipAddrs {
|
||
if ip := addr.IP.To16(); ip != nil && ip.To4() == nil {
|
||
ips = append(ips, ip.String())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(ips) == 0 {
|
||
return nil, fmt.Errorf("no IP addresses found for %s", host)
|
||
}
|
||
|
||
return ips, nil
|
||
}
|
||
|
||
// Refresh 刷新指定主机的缓存。
|
||
func (r *DNSResolver) Refresh(host string) error {
|
||
_, err := r.LookupHost(context.Background(), host)
|
||
return err
|
||
}
|
||
|
||
// Start 启动后台刷新协程。
|
||
func (r *DNSResolver) Start() error {
|
||
if !r.config.Enabled {
|
||
return nil
|
||
}
|
||
|
||
if r.started.Load() {
|
||
return nil
|
||
}
|
||
|
||
r.started.Store(true)
|
||
|
||
// 重建 stopCh 以支持 Start-Stop-Start 周期
|
||
select {
|
||
case <-r.stopCh:
|
||
r.stopCh = make(chan struct{})
|
||
default:
|
||
}
|
||
|
||
// 启动后台刷新协程
|
||
go r.refreshLoop()
|
||
|
||
return nil
|
||
}
|
||
|
||
// refreshLoop 后台刷新循环。
|
||
func (r *DNSResolver) refreshLoop() {
|
||
// 刷新间隔为 TTL / 2
|
||
interval := max(r.config.TTL()/2, time.Second)
|
||
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
r.doRefresh()
|
||
case <-r.stopCh:
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// doRefresh 执行刷新操作。
|
||
func (r *DNSResolver) doRefresh() {
|
||
r.mu.RLock()
|
||
hosts := make([]string, 0, len(r.refreshHosts))
|
||
for host := range r.refreshHosts {
|
||
hosts = append(hosts, host)
|
||
}
|
||
r.mu.RUnlock()
|
||
|
||
for _, host := range hosts {
|
||
ctx, cancel := context.WithTimeout(context.Background(), r.config.Timeout)
|
||
_, _ = r.LookupHost(ctx, host)
|
||
cancel()
|
||
}
|
||
}
|
||
|
||
// Stop 停止解析器。
|
||
func (r *DNSResolver) Stop() error {
|
||
if !r.started.Swap(false) {
|
||
return nil
|
||
}
|
||
|
||
close(r.stopCh)
|
||
return nil
|
||
}
|
||
|
||
// Stats 返回统计信息。
|
||
func (r *DNSResolver) Stats() Stats {
|
||
hits := r.hits.Load()
|
||
misses := r.misses.Load()
|
||
|
||
// 统计缓存条目数
|
||
r.mu.RLock()
|
||
entries := len(r.cache)
|
||
r.mu.RUnlock()
|
||
|
||
// 计算平均延迟
|
||
var avgLatency time.Duration
|
||
count := r.count.Load()
|
||
if count > 0 {
|
||
avgLatency = time.Duration(r.latencyNs.Load() / count)
|
||
}
|
||
|
||
return Stats{
|
||
CacheHits: hits,
|
||
CacheMisses: misses,
|
||
CacheEntries: entries,
|
||
ResolveErrors: r.errors.Load(),
|
||
AverageLatency: avgLatency,
|
||
}
|
||
}
|
||
|
||
// noopResolver 是禁用状态下的空实现。
|
||
type noopResolver struct{}
|
||
|
||
// LookupHost 解析主机名(空实现)。
|
||
// 在禁用状态下返回错误。
|
||
func (n *noopResolver) LookupHost(_ context.Context, _ string) ([]string, error) {
|
||
return nil, fmt.Errorf("resolver is disabled")
|
||
}
|
||
|
||
// LookupHostWithCache 带缓存解析主机名(空实现)。
|
||
// 直接委托给 LookupHost。
|
||
func (n *noopResolver) LookupHostWithCache(ctx context.Context, host string) ([]string, error) {
|
||
return n.LookupHost(ctx, host)
|
||
}
|
||
|
||
// Refresh 刷新解析器缓存(空实现)。
|
||
func (n *noopResolver) Refresh(_ string) error {
|
||
return nil
|
||
}
|
||
|
||
// Start 启动解析器(空实现)。
|
||
func (n *noopResolver) Start() error {
|
||
return nil
|
||
}
|
||
|
||
// Stop 停止解析器(空实现)。
|
||
func (n *noopResolver) Stop() error {
|
||
return nil
|
||
}
|
||
|
||
// Stats 返回解析器统计信息(空实现)。
|
||
func (n *noopResolver) Stats() Stats {
|
||
return Stats{}
|
||
}
|