📜 ⬆️ ⬇️

Own dynamic dns on Go using Cloudflare

Why do you need it at all?


It so happened that from work I often need to get ssh access to my home computer, and the provider issues a white, but dynamically changing ip address. Of course, the choice fell on dynamic dns and I picked up the first free no-ip provider. Their demon did an excellent job with the task, changing the dns record on the free third-level domain from the service, and on my domain CNAME was registered to their domain.

All this worked perfectly until I bought the Zyxel Keenetic Giga. He is friends with no-ip out of the box, but for some reason now I couldn’t log in from my domain. This problem could be solved by buying a static ip from the provider, writing in the ssh configuration on the beautiful guide from amarao , but also not interesting! So, it's time to write your service!

Where, in fact, take the ip address?


The first thing I asked was this question. It was possible to use one of the free STUN-servers (a stun-client for go, good, there is on github), it would be possible to terrorize any service, but I was going to check my address as often as possible. Since I have my own server on which I can install anything, I just decided to write a simple service to madness.

upd: some more ways to solve the problem:

')
A service that simply issues a client ip

Let's call it yourip. It only has to return ip on a GET request for / ip.
I decided to use httprouter for simplicity - the fastest and simplest router for go. Here is the first and only handler:
func PrintIp(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, r.Header.Get("X-Real-IP")) } 

Just write the value of the "X-Real-IP" header in response and that's it. This title will be passed to us by nginx, if we configure it. And if you plan to contact this service not through reverse proxy, but directly, you will need to use r.RemoteAddr instead of r.Header.Get ("X-Real-IP").

The program code is complete (you can also look at the githaba):
 package main import ( "fmt" "github.com/julienschmidt/httprouter" "log" "net/http" "flag" ) //   var ( port = flag.Int("port", 80, "port") // ,   host = flag.String("host", "", "host") //  prefix = flag.String("prefix", "/ip", "uri prefix") //   ) func PrintIp(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, r.Header.Get("X-Real-IP")) } func main() { //   flag.Parse() //   addr := fmt.Sprintf("%s:%d", *host, *port) log.Println("listening on", addr) router := httprouter.New() //    url router.GET(*prefix, PrintIp) //     log.Fatal(http.ListenAndServe(addr, router)) } 

It remains to configure nginx. It will be enough to have this configuration:
 upstream yourip { server locahost:888; #        } server { listen 80; location /ip { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://yourip; } } 

And run our service, for example ./yourip -port = 888
You can check the work of the service by clicking on this link, you can also use it if you have no place to get the service.

How to update an entry in Cloudflare?


Cloudflare api has a rec_edit method that can change the record for a specific domain.
Recognize the record ID

First you need to somehow find out the id of the record, another method will help us in this - rec_load_all

We need to make a POST request of something like this:
 curl https://www.cloudflare.com/api_json.html \ -d 'a=rec_load_all' \ -d 'tkn=8afbe6dea02407989af4dd4c97bb6e25' \ -d 'email=sample@example.com' \ -d 'z=example.com' 

And you have to make it go. This will help us great packages net / url and net / http
First, prepare the base url
 //    ,    func Url() (u url.URL) { u.Host = "www.cloudflare.com" u.Scheme = "https" u.Path = "api_json.html" return } 

This function will help us not to repeat the code, since we will do a total of two requests to api.
And now we add the parameters:
 u := Url() //    //  ()    url values := u.Query() values.Add("email", *email) values.Add("tkn", *token) values.Add("a", "rec_load_all") values.Add("z", *domain) //    RawQuery ,     u.RawQuery = values.Encode() reqUrl := u.String() 

For better understanding, you can see the URL types and Values .

It's time to create a query and execute it.
 client = http.Client{} req, _ := http.NewRequest("POST", reqUrl, nil) res, err := client.Do(req) 


To process the answer in json, we need to deserialize it into some kind of structure. Having looked at an example of the answer, I made this:
 type AllResponse struct { Response struct { Records struct { Objects []struct { Id string `json:"rec_id"` Name string `json:"name"` Type string `json:"type"` Content string `json:"content"` } `json:"objs"` } `json:"recs"` } `json:"response"` } 

Thus, we only get the data we need when we parse the answer:
 //  ,    response := &AllResponse{} //   decoder := json.NewDecoder(res.Body) //        err = decoder.Decode(response) 

Now we will process the received data, going through all the records:
 for _, v := range response.Response.Records.Objects { //        if v.Name == *target && v.Type == "A" { //       id, _ := strconv.Atoi(v.Id) return id, v.Content, nil } } 

Finally, we found what we need - an identifier

Change the record

We will need to create a query again. Let's start collecting url:
 u := Url() values := u.Query() values.Add("email", *email) values.Add("tkn", *token) values.Add("a", "rec_edit") values.Add("z", *domain) values.Add("type", "A") values.Add("name", *target) values.Add("service_mode", "0") values.Add("content", ip) values.Add("id", strconv.Itoa(id)) values.Add("ttl", fmt.Sprint(*ttl)) 

Now it has all the information that is needed to replace the ip address. It remains only to create a query and execute it as last time.
 req, _ := http.NewRequest("POST", reqUrl, nil) res, err := client.Do(req) 

Actually, this is where the fun ends. These two requests are placed into separate functions, all the necessary variables are placed into flags, and a main infinite loop is created.

 func main() { flag.Parse() //  id   ip id, previousIp, err := GetDnsId() if err != nil { log.Fatalln("unable to get dns record id:", err) } //  ,      // 5   ip  ticker := time.NewTicker(time.Second * 5) //     for _ = range ticker.C { ip, err := GetIp() if err != nil { continue } if previousIp != ip { err = SetIp(ip, id) if err != nil { continue } } log.Println("updated to", ip) previousIp = ip } } 


That's all. The code can be found on github

Full code
 package main import ( "encoding/json" "errors" "flag" "fmt" "io/ioutil" "log" "net/http" "net/url" "strconv" "time" ) //      api type AllResponse struct { Response struct { Records struct { Objects []struct { Id string `json:"rec_id"` Name string `json:"name"` Type string `json:"type"` Content string `json:"content"` } `json:"objs"` } `json:"recs"` } `json:"response"` } //     var ( yourIpUrl = flag.String("url", "https://cydev.ru/ip", "Yourip service url") domain = flag.String("domain", "cydev.ru", "Cloudflare domain") target = flag.String("target", "me.cydev.ru", "Target domain") email = flag.String("email", "ernado@ya.ru", "The e-mail address associated with the API key") token = flag.String("token", "-", "This is the API key made available on your Account page") ttl = flag.Int("ttl", 120, "TTL of record in seconds. 1 = Automatic, otherwise, value must in between 120 and 86400 seconds") // http  -     Do,    client = http.Client{} ) //    ,    func Url() (u url.URL) { u.Host = "www.cloudflare.com" u.Scheme = "https" u.Path = "api_json.html" return } // SetIp      id func SetIp(ip string, id int) error { u := Url() values := u.Query() values.Add("email", *email) values.Add("tkn", *token) values.Add("a", "rec_edit") values.Add("z", *domain) values.Add("type", "A") values.Add("name", *target) values.Add("service_mode", "0") values.Add("content", ip) values.Add("id", strconv.Itoa(id)) values.Add("ttl", fmt.Sprint(*ttl)) u.RawQuery = values.Encode() reqUrl := u.String() log.Println("POST", reqUrl) req, err := http.NewRequest("POST", reqUrl, nil) if err != nil { return err } res, err := client.Do(req) if err != nil { return err } if res.StatusCode != http.StatusOK { return errors.New(fmt.Sprintf("bad status %d", res.StatusCode)) } return nil } // GetDnsId  id      func GetDnsId() (int, string, error) { log.Println("getting dns record id") //   url u := Url() //    values := u.Query() values.Add("email", *email) values.Add("tkn", *token) values.Add("a", "rec_load_all") values.Add("z", *domain) u.RawQuery = values.Encode() reqUrl := u.String() //  ,      log.Println("POST", reqUrl) req, err := http.NewRequest("POST", reqUrl, nil) res, err := client.Do(req) if err != nil { return 0, "", err } if res.StatusCode != http.StatusOK { return 0, "", errors.New(fmt.Sprintf("bad status %d", res.StatusCode)) } response := &AllResponse{} //   decoder := json.NewDecoder(res.Body) //        err = decoder.Decode(response) if err != nil { return 0, "", err } //     for _, v := range response.Response.Records.Objects { //        if v.Name == *target && v.Type == "A" { //       id, _ := strconv.Atoi(v.Id) return id, v.Content, nil } } //      return 0, "", errors.New("not found") } // GetIp()   yourip     ip  func GetIp() (string, error) { res, err := client.Get(*yourIpUrl) if err != nil { return "", err } if res.StatusCode != http.StatusOK { return "", errors.New(fmt.Sprintf("bad status %d", res.StatusCode)) } body, err := ioutil.ReadAll(res.Body) if err != nil { return "", err } return string(body), nil } func main() { flag.Parse() id, previousIp, err := GetDnsId() if err != nil { log.Fatalln("unable to get dns record id:", err) } log.Println("found record", id, "=", previousIp) //  ,      // 5   ip  ticker := time.NewTicker(time.Second * 5) //     for _ = range ticker.C { ip, err := GetIp() if err != nil { log.Println("err", err) continue } if previousIp != ip { err = SetIp(ip, id) if err != nil { log.Println("unable to set ip:", err) continue } } log.Println("updated to", ip) previousIp = ip } } 

Source: https://habr.com/ru/post/229179/


All Articles