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))
+}