[Из песочницы] Свой dynamic dns на Go с помощью Cloudflare

Зачем вообще это нужно? Так получилось, что с работы мне довольно часто надо получить ssh доступ к своему домашнему компьютеру, а провайдер выдает белый, но динамически меняющийся ip адрес. Разумеется, выбор пал на динамический dns и я взял первого попавшегося бесплатного провайдера no-ip. Их демон прекрасно справлялся с задачей, меняя dns-запись на бесплатном домене третьего уровня от сервиса, а на моем домене был прописан CNAME на их домен.Все это прекрасно работало до того момента, как я купил себе Zyxel Keenetic Giga. Он дружит с no-ip из коробки, но почему-то с моего домена теперь зайти не получалось. Эту проблему можно было бы решить покупкой статического ip у провайдера, записью в конфигурации ssh по прекрасному гайду от amarao, но так же не интересно! Итак, пришло время написать свой сервис!

Откуда, собственно, брать ip адрес? Первым делом я задался именно этим вопросом. Можно было использовать один из бесплатных STUN-серверов (stun-клиент для go, благо, есть на github), можно было бы терроризировать какой-нибудь сервис, но я был намерен проверять свой адрес как можно чаще. Так как у меня есть свой сервер, на который я могу установить что угодно, то я просто решил написать до безумия простой сервис.Сервис, который просто выдает ip клиента Назовем его yourip. Он всего лишь должен возвращать ip по GET-запросу на /ip.Я решил использовать для простоты httprouter — cамый быстрый и простой роутер для go. Вот первый и единственный обработчик: func PrintIp (w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint (w, r.Header.Get («X-Real-IP»)) } Просто записываем значение заголовка «X-Real-IP» в ответ и все. Этот заголовок нам передаст nginx, если мы его настроим. А если к этому сервису обращаться планируется не через реверс-прокси, а напрямую, то потребуется использовать r.RemoteAddr вместо r.Header.Get («X-Real-IP»).Код программы полностью (также можно посмотреть на гитхабе):

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)) } Осталось настроить nginx. Достаточно будет примерно такой конфигурации: upstream yourip { server locahost:888; # пусть наш сервис висит на этом порту } server { listen 80; location /ip { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://yourip; } } И запустить наш сервис, например ./yourip -port=888Проверить работу сервиса можно, перейдя по этой ссылке, также её можете использовать, если вам негде захостить сервис.Как обновить запись в Cloudflare? У cloudflare api есть метод rec_edit, который может изменить запись для определенного домена.Узнаем идентификатор записи Для начала надо как-то узнать id записи, в этом нам поможет другой метод — rec_load_allНам надо сделать POST-запрос примерно такого содержания:

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' И надо его сделать в go. В этом нам помогут замечательные пакеты net/url и net/httpВначале приготовим базовый url // зададим заранее некоторые поля, чтобы не повторяться func Url () (u url.URL) { u.Host = «www.cloudflare.com» u.Scheme = «https» u.Path = «api_json.html» return } Эта функция поможет нам не повторять код, т.к. мы будем делать в общей сумме два запроса к api.А теперь добавим параметров: 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 () Для лучшего понимания можно посмотреть типы URL и Values.Пришло время создать запрос и выполнить его.

client = http.Client{} req, _ := http.NewRequest («POST», reqUrl, nil) res, err:= client.Do (req) Чтобы обработать ответ в json, нам надо его десериализировать в какую-то структуру. Посмотрев пример ответа, я составил вот такую:

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»` } Таким образом, мы получим лишь необходимые нам данные, когда будем парсить ответ: // созданим переменную, куда будем парсить response:= &AllResponse{} // создадим декодер decoder:= json.NewDecoder (res.Body) // и распарсим ответ сервера в нашу структуру err = decoder.Decode (response) Теперь обработаем полученные данные, пройдясь по всем записям: for _, v:= range response.Response.Records.Objects { // и найдем запись нужного типа и имени if v.Name == *target && v.Type == «A» { // конвертируем из строки в число идентификатор id, _ := strconv.Atoi (v.Id) return id, v.Content, nil } } Наконец, мы нашли то, что нам нужно — идентификаторМеняем запись Нам снова потребуется создать запрос. Начнем собирать 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)) Теперь в нем есть вся информация, которая нужна для замены ip адреса. Осталось только создать запрос и выполнить его, как в прошлый раз req, _ := http.NewRequest («POST», reqUrl, nil) res, err:= client.Do (req) Собственно, на этом самое интересное заканчивается. Эти два запроса выносятся в отдельные функции, все нужные переменные — в флаги, и создается главный бесконечный цикл. 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 } } На этом всё. Код можно найти на гитхабе

Код полностью 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 } }

© Habrahabr.ru