Всем привет! Спешу поделиться кое-какой разработкой. Golang-клиент для NIC.ru API.

У нас есть тесная интеграция с nic.ru, так сложилось исторически, уверен, многие пользуются DNS-услугами nic.ru, может быть, не так активно как мы, но, так или иначе, для нас это важно, ранее у нас была разработка на Python, но, в последнее время мы активно переезжаем на Go, поэтому пришлось поддержать это дело. Буду рад, если кому-то это облегчит жизнь / сэкономить время.

Исходные данные

Изначально есть вот такой пакет для python https://pypi.org/project/nic-api/

Его написал наш коллега некоторое время назад, не утверждаю, что оно работает, но и не утверждаю обратное, у нас не было возможности это проверить, так как не было необходимости этим пользоваться, тем не менее, нельзя это не упомянуть.

Формат API описан тут, веселого там мало, всё на xml, но, унывать не приходится, приходится работать с тем, что дают.

Структуры

После чтения документации у нас получились следующие структуры

Request

Это общая структура для создания записей

type Request struct {
	XMLName xml.Name `xml:"request"`
	Text    string   `xml:",chardata"`
	RrList  *RrList  `xml:"rr-list"`
}

RrList

Структура, описывающая список записей

type RrList struct {
	Text string `xml:",chardata"`
	Rr   []*RR  `xml:"rr"`
}

RR

Структура записи в DNS

type RR struct {
	Text    string `xml:",chardata"`
	ID      string `xml:"id,attr,omitempty"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
	Ttl     string `xml:"ttl"`
	Type    string `xml:"type"`
	Soa     *Soa   `xml:"soa"`
	Ns      *Ns    `xml:"ns"`
	A       *A     `xml:"a"`
	Txt     *Txt   `xml:"txt"`
	Cname   *Cname `xml:"cname"`
}

A

Структура, хранящая в себе IPv4-адрес А-записи. У нас, пока что, нет IPv6, не вникали даже, как жить с этим делом в NIC.ru, но, тут главная особенность в том, что, по сравнению с CNAME, нет никаких атрибутов, значение хранится как текст

type A string

func (a *A) String() string {
	return string(*a)
}

CNAME

Структура, хранящая в себе каскадное имя, всё взято из API, IdnName может быть чем-то типа "наш-любимый-сайт.рф", в то время, как Name буде хранить значение, которые понятно всем узлам в сети Интернет, в данном случае:

xn-----6kccd6bhcmmf0dp7e6b4b.xn--p1ai
type Cname struct {
	Text    string `xml:",chardata"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
}

Service

Структура, отражающая сервис NIC.ru, тут отражён тариф, количество доменов и т.п, сервисная информация.

type Service struct {
	Text         string `xml:",chardata"`
	Admin        string `xml:"admin,attr"`
	DomainsLimit string `xml:"domains-limit,attr"`
	DomainsNum   string `xml:"domains-num,attr"`
	Enable       string `xml:"enable,attr"`
	HasPrimary   string `xml:"has-primary,attr"`
	Name         string `xml:"name,attr"`
	Payer        string `xml:"payer,attr"`
	Tariff       string `xml:"tariff,attr"`
	RrLimit      string `xml:"rr-limit,attr"`
	RrNum        string `xml:"rr-num,attr"`
}

SOA

Структура SOA-записи

type Soa struct {
	Text  string `xml:",chardata"`
	Mname struct {
		Text    string `xml:",chardata"`
		Name    string `xml:"name"`
		IdnName string `xml:"idn-name,omitempty"`
	} `xml:"mname"`
	Rname struct {
		Text    string `xml:",chardata"`
		Name    string `xml:"name"`
		IdnName string `xml:"idn-name,omitempty"`
	} `xml:"rname"`
	Serial  string `xml:"serial"`
	Refresh string `xml:"refresh"`
	Retry   string `xml:"retry"`
	Expire  string `xml:"expire"`
	Minimum string `xml:"minimum"`
}

NS

Структура NS-записи

type Ns struct {
	Text    string `xml:",chardata"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
}

TXT

Структура TXT-записи

type Ns struct {
	Text    string `xml:",chardata"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
}

Zone

Структура зоны

type Zone struct {
	Text       string `xml:",chardata"`
	Admin      string `xml:"admin,attr"`
	Enable     string `xml:"enable,attr"`
	HasChanges string `xml:"has-changes,attr"`
	HasPrimary string `xml:"has-primary,attr"`
	ID         string `xml:"id,attr"`
	IdnName    string `xml:"idn-name,attr"`
	Name       string `xml:"name,attr"`
	Payer      string `xml:"payer,attr"`
	Service    string `xml:"service,attr"`
	Rr         []*RR  `xml:"rr"`
}

Revision

Структура ревизии

type Revision struct {
	Text   string `xml:",chardata"`
	Date   string `xml:"date,attr"`
	Ip     string `xml:"ip,attr"`
	Number string `xml:"number,attr"`
}

Error

Структура ошибки в ответе от API

type Error struct {
	Text string `xml:",chardata"`
	Code string `xml:"code,attr"`
}

Response

Структура ответа от API

type Response struct {
	XMLName xml.Name `xml:"response"`
	Text    string   `xml:",chardata"`
	Status  string   `xml:"status"`
	Errors  struct {
		Text  string `xml:",chardata"`
		Error *error `xml:"error"`
	} `xml:"errors"`
	Data struct {
		Text     string `xml:",chardata"`
		Service  []*Service
		Zone     []*Zone     `xml:"zone"`
		Address  []*A        `xml:"address"`
		Revision []*Revision `xml:"revision"`
	} `xml:"data"`
}

Как пользоваться

Клиент лежит тут https://github.com/maetx777/nic.ru-golang-client.git, готов к использованию. В examples описано как создавать и удалять A/CNAME записи, а также как скачать зону целиком.

Создание утилиты

Далее немного опишу как создать консольную утилиту для управления апишкой, код загружен в репозиторий.

cmd/nicru/config.go

для начала создадим структуру для хранения значений вводимых аргументов

package main

import api "github.com/maetx777/nic.ru-golang-client/client"

var config = &api.Config{
	Credentials: &api.Credentials{
		OAuth2: &api.OAuth2Creds{
			ClientID: "",
			SecretID: "",
		},
		Username: "",
		Password: "",
	},
	ZoneName:       "",
	DnsServiceName: "",
	CachePath:      "",
}

cmd/nicru/main.go

создадим файл cmd/nicru/main.go:

package main

import (
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

func main() {
	cmd := &cobra.Command{
		Use:   `nicru`,
		Short: `утилита для управления записями в DNS NIC.ru`,
		Long: `oauth2 ключи нужно получить в ЛК nic.ru - https://www.nic.ru/manager/oauth.cgi?step=oauth.app_register
имя DNS-сервиса можно посмотреть по адресу https://www.nic.ru/manager/services.cgi?step=srv.my_dns&view.order_by=domain
`,
	}
	//создаём флаги для заполнения конфига
	cmd.PersistentFlags().StringVar(&config.Credentials.OAuth2.ClientID, `oauth2-client-id`, ``, `oauth2 client id`)
	cmd.PersistentFlags().StringVar(&config.Credentials.OAuth2.SecretID, `oauth2-secret-id`, ``, `oauth2 secret id`)
	cmd.PersistentFlags().StringVar(&config.Credentials.Username, `username`, ``, `логин от ЛК nic.ru (******/NIC-D)`)
	cmd.PersistentFlags().StringVar(&config.Credentials.Password, `password`, ``, `пароль от nic.ru`)
	cmd.PersistentFlags().StringVar(&config.ZoneName, `zone-name`, `example.com`, `имя DNS-зоны`)
	cmd.PersistentFlags().StringVar(&config.DnsServiceName, `service-name`, `EXAMPLE`, `имя DNS-сервиса`)
	cmd.PersistentFlags().StringVar(&config.CachePath, `cache-path`, `/tmp/.nic.ru.token`, `путь до файла, где будет храниться авторизация от API`)

	cmd.AddCommand(addACmd())   // подключаем команду add-a
	cmd.AddCommand(commitCmd()) // подключаем команду add-a
	if err := cmd.Execute(); err != nil {
		logrus.Infoln(err.Error())
	}
}

Этот файл собирает всю утилиту, в ней описаны две команды

  • addAcmd - команда для добавления А-записи

  • commitCmd - команда для фиксации изменений в зоне

cmd/nicru/config.go

для начала инициализируем структуру конфига, которую можно использовать в разных командах:

cmd/nicru/add-a.go

Создаём команду для создания А-записи.

package main

import (
	"fmt"
	api "github.com/maetx777/nic.ru-golang-client/client"
	"github.com/spf13/cobra"
)

func addACmd() *cobra.Command {

	var (
		names  []string //здесь будем хранить список записей, которые нужно создать
		target string   //здесь хранится ipv4-адрес, на которые будет ссылаться каждое имя, описанное выше
		ttl    int      //TTL для каждой записи
	)

	cmd := &cobra.Command{
		Use:   `add-a`,
		Short: `добавление A-записей`,

		Run: func(cmd *cobra.Command, args []string) {
			//инициализируем клиента
			client := api.NewClient(config)
			if response, err := client.AddA(names, target, ttl); err != nil {
				//обрабатываем ошибку
				fmt.Printf("Add A-record error: %s\n", err.Error())
				return
			} else {
				//печатаем результат
				for _, record := range response.Data.Zone[0].Rr {
					fmt.Printf("Added A-record: %s -> %s\n", record.Name, record.A.String())
				}
			}
		},
	}
	//создаём флаги для указания имён создаваемых записей, ipv4-таргета и TTL
	cmd.PersistentFlags().StringSliceVar(&names, `names`, []string{}, `имена, которые нужно создать`)
	cmd.PersistentFlags().StringVar(&target, `target`, ``, `куда нужно отправить имена (например, 1.2.3.4)`)
	cmd.PersistentFlags().IntVar(&ttl, `ttl`, 600, `TTL для созданным записей`)

	return cmd
}

Создаём команду для фиксации изменений в зоне

package main

import (
	"fmt"
	api "github.com/maetx777/nic.ru-golang-client/client"
	"github.com/spf13/cobra"
)

func commitCmd() *cobra.Command {

	cmd := &cobra.Command{
		Use:   `commit`,
		Short: `фиксация изменений`,
		Run: func(cmd *cobra.Command, args []string) {
			//инициализируем клиента
			client := api.NewClient(config)
			//коммитим результат
			if _, err := client.CommitZone(); err != nil {
				fmt.Printf("Commit error: %s\n", err.Error())
			} else {
				fmt.Printf("Zone committed\n")
			}
		},
	}

	return cmd
}

Собираем утилиту:

go build ./cmd/nicru

Тестируем запуск утилиты

$ ./nicru --help
oauth2 ключи нужно получить в ЛК nic.ru - https://www.nic.ru/manager/oauth.cgi?step=oauth.app_register
имя DNS-сервиса можно посмотреть по адресу https://www.nic.ru/manager/services.cgi?step=srv.my_dns&view.order_by=domain

Usage:
  nicru [command]

Available Commands:
  add-a       добавление A-записей
  commit      фиксация изменений
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command

Flags:
      --cache-path string         путь до файла, где будет храниться авторизация от API (default "/tmp/.nic.ru.token")
  -h, --help                      help for nicru
      --oauth2-client-id string   oauth2 client id
      --oauth2-secret-id string   oauth2 secret id
      --password string           пароль от nic.ru
      --service-name string       имя DNS-сервиса (default "EXAMPLE")
      --username string           логин от ЛК nic.ru (******/NIC-D)
      --zone-name string          имя DNS-зоны (default "example.com")

Use "nicru [command] --help" for more information about a command.

Смотрим хелп команды add-a

$ ./nicru add-a --help
добавление A-записей

Usage:
  nicru add-a [flags]

Flags:
  -h, --help            help for add-a
      --names strings   имена, которые нужно создать
      --target string   куда нужно отправить имена (например, 1.2.3.4)
      --ttl int         TTL для созданным записей (default 600)

Global Flags:
      --cache-path string         путь до файла, где будет храниться авторизация от API (default "/tmp/.nic.ru.token")
      --oauth2-client-id string   oauth2 client id
      --oauth2-secret-id string   oauth2 secret id
      --password string           пароль от nic.ru
      --service-name string       имя DNS-сервиса (default "EXAMPLE")
      --username string           логин от ЛК nic.ru (******/NIC-D)
      --zone-name string          имя DNS-зоны (default "example.com")

Тестируем создание записи

$ ./nicru add-a --oauth2-client-id *** --oauth2-secret-id *** --username ***/NIC-D --password *** --service-name EXAMPLE --zone-name example.com --names foo123 --target 127.0.0.1 --ttl 3600
Added A-record: foo123 -> 127.0.0.1

Фиксируем изменения в зоне

$ ./nicru add-a --oauth2-client-id *** --oauth2-secret-id *** --username ***/NIC-D --password *** --service-name EXAMPLE --zone-name example.com
Zone committed

Запись успешно создана, аналогичным способом можно доработать создание CNAME, удаление A/CNAME, и другие функции, реализованные в модуле.

Что можно сделать для удобства

Чтобы не заморачиваться с подачей oauth2-кредов, а также логина и пароля, мы в своей версии утилиты используем vault, прямо в пакете cmd у нас есть функция, которые, при наличии файла ~/.vault-token, идёт в ваулт, забирает все нужны креды, а дальше подставляет их в команду в качестве дефолта, таким образом, нам не нужно каждый раз указывать всю портянку с авторизацией, но, при необходимости, можно это дело переопределить при каждом запуске.

Заключение

Изначально была идея делиться со всеми какими-то, полезными, по моему мнению, разработками, мы вынуждены много писать, иногда находим готовые решения, иногда - нет, и тогда мы пишем свои велосипеды.

Комментариев в коде пока что нет, в связи с тем, что сегодня карма резко понизилась из-за комментария в посте. С учётом стремительного ухудшения кармы, в скором времени я уже ничего не смогу писать тут, так что это завершающий пост.

Клиента обязательно разовьём, всё прокомментируем, разовьём ридми и сами будем им пользоваться непосредственно из гитхаба

Желаю всем мира и добра! Готов ответить на любые вопросы.

Комментарии (14)


  1. Altaev
    09.05.2022 04:58
    -3

    Глядя на карму автора, так не хочется быть первым комментатором - мало ли что :)

    К автору и сообществу: посоветуйте, чего почитать про go для общего понимания.


    1. tommyangelo27
      09.05.2022 10:16
      +1

      Например — go.dev/tour/list или gobyexample.com


      1. Altaev
        09.05.2022 12:03

        Спасибо.


  1. shepardeg
    09.05.2022 07:04
    +6

    Не хочется быть хейтером, но ник.ру самый худший регистратор которым довелось пользоваться. Рад что кто-то пытается облегчить его использование другим


  1. ky0
    09.05.2022 09:48

    Полезная штука, но вообще в 2022 году как-то ожидаешь, что если у DNS-хостинга есть апи, то есть и Терраформ…

    З.Ы. — не совсем в тему, просто вспомнилось, что у Яндекс.Облака нет DNS-модуля для certbot`а. Может, кто-то уже решал эту проблему как-то, помимо костыля с виртуалкой, реализующей HTTP challenge?


    1. skymal4ik
      09.05.2022 10:13

      Согласен, провайдер для терраформа выглядел бы заманчивее, чем библиотека на одном языке.


    1. Borz
      09.05.2022 12:04

      Для certbot нет, но есть для acme.sh: https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_yandex.sh


      1. ky0
        09.05.2022 12:21

        Это, к сожалению, не Яндекс.Облако, а PDD — почта для домена.


    1. trawl
      09.05.2022 13:00

      А вам прям обязательно wildcard нужны? А то может проще http challenge по каждому домену завести?


      1. ky0
        09.05.2022 13:15

        Проще, не спорю — но DNS-запись быстренько добавил-удалил, а в случае HTTP хоть какой-то конфиг сайта должен торчать наружу, что не всегда удобно для внутренних сервисов.


        1. trawl
          09.05.2022 13:59

          Я в своё время отказался от вилдкарда в сторону http challenge, и добавил локейшены в nginx, которые вели в абсолютно другой root, не имеющий отношения к самим проектам. Но да, понимаю, что такое подойдёт не всем


        1. Borz
          09.05.2022 19:20

          через DNS-запись можно в 2 шага делать - сперва получать значение для TXT-записи, а потом проходить окончательную проверку и генерацию


  1. kirill-scherba
    09.05.2022 18:58
    -1

    Хороший клиент. Всегда удобней пользоваться функциями на go вместо сырых вызов rest api. Спасибо, что поделились своей разработкой. Не могу сказать, что у меня много доменов в nic.ru, но при случае смогу воспользоваться вашим клиентом.

    Спасибо.


  1. edo1h
    10.05.2022 06:38
    +3

    интересно, почему независимое решение, а не ещё один модуль для libdns?