dodns

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2024 License: MIT Imports: 10 Imported by: 0

README

dodns

Golang wrapper library & cli for updating DNS records with DigitalOcean using their godo library.

Specifically:

  • it only updates IPv6 AAAA records
  • it currently only looks for a "stable" inbound IPv6 address
  • it only supports Linux

Usage

The main functionality is available in the module's public API, but a dodns cli is also provided.

A DigitalOcean API token is needed to perform DNS updates.

The token needs to include the Create, Read, and Update scopes on the "domain" resource.

The dodns cli expects the token to be available in the DO_TOKEN environment variable.

CLI usage:

$ dodns -h
Usage of dodns:
  -name string
        The DNS AAAA name to set
  -subscribe
        Subscribe to netlink address updates and keep running
  -zone string
        The DNS zone for the record
   string
        The interface whose address to find (DEFAULT 'eth0')

Example ad hoc usage:

$ dodns -name test -zone example.com eth1
INFO: 2024/11/10 22:06:19 dns_do.go:60: new record created for test.example.com with address 2001:db8:427f:3c01:2e0:4cff:fec8:753a
$

The -subscribe option tells dodns to subscribe to address update messages from netlink. dodns will parse these netlink address update messages and perform DNS updates based on these updates, as needed.

Caveats

NOTE: The -subscribe requires the cap_net_admin network capability to function. Use setcap to add the cap_net_admin capability to the dodns binary, e.g.:

$ sudo setcap cap_net_admin+eip dodns
$

This is because dodns creates a netlink subscription to listen for address updates when used with the subscribe option, and a netlink subscription requires the cap_net_admin capability.

Running dodns as root gets around this but is recommended against as a best practice.

How it works

Address discovery

Dynamic DNS utilities commonly will try to find the public IP address of a host by sending a request to an external service that will respond with the source IP of your request. An external view is needed for hosts that sit behind a NAT layer and e.g. are configured with an RFC1918 or other "private" IPv4 address and don't have direct knowledge of what IP address their traffic gets translated onto.

For IPv6, this has two problems.

First, if hosts are using rotating IPv6 addresses like privacy addresses, temporary IPv6 addresses are used for outbound connections. The source address on a request to an external "what is my IP adddress?" type service will show this temporary address rather than a more stable address. If this type of temporary address is set for a DNS record, the DNS record will need to be updated more frequently as temporary addresses expire and are replaced, making services hosted at that address less stable and more subject to disruption.

Second, unlike a NAT-heavy IPv4 world, IPv6 hosts in a sensible setup should actually be configured directly with one or more Globally Unicast Addresses (GUA). So, you should not need to ask an external service what your "post NAT" address is, since no NAT is being applied you can see your "public" address yourself locally.

To accomplish this, dodns uses netlink; this is what makes dodns only function on Linux, as netlink is a Linux-specific interface. IP address information is available through Golang's builtin and OS-agnostic net package, such as through net.InterfaceAddrs(). However, this does not provide us all of the information we need to find our stable inbound IPv6 address.

If we are using temporary / privacy addresses, iproute2 address output will show a mngtmpaddr flag on one address, e.g.:

    inet6 2001:db8:427f:3c01:e653:56d9:9d23:4e76/64 scope global temporary dynamic
       valid_lft 86153sec preferred_lft 14153sec
    inet6 2001:db8:427f:3c01:2e0:4cff:fec8:753a/64 scope global dynamic mngtmpaddr
       valid_lft 86153sec preferred_lft 14153sec

Without diving too far into the details of that flag, we do also notice that this address does not show the temporary flag present on the address address.

The mngtmpaddr flag is indicated in netlink info for the address through the IFA_F_MANAGETEMPADDR flag in ifa_flags, with a bit flag value of 0x100 (256).

dodns uses a netlink library to check the addresses on the requested interface for an address with the IFA_F_MANAGETEMPADDR flag set. This address is selected as the stable inbound address to be updated in the dynamic DNS entry update through the DigitalOcean API.

In the future, this may be shifted to instead use the IFA_F_PERMANENT flag (0x80) if that's found to be a better fit, but this works for the time being.

Dynamic DNS update

dodns uses DigitalOcean's godo library library to set the discovered IPv6 address as the value for your DNS record.

The cli will first check if there are any existing AAAA records for the provided name. It then uses the following logic for updating the DNS record:

  • If no matching record is found, it creates a new record
  • If a single matching record is found, it updates that existing record
  • If more than one matching record is found, it errors and will refuse to update the records

Documentation

Index

Constants

View Source
const (
	DefaultTTL int = 300

	ErrNoCachedRecord  string = "no record ID found for update"
	ErrMultipleRecords string = ">1 matching record exists; cannot update"
	ErrNoMatchingId    string = "no matching record ID found"
)
View Source
const (
	Ifa_F_Secondary AddrFlags = 1 << iota
	Ifa_F_NoDad
	Ifa_F_Optimistic
	Ifa_F_DadFailed
	Ifa_F_HomeAddress
	Ifa_F_Deprecated
	Ifa_F_Tentative
	Ifa_F_Permanent
	Ifa_F_ManageTempAddr
	Ifa_F_NoPrefixRoute
	Ifa_F_McAutoJoin
	Ifa_F_StablePrivacy

	ErrNoMngTmpAddr string = "no MngTmpAddr found"
	ErrDeleteUpdate string = "update was delete not add"
)
View Source
const (
	ErrNoDoEnvToken string = "no DigitalOcean Token (DO_TOKEN) found in os env"
)

Variables

This section is empty.

Functions

func GetIfaceMngTmpAddr added in v0.2.0

func GetIfaceMngTmpAddr(ifname string) (net.IP, error)

GetIfaceMngTmpAddr retrieves the addresses from the interface of the provided name. If an address with the `IFA_F_MANAGETEMPADDR` ifa_flag is set (0x100), it returns that address and a nil error. If no address with the flag is found, it returns a ErrNoMngTmpAddr error. Any other netlink errors from retrieving the interface or its addresses are bubbled up.

func GetMngTmpAddr

func GetMngTmpAddr(addr net.IP, flags AddrFlags) (net.IP, error)

GetMngTmpAddr takes a net.IP and AddrFlags. If the flags have the `IFA_F_MANAGETEMPADDR` ifa_flag set (0x100), it returns that address as a net.IP. If not, it returns nil and an error.

func GetMngTmpAddrFromUpdate added in v0.3.0

func GetMngTmpAddrFromUpdate(update netlink.AddrUpdate) (net.IP, error)

GetMngTmpAddrFromUpdate takes a netlink.AddrUpdate and tests whether it: * is a new/added address * has the `IFA_F_MANAGETEMPADDR` ifa_flag set (0x100) If so, it returns the address as a net.IP with no error If it is a new address but does not have the `IFA_F_MANAGETEMPADDR` flag set, it returns nil and a ErrNoMngTmpAddr error If is not a new/added address (i.e. it is a deleted address), it returns nil and a ErrDeleteUpdate error.

func IsIPv6 added in v0.2.0

func IsIPv6(addr net.IP) bool

IsIPv6 determines if the provided `net.IP` is an IPv6 address. It returns a bool.

func IsMngTmpAddr

func IsMngTmpAddr(flags AddrFlags) bool

IsMngTmpAddr responds with a bool whether the provided AddrFlags have the `IFA_F_MANAGETEMPADDR` ifa_flag set (0x100).

func MonitorUpdatesAndDispatch added in v0.3.0

func MonitorUpdatesAndDispatch(ch <-chan netlink.AddrUpdate, done chan bool, updater DnsUpdater, linkIndex int)

MonitorUpdatesAndDispatch monitors a channel for netlink.AddrUpdate messages. If the update indicates a new address and that address is a MngTmpAddr, it dispatches the address through the provided DnsUpdater in order to update the record in the DnsUpdater.

func NewDoClientWithEnvToken added in v0.3.0

func NewDoClientWithEnvToken() (*godo.Client, context.Context, error)

NewClientWithEnvToken retrieves a DigitalOcean API token from the env var `DO_TOKEN` and returns a *godo.Client and context.Context to be used to communicate with the DigitalOcean API.

func UpdateDnsRecord

func UpdateDnsRecord(client *godo.Client, ctx context.Context, name, zone string, value net.IP, force bool) error

UpdateDnsRecord uses the provided client & context to effectively perform an "upsert" of the provided name in the provided zone. It checks if an existing AAAA record exists for that name & zone. If not, it creates the record using *godo.Client.Domains.CreateRecord(). If a single matching record exists, it updates that record using *godo.Client.Domains.EditRecord(). If more than one matching record exists, it requires a `force` bool to be set to blindly overwrite the first matching result. It returns nothing except an error.

Types

type AddrFlags added in v0.3.0

type AddrFlags int

type DnsRecord added in v0.3.0

type DnsRecord struct {
	Name    string
	Zone    string
	Address net.IP
	ID      uuid.UUID
	TTL     int
}

type DnsUpdater added in v0.3.0

type DnsUpdater interface {
	SetAddress(net.IP)
	RefreshRecords() error
	CreateRecord() error
	UpdateRecord(bool) error
	CreateOrUpdateRecord() error
}

A DnsUpdater implements the needed RefreshRecords, CreateRecord, and UpdateRecord methods to be able to update an existing DNS AAAA record or create a new one.

type DoDnsUpdater added in v0.3.0

type DoDnsUpdater struct {
	// contains filtered or unexported fields
}

A DoDnsUpdater can search and update DNS records through the DigitalOcean godo package.

func NewDoDnsUpdaterWithEnvToken added in v0.3.0

func NewDoDnsUpdaterWithEnvToken(name, zone string) (DoDnsUpdater, error)

NewDoDnsUpdaterWithEnvToken creates a ctx.Context and *godo.Client, retrieves a DigitalOcean API token from the DO_TOKEN env var, and returns a DoDnsUpdater for the provided `name` and `zone` populated with those values as well as a ttl of 300 for the DoDnsUpdater.

func (*DoDnsUpdater) CreateOrUpdateRecord added in v0.3.0

func (u *DoDnsUpdater) CreateOrUpdateRecord() error

CreateOrUpdateRecord will try to update a record; if that fails, meaning no record currently exists with the provider, then it will create a new record.

func (*DoDnsUpdater) CreateRecord added in v0.3.0

func (u *DoDnsUpdater) CreateRecord() error

CreateRecord creates a new DNS AAAA record using DoDnsUpdater. Its only return is an error, if encountered.

func (*DoDnsUpdater) RefreshRecords added in v0.3.0

func (u *DoDnsUpdater) RefreshRecords() error

SearchRecords will search for instances of an AAAA record matching the provided name and zone using DoDnsUpdater. It returns a list of matching records as a godo.DomainRecord slice, and any encountered errors.

func (*DoDnsUpdater) SetAddress added in v0.3.0

func (u *DoDnsUpdater) SetAddress(addr net.IP)

SetAddress sets the address that the DoDnsUpdater will use when performing DNS updates for the record.

func (*DoDnsUpdater) UpdateRecord added in v0.3.0

func (u *DoDnsUpdater) UpdateRecord(tryRefresh bool) error

UpdateRecord uses the provided client & context to effectively perform an "upsert" of the provided name in the provided zone. It checks if an existing AAAA record exists for that name & zone. If not, it creates the record using *godo.Client.Domains.CreateRecord(). If a single matching record exists, it updates that record using *godo.Client.Domains.EditRecord(). If more than one matching record exists, it requires a `force` bool to be set to blindly overwrite the first matching result. It returns nothing except an error.

type FakeDnsUpdater added in v0.3.0

type FakeDnsUpdater struct {
	// contains filtered or unexported fields
}

func (*FakeDnsUpdater) CreateOrUpdateRecord added in v0.3.0

func (u *FakeDnsUpdater) CreateOrUpdateRecord() error

CreateOrUpdateRecord will try to update a record; if that fails, meaning no record currently exists with the provider, then it will create a new record.

func (*FakeDnsUpdater) CreateRecord added in v0.3.0

func (u *FakeDnsUpdater) CreateRecord() error

CreateRecord will create a new DNS AAAA record using the address set on the FakeDnsUpdater.

func (*FakeDnsUpdater) RefreshRecords added in v0.3.0

func (u *FakeDnsUpdater) RefreshRecords() error

RefreshRecords searches the u.recordStore for records that match u.recordName and stores them into u.records.

func (*FakeDnsUpdater) SetAddress added in v0.3.0

func (u *FakeDnsUpdater) SetAddress(addr net.IP)

SetAddress sets the address that the FakeDnsUpdater will use when performing DNS updates for the record.

func (*FakeDnsUpdater) UpdateRecord added in v0.3.0

func (u *FakeDnsUpdater) UpdateRecord(tryRefresh bool) error

UpdateRecord updates an existing dns record with the provider to the currently discovered u.address. If there is no existing local record cached, the updater doesn't know the ID of the record to update and it returns an error. If multiple matching records are found with the provider, the updater is unable to update the record and it returns an error. If the discovered address matches the current record value, the updater does nothing.

Directories

Path Synopsis
cmd
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL