Documentation
¶
Overview ¶
Package server implements the dddns HTTP listener that translates dyndns-style requests from UniFi's inadyn into Route53 updates.
Index ¶
Constants ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
NewServer wires the handler chain from a validated Config. Both Config.Validate and ServerConfig.Validate are called — fail-closed startup per §3 L6.
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.