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
- Variables
- func CanAllocate(estimatedBytes int64) bool
- func ClearProtectionState() error
- func DurableRename(src, dst string) error
- func FormatBytes(bytes int64) string
- func FsyncDir(dir string) error
- func GetEvictableBans(maxCount int) ([]string, error)
- func GetMaxCIDRsHard() int
- func GetMaxCIDRsHardWithTier() (limit int, tier string)
- func GetMemoryBudget() int64
- func GetPermanentBanStats() (total, protected, evictable int, err error)
- func GetProfileDescription() string
- func GetProtectionSummary() string
- func GetResourceLimits() (memBudget int64, maxWorkers int)
- func InitCPU(lim Limits)
- func InitMemory(lim Limits)
- func IsProtectionActive() bool
- func IsSymlink(path string) (bool, error)
- func RecordFilterState(total, filtered, bogon, oversize, kept int) error
- func RecordProtectionTriggered(feedsSkipped bool, geobanSkipped bool, feedsCIDRs int, geobanCIDRs int, ...) error
- func ReloadMemoryLimits()
- func RemovePermanentBan(ip string) error
- func SafeAppendFile(path string, data []byte, perm os.FileMode) error
- func SafeAppendFlags() int
- func SafeCreate(path string, perm os.FileMode) (*os.File, error)
- func SafeFileFlags() int
- func SafeMkdirAll(path string, perm os.FileMode) error
- func SafeOpenFile(path string, flag int, perm os.FileMode) (*os.File, error)
- func SafeWriteFile(path string, data []byte, perm os.FileMode) error
- func SavePermanentBans(state *PermanentBansState) error
- func SetBanProtected(ip string, protected bool) error
- func ShouldSkipFeeds() bool
- func ShouldSkipGeoban() bool
- func StageFile(dir, pattern string, data []byte, perm os.FileMode) (string, error)
- func SystemdMemBytes(b int64) string
- func TrackPermanentBan(ip, reason, source string, protected bool) error
- func ValidateDirectory(dir string) error
- func ValidatePath(path string) error
- func WriteFileDurable(path string, data []byte, perm os.FileMode) error
- type CIDRLoadResult
- type FilterState
- type HealthResourceProfile
- type Limits
- type MemAvail
- type MemoryLimits
- type PermanentBan
- type PermanentBansState
- type PressureLevel
- type ProtectionState
- type RenameOutcome
- type ResourceTier
- type ServerProfile
- type SystemIPs
Constants ¶
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
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" )
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" )
const (
// FilterStateFile is the CIDR filter state JSON file
FilterStateFile = "/var/lib/nftban/state/filter.json"
)
Variables ¶
var ErrInsecureDirectory = errors.New("target directory has insecure permissions (world-writable)")
ErrInsecureDirectory is returned when target directory has insecure permissions
var ErrPathTraversal = errors.New("path traversal detected")
ErrPathTraversal is returned when path traversal is detected
var ErrSymlinkDetected = errors.New("symlink detected in path (TOCTOU protection)")
ErrSymlinkDetected is returned when a symlink is detected in the path
Functions ¶
func CanAllocate ¶
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
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 ¶
FormatBytes converts bytes to human-readable format
func FsyncDir ¶ added in v1.222.0
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 ¶
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 ¶
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 ¶
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 ¶
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 RecordFilterState ¶
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 ¶
RemovePermanentBan removes a ban from tracking
func SafeAppendFile ¶
SafeAppendFile appends data to a file with TOCTOU protection
func SafeAppendFlags ¶
func SafeAppendFlags() int
SafeAppendFlags returns secure flags for appending to files
func SafeCreate ¶
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 ¶
SafeMkdirAll creates directories with TOCTOU protection Validates each path component is not a symlink
func SafeOpenFile ¶
SafeOpenFile opens a file with TOCTOU protection
func SafeWriteFile ¶
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 ¶
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
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
SystemdMemBytes renders a byte count as a systemd-safe integer string (bytes); systemd accepts a bare integer as bytes for MemoryHigh/MemoryMax.
func TrackPermanentBan ¶
TrackPermanentBan adds or updates a permanent ban in tracking
func ValidateDirectory ¶
ValidateDirectory checks if a directory is safe for file creation Returns error if directory is world-writable (o+w)
func ValidatePath ¶
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
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
type MemAvail ¶
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 ¶
DetectSystemIPs auto-detects all critical IPs that must be whitelisted
func (*SystemIPs) GetAllIPsWithCIDRs ¶
GetAllIPsWithCIDRs returns all IPs including loopback CIDRs
func (*SystemIPs) PrintSystemIPs ¶
func (s *SystemIPs) PrintSystemIPs()
PrintSystemIPs displays all detected IPs