abuse

package
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package abuse matches connection destinations against IP-reputation blocklists, so the operator learns that an account is reaching known-bad infrastructure without the panel having to keep everyone's browsing history.

Only matches are ever recorded. That is the whole point: matches are rare, so they cost almost nothing to store and are worth keeping for weeks, whereas the traffic they are drawn from is high-cardinality and is never persisted at all.

Why IP only

This package once matched domains too, against threat/piracy/gambling feeds. That was dropped because a domain can only be matched when the destination reaches the panel AS a domain, and on real traffic it usually does not: clients resolve DNS off the tunnel and encrypt the TLS SNI (ECH), so the access log records a bare IP. Measured on a live server it was 23 IPs to every 4 domains, and those 4 were connectivity checks. Three domain feeds cost ~12 MB of memory and a daily download to match almost nothing, so the destination is now matched as an address or not at all.

Index

Constants

This section is empty.

Variables

View Source
var Feeds = []Feed{
	{Category: CatBadIP, URLs: []string{
		"https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset",
		"https://cdn.jsdelivr.net/gh/firehol/blocklist-ipsets@master/firehol_level1.netset",
	}},
}

Feeds are the lists the panel ships with — IP reputation only, on purpose.

Domain feeds were shipped first and then dropped: a domain can only be matched when the destination reaches the panel AS a domain, and on real traffic it usually does not. Clients resolve DNS off the tunnel and encrypt the TLS SNI (ECH), so the access log records a bare IP; measured on a live server it was 23 IPs to every 4 domains, and those 4 were connectivity checks. Three domain lists cost ~12 MB of memory and a daily download to match almost nothing.

What survives is the one list that sees that traffic. FireHOL level 1 is the high-confidence, low-false-positive aggregate (spamhaus DROP, dshield, feodo/C2 trackers, …) — the tier FireHOL itself marks safe to block outright, curated to exclude CDNs and shared hosting, which is what an advisory abuse view needs.

The operator's own list is IP/CIDR too, for the same reason.

Functions

func ParseCustom

func ParseCustom(content string) []string

ParseCustom reads the operator's own list: one IP or CIDR per line, '#' comments. Lines that are not addresses (a hostname, a typo) are skipped rather than rejected — the field is free text and a bad line must not void the good ones.

func ParseIPList

func ParseIPList(r io.Reader) ([]string, error)

ParseIPList reads an IP blocklist: one CIDR or IP per line, with '#'/';' comments and optional trailing comments after the address (as Spamhaus DROP and FireHOL netsets carry).

Types

type Category

type Category string

Category is the kind of trouble a destination represents. These are the buckets the UI groups by and the operator reasons about.

The retired domain categories are still listed: rows written before the switch to IP-only carry them, and Title must keep rendering those rows correctly.

const (
	// CatCustom is the operator's own list. Checked first so a local entry wins.
	CatCustom Category = "custom"
	// CatBadIP is the IP-reputation feed (attack/malware infrastructure) — the only
	// downloaded list, and the one that sees the bare-IP traffic everything else misses.
	CatBadIP Category = "badip"

	// Retired domain categories, kept so historical rows still render.
	CatMalware  Category = "malware"
	CatPiracy   Category = "piracy"
	CatGambling Category = "gambling"
)

func (Category) Title

func (c Category) Title() string

Title is the Russian label shown in the panel.

type Feed

type Feed struct {
	Category Category
	// URLs are mirrors, tried in order. GitHub raw is sometimes DPI-degraded in RU,
	// so a jsDelivr mirror follows — the same reason package geo carries two.
	URLs []string
}

Feed is one downloadable category list.

type FileInfo

type FileInfo struct {
	Category string `json:"category"`
	Title    string `json:"title"`
	Enabled  bool   `json:"enabled"`
	Present  bool   `json:"present"`           // a cached feed on disk, or a non-empty custom list
	Entries  int    `json:"entries"`           // entries currently loaded in the matcher
	Size     int64  `json:"size,omitempty"`    // cached feed size (feeds only)
	Updated  int64  `json:"updated,omitempty"` // cached feed mtime (feeds only)
}

FileInfo is one category's state, for the settings UI.

type Matcher

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

Matcher answers "is this destination on a list, and which one". Safe for concurrent use; reads take a read lock so the access-log path is not serialised against itself, and a feed reload swaps whole sets under the write lock.

A nil *Matcher is valid and matches nothing, so a panel that has never downloaded a feed behaves as if the feature is off rather than crashing the access-log reader.

func New

func New() *Matcher

New returns an empty matcher that matches nothing until a list is set.

func (*Matcher) Clear

func (m *Matcher) Clear(cat Category)

Clear removes a category's set, so a disabled category matches nothing until it is loaded again.

func (*Matcher) Counts

func (m *Matcher) Counts() map[Category]int

Counts reports how many entries each loaded category holds, for the settings UI.

func (*Matcher) Match

func (m *Matcher) Match(dest string) (Category, bool)

Match reports the category a destination falls under, if any.

Anything that is not an IP address — a hostname the panel did happen to see — is simply not matched: there are no domain lists to test it against.

func (*Matcher) SetIP

func (m *Matcher) SetIP(cat Category, entries []string)

SetIP replaces one category's IP set from CIDRs and bare IPs.

type Store

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

Store downloads, caches and loads the feeds into a Matcher.

func NewStore

func NewStore(dir string) *Store

NewStore returns a store caching feeds under dir.

func (*Store) Configure

func (s *Store) Configure(enabled map[Category]bool, custom string)

Configure applies operator settings — which categories are active and the custom list — and reloads the matcher from cache to match (no network). A disabled category is cleared from the matcher. Called at boot and on every settings save.

func (*Store) LoadCached

func (s *Store) LoadCached()

LoadCached loads the enabled feeds' cached copies plus the custom list, without network. Called at boot so a panel that has run before starts matching immediately rather than being blind until the first download finishes.

func (*Store) Matcher

func (s *Store) Matcher() *Matcher

Matcher is the live matcher. Never nil; empty until lists load.

func (*Store) Refresh

func (s *Store) Refresh(ctx context.Context, force bool)

Refresh downloads any feed whose cache is missing or stale, then reloads it. The operator's own list is never downloaded, only re-read.

func (*Store) Run

func (s *Store) Run(ctx context.Context)

Run keeps the feeds fresh until ctx is cancelled. Intended as a goroutine.

func (*Store) Status

func (s *Store) Status() []FileInfo

Status reports each category's state: enabled, loaded entry count, and (for downloaded feeds) the cached copy's size and age.

Jump to

Keyboard shortcuts

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