You've already forked caddy-opnsense-blocker
Add on-demand IP investigation and richer IP details
This commit is contained in:
@@ -23,23 +23,29 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
cfg *config.Config
|
||||
store *store.Store
|
||||
evaluator *engine.Evaluator
|
||||
blocker opnsense.AliasClient
|
||||
logger *log.Logger
|
||||
cfg *config.Config
|
||||
store *store.Store
|
||||
evaluator *engine.Evaluator
|
||||
blocker opnsense.AliasClient
|
||||
investigator Investigator
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, db *store.Store, blocker opnsense.AliasClient, logger *log.Logger) *Service {
|
||||
type Investigator interface {
|
||||
Investigate(ctx context.Context, ip string, userAgents []string) (model.IPInvestigation, error)
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, db *store.Store, blocker opnsense.AliasClient, investigator Investigator, logger *log.Logger) *Service {
|
||||
if logger == nil {
|
||||
logger = log.New(io.Discard, "", 0)
|
||||
}
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
store: db,
|
||||
evaluator: engine.NewEvaluator(),
|
||||
blocker: blocker,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
store: db,
|
||||
evaluator: engine.NewEvaluator(),
|
||||
blocker: blocker,
|
||||
investigator: investigator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +81,40 @@ func (s *Service) GetIPDetails(ctx context.Context, ip string) (model.IPDetails,
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
return s.store.GetIPDetails(ctx, normalized, 100, 100, 100)
|
||||
details, err := s.store.GetIPDetails(ctx, normalized, 0, 100, 100)
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
return s.decorateDetails(ctx, details)
|
||||
}
|
||||
|
||||
func (s *Service) InvestigateIP(ctx context.Context, ip string) (model.IPDetails, error) {
|
||||
normalized, err := normalizeIP(ip)
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
details, err := s.store.GetIPDetails(ctx, normalized, 0, 100, 100)
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
if s.investigator != nil {
|
||||
investigation, found, err := s.store.GetInvestigation(ctx, normalized)
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
shouldRefresh := !found || time.Since(investigation.UpdatedAt) >= s.cfg.Investigation.RefreshAfter.Duration
|
||||
if shouldRefresh {
|
||||
fresh, err := s.investigator.Investigate(ctx, normalized, collectUserAgents(details.RecentEvents))
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
if err := s.store.SaveInvestigation(ctx, fresh); err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
details.Investigation = &fresh
|
||||
}
|
||||
}
|
||||
return s.decorateDetails(ctx, details)
|
||||
}
|
||||
|
||||
func (s *Service) ForceBlock(ctx context.Context, ip string, actor string, reason string) error {
|
||||
@@ -410,3 +449,58 @@ func defaultReason(reason string, fallback string) string {
|
||||
}
|
||||
return strings.TrimSpace(reason)
|
||||
}
|
||||
|
||||
func (s *Service) decorateDetails(ctx context.Context, details model.IPDetails) (model.IPDetails, error) {
|
||||
if details.State.IP != "" && details.Investigation == nil {
|
||||
investigation, found, err := s.store.GetInvestigation(ctx, details.State.IP)
|
||||
if err != nil {
|
||||
return model.IPDetails{}, err
|
||||
}
|
||||
if found {
|
||||
details.Investigation = &investigation
|
||||
}
|
||||
}
|
||||
details.OPNsense = s.resolveOPNsenseStatus(ctx, details.State)
|
||||
details.Actions = actionAvailability(details.State, details.OPNsense)
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (s *Service) resolveOPNsenseStatus(ctx context.Context, state model.IPState) model.OPNsenseStatus {
|
||||
status := model.OPNsenseStatus{Configured: s.blocker != nil}
|
||||
if s.blocker == nil || state.IP == "" {
|
||||
return status
|
||||
}
|
||||
status.CheckedAt = time.Now().UTC()
|
||||
present, err := s.blocker.IsIPPresent(ctx, state.IP)
|
||||
if err != nil {
|
||||
status.Error = err.Error()
|
||||
return status
|
||||
}
|
||||
status.Present = present
|
||||
return status
|
||||
}
|
||||
|
||||
func actionAvailability(state model.IPState, backend model.OPNsenseStatus) model.ActionAvailability {
|
||||
present := false
|
||||
if backend.Configured && backend.Error == "" {
|
||||
present = backend.Present
|
||||
} else {
|
||||
present = state.State == model.IPStateBlocked || state.ManualOverride == model.ManualOverrideForceBlock
|
||||
}
|
||||
return model.ActionAvailability{
|
||||
CanBlock: !present,
|
||||
CanUnblock: present,
|
||||
CanClearOverride: state.ManualOverride != model.ManualOverrideNone,
|
||||
}
|
||||
}
|
||||
|
||||
func collectUserAgents(events []model.Event) []string {
|
||||
items := make([]string, 0, len(events))
|
||||
for _, event := range events {
|
||||
if strings.TrimSpace(event.UserAgent) == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, event.UserAgent)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user