15 Commits
master ... v2.8

Author SHA1 Message Date
Quentin McGaw
e95816ab46 fix(update): fetch IPv6 AAAA records and not only IPv4 2024-11-21 22:47:59 +00:00
Quentin McGaw
75191c2876 fix(update): do not update if public IP is part of multiple IPs found in records 2024-11-21 22:41:27 +00:00
Quentin McGaw
4b65908a88 fix(netcup): allow wildcard domains (#863) 2024-11-21 22:39:52 +00:00
Quentin McGaw
f910ac9cfc docs(duckdns): fix domain documentation on eTLD being duckdns.org 2024-11-21 22:39:15 +00:00
Quentin McGaw
5a56464b38 docs(goip): fix domain field documentation
- eTLD must be goip.de or goip.it, not eTLD+1
- remove old `domain` optional parameter documentation
2024-11-21 22:39:00 +00:00
Quentin McGaw
8b9ca1204e fix(aliyun): error context fixed when handling bad request errors 2024-11-21 22:38:47 +00:00
Quentin McGaw
0b747f8323 fix(all): trim space and lower case all response plain bodies 2024-11-21 22:38:36 +00:00
Quentin McGaw
7f6ccdb4fb fix(ovh): handling of invalid JSON error bodies 2024-11-21 22:38:12 +00:00
Quentin McGaw
7b8505cff4 fix(noip): handle response body messages before checking status code 2024-11-21 22:37:59 +00:00
Quentin McGaw
40e4da4e93 fix(dondominio): build host with raw owner to support wildcards 2024-11-21 22:37:49 +00:00
Quentin McGaw
981bed1e13 fix(gandi.net): leave ttl as it is if not user specified 2024-11-21 22:37:41 +00:00
Quentin McGaw
d73be0a9e5 fix(gandi.net): update API url fix #852 2024-11-21 22:37:28 +00:00
Fred Cox
4c7f17e816 fix(gcp): prevent crash for missing record (#846) 2024-11-21 22:37:19 +00:00
likeaninja5
5ea1537d59 fix(porkbun): update API url (#837) 2024-11-21 22:36:37 +00:00
Benjamin Temple
9220585a98 Provider Porkbun: Delete Default Parked DNS Entry for *.domain.tld (#774)
* Provider Porkbun: Delete Default Parked DNS Entry for *.domain.tld

Description:

By default, Porkbun creates default ALIAS and CNAME domain records pointing to `pixie.porkbun.com` (Porkbun's parked domain website)

The current logic flow prior to this PR would look for an A or AAAA domain record, and if none exists, attempt to delete the ALIAS record for any subdomain.
This updates the logic flow to only look for a conflicting ALIAS record for the top level `domain.tld`, and a conflicting CNAME record for the `*.domain.tld`. Additionally, we verify that the content of this record matches `pixie.porkbun.com` and we only delete for the expected default values.
If the value does not match the expected `pixie.porkbun.com` we produce more helpful error messages.

Test-Plan:

Created a new domain.tld on Porkbun
Verified the default records were created:
`ALIAS domain.tld -> pixie.porkbun.com`
`CNAME *.domain.tld -> pixie.porkbun.com`
Started DDNS-Updater
Verified that both domain records were successfully deleted and updated

Reset the ALIAS domain record to point to `not-pixie.porkbun.com`
Reset the CNAME domain record to point to `not-pixie.porkbun.com`
Started DDNS-Updater
Verified that both domain records failed with the expected conflicting record error message.

![screenshot_2024-08-17-0210 20](https://github.com/user-attachments/assets/eb567401-ad4b-454d-a7aa-70ab1db1e3e9)

- add `deleteDefaultConflictingRecordsIfNeeded` method
- handle non conflicting errors from `deleteSingleMatchingRecord`
- simplify comments by linking to documentation
- improve error wrappings

---------

Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
2024-11-21 22:36:24 +00:00
41 changed files with 331 additions and 255 deletions

View File

@@ -20,7 +20,7 @@
### Compulsory parameters
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.duckdns.org`. The [eTLD+1](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `duckdns.org`.
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.duckdns.org`. The [eTLD](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `duckdns.org`.
- `"token"`
### Optional parameters

View File

@@ -9,7 +9,7 @@
"settings": [
{
"provider": "goip",
"domain": "mysubdomain.goip.de",
"domain": "mydomain.goip.de",
"username": "username",
"password": "password",
"ip_version": "",
@@ -22,12 +22,11 @@
### Compulsory parameters
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.goip.de`. The [eTLD+1](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `goip.de` or `goip.it`.
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.goip.de`. The [eTLD](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `goip.de` or `goip.it`.
- `"username"` is your goip.de username listed under "Routers"
- `"password"` is your router account password
### Optional parameters
- `"domain"` is the domain name which can be `goip.de` or `goip.it`, and defaults to `goip.de` if left unset. This is automatically disabled for an IPv6 public address since it is not supported.
- `"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`.
- `"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.

View File

@@ -26,7 +26,7 @@ Also keep in mind, that TTL, Expire, Retry and Refresh values of the given Domai
### Compulsory parameters
- `"domain"` is the domain to update. It can be `example.com` (root domain) or `sub.example.com` (subdomain of `example.com`).
- `"domain"` is the domain to update. It can be `example.com` (root domain) or `sub.example.com` (subdomain of `example.com`) or the wildcard `*.example.com`.
- `"api_key"` is your api key (generated in the [customercontrolpanel](https://www.customercontrolpanel.de))
- `"password"` is your api password (generated in the [customercontrolpanel](https://www.customercontrolpanel.de)). Netcup only allows one ApiPassword. This is not the account password. This password is used for all api keys.
- `"customer_number"` is your customer number (viewable in the [customercontrolpanel](https://www.customercontrolpanel.de) next to your name). As seen in the example above, provide the number as string value.

View File

@@ -41,5 +41,12 @@
## Record creation
In case you don't have an A or AAAA record for your host and domain combination, it will be created by DDNS-Updater.
However, to do so, the corresponding ALIAS record, that is automatically created by Porkbun, is automatically deleted to allow this.
Porkbun creates default DNS entries for new domains, which can conflict with creating a root or wildcard A/AAAA record. Therefore, ddns-updater automatically removes any conflicting default record before creating records, as described in the table below:
| Record type | Owner | Record value | Situation requiring a removal |
| --- | --- | --- | --- |
| `ALIAS` | `@` | pixie.porkbun.com | Creating A or AAAA record for the root domain **or** wildcard domain |
| `CNAME` | `*` | pixie.porkbun.com | Creating A or AAAA record for the wildcard domain |
More details is in [this comment by @everydaycombat](https://github.com/qdm12/ddns-updater/issues/546#issuecomment-1773960193).

View File

@@ -1,6 +0,0 @@
package constants
const (
A = "A"
AAAA = "AAAA"
)

View File

@@ -0,0 +1,8 @@
package constants
const (
A = "A"
AAAA = "AAAA"
CNAME = "CNAME"
ALIAS = "ALIAS"
)

View File

@@ -57,7 +57,7 @@ func (p *Provider) getRecordID(ctx context.Context, client *http.Client,
if err != nil || data.Code != "InvalidDomainName.NoExist" {
return "", fmt.Errorf("%w: %d: %s",
errors.ErrHTTPStatusNotValid, response.StatusCode,
utils.BodyToSingleLine(response.Body))
utils.ToSingleLine(string(bodyBytes)))
}
return "", fmt.Errorf("%w", errors.ErrRecordNotFound)

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -137,11 +136,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -125,19 +124,16 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
}
s = strings.ToLower(s)
switch {
case strings.Contains(s, "authorization failed"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -140,11 +139,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
@@ -158,7 +156,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.Contains(s, constants.Notfqdn):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.Contains(s, "Updated 1 hostname"):
case strings.Contains(s, "updated 1 hostname"):
return ip, nil
default:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -127,11 +126,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
switch response.StatusCode {
case http.StatusOK:

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -140,11 +139,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",

View File

@@ -120,7 +120,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
RawQuery: url.Values{
"user": {p.username},
"apikey": {p.key},
"host": {p.BuildDomainName()},
"host": {utils.BuildURLQueryHostname(p.owner, p.domain)},
"ip": {ip.String()},
"lang": {"en"},
}.Encode(),

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -161,11 +160,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
@@ -176,9 +174,9 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
switch {
case len(s) < minChars:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrResponseTooShort, s)
case s[0:minChars] == "KO":
case s[0:minChars] == "ko":
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case s[0:minChars] == "OK":
case s[0:minChars] == "ok":
var ips []netip.Addr
if ip.Is6() {
ips = ipextract.IPv6(s)

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -137,11 +136,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -146,11 +145,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -134,11 +133,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
@@ -148,13 +146,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
switch {
case s == "":
return netip.Addr{}, fmt.Errorf("%w", errors.ErrReceivedNoResult)
case strings.Contains(s, "NO_SERVICE"):
case strings.Contains(s, "no_service"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoService)
case strings.Contains(s, "NO_ACCESS"):
case strings.Contains(s, "no_access"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.Contains(s, "ILLEGAL_INPUT"), strings.Contains(s, "TOO_SOON"):
case strings.Contains(s, "illegal_input"), strings.Contains(s, "too_soon"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedAbuse)
case strings.Contains(s, "NO_ERROR"), strings.Contains(s, "OK"):
case strings.Contains(s, "no_error"), strings.Contains(s, "ok"):
return ip, nil
default:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -154,11 +153,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
// TODO handle the encoding of the response body properly. Often it can be JSON,
// see other provider code for examples on how to decode JSON.
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
// TODO handle every possible status codes from the provider API.
// If undocumented, try them out by sending bogus HTTP requests to see

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -124,22 +123,20 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
}
loweredResponse := strings.ToLower(s)
switch {
case loweredResponse == "":
case s == "":
return netip.Addr{}, fmt.Errorf("%w", errors.ErrReceivedNoResult)
case strings.HasPrefix(loweredResponse, "no ip change detected"),
strings.HasPrefix(loweredResponse, "updated "):
case strings.HasPrefix(s, "no ip change detected"),
strings.HasPrefix(s, "updated "):
return ip, nil
default:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))

View File

@@ -110,6 +110,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
// See https://api.gandi.net/docs/livedns/#v5-livedns-domains-fqdn-records
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.Is6() {
@@ -118,23 +119,18 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
u := url.URL{
Scheme: "https",
Host: "dns.api.gandi.net",
Path: fmt.Sprintf("/api/v5/domains/%s/records/%s/%s", p.domain, p.owner, recordType),
Host: "api.gandi.net",
Path: fmt.Sprintf("v5/livedns/domains/%s/records/%s/%s", p.domain, p.owner, recordType),
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
const defaultTTL uint32 = 3600
ttl := defaultTTL
if p.ttl != 0 {
ttl = p.ttl
}
requestData := struct {
Values [1]string `json:"rrset_values"`
TTL uint32 `json:"rrset_ttl"`
TTL uint32 `json:"rrset_ttl,omitempty"`
}{
Values: [1]string{ip.Unmap().String()},
TTL: ttl,
TTL: p.ttl,
}
err = encoder.Encode(requestData)
if err != nil {

View File

@@ -34,13 +34,6 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
}
for _, rrdata := range recordResourceSet.Rrdatas {
if rrdata == ip.String() {
// already up to date
return ip, nil
}
}
if !rrSetFound {
err = p.createRRSet(ctx, client, fqdn, recordType, ip)
if err != nil {
@@ -49,6 +42,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
return ip, nil
}
for _, rrdata := range recordResourceSet.Rrdatas {
if rrdata == ip.String() {
// already up to date
return ip, nil
}
}
err = p.patchRRSet(ctx, client, fqdn, recordType, ip)
if err != nil {
return netip.Addr{}, fmt.Errorf("updating record: %w", err)

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -170,15 +169,15 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
utils.BodyToSingleLine(response.Body))
}
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
switch {
case strings.HasPrefix(s, p.BuildDomainName()+" ("+ip.String()+")"):
return ip, nil
case strings.HasPrefix(strings.ToLower(s), "zugriff verweigert"):
case strings.HasPrefix(s, "zugriff verweigert"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
default:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -126,11 +125,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
switch s {
case "":

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -132,11 +131,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
switch response.StatusCode {
case http.StatusOK:

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -135,11 +134,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)

View File

@@ -37,7 +37,7 @@ func New(data json.RawMessage, domain, owner string,
return nil, fmt.Errorf("JSON decoding provider specific settings: %w", err)
}
err = validateSettings(domain, owner, extraSettings.CustomerNumber,
err = validateSettings(domain, extraSettings.CustomerNumber,
extraSettings.APIKey, extraSettings.Password)
if err != nil {
return nil, fmt.Errorf("validating provider specific settings: %w", err)
@@ -54,15 +54,13 @@ func New(data json.RawMessage, domain, owner string,
}, nil
}
func validateSettings(domain, owner, customerNumber, apiKey, password string) (err error) {
func validateSettings(domain, customerNumber, apiKey, password string) (err error) {
err = utils.CheckDomain(domain)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
}
switch {
case owner == "*":
return fmt.Errorf("%w", errors.ErrOwnerWildcard)
case customerNumber == "":
return fmt.Errorf("%w", errors.ErrCustomerNumberNotSet)
case apiKey == "":

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -139,14 +138,9 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
switch s {
@@ -166,7 +160,9 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
}
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
} else if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -128,11 +127,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
switch response.StatusCode {
case http.StatusOK:

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -132,11 +131,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)

View File

@@ -10,17 +10,17 @@ import (
)
func extractAPIError(response *http.Response) (err error) {
decoder := json.NewDecoder(response.Body)
b, err := io.ReadAll(response.Body)
if err != nil {
_ = response.Body.Close()
return fmt.Errorf("reading response body: %w", err)
}
var apiError struct {
Message string `json:"Message"`
}
err = decoder.Decode(&apiError)
err = json.Unmarshal(b, &apiError)
if err != nil {
b, err := io.ReadAll(response.Body)
if err != nil {
_ = response.Body.Close()
return fmt.Errorf("reading response body: %w", err)
}
apiError.Message = string(b)
}
queryID := response.Header.Get("X-Ovh-QueryID")

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -171,11 +170,10 @@ func (p *Provider) updateWithDynHost(ctx context.Context, client *http.Client,
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)

View File

@@ -11,13 +11,23 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/errors"
)
type dnsRecord struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL string `json:"ttl"`
Priority string `json:"prio"`
Notes string `json:"notes"`
}
// See https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type
func (p *Provider) getRecordIDs(ctx context.Context, client *http.Client, recordType string) (
recordIDs []string, err error) {
url := "https://porkbun.com/api/json/v3/dns/retrieveByNameType/" + p.domain + "/" + recordType + "/"
if p.owner != "@" {
func (p *Provider) getRecords(ctx context.Context, client *http.Client, recordType, owner string) (
records []dnsRecord, err error) {
url := "https://api.porkbun.com/api/json/v3/dns/retrieveByNameType/" + p.domain + "/" + recordType + "/"
if owner != "@" {
// Note Porkbun requires we send the unescaped '*' character.
url += p.owner
url += owner
}
postRecordsParams := struct {
@@ -29,29 +39,22 @@ func (p *Provider) getRecordIDs(ctx context.Context, client *http.Client, record
}
type jsonResponseData struct {
Records []struct {
ID string `json:"id"`
} `json:"records"`
Records []dnsRecord `json:"records"`
}
const decodeBody = true
responseData, err := httpPost[jsonResponseData](ctx, client, url, postRecordsParams, decodeBody)
if err != nil {
return nil, fmt.Errorf("for record type %s: %w",
recordType, err)
return nil, fmt.Errorf("for record type %s and record owner %s: %w",
recordType, owner, err)
}
recordIDs = make([]string, len(responseData.Records))
for i := range responseData.Records {
recordIDs[i] = responseData.Records[i].ID
}
return recordIDs, nil
return responseData.Records, nil
}
// See https://porkbun.com/api/json/v3/documentation#DNS%20Create%20Record
func (p *Provider) createRecord(ctx context.Context, client *http.Client,
recordType, ipStr string) (err error) {
url := "https://porkbun.com/api/json/v3/dns/create/" + p.domain
recordType, owner, ipStr string) (err error) {
url := "https://api.porkbun.com/api/json/v3/dns/create/" + p.domain
postRecordsParams := struct {
SecretAPIKey string `json:"secretapikey"`
APIKey string `json:"apikey"`
@@ -64,14 +67,14 @@ func (p *Provider) createRecord(ctx context.Context, client *http.Client,
APIKey: p.apiKey,
Content: ipStr,
Type: recordType,
Name: p.owner,
Name: owner,
TTL: strconv.FormatUint(uint64(p.ttl), 10),
}
const decodeBody = false
_, err = httpPost[struct{}](ctx, client, url, postRecordsParams, decodeBody)
if err != nil {
return fmt.Errorf("for record type %s: %w",
recordType, err)
return fmt.Errorf("for record type %s and record owner %s: %w",
recordType, owner, err)
}
return nil
@@ -79,8 +82,8 @@ func (p *Provider) createRecord(ctx context.Context, client *http.Client,
// See https://porkbun.com/api/json/v3/documentation#DNS%20Edit%20Record%20by%20Domain%20and%20ID
func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
recordType, ipStr, recordID string) (err error) {
url := "https://porkbun.com/api/json/v3/dns/edit/" + p.domain + "/" + recordID
recordType, owner, ipStr, recordID string) (err error) {
url := "https://api.porkbun.com/api/json/v3/dns/edit/" + p.domain + "/" + recordID
postRecordsParams := struct {
SecretAPIKey string `json:"secretapikey"`
APIKey string `json:"apikey"`
@@ -94,24 +97,24 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
Content: ipStr,
Type: recordType,
TTL: strconv.FormatUint(uint64(p.ttl), 10),
Name: p.owner,
Name: owner,
}
const decodeBody = false
_, err = httpPost[struct{}](ctx, client, url, postRecordsParams, decodeBody)
if err != nil {
return fmt.Errorf("for record type %s and record id %s: %w",
recordType, recordID, err)
return fmt.Errorf("for record type %s, record owner %s and record id %s: %w",
recordType, owner, recordID, err)
}
return nil
}
// See https://porkbun.com/api/json/v3/documentation#DNS%20Delete%20Records%20by%20Domain,%20Subdomain%20and%20Type
func (p *Provider) deleteAliasRecord(ctx context.Context, client *http.Client) (err error) {
url := "https://porkbun.com/api/json/v3/dns/deleteByNameType/" + p.domain + "/ALIAS/"
if p.owner != "@" {
func (p *Provider) deleteRecord(ctx context.Context, client *http.Client, recordType, owner string) (err error) {
url := "https://api.porkbun.com/api/json/v3/dns/deleteByNameType/" + p.domain + "/" + recordType + "/"
if owner != "@" {
// Note Porkbun requires we send the unescaped '*' character.
url += p.owner
url += owner
}
postRecordsParams := struct {
SecretAPIKey string `json:"secretapikey"`
@@ -124,7 +127,8 @@ func (p *Provider) deleteAliasRecord(ctx context.Context, client *http.Client) (
const decodeBody = false
_, err = httpPost[struct{}](ctx, client, url, postRecordsParams, decodeBody)
if err != nil {
return err
return fmt.Errorf("for record type %s and record owner %s: %w",
recordType, owner, err)
}
return nil

View File

@@ -3,6 +3,7 @@ package porkbun
import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
"net/netip"
@@ -119,46 +120,85 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
recordType = constants.AAAA
}
ipStr := ip.String()
recordIDs, err := p.getRecordIDs(ctx, client, recordType)
records, err := p.getRecords(ctx, client, recordType, p.owner)
if err != nil {
return netip.Addr{}, fmt.Errorf("getting record IDs: %w", err)
}
if len(recordIDs) == 0 {
// ALIAS record needs to be deleted to allow creating an A record.
err = p.deleteALIASRecordIfNeeded(ctx, client)
if len(records) == 0 {
err = p.deleteDefaultConflictingRecordsIfNeeded(ctx, client)
if err != nil {
return netip.Addr{}, fmt.Errorf("deleting ALIAS record if needed: %w", err)
return netip.Addr{}, fmt.Errorf("deleting default conflicting records: %w", err)
}
err = p.createRecord(ctx, client, recordType, ipStr)
err = p.createRecord(ctx, client, recordType, p.owner, ipStr)
if err != nil {
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
}
return ip, nil
}
for _, recordID := range recordIDs {
err = p.updateRecord(ctx, client, recordType, ipStr, recordID)
for _, record := range records {
err = p.updateRecord(ctx, client, recordType, p.owner, ipStr, record.ID)
if err != nil {
return netip.Addr{}, fmt.Errorf("updating record: %w", err)
}
}
return ip, nil
}
func (p *Provider) deleteALIASRecordIfNeeded(ctx context.Context, client *http.Client) (err error) {
aliasRecordIDs, err := p.getRecordIDs(ctx, client, "ALIAS")
if err != nil {
return fmt.Errorf("getting ALIAS record IDs: %w", err)
} else if len(aliasRecordIDs) == 0 {
// deleteDefaultConflictingRecordsIfNeeded deletes any default records that would conflict with a new record,
// see https://github.com/qdm12/ddns-updater/blob/master/docs/porkbun.md#record-creation
func (p *Provider) deleteDefaultConflictingRecordsIfNeeded(ctx context.Context, client *http.Client) (err error) {
const porkbunParkedDomain = "pixie.porkbun.com"
switch p.owner {
case "@":
err = p.deleteSingleMatchingRecord(ctx, client, constants.ALIAS, "@", porkbunParkedDomain)
if err != nil {
return fmt.Errorf("deleting default ALIAS @ parked domain record: %w", err)
}
return nil
case "*":
err = p.deleteSingleMatchingRecord(ctx, client, constants.CNAME, "*", porkbunParkedDomain)
if err != nil {
return fmt.Errorf("deleting default CNAME * parked domain record: %w", err)
}
err = p.deleteSingleMatchingRecord(ctx, client, constants.ALIAS, "@", porkbunParkedDomain)
if err == nil || stderrors.Is(err, errors.ErrConflictingRecord) {
// allow conflict ALIAS records to be set to something besides the parked domain
return nil
}
return fmt.Errorf("deleting default ALIAS @ parked domain record: %w", err)
default:
return nil
}
}
err = p.deleteAliasRecord(ctx, client)
// deleteSingleMatchingRecord deletes an eventually present record matching a specific record type if the content
// matches the expected content value.
// It returns an error if multiple records are found or if one record is found with an unexpected value.
func (p *Provider) deleteSingleMatchingRecord(ctx context.Context, client *http.Client,
recordType, owner, expectedContent string) (err error) {
records, err := p.getRecords(ctx, client, recordType, owner)
if err != nil {
return fmt.Errorf("deleting ALIAS record: %w", err)
return fmt.Errorf("getting records: %w", err)
}
switch {
case len(records) == 0:
return nil
case len(records) > 1:
return fmt.Errorf("%w: %d %s records are already set", errors.ErrConflictingRecord, len(records), recordType)
case records[0].Content != expectedContent:
return fmt.Errorf("%w: %s record has content %q mismatching expected content %q",
errors.ErrConflictingRecord, recordType, records[0].Content, expectedContent)
}
// Single record with content matching expected content.
err = p.deleteRecord(ctx, client, recordType, owner)
if err != nil {
return fmt.Errorf("deleting record: %w", err)
}
return nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -154,11 +153,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
switch {
case strings.HasPrefix(s, constants.Notfqdn):

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -148,11 +147,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
bodyString, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
bodyString := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -127,11 +126,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
str, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
str := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, str)

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -137,11 +136,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
@@ -135,18 +134,16 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
response.StatusCode, utils.ToSingleLine(s))
}
s = strings.ToLower(s)
switch {
case strings.Contains(s, `success_code="200"`):
ips := ipextract.IPv4(s)

View File

@@ -0,0 +1,26 @@
package utils
import (
"fmt"
"io"
"strings"
)
// ReadAndCleanBody reads the body, closes it, trims spaces from the body data
// and converts it to lowercase.
func ReadAndCleanBody(body io.ReadCloser) (cleanedBody string, err error) {
b, err := io.ReadAll(body)
if err != nil {
return "", fmt.Errorf("reading body: %w", err)
}
err = body.Close()
if err != nil {
return "", fmt.Errorf("closing body: %w", err)
}
cleanedBody = string(b)
cleanedBody = strings.TrimSpace(cleanedBody)
cleanedBody = strings.ToLower(cleanedBody)
return cleanedBody, nil
}

View File

@@ -32,14 +32,22 @@ func (s *Service) logInfoNoLookupUpdate(hostname, ipKind string, lastIP, ip neti
ipKind, hostname, lastIP, ipKind, ip))
}
func (s *Service) logDebugLookupSkip(hostname, ipKind string, recordIP, ip netip.Addr) {
func (s *Service) logDebugLookupSkip(hostname, ipKind string, recordIPs []netip.Addr, ip netip.Addr) {
s.logger.Debug(fmt.Sprintf("%s address of %s is %s and your %s address"+
" is %s, skipping update", ipKind, hostname, recordIP, ipKind, ip))
" is %s, skipping update", ipKind, hostname, ipsToString(recordIPs), ipKind, ip))
}
func (s *Service) logInfoLookupUpdate(hostname, ipKind string, recordIP, ip netip.Addr) {
s.logger.Info(fmt.Sprintf("%s address of %s is %s and your %s address is %s",
ipKind, hostname, recordIP, ipKind, ip))
func (s *Service) logInfoLookupUpdate(hostname, ipKind string, recordIPs []netip.Addr, ip netip.Addr) {
s.logger.Info(fmt.Sprintf("%s address of %s is %s and your %s address is %s",
ipKind, hostname, ipsToString(recordIPs), ipKind, ip))
}
func ipsToString(ips []netip.Addr) string {
ipStrings := make([]string, len(ips))
for i, ip := range ips {
ipStrings[i] = ip.String()
}
return strings.Join(ipStrings, ", ")
}
type joinedErrors struct { //nolint:errname

View File

@@ -3,8 +3,10 @@ package update
import (
"context"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"time"
"github.com/qdm12/ddns-updater/internal/constants"
@@ -34,7 +36,8 @@ type Service struct {
func NewService(db Database, updater UpdaterInterface, ipGetter PublicIPFetcher,
period time.Duration, cooldown time.Duration, logger Logger, resolver LookupIPer,
timeNow func() time.Time, hioClient HealthchecksIOClient) *Service {
timeNow func() time.Time, hioClient HealthchecksIOClient,
) *Service {
return &Service{
period: period,
db: db,
@@ -50,42 +53,59 @@ func NewService(db Database, updater UpdaterInterface, ipGetter PublicIPFetcher,
}
}
func (s *Service) lookupIPsResilient(ctx context.Context, hostname string, tries int) (
ipv4 netip.Addr, ipv6 netip.Addr, err error) {
for i := 0; i < tries; i++ {
ipv4, ipv6, err = s.lookupIPs(ctx, hostname)
if err == nil {
return ipv4, ipv6, nil
}
func (s *Service) lookupIPsResilient(ctx context.Context, hostname string, tries uint) (
ipv4, ipv6 []netip.Addr, err error,
) {
type result struct {
network string
ips []net.IP
err error
}
return netip.Addr{}, netip.Addr{}, err
}
func (s *Service) lookupIPs(ctx context.Context, hostname string) (
ipv4 netip.Addr, ipv6 netip.Addr, err error) {
netIPs, err := s.resolver.LookupIP(ctx, "ip", hostname)
if err != nil {
return netip.Addr{}, netip.Addr{}, err
}
ips := make([]netip.Addr, len(netIPs))
for i, netIP := range netIPs {
switch {
case netIP == nil:
case netIP.To4() != nil:
ips[i] = netip.AddrFrom4([4]byte(netIP.To4()))
default: // IPv6
ips[i] = netip.AddrFrom16([16]byte(netIP.To16()))
}
results := make(chan result)
networks := []string{"ip4", "ip6"}
lookupCtx, cancel := context.WithCancel(ctx)
for _, network := range networks {
go func(ctx context.Context, network string, results chan<- result) {
for range tries {
ips, err := s.resolver.LookupIP(ctx, network, hostname)
if err != nil {
if strings.HasSuffix(err.Error(), "no such host") {
results <- result{network: network} // no IP address for this network
return
}
continue // retry
}
results <- result{network: network, ips: ips, err: err}
return
}
}(lookupCtx, network, results)
}
for _, ip := range ips {
if ip.Is6() {
ipv6 = ip
} else {
ipv4 = ip
for range networks {
result := <-results
if result.err != nil {
if err == nil {
cancel()
err = fmt.Errorf("looking up %s addresses: %w", result.network, result.err)
}
continue
}
switch result.network {
case "ip4":
ipv4 = make([]netip.Addr, len(result.ips))
for i, ip := range result.ips {
ipv4[i] = netip.AddrFrom4([4]byte(ip))
}
case "ip6":
ipv6 = make([]netip.Addr, len(result.ips))
for i, ip := range result.ips {
ipv6[i] = netip.AddrFrom16([16]byte(ip))
}
}
}
return ipv4, ipv6, nil
cancel()
return ipv4, ipv6, err
}
func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
@@ -106,7 +126,8 @@ func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
}
func (s *Service) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool) (
ip, ipv4, ipv6 netip.Addr, errors []error) {
ip, ipv4, ipv6 netip.Addr, errors []error,
) {
var err error
if doIP {
ip, err = tryAndRepeatGettingIP(ctx, s.ipGetter.IP, s.logger, ipversion.IP4or6)
@@ -130,7 +151,8 @@ func (s *Service) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool) (
}
func (s *Service) getRecordIDsToUpdate(ctx context.Context, records []librecords.Record,
ip, ipv4, ipv6 netip.Addr) (recordIDs map[uint]struct{}) {
ip, ipv4, ipv6 netip.Addr,
) (recordIDs map[uint]struct{}) {
recordIDs = make(map[uint]struct{})
for i, record := range records {
shouldUpdate := s.shouldUpdateRecord(ctx, record, ip, ipv4, ipv6)
@@ -143,7 +165,8 @@ func (s *Service) getRecordIDsToUpdate(ctx context.Context, records []librecords
}
func (s *Service) shouldUpdateRecord(ctx context.Context, record librecords.Record,
ip, ipv4, ipv6 netip.Addr) (update bool) {
ip, ipv4, ipv6 netip.Addr,
) (update bool) {
now := s.timeNow()
isWithinCooldown := now.Sub(record.History.GetSuccessTime()) < s.cooldown
@@ -183,7 +206,8 @@ func (s *Service) shouldUpdateRecord(ctx context.Context, record librecords.Reco
}
func (s *Service) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion.IPVersion,
lastIP, publicIP netip.Addr) (update bool) {
lastIP, publicIP netip.Addr,
) (update bool) {
ipKind := ipVersionToIPKind(ipVersion)
if publicIP.IsValid() && publicIP.Compare(lastIP) != 0 {
s.logInfoNoLookupUpdate(hostname, ipKind, lastIP, publicIP)
@@ -194,9 +218,10 @@ func (s *Service) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversio
}
func (s *Service) shouldUpdateRecordWithLookup(ctx context.Context, hostname string,
ipVersion ipversion.IPVersion, publicIP netip.Addr) (update bool) {
ipVersion ipversion.IPVersion, publicIP netip.Addr,
) (update bool) {
const tries = 5
recordIPv4, recordIPv6, err := s.lookupIPsResilient(ctx, hostname, tries)
recordIPv4s, recordIPv6s, err := s.lookupIPsResilient(ctx, hostname, tries)
if err != nil {
ctxErr := ctx.Err()
if ctxErr != nil {
@@ -208,18 +233,27 @@ func (s *Service) shouldUpdateRecordWithLookup(ctx context.Context, hostname str
}
ipKind := ipVersionToIPKind(ipVersion)
recordIP := recordIPv4
recordIPs := recordIPv4s
if publicIP.Is6() {
recordIP = recordIPv6
recordIPs = recordIPv6s
}
recordIP = getIPMatchingVersion(recordIP, recordIPv4, recordIPv6, ipVersion)
recordIPs = getIPsMatchingVersion(recordIPs, recordIPv4s, recordIPv6s, ipVersion)
if publicIP.IsValid() && publicIP.Compare(recordIP) != 0 {
if publicIP.IsValid() && !ipsContainsIP(recordIPs, publicIP) {
// Note if the recordIP is not valid (not found), we want to update.
s.logInfoLookupUpdate(hostname, ipKind, recordIP, publicIP)
s.logInfoLookupUpdate(hostname, ipKind, recordIPs, publicIP)
return true
}
s.logDebugLookupSkip(hostname, ipKind, recordIP, publicIP)
s.logDebugLookupSkip(hostname, ipKind, recordIPs, publicIP)
return false
}
func ipsContainsIP(ips []netip.Addr, ip netip.Addr) bool {
for _, ip2 := range ips {
if ip.Compare(ip2) == 0 {
return true
}
}
return false
}
@@ -231,8 +265,22 @@ func getIPMatchingVersion(ip, ipv4, ipv6 netip.Addr, ipVersion ipversion.IPVersi
return ipv4
case ipversion.IP6:
return ipv6
default:
panic(fmt.Sprintf("invalid IP version %s", ipVersion))
}
}
func getIPsMatchingVersion(ip, ipv4, ipv6 []netip.Addr, ipVersion ipversion.IPVersion) []netip.Addr {
switch ipVersion {
case ipversion.IP4or6:
return ip
case ipversion.IP4:
return ipv4
case ipversion.IP6:
return ipv6
default:
panic(fmt.Sprintf("invalid IP version %s", ipVersion))
}
return netip.Addr{}
}
func setInitialUpToDateStatus(db Database, id uint, updateIP netip.Addr, now time.Time) error {
@@ -357,7 +405,8 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, startErr er
}
func (s *Service) run(ctx context.Context, ready chan<- struct{},
done chan<- struct{}) {
done chan<- struct{},
) {
defer close(done)
ticker := time.NewTicker(s.period)
close(ready)