Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ require (

require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
Expand All @@ -24,6 +27,9 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -47,6 +53,7 @@ github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
Expand Down Expand Up @@ -93,6 +100,7 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 h1:N0m3tKYbkRMmDobh/47ngz+AWeV7PcfXMDi8xu3Vrag=
Expand Down
210 changes: 91 additions & 119 deletions internal/provider/providers/route53/provider.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package route53

import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"net/netip"
"net/url"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"

Choose a reason for hiding this comment

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

Go SDK v1 is EOL https://aws.amazon.com/blogs/developer/announcing-end-of-support-for-aws-sdk-for-go-v1-on-july-31-2025/

You're gonna want to use github.com/aws/aws-sdk-go-v2/

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"

"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"
)
Expand All @@ -26,66 +25,99 @@ type Provider struct {
ipv6Suffix netip.Prefix
zoneID string
ttl uint32
signer *signer
session *session.Session
accessKey string // For static credentials
secretKey string // For static credentials
}

func New(data json.RawMessage, domain, owner string,
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
provider *Provider, err error,
) {
var providerSpecificSettings struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
ZoneID string `json:"zone_id"`
TTL *uint32 `json:"ttl,omitempty"`
func New(data json.RawMessage, domain, owner string, ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (*Provider, error) {
var settings struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
AWSProfile string `json:"aws_profile"`
ZoneID string `json:"zone_id"`
TTL *uint32 `json:"ttl,omitempty"`
}
err = json.Unmarshal(data, &providerSpecificSettings)
if err != nil {
if err := json.Unmarshal(data, &settings); err != nil {
return nil, fmt.Errorf("decoding provider specific settings: %w", err)
}

const defaultTTL = 300
ttl := uint32(defaultTTL)
if providerSpecificSettings.TTL != nil {
ttl = *providerSpecificSettings.TTL
if err := validateSettings(domain, settings.AccessKey, settings.SecretKey, settings.AWSProfile, settings.ZoneID); err != nil {
return nil, fmt.Errorf("validating provider specific settings: %w", err)
}

err = validateSettings(domain, providerSpecificSettings.AccessKey,
providerSpecificSettings.SecretKey, providerSpecificSettings.ZoneID)
if err != nil {
return nil, fmt.Errorf("validating provider specific settings: %w", err)
const defaultTTL = 300
ttl := defaultTTL
if settings.TTL != nil {
ttl = int(*settings.TTL)
}

// Global resources needs signature to us-east-1 globalRegion
// and update / insert operations to route53 are also in us-east-1.
const globalRegion = "us-east-1"
const route53Service = "route53"
const v4SignatureVersion = "aws4_request"
signer := &signer{
accessKey: providerSpecificSettings.AccessKey,
secretkey: providerSpecificSettings.SecretKey,
region: globalRegion,
service: route53Service,
signatureVersion: v4SignatureVersion,
var sess *session.Session
var accessKey, secretKey string

if settings.AWSProfile != "" {
fmt.Println("Using AWS profile:", settings.AWSProfile)
var err error
sess, err = session.NewSessionWithOptions(session.Options{
Profile: settings.AWSProfile,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return nil, fmt.Errorf("creating AWS session: %w", err)
}

// Verify credentials
_, err = sess.Config.Credentials.Get()
if err == nil {
stsSvc := sts.New(sess)
identity, err := stsSvc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err == nil {
fmt.Println("STS Identity:", *identity.Arn)
} else {
fmt.Println("Could not verify identity with STS:", err)
}
} else {
return nil, fmt.Errorf("resolving credentials from profile: %w", err)
}
} else {
fmt.Println("Using access key and secret key")
// Store credentials for static credential creation
accessKey = settings.AccessKey
secretKey = settings.SecretKey

// Create a basic session for non-profile usage
var err error
sess, err = session.NewSession()
if err != nil {
return nil, fmt.Errorf("creating AWS session: %w", err)
}
}

return &Provider{
domain: domain,
owner: owner,
ipVersion: ipVersion,
ipv6Suffix: ipv6Suffix,
signer: signer,
zoneID: providerSpecificSettings.ZoneID,
ttl: ttl,
zoneID: settings.ZoneID,
ttl: uint32(ttl),
session: sess,
accessKey: accessKey,
secretKey: secretKey,
}, nil
}

func validateSettings(domain, accessKey, secretKey, zoneID string) (err error) {
err = utils.CheckDomain(domain)
if err != nil {
func validateSettings(domain, accessKey, secretKey, awsProfile, zoneID string) error {
if err := utils.CheckDomain(domain); err != nil {
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
}

if awsProfile != "" {
if zoneID == "" {
return fmt.Errorf("%w", errors.ErrZoneIdentifierNotSet)
}
return nil // AWS SDK is expected to resolve credentials
}

switch {
case accessKey == "":
return fmt.Errorf("%w", errors.ErrAccessKeyNotSet)
Expand All @@ -101,29 +133,12 @@ func (p *Provider) String() string {
return utils.ToString(p.domain, p.owner, constants.Route53, 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) 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{
Expand All @@ -134,60 +149,17 @@ func (p *Provider) HTML() models.HTMLRow {
}
}

// See https://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: route53Domain,
Path: "/2013-04-01/hostedzone/" + p.zoneID + "/rrset",
}

changeRRSetRequest := newChangeRRSetRequest(p.BuildDomainName(), p.ttl, ip)

// Note the AWS API does not accept JSON for this endpoint
buffer := bytes.NewBuffer(nil)
encoder := xml.NewEncoder(buffer)
err = encoder.Encode(changeRRSetRequest)
if err != nil {
return netip.Addr{}, fmt.Errorf("XML encoding change RRSet request: %w", err)
}

request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
}

p.setHeaders(request, buffer.Bytes())

response, err := client.Do(request)
if err != nil {
return netip.Addr{}, err
}
defer response.Body.Close()

xmlDecoder := xml.NewDecoder(response.Body)
if response.StatusCode == http.StatusOK {
return ip, nil
}

var errorResponse errorResponse
err = xmlDecoder.Decode(&errorResponse)
if err != nil {
return netip.Addr{}, fmt.Errorf("XML decoding response body: %w", err)
}
return netip.Addr{}, fmt.Errorf("%w: %d: request %s %s/%s: %s",
errors.ErrHTTPStatusNotValid, response.StatusCode,
errorResponse.RequestID, errorResponse.Error.Type,
errorResponse.Error.Code, errorResponse.Error.Message)
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (netip.Addr, error) {
signer := p.createSigner()
return updateRecord(ctx, client, signer, p.zoneID, p.BuildDomainName(), p.ttl, ip)
}

func (p *Provider) setHeaders(request *http.Request, payload []byte) {
now := time.Now().UTC()
headers.SetUserAgent(request)
headers.SetContentType(request, "application/xml")
headers.SetAccept(request, "application/xml")
request.Header.Set("Date", now.Format(dateTimeFormat))
request.Header.Set("Host", route53Domain)
signature := p.signer.sign(request.Method, request.URL.Path, payload, now)
request.Header.Set("Authorization", signature)
func (p *Provider) createSigner() *Route53Signer {
if p.accessKey != "" && p.secretKey != "" {
// Use static credentials
creds := credentials.NewStaticCredentials(p.accessKey, p.secretKey, "")
return NewRoute53Signer(creds)
}
// Use session credentials (for profile-based authentication)
return NewRoute53Signer(p.session.Config.Credentials)
}
Loading