├── .gitignore ├── Dockerfile ├── run.sh ├── README.md ├── main_test.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | dnspod-ddns 2 | .idea 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.10 as builder 2 | WORKDIR /go/src/github.com/scofieldpeng/dnspod-ddns/ 3 | COPY main.go . 4 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 5 | 6 | FROM scofieldpeng/alpine:glibc-2.7 7 | RUN mkdir /app 8 | COPY --from=builder /go/src/github.com/scofieldpeng/dnspod-ddns/app /app/app 9 | RUN chmod +x /app/app 10 | WORKDIR /app/ 11 | 12 | ENTRYPOINT /app/app -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 填写你在dnspod申请的id 4 | DNSPOD_ID=123456 5 | # 在dnspod申请的token 6 | DNSPOD_TOKEN=123456 7 | # 在dnspod要更新的顶级域名 8 | DNSPOD_DOMAIN=example.com 9 | # 在dnspod要更新的子域名前缀,如果是根域名,填写@即可 10 | DNSPOD_SUBDOMAIN=example 11 | # 你的邮箱 12 | DNSPOD_EMAIL=example@example.com 13 | 14 | docker run --name=ddns --restart=always -d \ 15 | -e DNSPOD_ID=${DNSPOD_ID} \ 16 | -e DNSPOD_TOKEN=${DNSPOD_TOKEN} \ 17 | -e DNSPOD_DOMAIN=${DNSPOD_DOMAIN} \ 18 | -e DNSPOD_SUBDOMAIN=${DNSPOD_SUBDOMAIN} \ 19 | -e DNSPOD_EMAIL=${DNSPOD_EMAIL} \ 20 | scofieldpeng/dnspod-ddns:1.0.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dnspod-ddns 2 | 3 | 通过dnspod来进行ddns的小脚本,用来做什么,你懂得,本来不想写的,只是github.com另外一个哥们儿用py写的跑不起来,看了下代码有问题,懒得修改直接用golang撸了一个 4 | 5 | 如果对你有用,可以star一下,2333 6 | 7 | ## 使用 8 | 9 | **配置项:** 10 | 11 | 配置采用环境变量驱动,具体见下: 12 | 13 | ```ini 14 | # 填写你在dnspod申请的id 15 | DNSPOD_ID=123456 16 | # 在dnspod申请的token 17 | DNSPOD_TOKEN=123456 18 | # 在dnspod要更新的顶级域名 19 | DNSPOD_DOMAIN=example.com 20 | # 在dnspod要更新的子域名前缀,如果是根域名,填写@即可 21 | DNSPOD_SUBDOMAIN=example 22 | # 你的邮箱 23 | DNSPOD_EMAIL=example@example.com 24 | ``` 25 | 26 | ### docker运行 27 | 28 | ```bash 29 | docker run --name=ddns --restart=always -d \ 30 | -e DNSPOD_ID=${DNSPOD_ID} \ 31 | -e DNSPOD_TOKEN=${DNSPOD_TOKEN} \ 32 | -e DNSPOD_DOMAIN=${DNSPOD_DOMAIN} \ 33 | -e DNSPOD_SUBDOMAIN=${DNSPOD_SUBDOMAIN} \ 34 | -e DNSPOD_EMAIL=example@example.com \ 35 | scofieldpeng/dnspod-ddns:1.0.0 36 | ``` 37 | 38 | ### 源码编译 39 | 40 | 1. 安装go 41 | 2. 下载源码 42 | ``` 43 | go get github.com/scofieldpeng/dnspod-ddns 44 | ``` 45 | 3. 编译 46 | ``` 47 | go build -o app . 48 | ``` 49 | 50 | $GOPATH/src/github.com/scofieldpeng/dnspod-ddns/app文件即为二进制包,直接运行即可 51 | 52 | ## 申请dnspod的ID和token 53 | 54 | 教程详见dnspod官网[https://support.dnspod.cn/Kb/showarticle/tsid/227/](https://support.dnspod.cn/Kb/showarticle/tsid/227/) 55 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAppConfig_Validate(t *testing.T) { 8 | config = appConfig{} 9 | if err := config.Validate(); err == nil { 10 | t.Error("empty config should not be passed") 11 | } 12 | if err := config.Validate(); err == nil { 13 | t.Error("dnspodtoken should not empty") 14 | } 15 | config.dnspodId = "1234" 16 | if err := config.Validate(); err == nil { 17 | t.Error("only dnspod should not be passed") 18 | } 19 | config.dnspodToken = "bfc41abad5a9852380ba15a124690ec5" 20 | if err := config.Validate(); err == nil { 21 | t.Error("only dnspodToken should not be passed") 22 | } 23 | config.subDomain = "test" 24 | if err := config.Validate(); err == nil { 25 | t.Error("only dnspodId, domain and subDomain should not be passed") 26 | } 27 | config.recordId = "123456" 28 | if err := config.Validate(); err == nil { 29 | t.Error("only domain,subDomain and recordid should not be passed") 30 | } 31 | config.domain = "example.com" 32 | if err := config.Validate(); err == nil { 33 | t.Error("internal should be required") 34 | } 35 | config.internal = -1 36 | if err := config.Validate(); err == nil { 37 | t.Error("internal should exceed than 5") 38 | } 39 | config.internal = 10 40 | if err := config.Validate(); err != nil { 41 | t.Error("all config values are valid, should pass,get error:", err) 42 | } 43 | } 44 | 45 | func TestGetPublicIP(t *testing.T) { 46 | ip, err := GetPublicIP() 47 | if err != nil { 48 | t.Error(err.Error()) 49 | return 50 | } 51 | t.Log("public ip:", ip) 52 | } 53 | 54 | func TestGetRecord(t *testing.T) { 55 | config.dnspodId = "1234" 56 | config.dnspodToken = "helloworld" 57 | config.domain = "example.com" 58 | config.subDomain = "test" 59 | 60 | recordId, ip, err := GetRecord() 61 | if err != nil { 62 | t.Error(err.Error()) 63 | return 64 | } 65 | if ip != "127.0.0.1" { 66 | t.Error("ip not equal 127.0.0.1,get:", ip) 67 | } 68 | if recordId != "123456" { 69 | t.Error("recordid not correct,get:", recordId) 70 | } 71 | t.Log("ip:", ip, ",recordid:", recordId) 72 | } 73 | 74 | func TestUpdateRecord(t *testing.T) { 75 | config.dnspodId = "1234" 76 | config.dnspodToken = "helloworld" 77 | config.domain = "example.com" 78 | config.subDomain = "test" 79 | config.recordId = "123456" 80 | 81 | if err := UpdateRecord(config.recordId, "121.40.31.121"); err != nil { 82 | t.Error("update record fail!error:", err.Error()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type appConfig struct { 16 | dnspodId string 17 | dnspodToken string 18 | recordId string 19 | domain string 20 | subDomain string 21 | internal int 22 | email string 23 | } 24 | 25 | // 验证appConfig 26 | func (c appConfig) Validate() (err error) { 27 | if c.dnspodId == "" { 28 | return errors.New("environment DNSPOD_ID required") 29 | } 30 | if c.dnspodToken == "" { 31 | return errors.New("environment DNSPOD_TOKEN required") 32 | } 33 | 34 | if c.recordId == "" && c.subDomain == "" { 35 | return errors.New("environment DNSPOD_RECORDID or DNSPOD_SUBDOMAIN required") 36 | } 37 | if c.domain == "" { 38 | return errors.New("environment DNSPOD_DOMAIN required") 39 | } 40 | if c.internal < 5 { 41 | return errors.New("environment DNSPOD_INTERNAL should range from 0 to 5") 42 | } 43 | 44 | return 45 | } 46 | 47 | var ( 48 | config = appConfig{ 49 | dnspodId: os.Getenv("DNSPOD_ID"), 50 | dnspodToken: os.Getenv("DNSPOD_TOKEN"), 51 | domain: os.Getenv("DNSPOD_DOMAIN"), 52 | subDomain: os.Getenv("DNSPOD_SUBDOMAIN"), 53 | internal: 60, 54 | email: os.Getenv("DNSPOD_EMAIL"), 55 | } 56 | ) 57 | 58 | const ( 59 | ClientUserAgent = "DNSPOD-DDNS-CLIENT" 60 | Version = "1.0.0" 61 | StatusOk = "1" 62 | ) 63 | 64 | func init() { 65 | internal := os.Getenv("DNSPOD_INTERNAL") 66 | config.internal, _ = strconv.Atoi(internal) 67 | if config.internal < 5 { 68 | config.internal = 60 69 | } 70 | if config.email == "" { 71 | config.email = "example@example.com" 72 | } 73 | } 74 | 75 | func main() { 76 | var ( 77 | err error 78 | lastPublicIP string 79 | publicIP string 80 | ) 81 | 82 | if err := config.Validate(); err != nil { 83 | fmt.Println("[error]", err) 84 | os.Exit(1) 85 | } 86 | 87 | fmt.Println("start") 88 | for { 89 | publicIP, err = GetPublicIP() 90 | if err != nil { 91 | fmt.Println(err.Error()) 92 | time.Sleep(time.Duration(config.internal) * time.Second) 93 | continue 94 | } 95 | if config.recordId == "" || lastPublicIP == "" { 96 | config.recordId, lastPublicIP, err = GetRecord() 97 | if err != nil { 98 | fmt.Println(err.Error()) 99 | time.Sleep(time.Duration(config.internal) * time.Second) 100 | continue 101 | } 102 | } 103 | if publicIP != lastPublicIP { 104 | fmt.Println("发现公网IP变化,开始更新") 105 | if err = UpdateRecord(config.recordId, publicIP); err != nil { 106 | fmt.Println(err.Error()) 107 | time.Sleep(time.Duration(config.internal) * time.Second) 108 | continue 109 | } 110 | fmt.Println("公网IP更新成功,新的公网IP:", publicIP) 111 | lastPublicIP = publicIP 112 | } 113 | fmt.Println("下次更新时间:", time.Now().Add(time.Duration(config.internal)*time.Second).Format("2006-01-02 15:04:05")) 114 | time.Sleep(time.Duration(config.internal) * time.Second) 115 | } 116 | } 117 | 118 | // 公共返回参数 119 | type CommonResponse struct { 120 | Status struct { 121 | Code string `json:"code"` 122 | Message string `json:"message"` 123 | CreateTime string `json:"created_at"` 124 | } `json:"status"` 125 | } 126 | 127 | // 记录列表返回值 128 | type RecordListResponse struct { 129 | CommonResponse 130 | Records []struct { 131 | SubDomain string `json:"name"` 132 | Id string `json:"id"` 133 | PublicIP string `json:"value"` 134 | } `json:"records"` 135 | } 136 | 137 | // 更新record记录 138 | func UpdateRecord(recordId string, publicIP string) (err error) { 139 | var ( 140 | request *http.Request 141 | response *http.Response 142 | c *http.Client 143 | body = url.Values{} 144 | responseData CommonResponse 145 | ) 146 | body.Add("login_token", fmt.Sprintf("%s,%s", config.dnspodId, config.dnspodToken)) 147 | body.Add("format", "json") 148 | body.Add("lang", "cn") 149 | body.Add("error_on_empty", "no") 150 | body.Add("domain", config.domain) 151 | body.Add("sub_domain", config.subDomain) 152 | body.Add("record_id", recordId) 153 | body.Add("record_type", "A") 154 | body.Add("record_line", "默认") 155 | body.Add("value", publicIP) 156 | 157 | request, err = http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(body.Encode())) 158 | if err != nil { 159 | err = errors.New("request对象创建失败,err:" + err.Error()) 160 | return 161 | } 162 | request.Header.Add("User-Agent", fmt.Sprintf("%s/%s(%s)", ClientUserAgent, Version, config.email)) 163 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 164 | c = &http.Client{Timeout: time.Second * 30} 165 | response, err = c.Do(request) 166 | if err != nil { 167 | err = errors.New("请求出错,err:" + err.Error()) 168 | return 169 | } 170 | 171 | if err = json.NewDecoder(response.Body).Decode(&responseData); err != nil { 172 | err = errors.New("解析数据失败,err:" + err.Error()) 173 | return 174 | } 175 | defer response.Body.Close() 176 | 177 | if responseData.Status.Code != StatusOk { 178 | err = errors.New(fmt.Sprintf("更新失败,code:%s,message:%s", responseData.Status.Code, responseData.Status.Message)) 179 | return 180 | } 181 | 182 | return 183 | } 184 | 185 | // 获取recordid 186 | func GetRecord() (recordId, IP string, err error) { 187 | var ( 188 | request *http.Request 189 | response *http.Response 190 | c *http.Client 191 | body = url.Values{} 192 | responseData RecordListResponse 193 | ) 194 | body.Add("login_token", fmt.Sprintf("%s,%s", config.dnspodId, config.dnspodToken)) 195 | body.Add("format", "json") 196 | body.Add("lang", "cn") 197 | body.Add("error_on_empty", "no") 198 | 199 | body.Add("domain", config.domain) 200 | body.Add("sub_domain", config.subDomain) 201 | request, err = http.NewRequest("POST", "https://dnsapi.cn/Record.List", strings.NewReader(body.Encode())) 202 | if err != nil { 203 | err = errors.New("request对象创建失败,err:" + err.Error()) 204 | return 205 | } 206 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 207 | request.Header.Set("User-Agent", fmt.Sprintf("%s/%s(%s)", ClientUserAgent, Version, config.email)) 208 | c = &http.Client{Timeout: time.Second * 30} 209 | response, err = c.Do(request) 210 | if err != nil { 211 | err = errors.New("请求出错,err:" + err.Error()) 212 | return 213 | } 214 | 215 | if err = json.NewDecoder(response.Body).Decode(&responseData); err != nil { 216 | err = errors.New("解析数据失败,err:" + err.Error()) 217 | return 218 | } 219 | defer response.Body.Close() 220 | if responseData.Status.Code != StatusOk { 221 | err = errors.New(fmt.Sprintf("获取record失败,code:%s,message:%s", responseData.Status.Code, responseData.Status.Message)) 222 | return 223 | } 224 | 225 | for _, v := range responseData.Records { 226 | if v.SubDomain == config.subDomain { 227 | recordId = v.Id 228 | IP = v.PublicIP 229 | return 230 | } 231 | } 232 | 233 | err = errors.New("没有找到相关记录,请先前往dnspod进行添加") 234 | return 235 | } 236 | 237 | type getPublicIPResponse struct { 238 | IP string `json:"origin"` 239 | } 240 | 241 | // 获取公网IP,如果出错,返回第二个参数 242 | func GetPublicIP() (publicIP string, err error) { 243 | var ( 244 | response *http.Response 245 | responseData getPublicIPResponse 246 | ) 247 | if response, err = http.Get("http://www.httpbin.org/ip"); err != nil { 248 | err = errors.New("获取公网IP出错,err:" + err.Error()) 249 | return 250 | } 251 | if err = json.NewDecoder(response.Body).Decode(&responseData); err != nil { 252 | err = errors.New("获取公网IP出错,err:" + err.Error()) 253 | return 254 | } 255 | defer response.Body.Close() 256 | 257 | return responseData.IP, nil 258 | } 259 | --------------------------------------------------------------------------------