safety

package
v1.230.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MPL-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package safety provides protection mechanisms to prevent self-lockout and ensure system stability during firewall operations.

Features

The safety package implements several protection mechanisms:

  • System IP Detection: Auto-detects IPs that must never be blocked (server IP, gateway, DNS servers, active SSH connections)
  • Memory Limits: Monitors and enforces memory usage limits
  • File Safety: Validates file operations to prevent data loss
  • Resource Limits: Prevents runaway processes

System IP Detection

The Detect functions analyze the system to find critical IPs:

ips, err := safety.DetectSystemIPs()
// Returns: server IPs, gateway IP, DNS servers, active connections

These IPs are automatically added to the whitelist to prevent lockout.

Memory Protection

Memory limits prevent the watchdog and feeds from consuming excessive RAM:

if safety.IsMemoryPressureHigh() {
    // Trigger garbage collection or reduce operations
}

File Safety

File operations are validated to ensure they target allowed paths:

if safety.IsPathSafe(filepath) {
    // Proceed with write
}

SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="health_budget.go" meta:type="go" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:description="v1.222.1 HEALTH-OOM hotfix (Scope A+F): hardware-aware systemd memory budget for nftban-health.service. The health-check peak RSS is dominated by the Go validator buffering the full `nft -j list ruleset` (+ a transient ~2x while the nft child and Go parent both hold it) and 16 `nft -j list set` element reads; that buffer scales with ban-set/geoban/feeds ELEMENT count — not CPU cores, journal size, or concurrency (the check path is strictly sequential). Element count is itself capped per RAM tier, so the health memory need tracks the canonical ResourceTier. This is HEALTH-DOMAIN policy: it CONSUMES ClassifyResourceTier (the single RAM→tier authority in resource_tier.go) and maps the tier to health-specific MemoryHigh/MemoryMax, clamped to a safe fraction of total RAM with a floor so a tiny VPS is never handed an unsafe cap. It defines NO RAM thresholds and reads NO /proc facts of its own."

SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="resource_tier.go" meta:type="go" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:description="v1.222.1 EXTRACT_SHARED_AUTHORITY: the ONE canonical host RAM-tier authority for internal/safety. Before this, the ≤4GB/≤8GB/>8GB threshold table was duplicated inline in GetMaxCIDRsHard, GetMaxCIDRsHardWithTier, and GetResourceLimits (and a 4th copy was proposed in the health-budget lane). This is now the single classifier; CIDR policy, daemon resource limits, and health resource policy all CONSUME it and map the tier to their own domain values. It is a generic hardware dimension — no CIDR/health/domain name in the type. Facts come from the existing DetectServerProfile()/AvailableMem() authority; this adds NO new /proc parser."

Index

Constants

View Source
const (
	// MaxCIDRsWarning is the threshold for warning users about large CIDR sets
	MaxCIDRsWarning = 50000

	// BytesPerCIDREstimate is the estimated memory per CIDR during merge operations
	// Includes string storage + parsing + temporary merge buffers
	BytesPerCIDREstimate = 300

	// BanAgeThresholdDays is how long a permanent ban must be inactive before eviction
	BanAgeThresholdDays = 30
)

CIDR loading limits to prevent memory exhaustion

View Source
const (
	// ProtectionStateDir is where protection state is stored
	ProtectionStateDir = "/var/lib/nftban/state"
	// ProtectionStateFile is the protection state JSON file
	ProtectionStateFile = "/var/lib/nftban/state/protection.json"
)
View Source
const (
	// PermanentBansDir is where permanent ban tracking is stored
	PermanentBansDir = "/var/lib/nftban/state"
	// PermanentBansFile tracks permanent bans with metadata
	PermanentBansFile = "/var/lib/nftban/state/permanent_bans.json"
)
View Source
const (
	// FilterStateFile is the CIDR filter state JSON file
	FilterStateFile = "/var/lib/nftban/state/filter.json"
)

Variables

View Source
var ErrInsecureDirectory = errors.New("target directory has insecure permissions (world-writable)")

ErrInsecureDirectory is returned when target directory has insecure permissions

View Source
var ErrPathTraversal = errors.New("path traversal detected")

ErrPathTraversal is returned when path traversal is detected

View Source
var ErrSymlinkDetected = errors.New("symlink detected in path (TOCTOU protection)")

ErrSymlinkDetected is returned when a symlink is detected in the path

Functions

func CanAllocate

func CanAllocate(estimatedBytes int64) bool

CanAllocate checks if allocating the given bytes is safe Returns false if allocation would exceed available memory budget

func ClearProtectionState

func ClearProtectionState() error

ClearProtectionState removes protection state (called when sync succeeds fully)

func DurableRename added in v1.222.0

func DurableRename(src, dst string) error

DurableRename renames src to dst and fsyncs the destination directory. It is a convenience wrapper over DurableRenameResult for callers where a post-rename fsync failure is benign (the content has landed; a later re-run re-fsyncs).

WARNING — non-atomic error semantics: a non-nil error may be returned AFTER the rename has already landed (see RenameLandedNotDurable). Callers that must preserve src's pre-image on failure MUST use DurableRenameResult and reconcile against the reported outcome; DurableRename alone is unsafe for that case.

func FormatBytes

func FormatBytes(bytes int64) string

FormatBytes converts bytes to human-readable format

func FsyncDir added in v1.222.0

func FsyncDir(dir string) error

FsyncDir flushes a directory's own metadata (its entries) to stable storage so that a file created or renamed inside it survives a power loss. Without this, an atomic rename can be lost after a crash even though the file's data was fsync'd — the directory entry pointing at it was never flushed.

Some filesystems (and every non-directory target) reject fsync on a directory with EINVAL; that is not a durability failure on those filesystems, so it is reported as success. All other open/sync errors are propagated.

func GetEvictableBans

func GetEvictableBans(maxCount int) ([]string, error)

GetEvictableBans returns IPs that can be evicted (old, unprotected) Returns at most maxCount IPs, oldest first

func GetMaxCIDRsHard

func GetMaxCIDRsHard() int

GetMaxCIDRsHard returns the dynamic CIDR limit based on the canonical server tier Small servers (≤4GB): 75,000 max CIDRs Medium servers (4-8GB): 100,000 max CIDRs Large servers (>8GB): 150,000 max CIDRs

func GetMaxCIDRsHardWithTier

func GetMaxCIDRsHardWithTier() (limit int, tier string)

GetMaxCIDRsHardWithTier returns both the limit and tier name for logging

func GetMemoryBudget

func GetMemoryBudget() int64

GetMemoryBudget calculates a safe memory budget based on server profile Takes into account CPU cores, total RAM, and control panel presence Returns a value that leaves headroom for the OS, panel, and other processes

func GetPermanentBanStats

func GetPermanentBanStats() (total, protected, evictable int, err error)

GetPermanentBanStats returns stats about permanent bans

func GetProfileDescription

func GetProfileDescription() string

GetProfileDescription returns a human-readable description of the server profile

func GetProtectionSummary

func GetProtectionSummary() string

GetProtectionSummary returns a human-readable protection status

func GetResourceLimits

func GetResourceLimits() (memBudget int64, maxWorkers int)

GetResourceLimits calculates appropriate resource limits based on server profile Returns memory budget and max concurrent workers

func InitCPU

func InitCPU(lim Limits)

InitCPU sets GOMAXPROCS based on config This prevents the Go server from consuming all CPU cores

func InitMemory

func InitMemory(lim Limits)

InitMemory sets memory limit based on server profile and optional overrides Uses runtime/debug SetMemoryLimit (Go 1.19+)

The memory budget is calculated by GetResourceLimits() which considers:

  • Server RAM size (applies tier caps: ≤4GB→384MB, 4-8GB→512MB, >8GB→1GB)
  • Control panel presence (20% for panel servers, 35% for non-panel)
  • Minimum 64MB floor

Environment variable NFTBAN_MAX_MEMORY_BYTES can override this for special cases.

func IsProtectionActive

func IsProtectionActive() bool

IsProtectionActive returns true if memory protection is currently active

func IsSymlink(path string) (bool, error)

IsSymlink checks if a path is a symlink without following it

func RecordFilterState

func RecordFilterState(total, filtered, bogon, oversize, kept int) error

RecordFilterState writes CIDR filter statistics after sync

func RecordProtectionTriggered

func RecordProtectionTriggered(feedsSkipped bool, geobanSkipped bool, feedsCIDRs int, geobanCIDRs int, feedsMem int64, geobanMem int64) error

RecordProtectionTriggered writes protection state when feeds or geoban are skipped

func ReloadMemoryLimits

func ReloadMemoryLimits()

ReloadMemoryLimits reloads limits from environment (for testing/config changes)

func RemovePermanentBan

func RemovePermanentBan(ip string) error

RemovePermanentBan removes a ban from tracking

func SafeAppendFile

func SafeAppendFile(path string, data []byte, perm os.FileMode) error

SafeAppendFile appends data to a file with TOCTOU protection

func SafeAppendFlags

func SafeAppendFlags() int

SafeAppendFlags returns secure flags for appending to files

func SafeCreate

func SafeCreate(path string, perm os.FileMode) (*os.File, error)

SafeCreate creates a file with TOCTOU protection - Validates path for symlinks and traversal - Uses O_NOFOLLOW to prevent symlink following - Validates parent directory permissions

func SafeFileFlags

func SafeFileFlags() int

SafeFileFlags returns secure flags for file creation O_NOFOLLOW prevents following symlinks (TOCTOU protection) O_EXCL ensures file is created (not opened if exists)

func SafeMkdirAll

func SafeMkdirAll(path string, perm os.FileMode) error

SafeMkdirAll creates directories with TOCTOU protection Validates each path component is not a symlink

func SafeOpenFile

func SafeOpenFile(path string, flag int, perm os.FileMode) (*os.File, error)

SafeOpenFile opens a file with TOCTOU protection

func SafeWriteFile

func SafeWriteFile(path string, data []byte, perm os.FileMode) error

SafeWriteFile writes data to a file with TOCTOU protection using an atomic, power-loss-durable write pattern: write to a temp file in the same directory, fsync it, chmod, atomically rename into place, then fsync the parent directory so the replacement survives a crash. It delegates to WriteFileDurable so every caller of SafeWriteFile gets the parent-directory fsync (the v1.222 F2 fix).

func SavePermanentBans

func SavePermanentBans(state *PermanentBansState) error

SavePermanentBans saves the permanent bans state to disk

func SetBanProtected

func SetBanProtected(ip string, protected bool) error

SetBanProtected sets the protected flag for a permanent ban

func ShouldSkipFeeds

func ShouldSkipFeeds() bool

ShouldSkipFeeds returns true if memory pressure is critical

func ShouldSkipGeoban

func ShouldSkipGeoban() bool

ShouldSkipGeoban returns true if memory pressure is too high for geoban

func StageFile added in v1.222.0

func StageFile(dir, pattern string, data []byte, perm os.FileMode) (string, error)

StageFile writes data as a fresh candidate file inside dir (a trusted staging directory) and returns its path. The candidate's contents are fsync'd and the staging directory itself is fsync'd, so the staged file's directory entry is durable before it is later activated by a rename. It never touches or activates any final target. pattern follows os.CreateTemp semantics (a trailing '*' is replaced by a random string).

func SystemdMemBytes added in v1.222.1

func SystemdMemBytes(b int64) string

SystemdMemBytes renders a byte count as a systemd-safe integer string (bytes); systemd accepts a bare integer as bytes for MemoryHigh/MemoryMax.

func TrackPermanentBan

func TrackPermanentBan(ip, reason, source string, protected bool) error

TrackPermanentBan adds or updates a permanent ban in tracking

func ValidateDirectory

func ValidateDirectory(dir string) error

ValidateDirectory checks if a directory is safe for file creation Returns error if directory is world-writable (o+w)

func ValidatePath

func ValidatePath(path string) error

ValidatePath performs security checks on a file path - Rejects symlinks anywhere in the path - Rejects path traversal attempts (../) - Rejects world-writable directories

func WriteFileDurable added in v1.222.0

func WriteFileDurable(path string, data []byte, perm os.FileMode) error

WriteFileDurable writes data to path atomically AND durably: it inherits every guarantee of SafeWriteFile (symlink/traversal validation, temp-in-dir, file fsync, chmod, atomic rename, cleanup on failure) and ADDS a fsync of the target's parent directory after the rename, so the replacement survives a power loss. This is the primitive every atomic state writer should use; SafeWriteFile delegates to it so the whole repo gets the directory-fsync fix.

Types

type CIDRLoadResult

type CIDRLoadResult struct {
	Allowed      bool   // Whether to proceed with loading
	Warning      bool   // Whether to show a warning
	Message      string // Human-readable message
	EstimatedMem int64  // Estimated memory usage in bytes
}

CIDRLoadResult describes the result of ValidateCIDRLoad

func ValidateCIDRLoad

func ValidateCIDRLoad(ipv4Count, ipv6Count int) CIDRLoadResult

ValidateCIDRLoad checks if loading the given number of CIDRs is safe Returns result with recommendation and warning message if applicable

type FilterState

type FilterState struct {
	Timestamp     string `json:"timestamp"`
	Total         int    `json:"total"`          // Total input CIDRs
	Filtered      int    `json:"filtered"`       // CIDRs removed by filtering
	BogonCount    int    `json:"bogon_count"`    // Bogon/reserved ranges removed
	OversizeCount int    `json:"oversize_count"` // Oversized CIDRs removed (< /9)
	Kept          int    `json:"kept"`           // CIDRs that passed filtering
}

FilterState tracks CIDR filtering statistics from the last sync

func GetFilterState

func GetFilterState() (*FilterState, error)

GetFilterState reads current CIDR filter state

type HealthResourceProfile added in v1.222.1

type HealthResourceProfile struct {
	Tier       ResourceTier // canonical shared tier (from ClassifyResourceTier)
	CPUCores   int
	TotalRAM   int64 // bytes
	AvailRAM   int64 // bytes
	MemoryHigh int64 // bytes (systemd MemoryHigh)
	MemoryMax  int64 // bytes (systemd MemoryMax)
	Clamped    bool  // true if the min-host RAM clamp/floor changed a tier value
	Reason     string
}

HealthResourceProfile is the resolved, operator-visible health resource sizing decision: the inputs (tier/cores/RAM) and the effective limits + reason.

func HealthServiceMemoryLimits added in v1.222.1

func HealthServiceMemoryLimits() HealthResourceProfile

HealthServiceMemoryLimits resolves the health-service budget for THIS host, reusing the canonical fact authority (DetectServerProfile) and the canonical tier authority (ClassifyResourceTier). One classification, health-domain policy.

func HealthServiceMemoryLimitsFor added in v1.222.1

func HealthServiceMemoryLimitsFor(profile ServerProfile, tier ResourceTier) HealthResourceProfile

HealthServiceMemoryLimitsFor computes the health-service budget from an explicit profile + its canonical tier. Pure and unit-testable. Returns MemoryHigh and MemoryMax in bytes plus whether the min-host clamp/floor altered a tier value.

Invariants (asserted by tests): MemoryHigh < MemoryMax always; MemoryMax never exceeds TotalRAM/healthMaxRAMFractionDiv (when RAM is known) and never drops below healthFloorMaxMB.

type Limits

type Limits struct {
	// GOMAXPROCS limit (CPU cores)
	GoMaxProcs int // default: 2

	// Connection limits
	MaxConcurrentConns int // default: 100
	MaxConnsPerIP      int // default: 10

	// Request limits
	RequestTimeoutSec   int   // default: 30
	MaxRequestBodyMB    int   // default: 10
	MaxRequestBodyBytes int64 // computed from MB

	// Rate limiting
	RateLimitPerMin int // default: 60 requests per minute per IP

	// Memory limits
	MaxMemoryPercent int   // default: 20% of available
	MaxMemoryBytes   int64 // default: 512 MiB

	// Logging
	EnableMetrics bool // default: true
}

Limits holds all safety thresholds for the GUI server

func FromEnv

func FromEnv() Limits

FromEnv returns sane defaults that can be overridden via environment variables This matches the pattern from go-feeds/internal/safety/config.go

type MemAvail

type MemAvail struct {
	Total         int64
	Avail         int64
	CgroupLimit   int64
	CgroupCurrent int64
}

MemAvail holds available memory info (cgroup-aware) This matches the pattern from go-feeds/internal/safety/mem.go

func AvailableMem

func AvailableMem() MemAvail

AvailableMem returns available memory (cgroup-aware for containers) This is critical for running in Docker/Kubernetes where cgroup limits apply

type MemoryLimits

type MemoryLimits struct {
	// Scorer limits (internal/suricata/scorer.go)
	ScorerMaxIPs         int // Max unique IPs tracked in scorer (default: 50000)
	ScorerMaxEventsPerIP int // Max event timestamps per IP (default: 100)

	// Analytics limits (internal/analytics/state.go)
	AnalyticsMaxIPOrigins     int // Max IPs in ipOrigins map (default: 100000)
	AnalyticsMaxIPsPerCountry int // Max IPs per country (default: 10000)

	// Stats cache limits (internal/suricata/stats/cache.go)
	StatsMaxSIDs          int // Max SIDs tracked (default: 10000)
	StatsMaxSourcesPerSID int // Max unique sources per SID (default: 1000)

	// Queue limits (cli/lib/nftban/helpers/nftban_task_queue.sh)
	QueueMaxPending       int // Max pending tasks (default: 10000)
	QueueDLQAutoRetention int // Auto-purge DLQ entries older than N days (default: 7)
}

MemoryLimits holds caps and TTLs to prevent unbounded memory growth (CWE-400) All limits are configurable via environment variables with sane defaults.

func DefaultMemoryLimits

func DefaultMemoryLimits() MemoryLimits

DefaultMemoryLimits returns production-safe defaults

func GetMemoryLimits

func GetMemoryLimits() MemoryLimits

GetMemoryLimits returns the global memory limits

type PermanentBan

type PermanentBan struct {
	IP        string    `json:"ip"`
	AddedAt   time.Time `json:"added_at"`
	Reason    string    `json:"reason,omitempty"`
	Source    string    `json:"source,omitempty"`
	Protected bool      `json:"protected"` // If true, NEVER evict this IP
}

PermanentBan represents a permanent ban with tracking metadata

type PermanentBansState

type PermanentBansState struct {
	UpdatedAt time.Time                `json:"updated_at"`
	Bans      map[string]*PermanentBan `json:"bans"` // keyed by IP
}

PermanentBansState holds all tracked permanent bans

func LoadPermanentBans

func LoadPermanentBans() (*PermanentBansState, error)

LoadPermanentBans loads the permanent bans state from disk

type PressureLevel

type PressureLevel int

PressureLevel represents the current memory pressure state

const (
	// PressureNormal - below 70% of memory budget, full operations
	PressureNormal PressureLevel = iota
	// PressureWarning - 70-85% of budget, log warning but continue
	PressureWarning
	// PressureHigh - 85-95% of budget, skip geoban refresh
	PressureHigh
	// PressureCritical - >95% of budget, trim cold bans + skip feeds
	PressureCritical
)

func GetMemoryPressureLevel

func GetMemoryPressureLevel() PressureLevel

GetMemoryPressureLevel returns the current memory pressure level

func (PressureLevel) String

func (p PressureLevel) String() string

String returns the pressure level name

type ProtectionState

type ProtectionState struct {
	Timestamp       string `json:"timestamp"`
	FeedsSkipped    bool   `json:"feeds_skipped"`
	GeobanSkipped   bool   `json:"geoban_skipped"`
	FeedsCIDRCount  int    `json:"feeds_cidr_count,omitempty"`
	GeobanCIDRCount int    `json:"geoban_cidr_count,omitempty"`
	FeedsMemNeeded  int64  `json:"feeds_mem_needed,omitempty"`
	GeobanMemNeeded int64  `json:"geoban_mem_needed,omitempty"`
	AvailableMemory int64  `json:"available_memory"`
	Reason          string `json:"reason"`
}

ProtectionState tracks what was skipped due to memory limits

func GetProtectionState

func GetProtectionState() (*ProtectionState, error)

GetProtectionState reads current protection state

type RenameOutcome added in v1.222.0

type RenameOutcome int

RenameOutcome reports precisely what DurableRenameResult achieved, so a caller can reconcile a post-rename durability failure without guessing whether the rename landed.

const (
	// RenameFailed: the rename did not happen — src is intact, dst unchanged.
	RenameFailed RenameOutcome = iota
	// RenameLandedNotDurable: the rename happened (dst now holds src's content and
	// src is gone) but the destination-directory fsync failed, so the new
	// directory entry is not yet durable. The returned error is the fsync error.
	RenameLandedNotDurable
	// RenameDurable: the rename happened and the destination directory was fsync'd.
	RenameDurable
)

func DurableRenameResult added in v1.222.0

func DurableRenameResult(src, dst string) (RenameOutcome, error)

DurableRenameResult renames src to dst and fsyncs the destination directory, reporting exactly what was achieved. os.Rename+FsyncDir is NOT atomic: the rename can land and the subsequent dir fsync still fail. Callers that must never lose src's pre-image (e.g. a backup rename inside a transaction) MUST inspect the outcome — on RenameLandedNotDurable the file is already at dst, so recovering src means moving dst back; the error alone does NOT mean "nothing moved". Both paths must be on the same filesystem (rename is atomic only within a filesystem).

type ResourceTier added in v1.222.1

type ResourceTier string

ResourceTier is the canonical host RAM/hardware size class. It is a generic hardware dimension shared by every internal/safety resource-policy consumer (CIDR limits, daemon budget, health-service memory). It intentionally carries no domain name (not "CIDRTier", not "HealthTier").

const (
	ResourceTierSmall  ResourceTier = "small"  // <= 4 GiB total RAM
	ResourceTierMedium ResourceTier = "medium" // 4 GiB < RAM <= 8 GiB
	ResourceTierLarge  ResourceTier = "large"  // > 8 GiB total RAM
)

func ClassifyResourceTier added in v1.222.1

func ClassifyResourceTier(profile ServerProfile) ResourceTier

ClassifyResourceTier maps a host profile to its canonical RAM tier.

Semantics are preserved byte-for-byte from the pre-v1.222.1 inline switches: classification keys on profile.TotalRAM (NOT AvailRAM / NOT the cgroup current) with boundaries <=4GiB → small, <=8GiB → medium, else large. A zero/unknown TotalRAM falls into the small (most conservative) tier, matching the prior behavior where `0 <= 4*GB` selected the small branch — the safe default.

func CurrentResourceTier added in v1.222.1

func CurrentResourceTier() ResourceTier

CurrentResourceTier classifies THIS host using the canonical fact authority.

type ServerProfile

type ServerProfile struct {
	CPUCores        int    // Number of CPU cores
	TotalRAM        int64  // Total RAM in bytes
	AvailRAM        int64  // Available RAM in bytes
	HasControlPanel bool   // Whether a control panel is detected
	PanelType       string // "cpanel", "directadmin", "plesk", "cyberpanel", "none"
}

ServerProfile describes the server's resource characteristics

func DetectServerProfile

func DetectServerProfile() ServerProfile

DetectServerProfile analyzes the server and returns its resource profile

type SystemIPs

type SystemIPs struct {
	ServerIPs     []net.IP    // All server interface IPs
	CurrentUserIP net.IP      // IP of current SSH connection
	GatewayIPs    []net.IP    // Default gateway
	DNSServers    []net.IP    // DNS servers from /etc/resolv.conf
	LoopbackCIDRs []net.IPNet // 127.0.0.0/8, ::1/128
}

SystemIPs holds all critical IPs that must NEVER be blocked

func DetectSystemIPs

func DetectSystemIPs() (*SystemIPs, error)

DetectSystemIPs auto-detects all critical IPs that must be whitelisted

func (*SystemIPs) GetAllIPs

func (s *SystemIPs) GetAllIPs() []net.IP

GetAllIPs returns all IPs as a flat list

func (*SystemIPs) GetAllIPsWithCIDRs

func (s *SystemIPs) GetAllIPsWithCIDRs() ([]net.IP, []net.IPNet)

GetAllIPsWithCIDRs returns all IPs including loopback CIDRs

func (*SystemIPs) PrintSystemIPs

func (s *SystemIPs) PrintSystemIPs()

PrintSystemIPs displays all detected IPs

Jump to

Keyboard shortcuts

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