dyndns_handler

package module
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Feb 4, 2026 License: MIT Imports: 21 Imported by: 0

README

DDNS Handler for caddy

This module implements a handler capable of processing Dyn-style DDNS update requests, which many routers support natively and can be used to monitor LAN or WAN IP addresses and update DNS records accordingly.

It uses the DNS provider modules from caddy-dns to update the requested DNS records. To determine which records can be modified, the underlying libdns provider must implement the libdns.ZoneLister interface.

For providers that do not implement ZoneLister, a wrapper is included to provide compatible zone-listing functionality, ensuring consistent behavior across all supported providers.

IP Resolvin

The handler will first check if the query parameter myip exists. If it is missing or cannot be parsed as a valid IP address, it will fall back to the client’s address.

When running behind a proxy, the client’s remote address may belong to the proxy and therefore be invalid. To handle this, there are two options: trusted_remotes and no_local_ip.

  • trusted_remotes allows you to define a list of trusted IP ranges whose X-Forwarded-For headers will be validated.
  • no_local_ip ensures that only a public IP is returned. If no valid IP can be resolved, it will return the machine’s public IP.

For example, if you are running behind a local proxy:

ddns /nic/update {
    trusted_remotes 127.0.0.1/8
    ...
}

This configuration trusts the X-Forwarded-For header for requests from 127.0.0.1. In some cases, this may resolve to a local IP, which can be acceptable. In scenarios where only a public IP should be used, enabling no_local_ip guarantees that the returned IP is public.

Authorisation

Because clients expect a badauth response with a 200 HTTP status when authentication fails, we cannot rely on the standard basic_auth directive. Instead, the handler uses a simple username-password map to authenticate incoming requests.

For example:

example.com {

    tls {
        dns mijnhost <APIKEY>
    }

    ddns /nic/update {
        users {
            foo bar
        }
        providers {
            mijnhost <APIKEY>
            ...
        }
    }
}

With this configuration:

~/ curl 'https://example.com/nic/update?hostname=foo.example.com&myip=127.0.0.1'
badauth

When providing valid credentials:

~/ curl 'https://foo:bar@example.com/nic/update?hostname=foo.example.com&myip=127.0.0.1'
nochg 127.0.0.1

This approach ensures compatibility with Dyn-style DDNS clients while allowing per-user authentication.

DNS Providers

To also support providers that do not implement the libdns.ZoneLister interface, a DNS wrapper provider is included. This wrapper can wrap around any caddy-dns provider and return a predefined list of zones when the supported zones are queried.

For example:

ddns.static_zones {
    provider <provider_name>
    zones    bar.com foo.com
}

With this configuration, the wrapped provider will be called whenever a request comes in for hostnames in the bar.com or foo.com zones.

Build with xcaddy

$ xcaddy build --with github.com/pbergman/caddy-ddns

Config (Caddyfile)

example.com {

    tls {
        dns mijnhost <APIKEY>
    }

    ddns /nic/update {
        users {
            foo bar
        }
        providers {
            mijnhost <APIKEY>
            ....
        }
    }
}

or with static zones

example.com {

    tls {
        dns mijnhost <APIKEY>
    }

    ddns /nic/update {
        providers {
            ddns.static_zones {
                provider mijnhost APIKEY
                zones    example.nl foo.com
            }
        }
    }
}

Example Configuration (Vyatta / EdgeOS)

For EdgeRouters or any operating system based on Vyatta OS, the following commands configure the DDNS client to update the DNS records for the domains bar.example.com and foo.example.com using the ip on network interface eth1.

configure
set service dns dynamic interface eth1 service custom-caddy_ddns host-name bar.example.com
set service dns dynamic interface eth1 service custom-caddy_ddns host-name foo.example.com
set service dns dynamic interface eth1 service custom-caddy_ddns protocol dyndns2
set service dns dynamic interface eth1 service custom-caddy_ddns login foo
set service dns dynamic interface eth1 service custom-caddy_ddns password bar
set service dns dynamic interface eth1 service custom-caddy_ddns server example.com
commit; save; exit
Force an update

To manually trigger an update of the DDNS records for eth1, run:

update dns dynamic interface eth1
Show DDNS Status

To check the current DDNS status and update timestamps, run:

show dns dynamic status

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ProviderName

func ProviderName(module caddy.Module) string

Types

type Handler

type Handler struct {

	// The provider configurations with which will be used
	// to update records incoming reqeust.
	ProvidersRaw []json.RawMessage `json:"providers,omitempty" caddy:"namespace=dns.providers inline_key=name"`

	// List of trusted remotes which will be used to determine
	// the client ip based on x-forwarded-for header
	TrustedRemotes *IPPrefixList `json:"trusted_remotes"`

	// When true, we only satisfy by a public ip when trying
	// to determine the client ip. This means when no ip is
	// found, it will try to resolve "this" remote ip
	NoLocalIp bool `json:"no_local_ip"`

	// Because https://help.dyn.com/return-codes.html specifies
	// that we return "badauth" (with status 200) we cannot use
	// the basic_auth directive as it will return 401 as expected
	// in a normal authentication request.
	//
	// So a username – password map will be used for authentication
	// and when left empty, all requests will be allowed.
	//
	// At this moment the passwords are stored in plain text. Since
	// the dns provider API tokens and keys are also plain text,
	// there’s currently no real benefit for adding additional
	// hashing or encryption mechanisms.
	Users map[string]string `json:"users"`
	// contains filtered or unexported fields
}

func (Handler) CaddyModule

func (Handler) CaddyModule() caddy.ModuleInfo

func (*Handler) Provision

func (h *Handler) Provision(ctx caddy.Context) error

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(response http.ResponseWriter, request *http.Request, next caddyhttp.Handler) error

ServeHTTP will handle incoming request and return 200 with code as described in

https://help.dyn.com/return-codes.html

func (*Handler) UnmarshalCaddyfile

func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:

ddns {
    provider 	{
    	<name> ...
	}
	no_local_ip
	users {
		username password
	}
	trusted_remotes <ip prefix>...
}

type IPPrefixList

type IPPrefixList []netip.Prefix

func (*IPPrefixList) Contains

func (t *IPPrefixList) Contains(ip netip.Addr) bool

type Provider

type Provider interface {
	BaseProvider
	libdns.ZoneLister
}

type ReturnCode

type ReturnCode string
const (
	Good                        ReturnCode = "good"
	NoChange                    ReturnCode = "nochg"
	NotFullyQualifiedDomainName ReturnCode = "notfqdn"
	DNSError                    ReturnCode = "dnserr"
	NoHost                      ReturnCode = "nohost"
	BadAuthentication           ReturnCode = "badauth"
)

https://help.dyn.com/return-codes.html

type StaticZonesProvider

type StaticZonesProvider struct {
	ZonesRaw    []string        `json:"zones,omitempty"`
	ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
	// contains filtered or unexported fields
}

func (*StaticZonesProvider) AppendRecords

func (s *StaticZonesProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)

func (StaticZonesProvider) CaddyModule

func (StaticZonesProvider) CaddyModule() caddy.ModuleInfo

func (*StaticZonesProvider) DeleteRecords

func (s *StaticZonesProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)

func (*StaticZonesProvider) GetRecords

func (s *StaticZonesProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error)

func (*StaticZonesProvider) ListZones

func (s *StaticZonesProvider) ListZones(ctx context.Context) ([]libdns.Zone, error)

func (*StaticZonesProvider) Provision

func (p *StaticZonesProvider) Provision(ctx caddy.Context) error

func (*StaticZonesProvider) SetRecords

func (s *StaticZonesProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)

func (*StaticZonesProvider) UnmarshalCaddyfile

func (p *StaticZonesProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile sets up the Wrapped DNS provider from Caddyfile tokens. Syntax:

ddns.static_zones {
    provider 	<name> ...
	zones		<zone ...>
}

type WaitableLocker

type WaitableLocker interface {
	sync.Locker
	Wait()
}

func NewSemaphore

func NewSemaphore(x int) WaitableLocker

Jump to

Keyboard shortcuts

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