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
40 changes: 40 additions & 0 deletions docs/nextdns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# NextDNS

## Configuration

### Example

```json
{
"settings": [
{
"provider": "nextdns",
"domain": "link-ip.nextdns.io",
"endpoint": "endpoint",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```

### Compulsory parameters

- `"domain"` is the domain to update. For now, it must be "link-ip.nextdns.io".
- `"endpoint"`

### Optional parameters

- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifier suffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

## Domain setup

> NextDNS supports updating Linked IP via a DDNS hostname. If you're already using a DDNS service, configure your DDNS domain in the Linked IP card instead.

- Create an account on the [nextdns website](https://nextdns.io/)
- Go to your [account page](https://my.nextdns.io/), login and setup Linked IP
- Click `Show advanced options` button and copy the endpoint from the Linked IP card
- Update the configuration file with the endpoint

_See the [nextdns website](https://nextdns.io/)_
2 changes: 2 additions & 0 deletions internal/provider/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
NameCom models.Provider = "name.com"
NameSilo models.Provider = "namesilo"
Netcup models.Provider = "netcup"
NextDNS models.Provider = "nextdns"
Njalla models.Provider = "njalla"
NoIP models.Provider = "noip"
NowDNS models.Provider = "nowdns"
Expand Down Expand Up @@ -96,6 +97,7 @@ func ProviderChoices() []models.Provider {
Namecheap,
NameCom,
NameSilo,
NextDNS,
Njalla,
NoIP,
NowDNS,
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/providers/namecom"
"github.com/qdm12/ddns-updater/internal/provider/providers/namesilo"
"github.com/qdm12/ddns-updater/internal/provider/providers/netcup"
"github.com/qdm12/ddns-updater/internal/provider/providers/nextdns"
"github.com/qdm12/ddns-updater/internal/provider/providers/njalla"
"github.com/qdm12/ddns-updater/internal/provider/providers/noip"
"github.com/qdm12/ddns-updater/internal/provider/providers/nowdns"
Expand Down Expand Up @@ -160,6 +161,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
return namesilo.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Netcup:
return netcup.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.NextDNS:
return nextdns.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Njalla:
return njalla.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.NoIP:
Expand Down
144 changes: 144 additions & 0 deletions internal/provider/providers/nextdns/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package nextdns

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/netip"
"net/url"
"regexp"
"strings"

"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/provider/constants"
"github.com/qdm12/ddns-updater/internal/provider/errors"
"github.com/qdm12/ddns-updater/internal/provider/utils"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)

type Provider struct {
domain string
owner string
ipVersion ipversion.IPVersion
ipv6Suffix netip.Prefix
endpoint string
}

func New(data json.RawMessage, domain, owner string,
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
provider *Provider, err error,
) {
if owner == "link-ip" || domain == "" {
domain = "link-ip.nextdns.io"
owner = "@"
}
var providerSpecificSettings struct {
Endpoint string `json:"endpoint"`
}
err = json.Unmarshal(data, &providerSpecificSettings)
if err != nil {
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
}

err = validateSettings(domain, owner, providerSpecificSettings.Endpoint)
if err != nil {
return nil, fmt.Errorf("validating provider specific settings: %w", err)
}

return &Provider{
domain: domain,
owner: owner,
ipVersion: ipVersion,
ipv6Suffix: ipv6Suffix,
endpoint: providerSpecificSettings.Endpoint,
}, nil
}

var (
endpointRegex = regexp.MustCompile(`^[0-9a-fA-F]{6}\/[0-9a-fA-F]{16}$`)
)

func validateSettings(domain, owner, endpoint string) (err error) {
err = utils.CheckDomain(domain)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
}
switch {
case !strings.HasSuffix(domain, "nextdns.io"):
return fmt.Errorf(`%w: %q must end with "%s"`,
errors.ErrDomainNotValid, domain, "nextdns.io")
case owner == "*":
return fmt.Errorf("%w: %s", errors.ErrOwnerWildcard, owner)
case !endpointRegex.MatchString(endpoint):
return fmt.Errorf("%w: endpoint %q does not match regex %q",
errors.ErrTokenNotValid, endpoint, endpointRegex)
}
return nil
}

func (p *Provider) String() string {
return utils.ToString(p.domain, p.owner, constants.NextDNS, p.ipVersion)
}

func (p *Provider) Domain() string {
return p.domain
}

func (p *Provider) Owner() string {
return p.owner
}

func (p *Provider) IPVersion() ipversion.IPVersion {
return p.ipVersion
}

func (p *Provider) IPv6Suffix() netip.Prefix {
return p.ipv6Suffix
}

func (p *Provider) Proxied() bool {
return false
}

func (p *Provider) BuildDomainName() string {
return utils.BuildDomainName(p.owner, p.domain)
}

func (p *Provider) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
Owner: p.Owner(),
Provider: "<a href=\"https://nextdns.io/\">NextDNS</a>",
IPVersion: p.ipVersion.String(),
}
}

func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: p.Domain(),
Path: p.endpoint,
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
}

response, err := client.Do(request)
if err != nil {
return netip.Addr{}, err
}
defer response.Body.Close()

s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}

if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
}
return ip, nil
}