Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provider Porkbun: Delete Default Parked DNS Entry for *.domain.tld #774

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

bentemple
Copy link
Collaborator

@bentemple bentemple commented Jul 27, 2024

Description:

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 and then proceed with the record creation.
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 value.
If the value does not match the expected pixie.porkbun.com we produce more helpful error messages.

Note: Any other domain name we simply attempt to create a record if no record exists, which will return a 400 error with no helpful error reason if there is a conflicting record of another type. We may want to consider checking for any conflicting records to produce a more helpful error for the user, but that change is well outside of the scope of this PR, and should likely be done in a more general way and apply to all DNS providers, rather than porkbun exclusively.

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

@bentemple
Copy link
Collaborator Author

bentemple commented Jul 27, 2024

As commented elsewhere, probably should scrap this and just move the logic higher in the program. i.e. on a failed check, after 5 minutes re-check if the domain is in-fact wrong and needs updating still.

Though, this would get into a scenario where ostensibly you might be failing to update 1 provider, but another provider has the correct DNS, and so it obfuscates the error.

@bentemple bentemple force-pushed the ben.temple/porkbun-skip-duplicate-updates branch from e30c143 to f678492 Compare July 29, 2024 16:14
@Bananas-Are-Yellow
Copy link

Ultimately simply restarting the DDNS-Updater service after the domain has successfully propogated will resolve the issue

I'm having the 400 error problem, but the problem is not resolved by restarting the container with:

docker compose stop && docker compose up -d

The updates.json file shows the correct IP address recorded a few times, but the 400 error still occurs once the A records have been set. It's as if the updates.json file is not being read to see if the IP address has changed.

Here is my config.json file.

{
    "settings": [
        {
            "provider": "porkbun",
            "domain": "example.com",
            "host": "@,*",
            "api_key": "pk1_...",
            "secret_api_key": "sk1_...",
            "ip_version": "ipv4",
            "ipv6_suffix": ""
        }
    ]
}

@qdm12
Copy link
Owner

qdm12 commented Aug 7, 2024

As commented elsewhere, probably should scrap this and just move the logic higher in the program. i.e. on a failed check, after 5 minutes re-check if the domain is in-fact wrong and needs updating still.

I think #485 would solve it, and we can log a warning if that period is less than the ttl of the record. I will mark it as 🚨 urgent, but will hold off a few days to start working on it (got other issues to attend to!)

this would get into a scenario where ostensibly you might be failing to update 1 provider, but another provider has the correct DNS, and so it obfuscates the error.

One more reason we need a period per setting to clarify/simplify all this 😄

@Bananas-Are-Yellow First of all bananas can be brown and black too 🍌 !

I'm having the 400 error problem

That's odd, @bentemple any idea why that could be? @Bananas-Are-Yellow can you share your logs, running the container with LOG_LEVEL=debug (⚠️ and hide your credentials)

It's as if the updates.json file is not being read to see if the IP address has changed.

It's not, the program resolves the domain name (abc.domain.com) every period and compares it to your public ip address. updates.json is only used to store changes for the web ui AND is used by cloudflare when proxied is enabled, since ddns-updater cannot resolve the domain (since it's proxied).

@Bananas-Are-Yellow
Copy link

It looks like I misunderstood how updates.json is used. I thought it was used as a record of what had already been set, to avoid setting the same value again. My confusion came from this:

Ultimately simply restarting the DDNS-Updater service after the domain has successfully propogated will resolve the issue, but this can be quite confusing for someone who is dealing with setup issues, removes the updates.json and then gets into a bad state where the domain is constantly returning a 400 error and it never resolves itself without a service restart.

For me, "restarting the DDNS-Updater service after the domain has successfully propogated", makes no difference. But the main issue is certainly true:

Porkbun will return a 400 error if you attempt to update the IP for a DNS record that is already set to the same value.

That's what I see too.

@qdm12
Copy link
Owner

qdm12 commented Aug 7, 2024

We may end up merging this, since there is also another person with the same problem on Porkbun. But I would ideally like to understand the root cause of this since it may apply to other registrars. I'm also wondering if this is specific to porkbun being slow at propagating updates 🤔 since I have not seen similar issues for other registrars.

@bentemple

This comment was marked as resolved.

@bentemple

This comment was marked as off-topic.

@bentemple
Copy link
Collaborator Author

bentemple commented Aug 7, 2024

I wonder if it actually makes sense to update the logic to:

if (noRecords)
  if ( top level domain ) 
      look for domain.tld ALIAS
      if ALIAS == pixie.porkbun.com
        delete
      else 
        logical error denoting the conflicting entry and user action needed
  if ( *.domain.tld ) 
    look for *.domain.tld CNAME
    if CNAME == pixie.porkbun.com
      delete
    else
      logical error denoting the conflicting CNAME entry and user action needed

Ultimately, we want to create the A / AAAA entries if none exist, but we really only want to be messing with CNAME and ALIAS conflicts if they're expected values. Just need to queue the user in better to the reason for the failure, and sadly the porkbun API generates worthless error messages. "400, something went wrong"

@qdm12
Copy link
Owner

qdm12 commented Aug 8, 2024

I wonder if it actually makes sense to update the logic to

TLDR: let's do your version!

Longer story:
I've commented my own version in #776 (comment) which is quite similar. I just don't delete unless there is a 400 code, so the API calls count is 2 (get records+create) to 4 (get records+create+deletion+create).
In your case, API calls count is 3 (get records+get alias|cname+create) to 4 (get records+get alias|cname+delete+create) but it adds the safety to check the alias|cname are the default porkbun ones. Plus creation of records are rare so API calls count doesnt' matter much really.

@Bananas-Are-Yellow

This comment was marked as off-topic.

@qdm12

This comment was marked as off-topic.

@Bananas-Are-Yellow

This comment was marked as off-topic.

@qdm12

This comment was marked as off-topic.

@bentemple
Copy link
Collaborator Author

I wonder if it actually makes sense to update the logic to

TLDR: let's do your version!

Longer story: I've commented my own version in #776 (comment) which is quite similar. I just don't delete unless there is a 400 code, so the API calls count is 2 (get records+create) to 4 (get records+create+deletion+create). In your case, API calls count is 3 (get records+get alias|cname+create) to 4 (get records+get alias|cname+delete+create) but it adds the safety to check the alias|cname are the default porkbun ones. Plus creation of records are rare so API calls count doesnt' matter much really.

sounds good. I'll update it accordingly. Sorry went to Defcon this last week so just busy life.

@bentemple

This comment was marked as off-topic.

@qdm12
Copy link
Owner

qdm12 commented Aug 15, 2024

sounds good. I'll update it accordingly. Sorry went to Defcon this last week so just busy life.

No pressure at all, the existing version already works more or less fine 😉

Ps: I saved that picture 😄

@bentemple bentemple marked this pull request as draft August 16, 2024 06:30
@bentemple bentemple force-pushed the ben.temple/porkbun-skip-duplicate-updates branch from 8e8c50e to bf424f7 Compare August 17, 2024 07:57
@bentemple bentemple changed the title Provider Porkbun: Don't attempt to update a record if it already matches the expected ipStr Provider Porkbun: Delete Default Porkbun Parked DNS Entry for *.domain.tld Aug 17, 2024
@bentemple bentemple changed the title Provider Porkbun: Delete Default Porkbun Parked DNS Entry for *.domain.tld Provider Porkbun: Delete Default Parked DNS Entry for *.domain.tld Aug 17, 2024
@bentemple bentemple force-pushed the ben.temple/porkbun-skip-duplicate-updates branch 3 times, most recently from 5a4fbdf to 81875b6 Compare August 17, 2024 09:19
@bentemple bentemple force-pushed the ben.temple/porkbun-skip-duplicate-updates branch 2 times, most recently from 59ff9c0 to 8a11f19 Compare August 17, 2024 09:30
@bentemple bentemple marked this pull request as ready for review August 17, 2024 09:40
@bentemple bentemple requested a review from qdm12 August 17, 2024 09:40
Copy link
Owner

@qdm12 qdm12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

A few comments, many nits (apologies) and a few important changes to make (without the nit prefix 😄). Feel free to skip nits if you want.

docs/porkbun.md Outdated Show resolved Hide resolved
internal/provider/constants/ip.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/provider.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/provider.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/api.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/provider.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/api.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/provider.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/provider.go Outdated Show resolved Hide resolved
internal/provider/providers/porkbun/provider.go Outdated Show resolved Hide resolved
@bentemple
Copy link
Collaborator Author

bentemple commented Aug 28, 2024

I took a pass at deleting the @ ALIAS opportunistically when setting the * domain. From what I could tell, this required updating the API to pass in the owner, but maybe there's a better way I just wasn't seeing.

I kept that separate as it's own commit, so it's both easy to see the change, and we can trivially revert it if it's not the direction you were thinking.

Side-note: I really appreciate all the nits. Code homogeneity is really important, and nits are a really great way to level up code quality (imo) so thank you for taking the time!

@bentemple bentemple force-pushed the ben.temple/porkbun-skip-duplicate-updates branch 2 times, most recently from b91e9c1 to afeb6bb Compare August 28, 2024 06:04
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
return nil, fmt.Errorf("%s - %w: %d: %s", recordType, errors.ErrHTTPStatusNotValid,
Copy link
Collaborator Author

@bentemple bentemple Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't entirely sure the best way to format these error messages to include the recordType. I welcome any input (always)

I thought about just leaving them alone, but given how abysmal the error messages are that Porkbun returns, thought it would be better to keep it everywhere possible to provide the most context.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! And, although repetitive, it feels logical to wrap the recordType (and owner) within the function instead of by the caller I would say with the code as is.

However, I managed to refactor the code to use a generic function

func httpPost[T any](ctx context.Context, client *http.Client,
	url string, requestData any, decodeBody bool) (responseData T, err error)

to remove a lot of the repetition (in terms of raw http client code and error wrappings), and now the error wrapping gets done at the caller level, for example:

	responseData, err := httpPost[jsonResponseData](ctx, client, url, postRecordsParams, decodeBody)
	if err != nil {
		return nil, fmt.Errorf("for record type %s and record owner %s: %w",
			recordType, owner, err)
	}

@bentemple bentemple force-pushed the ben.temple/porkbun-skip-duplicate-updates branch 2 times, most recently from e617e37 to 8e9e23e Compare August 28, 2024 06:22
@bentemple bentemple requested a review from qdm12 August 28, 2024 06:26
Copy link
Owner

@qdm12 qdm12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the re-work! And sorry for the 3 weeks delay, but I'm more or less back on the keyboard now. 😉

}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
return nil, fmt.Errorf("%s - %w: %d: %s", recordType, errors.ErrHTTPStatusNotValid,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! And, although repetitive, it feels logical to wrap the recordType (and owner) within the function instead of by the caller I would say with the code as is.

However, I managed to refactor the code to use a generic function

func httpPost[T any](ctx context.Context, client *http.Client,
	url string, requestData any, decodeBody bool) (responseData T, err error)

to remove a lot of the repetition (in terms of raw http client code and error wrappings), and now the error wrapping gets done at the caller level, for example:

	responseData, err := httpPost[jsonResponseData](ctx, client, url, postRecordsParams, decodeBody)
	if err != nil {
		return nil, fmt.Errorf("for record type %s and record owner %s: %w",
			recordType, owner, err)
	}

err = p.createRecord(ctx, client, recordType, ipStr)
if err != nil {
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
if len(records) > 0 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted to len(recordIDs) == 0 given the new function deleteDefaultConflictingRecordsIfNeeded not taking as many lines in the if block 😉 (to have less deltas)

Comment on lines 137 to 141
// For new domains, Porkbun Creates 2 Default DNS Records which point to their "parked" domain page:
// ALIAS domain.tld -> pixie.porkbun.com
// CNAME *.domain.tld -> pixie.porkbun.com
// ALIAS and CNAME records conflict with A and AAAA records, and attempting to create an A or AAAA record
// will return a 400 error if they aren't first deleted.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resumed the comment with

// 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

// CNAME *.domain.tld -> pixie.porkbun.com
// ALIAS and CNAME records conflict with A and AAAA records, and attempting to create an A or AAAA record
// will return a 400 error if they aren't first deleted.
porkbunParkedDomain := "pixie.porkbun.com"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed this to

const porkbunParkedDomain = "pixie.porkbun.com"

because constants are always nicer if possible 😉

// ALIAS and CNAME records conflict with A and AAAA records, and attempting to create an A or AAAA record
// will return a 400 error if they aren't first deleted.
porkbunParkedDomain := "pixie.porkbun.com"
switch {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to switch p.owner { directly

// Delete ALIAS domain.tld -> pixie.porkbun.com record
err = p.deleteSingleMatchingRecord(ctx, client, constants.ALIAS, "@", porkbunParkedDomain)
if err != nil {
return netip.Addr{}, fmt.Errorf("deleting default parked domain record: %w", err)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed error wrapping to

fmt.Errorf("deleting default ALIAS @ parked domain record: %w", err)

Comment on lines 152 to 153
// Error is ignored as the ALIAS could be set to something besides the parked domain. Failure here is non-fatal.
_ = p.deleteSingleMatchingRecord(ctx, client, constants.ALIAS, "@", porkbunParkedDomain)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh it should be a fatal error if we get an API error for example, so I changed it to

		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 fmt.Errorf("deleting default ALIAS @ parked domain record: %w", err)
		}

@qdm12 qdm12 force-pushed the ben.temple/porkbun-skip-duplicate-updates branch 2 times, most recently from 55223ba to e94838b Compare September 16, 2024 13:29
Copy link
Owner

@qdm12 qdm12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I allowed myself to push that generic http post function refactor commit to the master branch at 8c1b3e5 in order to only have relevant deltas in this PR, and then rebased your branch on the master branch resolving conflicts. I also pushed 2 small-ish commits on top of your commits.

Please review the code and perhaps test the program to double check it works fine (even basic update should do the test trick), and then I'm happy to merge it 😉

bentemple and others added 8 commits September 17, 2024 07:39
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.

Note: Any other domain name we simply attempt to create a record if no record exists, which will return a 400 error with no helpful error reason if there is a conflicting record of another type. We may want to consider checking for any conflicting records to produce a more helpful error for the user, but that change is well outside of the scope of this PR, and should likely be done in a more general way and apply to all DNS providers, rather than porkbun exclusively.

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)
Imorting suggested changes. More fixes to come to address remaining comments.

Co-authored-by: Quentin McGaw <[email protected]>
Fix lint, again. :rip:

Remove href to https://pixie.porkbun.com/ since that url doesn't
actually load and caused the tests to fail
- add `deleteDefaultConflictingRecordsIfNeeded` method
- handle non conflicting errors from `deleteSingleMatchingRecord`
- simplify comments by linking to documentation
- improve error wrappings
@qdm12 qdm12 force-pushed the ben.temple/porkbun-skip-duplicate-updates branch from e94838b to 2827fd4 Compare September 17, 2024 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants