Skip to content

Commit

Permalink
Fix DDNS bugs and split up ddns module (#326)
Browse files Browse the repository at this point in the history
* fix: webhook retry logic
fix: adjust record type based on ipv4 ipv6
refract: move ddns providers to a new module

* refract: move ddns module to pkg/
  • Loading branch information
DarcJC authored Feb 26, 2024
1 parent 5e528c4 commit cf9d9bd
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 268 deletions.
177 changes: 177 additions & 0 deletions pkg/ddns/cloudflare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package ddns

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)

type ProviderCloudflare struct {
Secret string
}

func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}

zoneID, err := provider.getZoneID(domainConfig.FullDomain)
if err != nil {
log.Printf("无法获取 zone ID: %s\n", err)
return false
}

// 当IPv4和IPv6同时成功才算作成功
var resultV4 = true
var resultV6 = true
if domainConfig.EnableIPv4 {
if !provider.addDomainRecord(zoneID, domainConfig, true) {
resultV4 = false
}
}

if domainConfig.EnableIpv6 {
if !provider.addDomainRecord(zoneID, domainConfig, false) {
resultV6 = false
}
}

return resultV4 && resultV6
}

func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
return false
}

if record == nil {
// 添加 DNS 记录
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, isIpv4)
}
}

func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
_, realDomain := SplitDomain(domain)
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
body, err := provider.sendRequest("GET", url, nil)
if err != nil {
return "", err
}

var res map[string]interface{}
err = json.Unmarshal(body, &res)
if err != nil {
return "", err
}

result := res["result"].([]interface{})
if len(result) > 0 {
zoneID := result[0].(map[string]interface{})["id"].(string)
return zoneID, nil
}

return "", fmt.Errorf("找不到 Zone ID")
}

func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
ipType = "AAAA"
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, ipType, domain)
body, err := provider.sendRequest("GET", url, nil)
if err != nil {
return nil, err
}

var res map[string]interface{}
err = json.Unmarshal(body, &res)
if err != nil {
return nil, err
}

result := res["result"].([]interface{})
if len(result) > 0 {
return result[0].(map[string]interface{}), nil
}

return nil, nil // 没有找到 DNS 记录
}

func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
data := map[string]interface{}{
"type": ipType,
"name": domainConfig.FullDomain,
"content": ipAddr,
"ttl": 60,
"proxied": false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("POST", url, jsonData)
return err == nil
}

func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
data := map[string]interface{}{
"type": ipType,
"name": domainConfig.FullDomain,
"content": ipAddr,
"ttl": 60,
"proxied": false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("PATCH", url, jsonData)
return err == nil
}

// 以下为辅助方法,如发送 HTTP 请求等
func (provider ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
req.Header.Add("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
}
}(resp.Body)

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return body, nil
}
14 changes: 14 additions & 0 deletions pkg/ddns/ddns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ddns

type DomainConfig struct {
EnableIPv4 bool
EnableIpv6 bool
FullDomain string
Ipv4Addr string
Ipv6Addr string
}

type Provider interface {
// UpdateDomain Return is updated
UpdateDomain(domainConfig *DomainConfig) bool
}
7 changes: 7 additions & 0 deletions pkg/ddns/dummy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ddns

type ProviderDummy struct{}

func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
return false
}
61 changes: 61 additions & 0 deletions pkg/ddns/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ddns

import (
"fmt"
"net/http"
"strings"
)

func (provider ProviderWebHook) FormatWebhookString(s string, config *DomainConfig, ipType string) string {
if config == nil {
return s
}

result := strings.TrimSpace(s)
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
result = strings.Replace(result, "{type}", ipType, -1)
// remove \r
result = strings.Replace(result, "\r", "", -1)
return result
}

func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}

// SplitDomain 分割域名为前缀和一级域名
func SplitDomain(domain string) (prefix string, topLevelDomain string) {
// 带有二级TLD的一些常见例子,需要特别处理
secondLevelTLDs := map[string]bool{
".co.uk": true, ".com.cn": true, ".gov.cn": true, ".net.cn": true, ".org.cn": true,
}

// 分割域名为"."的各部分
parts := strings.Split(domain, ".")

// 处理特殊情况,例如 ".co.uk"
for i := len(parts) - 2; i > 0; i-- {
potentialTLD := fmt.Sprintf(".%s.%s", parts[i], parts[i+1])
if secondLevelTLDs[potentialTLD] {
if i > 1 {
return strings.Join(parts[:i-1], "."), strings.Join(parts[i-1:], ".")
}
return "", domain // 当域名仅为二级TLD时,无前缀
}
}

// 常规处理,查找最后一个"."前的所有内容作为前缀
if len(parts) > 2 {
return strings.Join(parts[:len(parts)-2], "."), strings.Join(parts[len(parts)-2:], ".")
}
return "", domain // 当域名不包含子域名时,无前缀
}
59 changes: 59 additions & 0 deletions pkg/ddns/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ddns

import (
"bytes"
"log"
"net/http"
"strings"
)

type ProviderWebHook struct {
URL string
RequestMethod string
RequestBody string
RequestHeader string
}

func (provider ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}

if domainConfig.FullDomain == "" {
log.Println("NEZHA>> Failed to update an empty domain")
return false
}
updated := false
client := &http.Client{}
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
} else {
updated = true
}
}
}
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
} else {
updated = true
}
}
}
return updated
}
3 changes: 2 additions & 1 deletion service/rpc/nezha.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rpc
import (
"context"
"fmt"
"github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/utils"
"log"
"time"
Expand Down Expand Up @@ -125,7 +126,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
if err == nil && serverDomain != "" {
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
config := &singleton.DDNSDomainConfig{
config := &ddns.DomainConfig{
EnableIPv4: true,
EnableIpv6: true,
FullDomain: serverDomain,
Expand Down
Loading

0 comments on commit cf9d9bd

Please sign in to comment.