Skip to content

Commit c023120

Browse files
committed
Add support for NextDNS
1 parent 20ac110 commit c023120

4 files changed

Lines changed: 189 additions & 0 deletions

File tree

docs/nextdns.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# NextDNS
2+
3+
## Configuration
4+
5+
### Example
6+
7+
```json
8+
{
9+
"settings": [
10+
{
11+
"provider": "nextdns",
12+
"domain": "link-ip.nextdns.io",
13+
"endpoint": "endpoint",
14+
"ip_version": "ipv4",
15+
"ipv6_suffix": ""
16+
}
17+
]
18+
}
19+
```
20+
21+
### Compulsory parameters
22+
23+
- `"domain"` is the domain to update. For now, it must be "link-ip.nextdns.io".
24+
- `"endpoint"`
25+
26+
### Optional parameters
27+
28+
- `"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`.
29+
- `"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.
30+
31+
## Domain setup
32+
33+
> 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.
34+
35+
- Create an account on the [nextdns website](https://nextdns.io/)
36+
- Go to your [account page](https://my.nextdns.io/), login and setup Linked IP
37+
- Click `Show advanced options` button and copy the endpoint from the Linked IP card
38+
- Update the configuration file with the endpoint
39+
40+
_See the [nextdns website](https://nextdns.io/)_

internal/provider/constants/providers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const (
4242
NameCom models.Provider = "name.com"
4343
NameSilo models.Provider = "namesilo"
4444
Netcup models.Provider = "netcup"
45+
NextDNS models.Provider = "nextdns"
4546
Njalla models.Provider = "njalla"
4647
NoIP models.Provider = "noip"
4748
NowDNS models.Provider = "nowdns"
@@ -96,6 +97,7 @@ func ProviderChoices() []models.Provider {
9697
Namecheap,
9798
NameCom,
9899
NameSilo,
100+
NextDNS,
99101
Njalla,
100102
NoIP,
101103
NowDNS,

internal/provider/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"github.com/qdm12/ddns-updater/internal/provider/providers/namecom"
4949
"github.com/qdm12/ddns-updater/internal/provider/providers/namesilo"
5050
"github.com/qdm12/ddns-updater/internal/provider/providers/netcup"
51+
"github.com/qdm12/ddns-updater/internal/provider/providers/nextdns"
5152
"github.com/qdm12/ddns-updater/internal/provider/providers/njalla"
5253
"github.com/qdm12/ddns-updater/internal/provider/providers/noip"
5354
"github.com/qdm12/ddns-updater/internal/provider/providers/nowdns"
@@ -160,6 +161,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
160161
return namesilo.New(data, domain, owner, ipVersion, ipv6Suffix)
161162
case constants.Netcup:
162163
return netcup.New(data, domain, owner, ipVersion, ipv6Suffix)
164+
case constants.NextDNS:
165+
return nextdns.New(data, domain, owner, ipVersion, ipv6Suffix)
163166
case constants.Njalla:
164167
return njalla.New(data, domain, owner, ipVersion, ipv6Suffix)
165168
case constants.NoIP:
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package nextdns
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/netip"
9+
"net/url"
10+
"regexp"
11+
"strings"
12+
13+
"github.com/qdm12/ddns-updater/internal/models"
14+
"github.com/qdm12/ddns-updater/internal/provider/constants"
15+
"github.com/qdm12/ddns-updater/internal/provider/errors"
16+
"github.com/qdm12/ddns-updater/internal/provider/utils"
17+
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
18+
)
19+
20+
type Provider struct {
21+
domain string
22+
owner string
23+
ipVersion ipversion.IPVersion
24+
ipv6Suffix netip.Prefix
25+
endpoint string
26+
}
27+
28+
func New(data json.RawMessage, domain, owner string,
29+
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
30+
provider *Provider, err error,
31+
) {
32+
if owner == "link-ip" || domain == "" {
33+
domain = "link-ip.nextdns.io"
34+
owner = "@"
35+
}
36+
var providerSpecificSettings struct {
37+
Endpoint string `json:"endpoint"`
38+
}
39+
err = json.Unmarshal(data, &providerSpecificSettings)
40+
if err != nil {
41+
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
42+
}
43+
44+
err = validateSettings(domain, owner, providerSpecificSettings.Endpoint)
45+
if err != nil {
46+
return nil, fmt.Errorf("validating provider specific settings: %w", err)
47+
}
48+
49+
return &Provider{
50+
domain: domain,
51+
owner: owner,
52+
ipVersion: ipVersion,
53+
ipv6Suffix: ipv6Suffix,
54+
endpoint: providerSpecificSettings.Endpoint,
55+
}, nil
56+
}
57+
58+
var (
59+
endpointRegex = regexp.MustCompile(`^[0-9a-fA-F]{6}\/[0-9a-fA-F]{16}$`)
60+
)
61+
62+
func validateSettings(domain, owner, endpoint string) (err error) {
63+
err = utils.CheckDomain(domain)
64+
if err != nil {
65+
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
66+
}
67+
switch {
68+
case !strings.HasSuffix(domain, "nextdns.io"):
69+
return fmt.Errorf(`%w: %q must end with "%s"`,
70+
errors.ErrDomainNotValid, domain, "nextdns.io")
71+
case owner == "*":
72+
return fmt.Errorf("%w: %s", errors.ErrOwnerWildcard, owner)
73+
case !endpointRegex.MatchString(endpoint):
74+
return fmt.Errorf("%w: endpoint %q does not match regex %q",
75+
errors.ErrTokenNotValid, endpoint, endpointRegex)
76+
}
77+
return nil
78+
}
79+
80+
func (p *Provider) String() string {
81+
return utils.ToString(p.domain, p.owner, constants.NextDNS, p.ipVersion)
82+
}
83+
84+
func (p *Provider) Domain() string {
85+
return p.domain
86+
}
87+
88+
func (p *Provider) Owner() string {
89+
return p.owner
90+
}
91+
92+
func (p *Provider) IPVersion() ipversion.IPVersion {
93+
return p.ipVersion
94+
}
95+
96+
func (p *Provider) IPv6Suffix() netip.Prefix {
97+
return p.ipv6Suffix
98+
}
99+
100+
func (p *Provider) Proxied() bool {
101+
return false
102+
}
103+
104+
func (p *Provider) BuildDomainName() string {
105+
return utils.BuildDomainName(p.owner, p.domain)
106+
}
107+
108+
func (p *Provider) HTML() models.HTMLRow {
109+
return models.HTMLRow{
110+
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
111+
Owner: p.Owner(),
112+
Provider: "<a href=\"https://nextdns.io/\">NextDNS</a>",
113+
IPVersion: p.ipVersion.String(),
114+
}
115+
}
116+
117+
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
118+
u := url.URL{
119+
Scheme: "https",
120+
Host: p.Domain(),
121+
Path: p.endpoint,
122+
}
123+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
124+
if err != nil {
125+
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
126+
}
127+
128+
response, err := client.Do(request)
129+
if err != nil {
130+
return netip.Addr{}, err
131+
}
132+
defer response.Body.Close()
133+
134+
s, err := utils.ReadAndCleanBody(response.Body)
135+
if err != nil {
136+
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
137+
}
138+
139+
if response.StatusCode != http.StatusOK {
140+
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
141+
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
142+
}
143+
return ip, nil
144+
}

0 commit comments

Comments
 (0)