在这篇博客中,我们将探讨如何使用 Go 语言构建一个自动更新 DNSPod DNS 记录的工具,主要用于动态 IPv6 地址的 DDNS 更新。
该工具的主要功能包括:
自动检测本地 IPv6 地址:工具会定期检查本地网络接口,获取当前的 IPv6 地址。
自动更新 DNSPod DNS 记录:当检测到 IPv6 地址变化时,工具会自动更新 DNSPod 上的 DNS 记录。
错误重试机制:在更新 DNS 记录失败时,工具会使用指数退避算法进行重试。
邮件通知功能:当连续多次更新失败时,工具会发送邮件通知管理员。
健康检查:记录成功和失败的更新尝试,以便进行健康状态监控。
反向代理:支持 HTTP 和 HTTPS 反向代理功能。
配置文件 config.yaml 用于存储工具的配置信息,包括腾讯云的凭证、域名信息、检查间隔、邮件通知设置和代理设置。
tencent:
secretId: "xxxxxxxxxxxxxxx"
secretKey: "xxxxxxxxxxxxxxxxxx"
domain:
domain: "xxxxx.com"
subDomain: "xxx"
checkInterval: 600
email:
smtpServer: "smtp.example.com"
smtpPort: 587
username: "your-email@example.com"
password: "your-email-password"
recipient: "recipient@example.com"
proxy:
enableHTTP: true
httpListenAddr: ":80"
httpTargetAddr: "http://localhost:82"
enableHTTPS: false
httpsListenAddr: ":443"
httpsTargetAddr: "http://localhost:8443"
certFile: "/path/to/cert.pem"
keyFile: "/path/to/key.pem"
主程序负责初始化组件、读取配置文件、检查 IPv6 连接、更新 DNS 记录,并在失败时发送通知。
func main() {
// 初始化组件
cache := dns.NewDNSCache()
healthCheck := health.NewHealthCheck()
// 读取配置文件
cfg, err := config.LoadConfig()
if err != nil {
logrus.Fatalf("Failed to load config: %v", err)
}
// 判断是否需要启动 HTTP 反向代理
if cfg.Proxy.EnableHTTP {
go proxy.StartReverseProxy(cfg.Proxy.HTTPListenAddr, cfg.Proxy.HTTPTargetAddr)
}
// 判断是否需要启动 HTTPS 反向代理
if cfg.Proxy.EnableHTTPS {
go proxy.StartReverseProxyTLS(cfg.Proxy.HTTPSListenAddr, cfg.Proxy.HTTPSTargetAddr, cfg.Proxy.CertFile, cfg.Proxy.KeyFile)
}
// 创建腾讯云客户端
logrus.Println("Creating Tencent Cloud client...")
credential := common.NewCredential(
cfg.Tencent.SecretId,
cfg.Tencent.SecretKey,
)
cpf := profile.NewClientProfile()
client, err := dnspod.NewClient(credential, "ap-guangzhou", cpf)
if err != nil {
logrus.Fatalf("Failed to create DNSPod client: %v", err)
}
logrus.Println("Tencent Cloud client created successfully.")
logrus.Printf("Starting IPv6 DDNS service...")
// 检查IPv6连接
if !checkIPv6Connectivity() {
logrus.Println("IPv6 connectivity check failed, sending notification...")
notification.SendNotification(cfg.Email,
"IPv6 DDNS 更新失败",
"无法连接到公共 IPv6 地址")
}
// 定期检查并更新IP
for {
logrus.Println("Checking local IPv6 address...")
ipv6, err := iputil.GetLocalIPv6()
if err != nil {
logrus.Printf("Failed to get IPv6 address: %v", err)
if healthCheck.RecordError() >= 3 {
logrus.Println("Error threshold reached, sending notification...")
notification.SendNotification(cfg.Email,
"IPv6 DDNS 更新失败",
fmt.Sprintf("获取IPv6地址失败: %v", err))
}
time.Sleep(time.Duration(cfg.CheckInterval) * time.Second)
continue
}
logrus.Printf("Local IPv6 address: %s", ipv6)
// 检查缓存,避免重复更新
cachedIP, _ := cache.GetIP()
if cachedIP == ipv6 {
logrus.Printf("IP未变化,跳过更新")
time.Sleep(time.Duration(cfg.CheckInterval) * time.Second)
continue
}
logrus.Println("Updating DNS record...")
// 使用重试机制更新DNS记录
err = dns.UpdateDNSRecordWithRetry(client, *cfg, ipv6)
if err != nil {
logrus.Printf("Failed to update DNS record: %v", err)
if healthCheck.RecordError() >= 3 {
logrus.Println("Error threshold reached, sending notification...")
notification.SendNotification(cfg.Email,
"IPv6 DDNS 更新失败",
fmt.Sprintf("更新DNS记录失败: %v", err))
}
} else {
logrus.Printf("Successfully updated DNS record: %s.%s -> %s",
cfg.Domain.SubDomain, cfg.Domain.Domain, ipv6)
cache.UpdateIP(ipv6)
healthCheck.RecordSuccess()
}
time.Sleep(time.Duration(cfg.CheckInterval) * time.Second)
}
}
使用腾讯云的 SDK 更新 DNS 记录,并实现了重试机制以提高可靠性。
// UpdateDNSRecord 更新域名解析记录
func UpdateDNSRecord(client *dnspod.Client, config config.Config, ipv6 string) error {
// 获取记录列表以找到需要更新的记录ID
listRequest := dnspod.NewDescribeRecordListRequest()
listRequest.Domain = common.StringPtr(config.Domain.Domain)
listRequest.Subdomain = common.StringPtr(config.Domain.SubDomain)
listResponse, err := client.DescribeRecordList(listRequest)
if err != nil {
return err
}
var recordID *uint64
for _, record := range listResponse.Response.RecordList {
if *record.Type == "AAAA" && *record.Name == config.Domain.SubDomain {
recordID = record.RecordId
break
}
}
if recordID == nil {
return fmt.Errorf("no matching AAAA record found for subdomain %s", config.Domain.SubDomain)
}
// 更新记录
modifyRequest := dnspod.NewModifyRecordRequest()
modifyRequest.Domain = common.StringPtr(config.Domain.Domain)
modifyRequest.RecordId = recordID
modifyRequest.SubDomain = common.StringPtr(config.Domain.SubDomain)
modifyRequest.RecordType = common.StringPtr("AAAA")
modifyRequest.RecordLine = common.StringPtr("默认")
modifyRequest.Value = common.StringPtr(ipv6)
_, err = client.ModifyRecord(modifyRequest)
return err
}
// UpdateDNSRecordWithRetry 添加重试机制的更新函数
func UpdateDNSRecordWithRetry(client *dnspod.Client, config config.Config, ipv6 string) error {
operation := func() error {
return UpdateDNSRecord(client, config, ipv6)
}
backoffConfig := backoff.NewExponentialBackOff()
backoffConfig.MaxElapsedTime = 5 * time.Minute
return backoff.Retry(operation, backoffConfig)
}
在更新失败时,工具会发送邮件通知。
// SendNotification 发送邮件通知
func SendNotification(emailCfg config.Email, subject, body string) error {
auth := smtp.PlainAuth("", emailCfg.Username, emailCfg.Password, emailCfg.SMTPServer)
msg := fmt.Sprintf("From: %s\r\n"+
"To: %s\r\n"+
"Subject: %s\r\n"+
"\r\n"+
"%s\r\n", emailCfg.Username, emailCfg.Recipient, subject, body)
err := smtp.SendMail(
fmt.Sprintf("%s:%d", emailCfg.SMTPServer, emailCfg.SMTPPort),
auth,
emailCfg.Username,
[]string{emailCfg.Recipient},
[]byte(msg),
)
return err
}
通过这篇博客,我们了解了如何使用 Go 语言构建一个功能齐全的 IPv6 动态 DNS 更新工具。该工具不仅可以自动更新 DNS 记录,还具备错误重试和邮件通知功能,确保在网络环境变化时能够及时响应。