challenge

package
v0.0.0-...-c292553 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2026 License: AGPL-3.0 Imports: 30 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultMapPath = "/run/csm/challenge_ips.txt"

DefaultMapPath is the webserver-readable Apache / LSWS RewriteMap. It lives under /run rather than state_path because state_path is mode 0700 and must stay private to CSM's bbolt database.

View Source
const DefaultNginxMapPath = "/run/csm/challenge_ips.nginx.map"

DefaultNginxMapPath is the webserver-readable Nginx map include.

Variables

View Source
var ErrSessionBadSignature = errors.New("session signature invalid")

ErrSessionBadSignature is returned by Verify for cookies whose HMAC does not match. Includes tampered payloads and cookies signed by a previous AdminSessionSigner instance (post-rotation).

View Source
var ErrSessionExpired = errors.New("session expired")

ErrSessionExpired is returned by Verify for cookies whose embedded expiry has passed.

View Source
var ErrSessionIPMismatch = errors.New("session IP mismatch")

ErrSessionIPMismatch is returned when the cookie was issued for a different IP than the one presenting it. Stops cookie theft from a different network.

View Source
var ErrSessionMalformed = errors.New("session payload malformed")

ErrSessionMalformed wraps decoding errors so a corrupt cookie has a distinct sentinel from a tampered one.

Functions

func CompareAdminSecret

func CompareAdminSecret(stored, presented string) bool

CompareAdminSecret returns true when stored and presented secrets match in constant time. Empty stored secret always returns false so a misconfigured admin_secret cannot accidentally accept any caller.

func EnsureMapFile

func EnsureMapFile(path string) error

EnsureMapFile creates a readable empty map when path is absent and leaves existing map contents intact. Apache validates txt: RewriteMap sources even when challenge mode is disabled, before an IPList would otherwise create the file.

Types

type AdminSessionSigner

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

AdminSessionSigner mints and verifies the signed cookies that let authenticated operators bypass the PoW. The signing key is generated on construction; rebuilding the signer (i.e., daemon restart) invalidates every previously-issued cookie -- the rotation contract.

func NewAdminSessionSigner

func NewAdminSessionSigner(ttl time.Duration) (*AdminSessionSigner, error)

NewAdminSessionSigner generates a fresh 32-byte signing key. The caller must keep the returned pointer for the lifetime of the challenge server; never construct a second signer for the same server, or already-issued cookies will be invalidated mid-session.

func (*AdminSessionSigner) Issue

func (s *AdminSessionSigner) Issue(ip string) string

Issue returns a cookie value of the form "<base64(payload)>.<base64 hmac>". The payload binds the cookie to a single IP and a single expiry so a stolen cookie does not work elsewhere or after the TTL.

func (*AdminSessionSigner) TTL

func (s *AdminSessionSigner) TTL() time.Duration

TTL exposes the configured cookie lifetime so the server can set the matching Max-Age on the Set-Cookie header.

func (*AdminSessionSigner) Verify

func (s *AdminSessionSigner) Verify(cookieValue, ip string) error

Verify checks the HMAC, payload format, expiry, and IP binding. Use errors.Is to branch on the failure mode.

type CaptchaProvider

type CaptchaProvider interface {
	Name() string
	Verify(ctx context.Context, token, remoteIP string) (bool, error)
}

CaptchaProvider verifies a third-party CAPTCHA token. Implementations post the operator's secret + the visitor's response token to the provider's siteverify endpoint and return a single bool: did the provider accept this submission?

func NewCaptchaProvider

func NewCaptchaProvider(name, secret string, timeout time.Duration) (CaptchaProvider, error)

NewCaptchaProvider returns the right provider for the configured name. Returns nil + nil when the operator has not enabled CAPTCHA; the server treats nil as "feature off".

type CrawlerVerifier

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

CrawlerVerifier classifies an IP as a verified search crawler iff the IP's reverse-DNS PTR matches one of the configured suffixes AND the PTR forward-resolves back to the same IP. The verifier caches both positive and negative results; positive cache TTL is the configured cacheTTL, negative is one-fifth of that to keep a transiently-broken resolver from locking out a legitimate crawler.

func NewCrawlerVerifier

func NewCrawlerVerifier(providers []string, cacheTTL time.Duration, resolver Resolver) *CrawlerVerifier

NewCrawlerVerifier builds a verifier with the named crawler families enabled. Unknown names are ignored (operators may have configured a family this binary does not know about; that is harmless).

func (*CrawlerVerifier) Enabled

func (v *CrawlerVerifier) Enabled() bool

Enabled reports whether at least one crawler family is configured; the server uses this to skip the verifier entirely (no DNS round trip) when the operator has not opted in.

func (*CrawlerVerifier) Verified

func (v *CrawlerVerifier) Verified(ctx context.Context, ip string) bool

Verified does the reverse-DNS + forward-confirm dance for ip and caches the result. Returns true only when the PTR ends in one of the allowed suffixes AND a forward lookup of the PTR includes ip in the result set.

type ExpiredEntry

type ExpiredEntry struct {
	IP     string
	Reason string
}

ExpiredEntry is returned by ExpiredEntries for escalation.

type IPList

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

IPList manages the set of IPs that should see challenge pages. Webserver integrations read its maps to redirect IPs to the challenge server.

func NewIPList

func NewIPList(statePath string) *IPList

NewIPList creates an IP list writer.

func NewIPListWithMapPath

func NewIPListWithMapPath(statePath, mapPath string) *IPList

NewIPListWithMapPath creates an IP list writer with an explicit webserver-facing map path.

func (*IPList) Add

func (l *IPList) Add(ip string, reason string, duration time.Duration)

Add marks an IP for challenge with the given reason.

func (*IPList) AddNonEscalating

func (l *IPList) AddNonEscalating(ip string, reason string, duration time.Duration)

AddNonEscalating marks an IP for challenge without timeout-to-block escalation.

func (*IPList) CleanExpired

func (l *IPList) CleanExpired()

CleanExpired removes expired entries without returning them. Use ExpiredEntries() instead when escalation is needed.

func (*IPList) Contains

func (l *IPList) Contains(ip string) bool

Contains returns true if the IP is currently on the challenge list.

func (*IPList) Count

func (l *IPList) Count() int

Count returns the number of IPs currently waiting on a challenge.

func (*IPList) ExpiredEntries

func (l *IPList) ExpiredEntries() []ExpiredEntry

ExpiredEntries removes expired entries and returns those eligible for escalation. The caller is expected to hard-block returned IPs.

func (*IPList) Remove

func (l *IPList) Remove(ip string)

Remove stops challenging an IP (passed or manually removed).

func (*IPList) SetNginxMap

func (l *IPList) SetNginxMap(path string, reload func() error)

SetNginxMap attaches a second map writer for Nginx stacks. The callback runs only when the rendered include content changes.

func (*IPList) SetPortGate

func (l *IPList) SetPortGate(g PortGate)

SetPortGate attaches a PortGate so every Add/Remove also opens or closes the kernel-level allow. Nil is a no-op (callers don't have to branch on whether the gate is configured). Safe to call before any Add/Remove; not safe to swap a non-nil gate for another at runtime.

type IPUnblocker

type IPUnblocker interface {
	TempAllowIP(ip string, reason string, timeout time.Duration) error
}

IPUnblocker is the interface for temporarily allowing an IP.

type PortGate

type PortGate interface {
	// Allow opens the gate for the source IP for at most ttl. The
	// underlying firewall enforces the TTL via the set's own timeout
	// so the entry expires even if Revoke is never called (daemon
	// crash, missed expiry). Returns nil on success or when the IP
	// cannot be parsed (best-effort; the IPList accepts only validated
	// IPs upstream, so a parse miss here is a bug to log, not block).
	Allow(ip string, ttl time.Duration) error
	// Revoke closes the gate for ip immediately. Safe to call for IPs
	// that were never on the gate (no-op).
	Revoke(ip string) error
	// Close tears down the gate's nftables footprint (chain, sets,
	// table). The port reverts to whatever the rest of the host
	// firewall would do with it.
	Close() error
}

PortGate locks the challenge listener TCP port to specific source IPs via the host firewall. An IP is allowed only while it is on the challenge IPList (plus operator infra IPs and loopback). Everything else gets dropped at the kernel before the listener sees the SYN, so the listener is invisible to port scanners and stays reachable only for the visitors the daemon has actually redirected.

Implementations are pluggable so the netlink-backed Linux variant can be swapped for a stub on platforms that do not have nftables. All methods are safe to call on a nil PortGate (no-op), so callers do not need to nil-check at every IPList Add/Remove site.

func NewPortGate

func NewPortGate(cfg PortGateConfig) (PortGate, error)

NewPortGate returns the platform-appropriate gate. On Linux it installs a dedicated `csm_chal` inet table; on non-Linux it returns nil so callers naturally no-op via the nil PortGate handling on the IPList side.

Returns nil + nil when the listen address is loopback because no gate is needed (loopback traffic cannot originate from off-host). Caller treats nil as "gate not active" and proceeds without it.

type PortGateConfig

type PortGateConfig struct {
	ListenAddr string
	ListenPort int
	InfraCIDRs []string
}

PortGateConfig wraps the inputs the gate needs to install rules.

type Resolver

type Resolver interface {
	LookupAddr(ctx context.Context, addr string) (names []string, err error)
	LookupHost(ctx context.Context, host string) (addrs []string, err error)
}

Resolver matches the subset of net.Resolver that CrawlerVerifier uses, so tests can swap in a fake without spinning up a real DNS server.

type Server

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

Server serves challenge pages to gray-listed IPs. When an IP passes the challenge, it gets a temporary allow.

func New

func New(cfg *config.Config, unblocker IPUnblocker, ipList *IPList) *Server

New creates a challenge server.

func (*Server) CleanExpired

func (s *Server) CleanExpired()

CleanExpired removes old verification records, prunes the admin-failure log, and evicts stale crawler-cache entries. Called from the daemon's challengeEscalator ticker every 60 seconds; under a sustained scan from many source IPs, this is the only thing keeping per-IP map entries from accumulating until restart.

func (*Server) Shutdown

func (s *Server) Shutdown()

Shutdown gracefully stops the server.

func (*Server) Start

func (s *Server) Start() error

Start begins serving challenge pages. Explicit challenge TLS makes the listener HTTPS. Direct/public listeners can reuse the WebUI TLS pair. Loopback listeners stay plain HTTP by default.

Resolution order:

  1. challenge.tls_cert + challenge.tls_key (explicit per-service)
  2. webui.tls_cert + webui.tls_key (direct/public binds only)
  3. plain HTTP (loopback-only default)

Jump to

Keyboard shortcuts

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