mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
5
.github/workflows/pr_test.yml
vendored
5
.github/workflows/pr_test.yml
vendored
@ -87,7 +87,7 @@ jobs:
|
||||
Write-Host "Integration test providers: $Providers"
|
||||
echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
|
||||
env:
|
||||
PROVIDERS: "['AZURE_DNS','BIND','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']"
|
||||
PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']"
|
||||
ENV_CONTEXT: ${{ toJson(env) }}
|
||||
VARS_CONTEXT: ${{ toJson(vars) }}
|
||||
SECRETS_CONTEXT: ${{ toJson(secrets) }}
|
||||
@ -106,6 +106,7 @@ jobs:
|
||||
# Set it to the domain name to use during the test.
|
||||
AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
|
||||
BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
|
||||
BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
|
||||
CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
|
||||
CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
|
||||
CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
|
||||
@ -129,6 +130,8 @@ jobs:
|
||||
AZURE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_DNS_SUBSCRIPTION_ID }}
|
||||
AZURE_DNS_TENANT_ID: ${{ secrets.AZURE_DNS_TENANT_ID }}
|
||||
|
||||
BUNNY_DNS_API_KEY: ${{ secrets.BUNNY_DNS_API_KEY }}
|
||||
|
||||
CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
|
||||
CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
|
||||
|
||||
|
@ -36,7 +36,7 @@ changelog:
|
||||
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: 'Provider-specific changes:'
|
||||
regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*"
|
||||
regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*"
|
||||
order: 2
|
||||
- title: 'Documentation:'
|
||||
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
|
||||
|
1
OWNERS
1
OWNERS
@ -4,6 +4,7 @@ providers/axfrddns @hnrgrgr
|
||||
providers/azuredns @vatsalyagoel
|
||||
providers/azureprivatedns @matthewmgamble
|
||||
providers/bind @tlimoncelli
|
||||
providers/bunnydns @ppmathis
|
||||
providers/cloudflare @tresni
|
||||
providers/cloudns @pragmaton
|
||||
providers/cscglobal @mikenz
|
||||
|
@ -23,6 +23,7 @@ Currently supported DNS providers:
|
||||
- Azure DNS
|
||||
- Azure Private DNS
|
||||
- BIND
|
||||
- Bunny DNS
|
||||
- Cloudflare
|
||||
- ClouDNS
|
||||
- deSEC
|
||||
|
@ -104,6 +104,7 @@
|
||||
* [Azure DNS](providers/azure_dns.md)
|
||||
* [Azure Private DNS](providers/azure_private_dns.md)
|
||||
* [BIND](providers/bind.md)
|
||||
* [Bunny DNS](providers/bunny_dns.md)
|
||||
* [Cloudflare](providers/cloudflareapi.md)
|
||||
* [ClouDNS](providers/cloudns.md)
|
||||
* [CSC Global](providers/cscglobal.md)
|
||||
|
@ -20,6 +20,7 @@ If a feature is definitively not supported for whatever reason, we would also li
|
||||
| [`AZURE_DNS`](providers/azure_dns.md) | ✅ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [`AZURE_PRIVATE_DNS`](providers/azure_private_dns.md) | ✅ | ✅ | ❌ | ❌ | ❌ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [`BIND`](providers/bind.md) | ✅ | ✅ | ❌ | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| [`BUNNY_DNS`](providers/bunny_dns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
| [`CLOUDFLAREAPI`](providers/cloudflareapi.md) | ✅ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❌ | ✅ | ✅ | ✅ |
|
||||
| [`CLOUDNS`](providers/cloudns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ |
|
||||
| [`CSCGLOBAL`](providers/cscglobal.md) | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ✅ |
|
||||
@ -110,6 +111,7 @@ Providers in this category and their maintainers are:
|
||||
|[`AZURE_PRIVATE_DNS`](providers/azure_private_dns.md)|@matthewmgamble|
|
||||
|[`AKAMAIEDGEDNS`](providers/akamaiedgedns.md)|@svernick|
|
||||
|[`AXFRDDNS`](providers/axfrddns.md)|@hnrgrgr|
|
||||
|[`BUNNY_DNS`](providers/bunny_dns.md)|@ppmathis|
|
||||
|[`CLOUDFLAREAPI`](providers/cloudflareapi.md)|@tresni|
|
||||
|[`CLOUDNS`](providers/cloudns.md)|@pragmaton|
|
||||
|[`CSCGLOBAL`](providers/cscglobal.md)|@Air-New-Zealand|
|
||||
@ -154,7 +156,6 @@ code to support this provider, we'd be glad to help in any way.
|
||||
|
||||
* [1984 Hosting](https://github.com/StackExchange/dnscontrol/issues/1251) (#1251)
|
||||
* [Alibaba Cloud DNS](https://github.com/StackExchange/dnscontrol/issues/420)(#420)
|
||||
* [BunnyDNS](https://github.com/StackExchange/dnscontrol/issues/2265)(#2265)
|
||||
* [Constellix (DNSMadeEasy)](https://github.com/StackExchange/dnscontrol/issues/842) (#842)
|
||||
* [CoreDNS](https://github.com/StackExchange/dnscontrol/issues/1284) (#1284)
|
||||
* [EU.ORG](https://github.com/StackExchange/dnscontrol/issues/1176) (#1176)
|
||||
|
69
documentation/providers/bunny_dns.md
Normal file
69
documentation/providers/bunny_dns.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Configuration
|
||||
|
||||
To use this provider, add an entry to `creds.json` with `TYPE` set to `BUNNY_DNS` along with
|
||||
your [Bunny API Key](https://dash.bunny.net/account/settings).
|
||||
|
||||
Example:
|
||||
|
||||
{% code title="creds.json" %}
|
||||
```json
|
||||
{
|
||||
"bunny_dns": {
|
||||
"TYPE": "BUNNY_DNS",
|
||||
"api_key": "your-bunny-api-key"
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
You can also use environment variables:
|
||||
|
||||
```shell
|
||||
export BUNNY_API_KEY=XXXXXXXXX
|
||||
```
|
||||
|
||||
{% code title="creds.json" %}
|
||||
```json
|
||||
{
|
||||
"bunny_dns": {
|
||||
"TYPE": "BUNNY_DNS",
|
||||
"api_key": "$BUNNY_API_KEY"
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
## Metadata
|
||||
|
||||
This provider does not recognize any special metadata fields unique to Bunny DNS.
|
||||
|
||||
## Usage
|
||||
|
||||
An example configuration:
|
||||
|
||||
{% code title="dnsconfig.js" %}
|
||||
```javascript
|
||||
var REG_NONE = NewRegistrar("none");
|
||||
var DSP_BUNNY_DNS = NewDnsProvider("bunny_dns");
|
||||
|
||||
D("example.com", REG_NONE, DnsProvider(DSP_BUNNY_DNS),
|
||||
A("test", "1.2.3.4")
|
||||
);
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
# Activation
|
||||
|
||||
DNSControl depends on the [Bunny API](https://docs.bunny.net/reference/bunnynet-api-overview) to manage your DNS
|
||||
records. You will need to generate an [API key](https://dash.bunny.net/account/settings) to use this provider.
|
||||
|
||||
## New domains
|
||||
|
||||
If a domain does not exist in your Bunny account, DNSControl will automatically add it with the `push` command.
|
||||
|
||||
## Caveats
|
||||
|
||||
- Bunny DNS does not support dual-hosting or configuring custom TTLs for NS records on the zone apex.
|
||||
- While custom nameservers are properly recognized by this provider, it is currently not possible to configure them.
|
||||
- Any custom record types like Script, Redirect, Flatten or Pull Zone are currently not supported by this provider. Such
|
||||
records will be completely ignored by DNSControl and left as-is.
|
@ -38,6 +38,11 @@
|
||||
"TYPE": "BIND",
|
||||
"domain": "$BIND_DOMAIN"
|
||||
},
|
||||
"BUNNY_DNS": {
|
||||
"TYPE": "BUNNY_DNS",
|
||||
"domain": "$BUNNY_DNS_DOMAIN",
|
||||
"api_key": "$BUNNY_DNS_API_KEY"
|
||||
},
|
||||
"CLOUDFLAREAPI": {
|
||||
"TYPE": "CLOUDFLAREAPI",
|
||||
"accountid": "$CLOUDFLAREAPI_ACCOUNTID",
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/azuredns"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/azureprivatedns"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/bind"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/bunnydns"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudns"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/cscglobal"
|
||||
|
227
providers/bunnydns/api.go
Normal file
227
providers/bunnydns/api.go
Normal file
@ -0,0 +1,227 @@
|
||||
package bunnydns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||
"golang.org/x/exp/slices"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://api.bunny.net"
|
||||
pageSize = 100
|
||||
)
|
||||
|
||||
type zone struct {
|
||||
ID int64 `json:"Id"`
|
||||
Domain string `json:"Domain"`
|
||||
Nameserver1 string `json:"Nameserver1"`
|
||||
Nameserver2 string `json:"Nameserver2"`
|
||||
}
|
||||
|
||||
func (zone *zone) Nameservers() []string {
|
||||
return []string{zone.Nameserver1, zone.Nameserver2}
|
||||
}
|
||||
|
||||
type record struct {
|
||||
ID int64 `json:"Id,omitempty"`
|
||||
Type recordType `json:"Type"`
|
||||
Name string `json:"Name"`
|
||||
Value string `json:"Value"`
|
||||
Disabled bool `json:"Disabled"`
|
||||
TTL uint32 `json:"Ttl"`
|
||||
Flags uint8 `json:"Flags"`
|
||||
Priority uint16 `json:"Priority"`
|
||||
Weight uint16 `json:"Weight"`
|
||||
Port uint16 `json:"Port"`
|
||||
Tag string `json:"Tag"`
|
||||
}
|
||||
|
||||
type listZonesResponse struct {
|
||||
Items []zone `json:"Items"`
|
||||
TotalItems int32 `json:"TotalItems"`
|
||||
HasMoreItems bool `json:"HasMoreItems"`
|
||||
}
|
||||
|
||||
type getZoneResponse struct {
|
||||
zone
|
||||
Records []record `json:"Records"`
|
||||
}
|
||||
|
||||
type queryParams map[string]string
|
||||
|
||||
func (b *bunnydnsProvider) getImplicitRecordConfigs(zone *zone) (models.Records, error) {
|
||||
nameservers := zone.Nameservers()
|
||||
records := make(models.Records, 0, len(nameservers))
|
||||
|
||||
// NS records on the zone apex must be implicitly added, as Bunny DNS does not expose them via API
|
||||
for _, ns := range nameservers {
|
||||
rc := &models.RecordConfig{
|
||||
Type: "NS",
|
||||
Original: &record{},
|
||||
}
|
||||
rc.SetLabelFromFQDN(zone.Domain, zone.Domain)
|
||||
if err := rc.SetTarget(ns + "."); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records = append(records, rc)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) findZoneByDomain(domain string) (*zone, error) {
|
||||
if b.zones == nil {
|
||||
zones, err := b.getAllZones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.zones = make(map[string]*zone, len(zones))
|
||||
for _, zone := range zones {
|
||||
b.zones[zone.Domain] = zone
|
||||
}
|
||||
}
|
||||
|
||||
zone, ok := b.zones[domain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not a zone in this BUNNY_DNS account", domain)
|
||||
}
|
||||
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) getAllZones() ([]*zone, error) {
|
||||
var zones []*zone
|
||||
page := 1
|
||||
|
||||
for {
|
||||
res := listZonesResponse{}
|
||||
query := queryParams{"page": strconv.Itoa(page), "perPage": strconv.Itoa(pageSize)}
|
||||
if err := b.request("GET", "/dnszone", query, nil, &res, nil); err != nil {
|
||||
return nil, fmt.Errorf("could not fetch zones: %w", err)
|
||||
}
|
||||
|
||||
if zones == nil {
|
||||
zones = make([]*zone, 0, res.TotalItems)
|
||||
}
|
||||
for i := range res.Items {
|
||||
zones = append(zones, &res.Items[i])
|
||||
}
|
||||
|
||||
if !res.HasMoreItems {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) createZone(domain string) (*zone, error) {
|
||||
zone := &zone{}
|
||||
body := map[string]string{"domain": domain}
|
||||
err := b.request("POST", "/dnszone", nil, body, &zone, []int{http.StatusCreated})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.zones[domain] = zone
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) getAllRecords(zoneID int64) ([]*record, error) {
|
||||
zone := &getZoneResponse{}
|
||||
err := b.request("GET", fmt.Sprintf("/dnszone/%d", zoneID), nil, nil, zone, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := make([]*record, 0, len(zone.Records))
|
||||
for i := range zone.Records {
|
||||
records = append(records, &zone.Records[i])
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) createRecord(zoneID int64, r *record) error {
|
||||
url := fmt.Sprintf("/dnszone/%d/records", zoneID)
|
||||
return b.request("PUT", url, nil, r, nil, []int{http.StatusCreated})
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) modifyRecord(zoneID int64, recordID int64, r *record) error {
|
||||
url := fmt.Sprintf("/dnszone/%d/records/%d", zoneID, recordID)
|
||||
return b.request("POST", url, nil, r, nil, []int{http.StatusNoContent})
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) deleteRecord(zoneID, recordID int64) error {
|
||||
url := fmt.Sprintf("/dnszone/%d/records/%d", zoneID, recordID)
|
||||
return b.request("DELETE", url, nil, nil, nil, []int{http.StatusNoContent})
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) request(method, endpoint string, query queryParams, body, target any, validStatus []int) error {
|
||||
if validStatus == nil {
|
||||
validStatus = []int{http.StatusOK}
|
||||
}
|
||||
|
||||
var requestBody io.Reader
|
||||
if body != nil {
|
||||
requestBodyJSON, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestBody = bytes.NewBuffer(requestBodyJSON)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("AccessKey", b.apiKey)
|
||||
if requestBody != nil {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
if query != nil {
|
||||
q := req.URL.Query()
|
||||
for k, v := range query {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanup := func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
printer.Printf("BUNNY_DNS: Could not close response body after API call: %q\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(validStatus, resp.StatusCode) {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
printer.Println(fmt.Sprintf("BUNNY_DNS: Bad API response for %s %s: %s", method, endpoint, string(data)))
|
||||
cleanup()
|
||||
return fmt.Errorf("bad status code from BUNNY_DNS: %d not in %v", resp.StatusCode, validStatus)
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
cleanup()
|
||||
return nil
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(target)
|
||||
cleanup()
|
||||
return err
|
||||
}
|
17
providers/bunnydns/auditrecords.go
Normal file
17
providers/bunnydns/auditrecords.go
Normal file
@ -0,0 +1,17 @@
|
||||
package bunnydns
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
|
||||
)
|
||||
|
||||
// AuditRecords returns a list of errors corresponding to the records
|
||||
// that aren't supported by this provider. If all records are
|
||||
// supported, an empty list is returned.
|
||||
func AuditRecords(records []*models.RecordConfig) []error {
|
||||
a := rejectif.Auditor{}
|
||||
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-01-02
|
||||
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2024-01-02
|
||||
|
||||
return a.Audit(records)
|
||||
}
|
61
providers/bunnydns/bunnydnsProvider.go
Normal file
61
providers/bunnydns/bunnydnsProvider.go
Normal file
@ -0,0 +1,61 @@
|
||||
package bunnydns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||
)
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
providers.CanAutoDNSSEC: providers.Cannot(),
|
||||
providers.CanGetZones: providers.Can(),
|
||||
providers.CanUseAlias: providers.Cannot(),
|
||||
providers.CanUseCAA: providers.Can(),
|
||||
providers.CanUseDHCID: providers.Cannot(),
|
||||
providers.CanUseDS: providers.Cannot(),
|
||||
providers.CanUseDSForChildren: providers.Cannot(),
|
||||
providers.CanUseLOC: providers.Cannot(),
|
||||
providers.CanUseNAPTR: providers.Cannot(),
|
||||
providers.CanUsePTR: providers.Can(),
|
||||
providers.CanUseSOA: providers.Cannot(),
|
||||
providers.CanUseSRV: providers.Can(),
|
||||
providers.CanUseSSHFP: providers.Cannot(),
|
||||
providers.CanUseTLSA: providers.Cannot(),
|
||||
providers.DocCreateDomains: providers.Can(),
|
||||
providers.DocDualHost: providers.Cannot(),
|
||||
providers.DocOfficiallySupported: providers.Cannot(),
|
||||
}
|
||||
|
||||
type bunnydnsProvider struct {
|
||||
apiKey string
|
||||
zones map[string]*zone
|
||||
}
|
||||
|
||||
func init() {
|
||||
fns := providers.DspFuncs{
|
||||
Initializer: newBunnydns,
|
||||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
providers.RegisterDomainServiceProviderType("BUNNY_DNS", fns, features)
|
||||
}
|
||||
|
||||
func newBunnydns(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
apiKey := settings["api_key"]
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("missing BUNNY_DNS api_key")
|
||||
}
|
||||
|
||||
return &bunnydnsProvider{
|
||||
apiKey: apiKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
zone, err := b.findZoneByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.ToNameservers(zone.Nameservers())
|
||||
}
|
161
providers/bunnydns/convert.go
Normal file
161
providers/bunnydns/convert.go
Normal file
@ -0,0 +1,161 @@
|
||||
package bunnydns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"golang.org/x/exp/slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var fqdnTypes = []recordType{recordTypeCNAME, recordTypeMX, recordTypeNS, recordTypePTR, recordTypeSRV}
|
||||
|
||||
func fromRecordConfig(rc *models.RecordConfig) (*record, error) {
|
||||
r := record{
|
||||
Type: recordTypeFromString(rc.Type),
|
||||
Name: rc.GetLabel(),
|
||||
Value: rc.GetTargetField(),
|
||||
TTL: rc.TTL,
|
||||
}
|
||||
|
||||
// While Bunny DNS does not use trailing dots, it still accepts and even preserves them for certain record types.
|
||||
// To avoid confusion, any trailing dots are removed from the record value.
|
||||
if slices.Contains(fqdnTypes, r.Type) && strings.HasSuffix(r.Value, ".") {
|
||||
r.Value = strings.TrimSuffix(r.Value, ".")
|
||||
}
|
||||
|
||||
switch r.Type {
|
||||
case recordTypeNS:
|
||||
if r.Name == "" {
|
||||
r.TTL = 0
|
||||
}
|
||||
case recordTypeSRV:
|
||||
r.Priority = rc.SrvPriority
|
||||
r.Weight = rc.SrvWeight
|
||||
r.Port = rc.SrvPort
|
||||
case recordTypeCAA:
|
||||
r.Flags = rc.CaaFlag
|
||||
r.Tag = rc.CaaTag
|
||||
case recordTypeMX:
|
||||
r.Priority = rc.MxPreference
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func toRecordConfig(domain string, r *record) (*models.RecordConfig, error) {
|
||||
rc := models.RecordConfig{
|
||||
Type: recordTypeToString(r.Type),
|
||||
TTL: r.TTL,
|
||||
Original: r,
|
||||
}
|
||||
rc.SetLabel(r.Name, domain)
|
||||
|
||||
// Bunny DNS always operates with fully-qualified names and does not use any trailing dots.
|
||||
// If a record already contains a trailing dot, which the provider UI also accepts, the record value is left as-is.
|
||||
recordValue := r.Value
|
||||
if slices.Contains(fqdnTypes, r.Type) && !strings.HasSuffix(r.Value, ".") {
|
||||
recordValue = dnsutil.AddOrigin(r.Value+".", domain)
|
||||
}
|
||||
|
||||
var err error
|
||||
switch rc.Type {
|
||||
case "CAA":
|
||||
err = rc.SetTargetCAA(r.Flags, r.Tag, recordValue)
|
||||
case "MX":
|
||||
err = rc.SetTargetMX(r.Priority, recordValue)
|
||||
case "SRV":
|
||||
err = rc.SetTargetSRV(r.Priority, r.Weight, r.Port, recordValue)
|
||||
default:
|
||||
err = rc.PopulateFromStringFunc(rc.Type, recordValue, domain, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &rc, nil
|
||||
}
|
||||
|
||||
type recordType int
|
||||
|
||||
const (
|
||||
recordTypeA recordType = 0
|
||||
recordTypeAAAA recordType = 1
|
||||
recordTypeCNAME recordType = 2
|
||||
recordTypeTXT recordType = 3
|
||||
recordTypeMX recordType = 4
|
||||
recordTypeRedirect recordType = 5
|
||||
recordTypeFlatten recordType = 6
|
||||
recordTypePullZone recordType = 7
|
||||
recordTypeSRV recordType = 8
|
||||
recordTypeCAA recordType = 9
|
||||
recordTypePTR recordType = 10
|
||||
recordTypeScript recordType = 11
|
||||
recordTypeNS recordType = 12
|
||||
)
|
||||
|
||||
func recordTypeFromString(t string) recordType {
|
||||
switch t {
|
||||
case "A":
|
||||
return recordTypeA
|
||||
case "AAAA":
|
||||
return recordTypeAAAA
|
||||
case "CNAME":
|
||||
return recordTypeCNAME
|
||||
case "TXT":
|
||||
return recordTypeTXT
|
||||
case "MX":
|
||||
return recordTypeMX
|
||||
case "REDIRECT":
|
||||
return recordTypeRedirect
|
||||
case "FLATTEN":
|
||||
return recordTypeFlatten
|
||||
case "PULL_ZONE":
|
||||
return recordTypePullZone
|
||||
case "SRV":
|
||||
return recordTypeSRV
|
||||
case "CAA":
|
||||
return recordTypeCAA
|
||||
case "PTR":
|
||||
return recordTypePTR
|
||||
case "SCRIPT":
|
||||
return recordTypeScript
|
||||
case "NS":
|
||||
return recordTypeNS
|
||||
default:
|
||||
panic(fmt.Errorf("BUNNY_DNS: rtype %v unimplemented", t))
|
||||
}
|
||||
}
|
||||
|
||||
func recordTypeToString(t recordType) string {
|
||||
switch t {
|
||||
case recordTypeA:
|
||||
return "A"
|
||||
case recordTypeAAAA:
|
||||
return "AAAA"
|
||||
case recordTypeCNAME:
|
||||
return "CNAME"
|
||||
case recordTypeTXT:
|
||||
return "TXT"
|
||||
case recordTypeMX:
|
||||
return "MX"
|
||||
case recordTypeRedirect:
|
||||
return "REDIRECT"
|
||||
case recordTypeFlatten:
|
||||
return "FLATTEN"
|
||||
case recordTypePullZone:
|
||||
return "PULL_ZONE"
|
||||
case recordTypeSRV:
|
||||
return "SRV"
|
||||
case recordTypeCAA:
|
||||
return "CAA"
|
||||
case recordTypePTR:
|
||||
return "PTR"
|
||||
case recordTypeScript:
|
||||
return "SCRIPT"
|
||||
case recordTypeNS:
|
||||
return "NS"
|
||||
default:
|
||||
panic(fmt.Errorf("BUNNY_DNS: native rtype %v unimplemented", t))
|
||||
}
|
||||
}
|
32
providers/bunnydns/listzones.go
Normal file
32
providers/bunnydns/listzones.go
Normal file
@ -0,0 +1,32 @@
|
||||
package bunnydns
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||
|
||||
func (b *bunnydnsProvider) ListZones() ([]string, error) {
|
||||
zones, err := b.getAllZones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zoneNames := make([]string, 0, len(zones))
|
||||
for _, zone := range zones {
|
||||
zoneNames = append(zoneNames, zone.Domain)
|
||||
}
|
||||
|
||||
return zoneNames, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) EnsureZoneExists(domain string) error {
|
||||
_, err := b.findZoneByDomain(domain)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
zone, err := b.createZone(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Warnf("BUNNY_DNS: Added zone %s with ID %d", domain, zone.ID)
|
||||
return nil
|
||||
}
|
147
providers/bunnydns/records.go
Normal file
147
providers/bunnydns/records.go
Normal file
@ -0,0 +1,147 @@
|
||||
package bunnydns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func (b *bunnydnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
||||
zone, err := b.findZoneByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nativeRecs, err := b.getAllRecords(zone.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
implicitRecs, err := b.getImplicitRecordConfigs(zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recs := make(models.Records, 0, len(nativeRecs)+len(implicitRecs))
|
||||
recs = append(recs, implicitRecs...)
|
||||
|
||||
// Define a list of record types that are currently not supported by this provider.
|
||||
unsupportedTypes := []recordType{
|
||||
recordTypeRedirect,
|
||||
recordTypeFlatten,
|
||||
recordTypePullZone,
|
||||
recordTypeScript,
|
||||
}
|
||||
|
||||
// Loop through all native records and convert them to standardized RecordConfigs
|
||||
// Unsupported record types are ignored with a warning and will remain untouched in the zone.
|
||||
for _, nativeRec := range nativeRecs {
|
||||
if slices.Contains(unsupportedTypes, nativeRec.Type) {
|
||||
printer.Warnf("BUNNY_DNS: ignoring unsupported record type %s\n", recordTypeToString(nativeRec.Type))
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := toRecordConfig(zone.Domain, nativeRec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recs = append(recs, rc)
|
||||
}
|
||||
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
|
||||
// Bunny DNS never returns NS records for the apex domain, so these are artificially added when retrieving records.
|
||||
// As no TTL can be configured or retrieved for these NS records, we set it to 0 to avoid unnecessary updates.
|
||||
for _, rc := range dc.Records {
|
||||
if rc.Name == "@" && rc.Type == "NS" {
|
||||
rc.TTL = 0
|
||||
}
|
||||
}
|
||||
|
||||
zone, err := b.findZoneByDomain(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instructions, err := diff2.ByRecord(existing, dc, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var corrections []*models.Correction
|
||||
for _, inst := range instructions {
|
||||
switch inst.Type {
|
||||
case diff2.REPORT:
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: inst.MsgsJoined,
|
||||
})
|
||||
case diff2.CREATE:
|
||||
corrections = append(corrections, b.mkCreateCorrection(
|
||||
zone.ID, inst.New[0], inst.Msgs[0],
|
||||
))
|
||||
case diff2.CHANGE:
|
||||
corrections = append(corrections, b.mkChangeCorrection(
|
||||
zone.ID, inst.Old[0], inst.New[0], inst.Msgs[0],
|
||||
))
|
||||
case diff2.DELETE:
|
||||
corrections = append(corrections, b.mkDeleteCorrection(
|
||||
zone.ID, inst.Old[0], inst.Msgs[0],
|
||||
))
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
|
||||
}
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) mkCreateCorrection(zoneID int64, newRec *models.RecordConfig, msg string) *models.Correction {
|
||||
return &models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
desired, err := fromRecordConfig(newRec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.createRecord(zoneID, desired)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) mkChangeCorrection(zoneID int64, oldRec, newRec *models.RecordConfig, msg string) *models.Correction {
|
||||
return &models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
existingID := oldRec.Original.(*record).ID
|
||||
if existingID == 0 {
|
||||
return fmt.Errorf("BUNNY_DNS: cannot change implicit records")
|
||||
}
|
||||
|
||||
desired, err := fromRecordConfig(newRec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.modifyRecord(zoneID, existingID, desired)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bunnydnsProvider) mkDeleteCorrection(zoneID int64, oldRec *models.RecordConfig, msg string) *models.Correction {
|
||||
return &models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
existingID := oldRec.Original.(*record).ID
|
||||
if existingID == 0 {
|
||||
return fmt.Errorf("BUNNY_DNS: cannot delete implicit records")
|
||||
}
|
||||
|
||||
return b.deleteRecord(zoneID, existingID)
|
||||
},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user