server

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package server implements the dddns HTTP listener that translates dyndns-style requests from UniFi's inadyn into Route53 updates.

Index

Constants

View Source
const (
	MaxFailuresPerWindow = 5
	FailureWindow        = 60 * time.Second
	LockoutDuration      = 5 * time.Minute
)

Lockout policy (layer L3 in the security model): if MaxFailuresPerWindow or more auth failures occur within FailureWindow of each other, reject every subsequent attempt for LockoutDuration. This is a sliding window keyed on the process — an attacker who can restart dddns loses little because the supervisor respawns on a 5-second backoff.

View Source
const AuditMaxSize int64 = 10 * 1024 * 1024

AuditMaxSize is the default rotation threshold for the audit log — when the file reaches this size it is renamed to path+".old" before the next write appends to a fresh file. Matches the operational log policy in scripts/install-on-unifi-os.sh.

Variables

This section is empty.

Functions

func AuditPath

func AuditPath(cfg *config.Config) string

AuditPath returns the audit-log path — user-configured or derived from the data directory. Exported so cmd/ callers (e.g. `dddns serve status`) don't need to duplicate the path logic.

func IsAllowed

func IsAllowed(remoteAddr string, cidrs []string) bool

IsAllowed reports whether remoteAddr falls within any of the supplied CIDR blocks. The input is typically http.Request.RemoteAddr ("host:port") but a bare IP is also accepted. The function fails closed:

  • An unparseable host or a missing match returns false.
  • A malformed CIDR entry is silently skipped (ServerConfig.Validate rejects these upstream; skipping here is defense in depth).
  • An empty cidrs slice returns false.

Both IPv4 and IPv6 literals are supported, including the "%zone" suffix occasionally seen in IPv6 addresses.

func StatusPath

func StatusPath(cfg *config.Config) string

StatusPath returns the serve-status.json path — always in the same directory as the IP cache.

Types

type AuditEntry

type AuditEntry struct {
	Timestamp       time.Time `json:"ts"`
	RemoteAddr      string    `json:"remote"`
	Hostname        string    `json:"hostname,omitempty"`
	MyIPClaimed     string    `json:"myip_claimed,omitempty"`
	MyIPVerified    string    `json:"myip_verified,omitempty"`
	AuthOutcome     string    `json:"auth,omitempty"`
	Action          string    `json:"action,omitempty"`
	Route53ChangeID string    `json:"route53_change_id,omitempty"`
	Err             string    `json:"error,omitempty"`
}

AuditEntry is one line of the JSONL audit log. The handler fills in the relevant fields for the request it just processed; omitted fields are elided from the serialized form.

type AuditLog

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

AuditLog is an append-only JSONL writer with size-based rotation. All writes are serialized under a mutex; the on-disk append is a single os.File.Write on O_APPEND-opened FD, which is atomic for typical audit line sizes (well below PIPE_BUF).

func NewAuditLog

func NewAuditLog(path string) *AuditLog

NewAuditLog constructs an AuditLog writing to path with the default rotation threshold.

func (*AuditLog) Write

func (a *AuditLog) Write(entry AuditEntry) error

Write serializes entry as one JSON line and appends it to the log, rotating first if the file has reached the size threshold. entry.Timestamp is overwritten with the current time.

type AuthResult

type AuthResult int

AuthResult is the three-valued outcome of Authenticator.Check.

const (
	AuthOK AuthResult = iota
	AuthBadCredentials
	AuthLockedOut
)

type Authenticator

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

Authenticator verifies a Basic Auth password against a shared secret and enforces the sliding-window lockout. The zero value is not usable — construct with NewAuthenticator.

All methods are safe for concurrent use.

func NewAuthenticator

func NewAuthenticator(secret string) *Authenticator

NewAuthenticator returns an Authenticator bound to the given shared secret. The secret is stored verbatim — the caller is expected to have already decrypted it from config.secure if applicable.

func (*Authenticator) Check

func (a *Authenticator) Check(password string) AuthResult

Check returns AuthOK on a matching password, AuthLockedOut if the Authenticator is in a lockout window, and AuthBadCredentials otherwise (after recording the failure for lockout tracking).

A successful authentication clears the pending-failures tally — legitimate callers do not pay for historical typos.

type Handler

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

Handler processes dyndns-style requests from UniFi's inadyn. It owns the CIDR allowlist, Basic-Auth check, query validation, authoritative WAN-IP lookup, Route53 UPSERT (via updater), audit logging, and status snapshot.

func NewHandler

func NewHandler(cfg *config.Config, auth *Authenticator, audit *AuditLog, status *StatusWriter) *Handler

NewHandler constructs a Handler with production dependencies.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler for the dyndns update endpoint. See §10 of the design doc for the full response-code table.

type Server

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

Server wraps the HTTP listener that backs `dddns serve`. Dependencies are constructed in NewServer; Run blocks until the provided context is cancelled, then shuts down gracefully.

func NewServer

func NewServer(cfg *config.Config) (*Server, error)

NewServer wires the handler chain from a validated Config. Both Config.Validate and ServerConfig.Validate are called — fail-closed startup per §3 L6.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the underlying http.Handler. Exposed for tests that want to use httptest.NewServer instead of binding a real port.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run starts the listener and blocks until ctx is cancelled. On cancellation it performs an http.Server.Shutdown with a 5-second deadline, draining any in-flight request.

type StatusSnapshot

type StatusSnapshot struct {
	LastRequestAt   time.Time `json:"last_request_at"`
	LastRemoteAddr  string    `json:"last_remote_addr,omitempty"`
	LastAuthOutcome string    `json:"last_auth_outcome,omitempty"`
	LastAction      string    `json:"last_action,omitempty"`
	LastError       string    `json:"last_error,omitempty"`
}

StatusSnapshot is the last-request summary written to serve-status.json. It is consumed by `dddns serve status` (reader added in D1).

func ReadStatus

func ReadStatus(path string) (StatusSnapshot, error)

ReadStatus loads the latest StatusSnapshot from path. An absent file or malformed JSON is returned as an error — callers typically print the error verbatim (e.g. "status file not found — the server has not recorded any request yet").

type StatusWriter

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

StatusWriter overwrites a single JSON file with the outcome of the most recent request. Writes are atomic (write-to-temp + rename) so a concurrent reader never sees a partially-written file.

func NewStatusWriter

func NewStatusWriter(path string) *StatusWriter

NewStatusWriter constructs a StatusWriter that targets the given path. The directory must exist; it is not created lazily.

func (*StatusWriter) Write

func (s *StatusWriter) Write(snap StatusSnapshot) error

Write serializes snap as pretty-printed JSON and replaces the target file atomically.

Jump to

Keyboard shortcuts

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