ddns

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package ddns keeps a dynamic DNS record pointed at this host's public IPv4.

Index

Constants

View Source
const (
	EnvEnabled        = "APP_DDNS_ENABLED"
	EnvInterval       = "APP_DDNS_INTERVAL"
	EnvProbeURLs      = "APP_DDNS_PROBE_URLS"
	EnvProbeQuorum    = "APP_DDNS_PROBE_QUORUM"
	EnvTimeout        = "APP_DDNS_TIMEOUT"
	EnvMinUpdateGap   = "APP_DDNS_MIN_UPDATE_GAP"
	EnvReassert       = "APP_DDNS_REASSERT_INTERVAL"
	EnvFailAlertAfter = "APP_DDNS_FAIL_ALERT_AFTER"
	EnvAllowPrivate   = "APP_DDNS_ALLOW_PRIVATE"
	EnvAllowInsecure  = "APP_DDNS_ALLOW_INSECURE_HTTP"
	EnvUpdateURL      = "APP_DDNS_UPDATE_URL"
	EnvDuckDomains    = "APP_DDNS_DUCKDNS_DOMAINS"
	EnvDuckToken      = "APP_DDNS_DUCKDNS_TOKEN" //nolint:gosec // env var name, not a credential

)

Env names. The updater is off unless APP_DDNS_ENABLED is on and a target is set.

View Source
const (

	// ProviderGeneric is an operator-supplied update URL with an {ip} placeholder.
	ProviderGeneric = "generic"
	// ProviderDuckDNS is the DuckDNS preset (domains plus token).
	ProviderDuckDNS = "duckdns"

	// SourceStored says the setting came from the stored row.
	SourceStored = "stored"
	// SourceDefault says nothing is stored, so env defaults apply.
	SourceDefault = "default"
)

Variables

View Source
var (
	ErrNoQuorum  = errors.New("ddns: probes did not agree")
	ErrNonPublic = errors.New("ddns: detected address is not public")
)

Detection outcomes the loop reports as warnings, never as writes.

View Source
var ErrNoSecrets = errors.New("storing the dynamic DNS target needs a configured master key")

ErrNoSecrets means no master key is configured, so nothing can be stored.

View Source
var ErrNoTarget = errors.New("ddns: enabled but no update target configured")

ErrNoTarget means the updater is enabled but no provider is configured.

Functions

func Enabled

func Enabled(getenv func(string) string) bool

Enabled reports whether the operator turned the updater on and left detection enabled.

func OSEnv

func OSEnv(k string) string

OSEnv reads the process environment.

func StartFromEnv

func StartFromEnv(ctx context.Context, getenv func(string) string, log *slog.Logger, hooks Hooks) (bool, error)

StartFromEnv launches the loop only when enabled; it returns false when off.

Types

type Carry

type Carry struct {
	LastIP     string
	LastChange time.Time
	LastWrite  time.Time
}

Carry is the write history a replacement loop inherits so the minimum gap and reassert timer keep applying.

type Config

type Config struct {
	Interval       time.Duration
	ProbeURLs      []string
	Quorum         int
	Timeout        time.Duration
	MinUpdateGap   time.Duration
	Reassert       time.Duration
	FailAlertAfter int
	AllowPrivate   bool
	AllowInsecure  bool
}

Config holds the loop settings; zero durations are replaced by defaults.

func ConfigFromEnv

func ConfigFromEnv(getenv func(string) string) Config

ConfigFromEnv reads the loop settings; getenv is injectable for tests.

type Detector

type Detector struct {
	URLs   []string
	Quorum int
	Fetch  FetchFunc
}

Detector asks several probe URLs and accepts an address only on agreement.

func NewDetector

func NewDetector(cfg Config) *Detector

NewDetector uses the guarded client, so a probe URL cannot point inside the network.

func (*Detector) Detect

func (d *Detector) Detect(ctx context.Context) (string, error)

Detect returns the IPv4 at least Quorum probes agree on.

type FetchFunc

type FetchFunc func(ctx context.Context, url string) (string, error)

FetchFunc returns the raw answer of one probe URL.

type HTTPProvider

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

HTTPProvider calls an operator-supplied update URL with {ip} substituted.

func NewDuckDNS

func NewDuckDNS(domains, token string, cfg Config) (*HTTPProvider, error)

NewDuckDNS is the generic provider preset for the DuckDNS update endpoint.

func NewHTTPProvider

func NewHTTPProvider(template string, cfg Config, accept func(string) bool) (*HTTPProvider, error)

NewHTTPProvider validates the template and registers it with the log masker. accept may be nil (any 2xx).

func (*HTTPProvider) Update

func (p *HTTPProvider) Update(ctx context.Context, ip string) error

Update sends the request. Errors never carry the URL.

type Hooks

type Hooks struct {
	OnAlert   func(msg string)
	OnRecover func()
}

Hooks connect the loop to the alert path owned elsewhere; both may be nil.

type Manager

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

Manager owns the stored setting and keeps one updater loop matching it.

func NewManager

func NewManager(ctx context.Context, s Secrets, getenv func(string) string, log *slog.Logger, hooks Hooks) *Manager

NewManager wires the manager; secrets may be nil, then only env defaults apply and nothing can be stored.

func (*Manager) Apply

func (m *Manager) Apply(ctx context.Context, u Update) (Resource, error)

Apply validates and stores a change, then reconciles the loop so it takes effect without a restart.

func (*Manager) Get

func (m *Manager) Get(ctx context.Context) (Resource, error)

Get returns the effective setting. The target is never part of it.

func (*Manager) Reconcile

func (m *Manager) Reconcile(ctx context.Context) error

Reconcile starts, restarts or stops the loop so it matches the effective setting.

func (*Manager) Status

func (m *Manager) Status() StatusView

Status returns the loop snapshot; Running is false when the loop is stopped.

type Provider

type Provider interface {
	Update(ctx context.Context, ip string) error
}

Provider writes the address to one DNS target.

func ProviderFromEnv

func ProviderFromEnv(getenv func(string) string, cfg Config) (Provider, error)

ProviderFromEnv builds the configured provider: the generic update URL wins, then the DuckDNS preset.

type Resource

type Resource struct {
	Settings
	Source           string `json:"source"`
	TargetConfigured bool   `json:"target_configured"`
	Host             string `json:"host,omitempty"`
	Running          bool   `json:"running"`
}

Resource is what the API returns: settings, where they came from, and the host only, never the target.

type Secrets

type Secrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteValue(ctx context.Context, serviceName, envKey string) error
}

Secrets is the encrypted store the setting lives in (the secrets manager satisfies it).

type Settings

type Settings struct {
	Enabled           bool     `json:"enabled"`
	Provider          string   `json:"provider"`
	Domains           string   `json:"domains,omitempty"`
	IntervalSecs      int      `json:"interval_seconds"`
	ProbeURLs         []string `json:"probe_urls"`
	ProbeQuorum       int      `json:"probe_quorum"`
	TimeoutSecs       int      `json:"timeout_seconds"`
	MinUpdateGapSecs  int      `json:"min_update_gap_seconds"`
	ReassertSecs      int      `json:"reassert_seconds"`
	FailAlertAfter    int      `json:"fail_alert_after"`
	AllowPrivate      bool     `json:"allow_private"`
	AllowInsecureHTTP bool     `json:"allow_insecure_http"`
}

Settings is the stored, non-secret part. Durations are whole seconds.

func DefaultSettings

func DefaultSettings(getenv func(string) string) Settings

DefaultSettings seeds the setting from env; it applies only until something is stored.

func (Settings) Config

func (s Settings) Config() Config

Config converts the stored seconds into loop settings.

type Status

type Status struct {
	LastCheck   time.Time
	LastChange  time.Time
	LastIP      string
	LastError   string
	Failures    int
	AlertActive bool
}

Status is a snapshot for later API and CLI slices. It holds no secrets.

type StatusView

type StatusView struct {
	Running     bool      `json:"running"`
	LastCheck   time.Time `json:"last_check,omitzero"`
	LastChange  time.Time `json:"last_change,omitzero"`
	LastIP      string    `json:"last_ip,omitempty"`
	LastResult  string    `json:"last_result"`
	LastError   string    `json:"last_error,omitempty"`
	Failures    int       `json:"consecutive_failures"`
	AlertActive bool      `json:"alert_active"`
}

StatusView is the loop state for the status read.

type Update

type Update struct {
	Enabled           *bool
	Provider          *string
	Domains           *string
	Target            *string
	IntervalSecs      *int
	ProbeURLs         *[]string
	ProbeQuorum       *int
	TimeoutSecs       *int
	MinUpdateGapSecs  *int
	ReassertSecs      *int
	FailAlertAfter    *int
	AllowPrivate      *bool
	AllowInsecureHTTP *bool
}

Update is a partial change; nil fields keep the current value, a non-nil empty Target clears it.

type Updater

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

Updater detects the address and writes it through the provider.

func New

func New(cfg Config, d *Detector, p Provider, log *slog.Logger, hooks Hooks) *Updater

New wires an updater; a nil logger discards output.

func (*Updater) Carry

func (u *Updater) Carry() Carry

Carry snapshots the write history; it is zero until a write succeeded.

func (*Updater) Run

func (u *Updater) Run(ctx context.Context)

Run ticks until ctx ends.

func (*Updater) Seed

func (u *Updater) Seed(c Carry)

Seed restores write history from an earlier loop before Run starts.

func (*Updater) Status

func (u *Updater) Status() Status

Status returns a copy of the current state.

func (*Updater) Tick

func (u *Updater) Tick(ctx context.Context)

Tick runs one check. A panic or error here never escapes the loop.

Jump to

Keyboard shortcuts

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