-
-
Notifications
You must be signed in to change notification settings - Fork 227
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from kerma/master
Support for Cloudflare
- Loading branch information
Showing
6 changed files
with
449 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ GoDNS is a dynamic DNS (DDNS) client tool, it is based on my early open source p | |
Now I rewrite [DynDNS](https://github.com/TimothyYe/DynDNS) by Golang and call it [GoDNS](https://github.com/TimothyYe/godns). | ||
|
||
## Supported DNS Provider | ||
* Cloudflare ([https://cloudflare.com](https://cloudflare.com)) | ||
* DNSPod ([https://www.dnspod.cn/](https://www.dnspod.cn/)) | ||
* HE.net (Hurricane Electric) ([https://dns.he.net/](https://dns.he.net/)) | ||
|
||
|
@@ -95,6 +96,28 @@ Usage of ./godns: | |
* Configure the SMTP options if you want, a mail notification will sent to your mailbox once the IP is changed. | ||
* Save it in the same directory of GoDNS, or use -c=your_conf_path command. | ||
|
||
### Config example for Cloudflare | ||
|
||
For Cloudflare, you need to provide email & Global API Key as password, and config all the domains & subdomains. | ||
|
||
```json | ||
{ | ||
"provider": "Cloudflare", | ||
"email": "[email protected]" | ||
"password": "Global API Key", | ||
"domains": [{ | ||
"domain_name": "example.com", | ||
"sub_domains": ["www","test"] | ||
},{ | ||
"domain_name": "example2.com", | ||
"sub_domains": ["www","test"] | ||
} | ||
], | ||
"ip_url": "https://ifconfig.co/ip", | ||
"socks5_proxy": "" | ||
} | ||
``` | ||
|
||
### Config example for DNSPod | ||
|
||
For DNSPod, you need to provide email & password, and config all the domains & subdomains. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
package handler | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"io" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"runtime/debug" | ||
"time" | ||
|
||
"github.com/TimothyYe/godns" | ||
"golang.org/x/net/proxy" | ||
) | ||
|
||
// CloudflareHandler struct definition | ||
type CloudflareHandler struct { | ||
Configuration *godns.Settings | ||
API string | ||
} | ||
|
||
// DNSRecordResponse struct | ||
type DNSRecordResponse struct { | ||
Records []DNSRecord `json:"result"` | ||
Success bool `json:"success"` | ||
} | ||
|
||
// DNSRecordUpdateResponse struct | ||
type DNSRecordUpdateResponse struct { | ||
Record DNSRecord `json:"result"` | ||
Success bool `json:"success"` | ||
} | ||
|
||
// DNSRecord for Cloudflare API | ||
type DNSRecord struct { | ||
ID string `json:"id"` | ||
IP string `json:"content"` | ||
Name string `json:"name"` | ||
Proxied bool `json:"proxied"` | ||
Type string `json:"type"` | ||
ZoneID string `json:"zone_id"` | ||
} | ||
|
||
// SetIP updates DNSRecord.IP | ||
func (r *DNSRecord) SetIP(ip string) { | ||
r.IP = ip | ||
} | ||
|
||
// ZoneResponse is a wrapper for Zones | ||
type ZoneResponse struct { | ||
Zones []Zone `json:"result"` | ||
Success bool `json:"success"` | ||
} | ||
|
||
// Zone object with id and name | ||
type Zone struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
} | ||
|
||
// SetConfiguration pass dns settings and store it to handler instance | ||
func (handler *CloudflareHandler) SetConfiguration(conf *godns.Settings) { | ||
handler.Configuration = conf | ||
handler.API = "https://api.cloudflare.com/client/v4" | ||
} | ||
|
||
// DomainLoop the main logic loop | ||
func (handler *CloudflareHandler) DomainLoop(domain *godns.Domain, panicChan chan<- godns.Domain) { | ||
defer func() { | ||
if err := recover(); err != nil { | ||
log.Printf("Recovered in %v: %v\n", err, debug.Stack()) | ||
panicChan <- *domain | ||
} | ||
}() | ||
|
||
for { | ||
currentIP, err := godns.GetCurrentIP(handler.Configuration) | ||
if err != nil { | ||
log.Println("Error in GetCurrentIP:", err) | ||
continue | ||
} | ||
log.Println("Current IP is:", currentIP) | ||
// TODO: check against locally cached IP, if no change, skip update | ||
|
||
log.Println("Checking IP for domain", domain.DomainName) | ||
zoneID := handler.getZone(domain.DomainName) | ||
if zoneID != "" { | ||
records := handler.getDNSRecords(zoneID) | ||
|
||
// update records | ||
for _, rec := range records { | ||
if recordTracked(domain, &rec) != true { | ||
log.Println("Skiping record:", rec.Name) | ||
continue | ||
} | ||
if rec.IP != currentIP { | ||
log.Printf("IP mismatch: Current(%+v) vs Cloudflare(%+v)\r\n", currentIP, rec.IP) | ||
handler.updateRecord(rec, currentIP) | ||
} else { | ||
log.Printf("Record OK: %+v - %+v\r\n", rec.Name, rec.IP) | ||
} | ||
} | ||
} else { | ||
log.Println("Failed to find zone for domain:", domain.DomainName) | ||
} | ||
|
||
// Interval is 5 minutes | ||
log.Printf("Going to sleep, will start next checking in %d minutes...\r\n", godns.INTERVAL) | ||
time.Sleep(time.Minute * godns.INTERVAL) | ||
} | ||
} | ||
|
||
// Check if record is present in domain conf | ||
func recordTracked(domain *godns.Domain, record *DNSRecord) bool { | ||
|
||
if record.Name == domain.DomainName { | ||
return true | ||
} | ||
|
||
for _, subDomain := range domain.SubDomains { | ||
sd := subDomain + "." + domain.DomainName | ||
if record.Name == sd { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// Create a new request with auth in place and optional proxy | ||
func (handler *CloudflareHandler) newRequest(method, url string, body io.Reader) (*http.Request, *http.Client) { | ||
client := &http.Client{} | ||
|
||
if handler.Configuration.Socks5Proxy != "" { | ||
log.Println("use socks5 proxy:" + handler.Configuration.Socks5Proxy) | ||
dialer, err := proxy.SOCKS5("tcp", handler.Configuration.Socks5Proxy, nil, proxy.Direct) | ||
if err != nil { | ||
log.Println("can't connect to the proxy:", err) | ||
} else { | ||
httpTransport := &http.Transport{} | ||
client.Transport = httpTransport | ||
httpTransport.Dial = dialer.Dial | ||
} | ||
} | ||
|
||
req, _ := http.NewRequest(method, handler.API+url, body) | ||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("X-Auth-Email", handler.Configuration.Email) | ||
req.Header.Set("X-Auth-Key", handler.Configuration.Password) | ||
return req, client | ||
} | ||
|
||
// Find the correct zone via domain name | ||
func (handler *CloudflareHandler) getZone(domain string) string { | ||
|
||
var z ZoneResponse | ||
|
||
req, client := handler.newRequest("GET", "/zones", nil) | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
log.Println("Request error:", err.Error()) | ||
return "" | ||
} | ||
|
||
body, _ := ioutil.ReadAll(resp.Body) | ||
err = json.Unmarshal(body, &z) | ||
if err != nil { | ||
log.Printf("Decoder error: %+v\n", err) | ||
log.Printf("Response body: %+v\n", string(body)) | ||
return "" | ||
} | ||
if z.Success != true { | ||
log.Printf("Response failed: %+v\n", string(body)) | ||
return "" | ||
} | ||
|
||
for _, zone := range z.Zones { | ||
if zone.Name == domain { | ||
return zone.ID | ||
} | ||
} | ||
return "" | ||
} | ||
|
||
// Get all DNS A records for a zone | ||
func (handler *CloudflareHandler) getDNSRecords(zoneID string) []DNSRecord { | ||
|
||
var empty []DNSRecord | ||
var r DNSRecordResponse | ||
|
||
req, client := handler.newRequest("GET", "/zones/"+zoneID+"/dns_records?type=A", nil) | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
log.Println("Request error:", err.Error()) | ||
return empty | ||
} | ||
|
||
body, _ := ioutil.ReadAll(resp.Body) | ||
err = json.Unmarshal(body, &r) | ||
if err != nil { | ||
log.Printf("Decoder error: %+v\n", err) | ||
log.Printf("Response body: %+v\n", string(body)) | ||
return empty | ||
} | ||
if r.Success != true { | ||
body, _ := ioutil.ReadAll(resp.Body) | ||
log.Printf("Response failed: %+v\n", string(body)) | ||
return empty | ||
|
||
} | ||
return r.Records | ||
} | ||
|
||
// Update DNS A Record with new IP | ||
func (handler *CloudflareHandler) updateRecord(record DNSRecord, newIP string) { | ||
|
||
var r DNSRecordUpdateResponse | ||
record.SetIP(newIP) | ||
|
||
j, _ := json.Marshal(record) | ||
req, client := handler.newRequest("PUT", | ||
"/zones/"+record.ZoneID+"/dns_records/"+record.ID, | ||
bytes.NewBuffer(j), | ||
) | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
log.Println("Request error:", err.Error()) | ||
return | ||
} | ||
|
||
body, _ := ioutil.ReadAll(resp.Body) | ||
err = json.Unmarshal(body, &r) | ||
if err != nil { | ||
log.Printf("Decoder error: %+v\n", err) | ||
log.Printf("Response body: %+v\n", string(body)) | ||
return | ||
} | ||
if r.Success != true { | ||
body, _ := ioutil.ReadAll(resp.Body) | ||
log.Printf("Response failed: %+v\n", string(body)) | ||
} else { | ||
log.Printf("Record updated: %+v - %+v", record.Name, record.IP) | ||
} | ||
} |
Oops, something went wrong.