lolly/internal/proxy/proxy_dns.go

196 lines
4.9 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 proxy 反向代理包,为 Lolly HTTP 服务器提供反向代理功能。
//
// 该文件实现 DNS 动态解析和刷新功能,支持后端目标的域名自动解析、
// IP 缓存、定时刷新和故障恢复。
//
// 主要功能:
// - DNS 解析器集成:支持自定义 resolver 实现域名解析
// - 定时刷新循环:根据 TTL 自动刷新已解析目标的 IP 地址
// - 连接池同步更新DNS 解析结果自动同步到 HostClient 连接池
// - 统计信息查询:暴露 DNS 解析器的运行统计数据
//
// 注意事项:
// - 所有公开方法均为并发安全
// - DNS 刷新在后台 goroutine 中运行,通过 stopCh 控制生命周期
//
// 作者xfy
package proxy
import (
"context"
"fmt"
"net"
"net/url"
"time"
"rua.plus/lolly/internal/loadbalance"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/resolver"
)
// SetResolver 设置 DNS 解析器。
//
// 该方法为代理实例配置自定义的 DNS 解析器,用于动态解析后端目标的域名。
// 必须在调用 Start() 之前设置,否则 DNS 刷新循环不会启动。
//
// 参数:
// - r: DNS 解析器实例,需实现 resolver.Resolver 接口
func (p *Proxy) SetResolver(r resolver.Resolver) {
p.mu.Lock()
defer p.mu.Unlock()
p.resolver = r
}
// Start 启动代理服务,包括 DNS 刷新循环。
//
// 该方法标记代理为已启动状态,如果配置了 resolver则启动解析器并
// 在后台 goroutine 中启动 DNS 定时刷新循环。该方法是幂等的,
// 重复调用不会重复启动。
//
// 返回值:
// - error: 启动 resolver 失败时返回非 nil 错误
func (p *Proxy) Start() error {
if p.started.Load() {
return nil
}
p.started.Store(true)
// 启动 DNS 刷新循环(如果配置了 resolver
if p.resolver != nil {
if err := p.resolver.Start(); err != nil {
return fmt.Errorf("failed to start resolver: %w", err)
}
go p.startDNSRefreshLoop()
}
return nil
}
// startDNSRefreshLoop 启动 DNS 刷新后台循环。
//
// 根据 resolver 的 TTL 计算刷新间隔TTL / 2最小 1 秒),
// 定时调用 refreshDNS 刷新所有需要解析的目标。
// 该方法阻塞运行,直到收到 stopCh 信号。
func (p *Proxy) startDNSRefreshLoop() {
if p.resolver == nil {
return
}
ttl := p.getResolverTTL()
if ttl == 0 {
ttl = 30 * time.Second
}
// 刷新间隔为 TTL / 2
interval := max(ttl/2, time.Second)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.refreshDNS()
case <-p.stopCh:
return
}
}
}
// refreshDNS 刷新所有需要解析的目标。
//
// 遍历所有后端目标,对超过 TTL 的域名执行 DNS 查询,
// 更新目标的已解析 IP 列表,并同步更新对应 HostClient 的地址。
func (p *Proxy) refreshDNS() {
if p.resolver == nil {
return
}
ttl := p.getResolverTTL()
p.mu.RLock()
targets := p.targets
p.mu.RUnlock()
for _, target := range targets {
if !target.NeedsResolve(ttl) {
continue
}
hostname := target.Hostname()
if hostname == "" {
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ips, err := p.resolver.LookupHostWithCache(ctx, hostname)
cancel()
if err != nil {
logging.Debug().Msgf("DNS refresh failed for %s: %v", hostname, err)
continue
}
if len(ips) > 0 {
target.SetResolvedIPs(ips)
p.updateHostClientAddr(target, ips[0])
}
}
}
// updateHostClientAddr 更新 HostClient 的连接地址。
//
// 从目标 URL 中解析出端口,与新的 IP 地址组合后更新对应
// HostClient 的 Addr 字段。旧连接不受影响,新连接将使用新地址。
//
// 参数:
// - target: 负载均衡目标,包含原始 URL
// - ip: 新解析出的 IP 地址
func (p *Proxy) updateHostClientAddr(target *loadbalance.Target, ip string) {
p.mu.Lock()
defer p.mu.Unlock()
// 从 URL 解析出端口
u, err := url.Parse(target.URL)
if err != nil {
return
}
_, port, err := net.SplitHostPort(u.Host)
if err != nil {
// 没有端口,使用默认端口
if u.Scheme == "https" {
port = "443"
} else {
port = "80"
}
}
newAddr := net.JoinHostPort(ip, port)
// 更新 HostClient 的 Addr
// 注意:新连接将使用新 IP旧连接继续使用旧 IP 直到超时
if client, ok := p.clients[target.URL]; ok {
client.Addr = newAddr
logging.Debug().Msgf("Updated HostClient addr for %s to %s", target.URL, newAddr)
}
}
// getResolverTTL 获取 DNS 解析记录的过期时间。
//
// 返回 resolver 的 TTL 配置,默认值为 30 秒。
// 该方法当前返回固定值,未来可从 resolver stats 中动态推断。
//
// 返回值:
// - time.Duration: DNS 记录的有效期
func (p *Proxy) getResolverTTL() time.Duration {
if p.resolver == nil {
return 0
}
// 从 stats 中推断 TTL如果实现了相应接口
// 这里返回默认值
return 30 * time.Second
}