diff --git a/README.md b/README.md index ff7a4fa4b..d7f88bd7d 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ Check the documentation for your DNS provider: - [Infomaniak](docs/infomaniak.md) - [INWX](docs/inwx.md) - [Ionos](docs/ionos.md) +- [IPv64](docs/ipv64.md) - [Linode](docs/linode.md) - [Loopia](docs/loopia.md) - [LuaDNS](docs/luadns.md) diff --git a/docs/ipv64.md b/docs/ipv64.md new file mode 100644 index 000000000..0b54d6212 --- /dev/null +++ b/docs/ipv64.md @@ -0,0 +1,31 @@ +# IPv64 + +## Configuration + +### Example + +```json +{ + "settings": [ + { + "provider": "ipv64", + "domain": "domain.com", + "key": "key", + "ip_version": "ipv4", + "ipv6_suffix": "" + } + ] +} +``` + +### Compulsory parameters + +- `"domain"` is the domain to update. It can be `example.com` (root domain) or `sub.example.com` (subdomain of `example.com`). +- `"key"` that you can obtain [here](https://ipv64.net/account) + +### 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 diff --git a/internal/provider/constants/providers.go b/internal/provider/constants/providers.go index 71b48c9d1..984ed40b5 100644 --- a/internal/provider/constants/providers.go +++ b/internal/provider/constants/providers.go @@ -34,6 +34,7 @@ const ( Infomaniak models.Provider = "infomaniak" INWX models.Provider = "inwx" Ionos models.Provider = "ionos" + IPv64 models.Provider = "ipv64" Linode models.Provider = "linode" Loopia models.Provider = "loopia" LuaDNS models.Provider = "luadns" @@ -89,6 +90,7 @@ func ProviderChoices() []models.Provider { Infomaniak, INWX, Ionos, + IPv64, Linode, Loopia, LuaDNS, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 06e002845..9090ca2ef 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -40,6 +40,7 @@ import ( "github.com/qdm12/ddns-updater/internal/provider/providers/infomaniak" "github.com/qdm12/ddns-updater/internal/provider/providers/inwx" "github.com/qdm12/ddns-updater/internal/provider/providers/ionos" + "github.com/qdm12/ddns-updater/internal/provider/providers/ipv64" "github.com/qdm12/ddns-updater/internal/provider/providers/linode" "github.com/qdm12/ddns-updater/internal/provider/providers/loopia" "github.com/qdm12/ddns-updater/internal/provider/providers/luadns" @@ -144,6 +145,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin return inwx.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.Ionos: return ionos.New(data, domain, owner, ipVersion, ipv6Suffix) + case constants.IPv64: + return ipv64.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.Linode: return linode.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.Loopia: diff --git a/internal/provider/providers/ipv64/provider.go b/internal/provider/providers/ipv64/provider.go new file mode 100644 index 000000000..ab5fd2abf --- /dev/null +++ b/internal/provider/providers/ipv64/provider.go @@ -0,0 +1,139 @@ +package ipv64 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/netip" + "net/url" + + "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/headers" + "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 + key string +} + +func New(data json.RawMessage, domain, owner string, + ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) ( + p *Provider, err error, +) { + extraSettings := struct { + Key string `json:"key"` + }{} + err = json.Unmarshal(data, &extraSettings) + if err != nil { + return nil, err + } + + err = validateSettings(domain, extraSettings.Key) + if err != nil { + return nil, fmt.Errorf("validating provider specific settings: %w", err) + } + + return &Provider{ + domain: domain, + owner: owner, + ipVersion: ipVersion, + ipv6Suffix: ipv6Suffix, + key: extraSettings.Key, + }, nil +} + +func validateSettings(domain, key string) (err error) { + err = utils.CheckDomain(domain) + if err != nil { + return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err) + } + + if key == "" { + return fmt.Errorf("%w", errors.ErrKeyNotSet) + } + return nil +} + +func (p *Provider) String() string { + return utils.ToString(p.domain, p.owner, constants.IPv64, 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("%s", p.BuildDomainName(), p.BuildDomainName()), + Owner: p.Owner(), + Provider: "IPv64 DNS", + IPVersion: p.ipVersion.String(), + } +} + +// see https://ipv64.net/dyndns_updater_api +func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) { + u := url.URL{ + Scheme: "https", + Host: "ipv64.net", + Path: "/nic/update", + } + + values := url.Values{} + values.Set("key", p.key) + values.Set("domain", utils.BuildURLQueryHostname(p.owner, p.domain)) + + if ip.Is4() { + values.Set("ip", ip.String()) + } else { + values.Set("ip6", ip.String()) + } + + u.RawQuery = values.Encode() + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return netip.Addr{}, fmt.Errorf("creating http request: %w", err) + } + headers.SetUserAgent(request) + + response, err := client.Do(request) + if err != nil { + return netip.Addr{}, err + } + defer response.Body.Close() + + if response.StatusCode == http.StatusOK { + return ip, nil + } + return netip.Addr{}, fmt.Errorf("%w: %d: %s", + errors.ErrHTTPStatusNotValid, response.StatusCode, utils.BodyToSingleLine(response.Body)) +}