Matuto的博客

Matuto的博客

马图图

岁月变迁何必不悔,尘世喧嚣怎能无愧。

25 文章数
1 评论数
Go

使用 Go 实现 IPv6 动态 DNS 更新工具

马图图
2024-12-13 / 0 评论 / 119 阅读 / 72 点赞

使用 Go 实现 IPv6 动态 DNS 更新工具

在这篇博客中,我们将探讨如何使用 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)
	}
}

DNS 更新

使用腾讯云的 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 记录,还具备错误重试和邮件通知功能,确保在网络环境变化时能够及时响应。

上一篇 下一篇
评论
来首音乐
最新回复
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
文章目录
每日一句