Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/pr_integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
Write-Host "Integration test providers: $Providers"
echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
env:
PROVIDERS: "['ALIDNS', 'AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','GIDINET','HEDNS','HETZNER_V2','HUAWEICLOUD','INWX','JOKER','MIKROTIK','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP','UNIFI']"
PROVIDERS: "['ALIDNS', 'AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','GIDINET','HEDNS','HETZNER_V2','HUAWEICLOUD','INWX','JOKER','MIKROTIK','MYTHICBEASTS', 'NAMEDOTCOM','NETBIRD','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP','UNIFI']"
ENV_CONTEXT: ${{ toJson(env) }}
VARS_CONTEXT: ${{ toJson(vars) }}
SECRETS_CONTEXT: ${{ toJson(secrets) }}
Expand Down Expand Up @@ -108,6 +108,7 @@ jobs:
MIKROTIK_DOMAIN: ${{ vars.MIKROTIK_DOMAIN }}
MYTHICBEASTS_DOMAIN: ${{ vars.MYTHICBEASTS_DOMAIN }}
NAMEDOTCOM_DOMAIN: ${{ vars.NAMEDOTCOM_DOMAIN }}
NETBIRD_DOMAIN: ${{ vars.NETBIRD_DOMAIN }}
NS1_DOMAIN: ${{ vars.NS1_DOMAIN }}
POWERDNS_DOMAIN: ${{ vars.POWERDNS_DOMAIN }}
ROUTE53_DOMAIN: ${{ vars.ROUTE53_DOMAIN }}
Expand Down Expand Up @@ -197,6 +198,8 @@ jobs:
NAMEDOTCOM_URL: ${{ secrets.NAMEDOTCOM_URL }}
NAMEDOTCOM_USER: ${{ secrets.NAMEDOTCOM_USER }}
#
NETBIRD_TOKEN: ${{ secrets.NETBIRD_TOKEN }}
#
NS1_TOKEN: ${{ secrets.NS1_TOKEN }}
#
POWERDNS_APIKEY: ${{ secrets.POWERDNS_APIKEY }}
Expand Down
1 change: 1 addition & 0 deletions OWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ providers/mikrotik @hedger
providers/mythicbeasts @tomfitzhenry
providers/namecheap @willpower232
# providers/namedotcom NEEDS VOLUNTEER
providers/netbird @yzqzss
providers/netcup @kordianbruck
providers/netlify @SphericalKat
providers/ns1 @costasd
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Currently supported DNS providers:
- Mythic Beasts
- Name.com
- Namecheap
- NetBird
- Netcup
- Netlify
- NS1
Expand Down
1 change: 1 addition & 0 deletions documentation/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
* [Mythic Beasts](provider/mythicbeasts.md)
* [Namecheap](provider/namecheap.md)
* [Name.com](provider/namedotcom.md)
* [NetBird](provider/netbird.md)
* [Netcup](provider/netcup.md)
* [Netlify](provider/netlify.md)
* [NS1](provider/ns1.md)
Expand Down
6 changes: 6 additions & 0 deletions documentation/provider/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Jump to a table:
| [`MYTHICBEASTS`](mythicbeasts.md) | ❌ | ✅ | ❌ |
| [`NAMECHEAP`](namecheap.md) | ❌ | ✅ | ✅ |
| [`NAMEDOTCOM`](namedotcom.md) | ❌ | ✅ | ✅ |
| [`NETBIRD`](netbird.md) | ❌ | ✅ | ❌ |
| [`NETCUP`](netcup.md) | ❌ | ✅ | ❌ |
| [`NETLIFY`](netlify.md) | ❌ | ✅ | ❌ |
| [`NS1`](ns1.md) | ❌ | ✅ | ❌ |
Expand Down Expand Up @@ -136,6 +137,7 @@ Jump to a table:
| [`MYTHICBEASTS`](mythicbeasts.md) | ✅ | ✅ | ❌ | ✅ |
| [`NAMECHEAP`](namecheap.md) | ✅ | ❌ | ❌ | ✅ |
| [`NAMEDOTCOM`](namedotcom.md) | ❔ | ✅ | ❌ | ✅ |
| [`NETBIRD`](netbird.md) | ✅ | ❌ | ✅ | ✅ |
| [`NETCUP`](netcup.md) | ❔ | ❌ | ❌ | ❌ |
| [`NETLIFY`](netlify.md) | ✅ | ❌ | ❌ | ✅ |
| [`NS1`](ns1.md) | ✅ | ✅ | ✅ | ✅ |
Expand Down Expand Up @@ -199,6 +201,7 @@ Jump to a table:
| [`MYTHICBEASTS`](mythicbeasts.md) | ❌ | ❔ | ❌ | ✅ | ❔ |
| [`NAMECHEAP`](namecheap.md) | ✅ | ❔ | ❌ | ❌ | ❔ |
| [`NAMEDOTCOM`](namedotcom.md) | ✅ | ❔ | ❌ | ❌ | ❔ |
| [`NETBIRD`](netbird.md) | ❌ | ❌ | ❌ | ❌ | ❌ |
| [`NETCUP`](netcup.md) | ❔ | ❔ | ❌ | ❌ | ❔ |
| [`NETLIFY`](netlify.md) | ✅ | ❔ | ❌ | ❌ | ❔ |
| [`NS1`](ns1.md) | ✅ | ✅ | ❌ | ✅ | ❔ |
Expand Down Expand Up @@ -260,6 +263,7 @@ Jump to a table:
| [`MYTHICBEASTS`](mythicbeasts.md) | ❔ | ❔ | ✅ | ❔ |
| [`NAMECHEAP`](namecheap.md) | ❔ | ❔ | ❌ | ❔ |
| [`NAMEDOTCOM`](namedotcom.md) | ❔ | ❔ | ✅ | ❔ |
| [`NETBIRD`](netbird.md) | ❌ | ❌ | ❌ | ❌ |
| [`NETCUP`](netcup.md) | ❔ | ❔ | ✅ | ❔ |
| [`NETLIFY`](netlify.md) | ❔ | ❌ | ✅ | ❔ |
| [`NS1`](ns1.md) | ✅ | ✅ | ✅ | ✅ |
Expand Down Expand Up @@ -320,6 +324,7 @@ Jump to a table:
| [`MIKROTIK`](mikrotik.md) | ❌ | ❌ | ❔ | ❌ | ❌ |
| [`MYTHICBEASTS`](mythicbeasts.md) | ✅ | ❔ | ❔ | ✅ | ✅ |
| [`NAMECHEAP`](namecheap.md) | ✅ | ❔ | ❔ | ❔ | ❌ |
| [`NETBIRD`](netbird.md) | ❌ | ❌ | ❌ | ❌ | ❌ |
| [`NETCUP`](netcup.md) | ✅ | ❔ | ❔ | ❔ | ✅ |
| [`NETLIFY`](netlify.md) | ✅ | ❔ | ❔ | ❌ | ❌ |
| [`NS1`](ns1.md) | ✅ | ✅ | ❔ | ❔ | ✅ |
Expand Down Expand Up @@ -368,6 +373,7 @@ Jump to a table:
| [`JOKER`](joker.md) | ❔ | ❌ | ❌ |
| [`LOOPIA`](loopia.md) | ❌ | ❌ | ❌ |
| [`MIKROTIK`](mikrotik.md) | ❌ | ❔ | ❌ |
| [`NETBIRD`](netbird.md) | ❌ | ❌ | ❌ |
| [`NETLIFY`](netlify.md) | ❌ | ❔ | ❌ |
| [`NS1`](ns1.md) | ✅ | ❔ | ✅ |
| [`ORACLE`](oracle.md) | ❔ | ❔ | ❌ |
Expand Down
76 changes: 76 additions & 0 deletions documentation/provider/netbird.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## Configuration

To use this provider, add an entry to `creds.json` with `TYPE` set to `NETBIRD` along with a NetBird API token.

Example:

{% code title="creds.json" %}
```json
{
"netbird": {
"TYPE": "NETBIRD",
"token": "your-netbird-api-token"
}
}
```
{% endcode %}

## Metadata

This provider recognizes the following metadata fields:

| Key | Type | Value | Description |
|-------|------|---------|-------------|
| `enabled` | string | `"true"`/`"false"` | Whether the zone is enabled. |
| `enable_search_domain` | string | `"true"`/`"false"` | Whether to enable this zone as a search domain. |

**Note:** If metadata fields are not set, DNSControl will leave them unchanged in NetBird.

## Usage

An example configuration:

{% code title="dnsconfig.js" %}
```javascript
var DSP_NETBIRD = NewDnsProvider("netbird");

D("example.com", REG_DNSIMPLE, DnsProvider(DSP_NETBIRD),
{ no_ns: "true" }, // NetBird does not expose nameservers
A("test", "1.2.3.4"),
AAAA("ipv6test", "2001:db8::1"),
CNAME("www", "example.com"),
);
```
{% endcode %}

**Note:** NetBird does not expose nameservers, so `{no_ns: "true"}` should be set on all domains to suppress the "Skipping registrar" warning.

To configure zone options, use metadata:

{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_DNSIMPLE,
{
no_ns: "true",
enabled: "true",
enable_search_domain: "true",
},
DnsProvider(DSP_NETBIRD),
A("test", "1.2.3.4"),
);
```
{% endcode %}

## Activation

NetBird depends on a NetBird API token. You can generate a personal access token in the NetBird dashboard.

## Supported Record Types

NetBird API currently supports the following DNS record types:

- **A**
- **AAAA**
- **CNAME**

For more information, see the [NetBird API documentation](https://docs.netbird.io/api/resources/dns-zones).
5 changes: 5 additions & 0 deletions integrationTest/profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@
"apiuser": "$NAMEDOTCOM_USER",
"domain": "$NAMEDOTCOM_DOMAIN"
},
"NETBIRD": {
"TYPE": "NETBIRD",
"domain": "$NETBIRD_DOMAIN",
"token": "$NETBIRD_TOKEN"
},
"NETCUP": {
"TYPE": "NETCUP",
"api-key": "$NETCUP_KEY",
Expand Down
1 change: 1 addition & 0 deletions pkg/providers/_all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v4/providers/mythicbeasts"
_ "github.com/StackExchange/dnscontrol/v4/providers/namecheap"
_ "github.com/StackExchange/dnscontrol/v4/providers/namedotcom"
_ "github.com/StackExchange/dnscontrol/v4/providers/netbird"
_ "github.com/StackExchange/dnscontrol/v4/providers/netcup"
_ "github.com/StackExchange/dnscontrol/v4/providers/netlify"
_ "github.com/StackExchange/dnscontrol/v4/providers/ns1"
Expand Down
76 changes: 76 additions & 0 deletions providers/netbird/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package netbird

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)

var initBackoff = time.Second * 2

const maxBackoff = time.Second * 30

// doRequest makes an HTTP request to the NetBird API.
func (api *netbirdProvider) doRequest(method, path string, body interface{}, result interface{}) error {
url := api.apiURL + path

var backoff = initBackoff

retry:
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
bodyReader = bytes.NewReader(jsonBody)
}

req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Authorization", "Token "+api.token)
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

resp, err := api.client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

// Handle rate limiting
if resp.StatusCode == http.StatusTooManyRequests {
log.Printf("[NETBIRD] Rate limited. Sleeping %v before retry...", backoff)
time.Sleep(backoff)
backoff = min(backoff*2, maxBackoff)
goto retry
}
// Reset backoff on success
backoff = initBackoff

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}

if result != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, result); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
}

return nil
}
84 changes: 84 additions & 0 deletions providers/netbird/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package netbird

import (
"strings"

"github.com/StackExchange/dnscontrol/v4/models"
dnsutilv1 "github.com/miekg/dns/dnsutil"
)

// nativeToRecordConfig converts a NetBird record to a dnscontrol RecordConfig.
func nativeToRecordConfig(domain string, r *Record) (*models.RecordConfig, error) {
// NetBird API returns FQDNs, so we need to handle them properly
name := r.Name

// If the name doesn't end with a dot, it might be a FQDN from NetBird
// Check if it already contains the domain
if len(name) > 0 && name[len(name)-1] != '.' {
// Name doesn't end with dot, check if it's already a FQDN
if strings.HasSuffix(name, domain) {
// FQDN, add the dot
name = name + "."
} else {
// short name, use dnsutilv1.AddOrigin
name = dnsutilv1.AddOrigin(r.Name, domain)
}
} else if len(name) > 0 && name[len(name)-1] == '.' {
// FQDN, already has the dot, do nothing
} else {
// Empty name (apex record)
name = dnsutilv1.AddOrigin(r.Name, domain)
}

target := r.Content
// Make target FQDN for CNAME records
if r.Type == "CNAME" {
if target == "@" {
target = domain
}
if target != "" && target[len(target)-1] != '.' {
target = target + "."
}
}

rc := &models.RecordConfig{
Type: r.Type,
TTL: uint32(r.TTL),
Original: r,
}
rc.SetLabelFromFQDN(name, domain)

switch r.Type {
default:
if err := rc.SetTarget(target); err != nil {
return nil, err
}
}
return rc, nil
}

// recordConfigToNative converts a dnscontrol RecordConfig to a NetBird record.
func recordConfigToNative(rc *models.RecordConfig, _ string) *CreateRecordRequest {
// Remove trailing dot as NetBird API doesn't expect it
name := rc.GetLabelFQDN()
if len(name) > 0 && name[len(name)-1] == '.' {
name = name[:len(name)-1]
}

target := rc.GetTargetField()

switch rc.Type {
case "CNAME":
// Remove trailing dot
if len(target) > 0 && target[len(target)-1] == '.' {
target = target[:len(target)-1]
}
}

return &CreateRecordRequest{
Name: name,
Type: rc.Type,
Content: target,
TTL: int(rc.TTL),
}
}
Loading