Add support for verdict and decision reason context

This commit is contained in:
Patrick Pacher 2020-04-20 17:19:48 +02:00
parent eeb358425d
commit 8c5526a69b
No known key found for this signature in database
GPG key ID: E8CD2DA160925A6D
17 changed files with 246 additions and 148 deletions

View file

@ -85,11 +85,11 @@ func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict
verdict = network.VerdictDrop
continueInspection = true
case BLOCK_CONN:
conn.SetVerdict(network.VerdictBlock)
conn.SetVerdict(network.VerdictBlock, "", nil)
verdict = conn.Verdict
activeInspectors[key] = true
case DROP_CONN:
conn.SetVerdict(network.VerdictDrop)
conn.SetVerdict(network.VerdictDrop, "", nil)
verdict = conn.Verdict
activeInspectors[key] = true
case STOP_INSPECTING:

View file

@ -143,9 +143,6 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint:
}
}
var result endpoints.EPResult
var reason string
if p.PreventBypassing() {
// check for bypass protection
result, reason := PreventBypassing(conn)
@ -160,6 +157,9 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint:
}
}
var result endpoints.EPResult
var reason endpoints.Reason
// check endpoints list
if conn.Inbound {
result, reason = p.MatchServiceEndpoint(conn.Entity)
@ -168,10 +168,10 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint:
}
switch result {
case endpoints.Denied:
conn.Deny("endpoint is blacklisted: " + reason) // Block Outbound / Drop Inbound
conn.DenyWithContext(reason.String(), reason.Context())
return
case endpoints.Permitted:
conn.Accept("endpoint is whitelisted: " + reason)
conn.AcceptWithContext(reason.String(), reason.Context())
return
}
// continuing with result == NoMatch
@ -180,7 +180,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint:
result, reason = p.MatchFilterLists(conn.Entity)
switch result {
case endpoints.Denied:
conn.Deny("endpoint in filterlists: " + reason)
conn.DenyWithContext(reason.String(), reason.Context())
return
case endpoints.NoMatch:
// nothing to do

51
intel/block_reason.go Normal file
View file

@ -0,0 +1,51 @@
package intel
import (
"fmt"
"strings"
)
// ListMatch represents an entity that has been
// matched against filterlists.
type ListMatch struct {
Entity string
ActiveLists []string
InactiveLists []string
}
func (lm *ListMatch) String() string {
inactive := ""
if len(lm.InactiveLists) > 0 {
inactive = " and in deactivated lists " + strings.Join(lm.InactiveLists, ", ")
}
return fmt.Sprintf(
"%s in activated lists %s%s",
lm.Entity,
strings.Join(lm.ActiveLists, ","),
inactive,
)
}
// ListBlockReason is a list of list matches.
type ListBlockReason []ListMatch
func (br ListBlockReason) String() string {
if len(br) == 0 {
return ""
}
matches := make([]string, len(br))
for idx, lm := range br {
matches[idx] = lm.String()
}
return strings.Join(matches, " and ")
}
// Context returns br wrapped into a map. It implements
// the endpoints.Reason interface.
func (br ListBlockReason) Context() interface{} {
return map[string]interface{}{
"filterlists": br,
}
}

View file

@ -16,43 +16,6 @@ import (
"golang.org/x/net/publicsuffix"
)
// ListMatch represents an entity that has been
// matched against filterlists.
type ListMatch struct {
Entity string
ActiveLists []string
InactiveLists []string
}
func (lm *ListMatch) String() string {
inactive := ""
if len(lm.InactiveLists) > 0 {
inactive = " and in deactivated lists " + strings.Join(lm.InactiveLists, ", ")
}
return fmt.Sprintf(
"%s in activated lists %s%s",
lm.Entity,
strings.Join(lm.ActiveLists, ","),
inactive,
)
}
// ListBlockReason is a list of list matches.
type ListBlockReason []ListMatch
func (br ListBlockReason) String() string {
if len(br) == 0 {
return ""
}
matches := make([]string, len(br))
for idx, lm := range br {
matches[idx] = lm.String()
}
return strings.Join(matches, " and ")
}
// Entity describes a remote endpoint in many different ways.
// It embeddes a sync.Mutex but none of the endpoints own
// functions performs locking. The caller MUST ENSURE

View file

@ -278,7 +278,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
result, reason := conn.Process().Profile().MatchEndpoint(conn.Entity)
if result == endpoints.Denied {
conn.Block("endpoint in blocklist: " + reason)
conn.BlockWithContext(reason.String(), reason.Context())
returnNXDomain(w, query, conn.Reason)
return nil
}
@ -286,7 +286,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
if result == endpoints.NoMatch {
result, reason = conn.Process().Profile().MatchFilterLists(conn.Entity)
if result == endpoints.Denied {
conn.Block("endpoint in filterlists: " + reason)
conn.BlockWithContext(reason.String(), reason.Context())
returnNXDomain(w, query, conn.Reason)
return nil
}

View file

@ -31,9 +31,10 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
Entity *intel.Entity // needs locking, instance is never shared
process *process.Process
Verdict Verdict
Reason string
ReasonID string // format source[:id[:id]] // TODO
Verdict Verdict
Reason string
ReasonContext interface{}
ReasonID string // format source[:id[:id]] // TODO
Started int64
Ended int64
@ -164,59 +165,82 @@ func GetConnection(id string) (*Connection, bool) {
return conn, ok
}
// Accept accepts the connection.
func (conn *Connection) Accept(reason string) {
if conn.SetVerdict(VerdictAccept) {
conn.Reason = reason
// AcceptWithContext accepts the connection.
func (conn *Connection) AcceptWithContext(reason string, ctx interface{}) {
if conn.SetVerdict(VerdictAccept, reason, ctx) {
log.Infof("filter: granting connection %s, %s", conn, conn.Reason)
} else {
log.Warningf("filter: tried to accept %s, but current verdict is %s", conn, conn.Verdict)
}
}
// Block blocks the connection.
func (conn *Connection) Block(reason string) {
if conn.SetVerdict(VerdictBlock) {
conn.Reason = reason
// Accept is like AcceptWithContext but only accepts a reason.
func (conn *Connection) Accept(reason string) {
conn.AcceptWithContext(reason, nil)
}
// BlockWithContext blocks the connection.
func (conn *Connection) BlockWithContext(reason string, ctx interface{}) {
if conn.SetVerdict(VerdictBlock, reason, ctx) {
log.Infof("filter: blocking connection %s, %s", conn, conn.Reason)
} else {
log.Warningf("filter: tried to block %s, but current verdict is %s", conn, conn.Verdict)
}
}
// Drop drops the connection.
func (conn *Connection) Drop(reason string) {
if conn.SetVerdict(VerdictDrop) {
conn.Reason = reason
// Block is like BlockWithContext but does only accepts a reason.
func (conn *Connection) Block(reason string) {
conn.BlockWithContext(reason, nil)
}
// DropWithContext drops the connection.
func (conn *Connection) DropWithContext(reason string, ctx interface{}) {
if conn.SetVerdict(VerdictDrop, reason, ctx) {
log.Infof("filter: dropping connection %s, %s", conn, conn.Reason)
} else {
log.Warningf("filter: tried to drop %s, but current verdict is %s", conn, conn.Verdict)
}
}
// Deny blocks or drops the link depending on the connection direction.
func (conn *Connection) Deny(reason string) {
// Drop is like DropWithContext but does only accepts a reason.
func (conn *Connection) Drop(reason string) {
conn.DropWithContext(reason, nil)
}
// DenyWithContext blocks or drops the link depending on the connection direction.
func (conn *Connection) DenyWithContext(reason string, ctx interface{}) {
if conn.Inbound {
conn.Drop(reason)
conn.DropWithContext(reason, ctx)
} else {
conn.Block(reason)
conn.BlockWithContext(reason, ctx)
}
}
// Failed marks the connection with VerdictFailed and stores the reason.
func (conn *Connection) Failed(reason string) {
if conn.SetVerdict(VerdictFailed) {
conn.Reason = reason
// Deny is like DenyWithContext but only accepts a reason.
func (conn *Connection) Deny(reason string) {
conn.DenyWithContext(reason, nil)
}
// FailedWithContext marks the connection with VerdictFailed and stores the reason.
func (conn *Connection) FailedWithContext(reason string, ctx interface{}) {
if conn.SetVerdict(VerdictFailed, reason, ctx) {
log.Infof("filter: dropping connection %s because of an internal error: %s", conn, reason)
} else {
log.Warningf("filter: tried to drop %s due to error but current verdict is %s", conn, conn.Verdict)
}
}
// Failed is like FailedWithContext but only accepts a string.
func (conn *Connection) Failed(reason string) {
conn.FailedWithContext(reason, nil)
}
// SetVerdict sets a new verdict for the connection, making sure it does not interfere with previous verdicts.
func (conn *Connection) SetVerdict(newVerdict Verdict) (ok bool) {
func (conn *Connection) SetVerdict(newVerdict Verdict, reason string, ctx interface{}) (ok bool) {
if newVerdict >= conn.Verdict {
conn.Verdict = newVerdict
conn.Reason = reason
conn.ReasonContext = ctx
return true
}
return false

View file

@ -8,8 +8,8 @@ type EndpointAny struct {
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointAny) Matches(entity *intel.Entity) (result EPResult, reason string) {
return ep.matchesPPP(entity), "matches *"
func (ep *EndpointAny) Matches(entity *intel.Entity) (EPResult, Reason) {
return ep.match(ep, entity, "*", "matches")
}
func (ep *EndpointAny) String() string {

View file

@ -16,24 +16,22 @@ var (
type EndpointASN struct {
EndpointBase
ASN uint
Reason string
ASN uint
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointASN) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.IP == nil {
return Undeterminable, ""
}
func (ep *EndpointASN) Matches(entity *intel.Entity) (EPResult, Reason) {
asn, ok := entity.GetASN()
if !ok {
return Undeterminable, ""
return Undeterminable, nil
}
if asn == ep.ASN {
return ep.matchesPPP(entity), ep.Reason
asnStr := strconv.Itoa(int(ep.ASN))
return ep.match(ep, entity, asnStr, "IP is part of AS")
}
return NoMatch, ""
return NoMatch, nil
}
func (ep *EndpointASN) String() string {
@ -48,8 +46,7 @@ func parseTypeASN(fields []string) (Endpoint, error) {
}
ep := &EndpointASN{
ASN: uint(asn),
Reason: "IP is part of AS" + strconv.FormatInt(int64(asn), 10),
ASN: uint(asn),
}
return ep.parsePPP(ep, fields)
}

View file

@ -19,19 +19,16 @@ type EndpointCountry struct {
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointCountry) Matches(entity *intel.Entity) (result EPResult, reason string) {
if entity.IP == nil {
return Undeterminable, ""
}
func (ep *EndpointCountry) Matches(entity *intel.Entity) (EPResult, Reason) {
country, ok := entity.GetCountry()
if !ok {
return Undeterminable, ""
return Undeterminable, nil
}
if country == ep.Country {
return ep.matchesPPP(entity), "IP is located in " + country
return ep.match(ep, entity, country, "IP is located in")
}
return NoMatch, ""
return NoMatch, nil
}
func (ep *EndpointCountry) String() string {

View file

@ -28,47 +28,48 @@ type EndpointDomain struct {
Domain string
DomainZone string
MatchType uint8
Reason string
}
func (ep *EndpointDomain) check(entity *intel.Entity, domain string) (EPResult, string) {
func (ep *EndpointDomain) check(entity *intel.Entity, domain string) (EPResult, Reason) {
result, reason := ep.match(ep, entity, ep.Domain, "domain matches")
switch ep.MatchType {
case domainMatchTypeExact:
if domain == ep.Domain {
return ep.matchesPPP(entity), ep.Reason
return result, reason
}
case domainMatchTypeZone:
if domain == ep.Domain {
return ep.matchesPPP(entity), ep.Reason
return result, reason
}
if strings.HasSuffix(domain, ep.DomainZone) {
return ep.matchesPPP(entity), ep.Reason
return result, reason
}
case domainMatchTypeSuffix:
if strings.HasSuffix(domain, ep.Domain) {
return ep.matchesPPP(entity), ep.Reason
return result, reason
}
case domainMatchTypePrefix:
if strings.HasPrefix(domain, ep.Domain) {
return ep.matchesPPP(entity), ep.Reason
return result, reason
}
case domainMatchTypeContains:
if strings.Contains(domain, ep.Domain) {
return ep.matchesPPP(entity), ep.Reason
return result, reason
}
}
return NoMatch, ""
return NoMatch, nil
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason string) {
func (ep *EndpointDomain) Matches(entity *intel.Entity) (EPResult, Reason) {
if entity.Domain == "" {
return NoMatch, ""
return NoMatch, nil
}
result, reason = ep.check(entity, entity.Domain)
result, reason := ep.check(entity, entity.Domain)
if result != NoMatch {
return
return result, reason
}
if entity.CNAMECheckEnabled() {
@ -80,7 +81,7 @@ func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason
}
}
return NoMatch, ""
return NoMatch, nil
}
func (ep *EndpointDomain) String() string {
@ -93,7 +94,6 @@ func parseTypeDomain(fields []string) (Endpoint, error) {
if domainRegex.MatchString(domain) || altDomainRegex.MatchString(domain) {
ep := &EndpointDomain{
OriginalValue: domain,
Reason: "domain matches " + domain,
}
// fix domain ending

View file

@ -10,19 +10,19 @@ import (
type EndpointIP struct {
EndpointBase
IP net.IP
Reason string
IP net.IP
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointIP) Matches(entity *intel.Entity) (result EPResult, reason string) {
func (ep *EndpointIP) Matches(entity *intel.Entity) (EPResult, Reason) {
if entity.IP == nil {
return Undeterminable, ""
return Undeterminable, nil
}
if ep.IP.Equal(entity.IP) {
return ep.matchesPPP(entity), ep.Reason
return ep.match(ep, entity, ep.IP.String(), "IP matches")
}
return NoMatch, ""
return NoMatch, nil
}
func (ep *EndpointIP) String() string {
@ -33,8 +33,7 @@ func parseTypeIP(fields []string) (Endpoint, error) {
ip := net.ParseIP(fields[1])
if ip != nil {
ep := &EndpointIP{
IP: ip,
Reason: "IP is " + ip.String(),
IP: ip,
}
return ep.parsePPP(ep, fields)
}

View file

@ -10,19 +10,18 @@ import (
type EndpointIPRange struct {
EndpointBase
Net *net.IPNet
Reason string
Net *net.IPNet
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointIPRange) Matches(entity *intel.Entity) (result EPResult, reason string) {
func (ep *EndpointIPRange) Matches(entity *intel.Entity) (EPResult, Reason) {
if entity.IP == nil {
return Undeterminable, ""
return Undeterminable, nil
}
if ep.Net.Contains(entity.IP) {
return ep.matchesPPP(entity), ep.Reason
return ep.match(ep, entity, ep.Net.String(), "IP is in")
}
return NoMatch, ""
return NoMatch, nil
}
func (ep *EndpointIPRange) String() string {
@ -33,8 +32,7 @@ func parseTypeIPRange(fields []string) (Endpoint, error) {
_, net, err := net.ParseCIDR(fields[1])
if err == nil {
ep := &EndpointIPRange{
Net: net,
Reason: "IP is part of " + net.String(),
Net: net,
}
return ep.parsePPP(ep, fields)
}

View file

@ -12,18 +12,19 @@ type EndpointLists struct {
ListSet []string
Lists string
Reason string
}
// Matches checks whether the given entity matches this endpoint definition.
func (ep *EndpointLists) Matches(entity *intel.Entity) (result EPResult, reason string) {
entity.LoadLists()
if entity.MatchLists(ep.ListSet) {
return ep.matchesPPP(entity), entity.ListBlockReason().String()
func (ep *EndpointLists) Matches(entity *intel.Entity) (EPResult, Reason) {
if !entity.LoadLists() {
return Undeterminable, nil
}
return NoMatch, ""
if entity.MatchLists(ep.ListSet) {
return ep.match(ep, entity, ep.Lists, "filterlist contains", "filterlist", entity.ListBlockReason())
}
return NoMatch, nil
}
func (ep *EndpointLists) String() string {
@ -36,7 +37,6 @@ func parseTypeList(fields []string) (Endpoint, error) {
ep := &EndpointLists{
ListSet: lists,
Lists: "L:" + strings.Join(lists, ","),
Reason: "matched lists " + strings.Join(lists, ","),
}
return ep.parsePPP(ep, fields)
}

View file

@ -11,7 +11,7 @@ import (
// Endpoint describes an Endpoint Matcher
type Endpoint interface {
Matches(entity *intel.Entity) (EPResult, string)
Matches(entity *intel.Entity) (EPResult, Reason)
String() string
}
@ -24,6 +24,35 @@ type EndpointBase struct { //nolint:maligned // TODO
Permitted bool
}
func (ep *EndpointBase) match(s fmt.Stringer, entity *intel.Entity, value, desc string, keval ...interface{}) (EPResult, Reason) {
result := ep.matchesPPP(entity)
if result == Undeterminable || result == NoMatch {
return result, nil
}
return result, ep.makeReason(s, value, desc)
}
func (ep *EndpointBase) makeReason(s fmt.Stringer, value, desc string, keyval ...interface{}) Reason {
r := &reason{
description: desc,
Filter: ep.renderPPP(s.String()),
Permitted: ep.Permitted,
Value: value,
}
r.Extra = make(map[string]interface{})
for idx := 0; idx < int(len(keyval)/2); idx += 2 {
key := keyval[idx]
val := keyval[idx+1]
r.Extra[key.(string)] = val
}
return r
}
func (ep *EndpointBase) matchesPPP(entity *intel.Entity) (result EPResult) {
// only check if protocol is defined
if ep.Protocol > 0 {

View file

@ -21,6 +21,12 @@ const (
Permitted
)
// IsDecision returns true if result represents a decision
// and false if result is NoMatch or Undeterminable.
func IsDecision(result EPResult) bool {
return result == Denied || result == Permitted
}
// ParseEndpoints parses a list of endpoints and returns a list of Endpoints for matching.
func ParseEndpoints(entries []string) (Endpoints, error) {
var firstErr error
@ -57,7 +63,7 @@ func (e Endpoints) IsSet() bool {
}
// Match checks whether the given entity matches any of the endpoint definitions in the list.
func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason string) {
func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason Reason) {
for _, entry := range e {
if entry != nil {
if result, reason = entry.Matches(entity); result != NoMatch {
@ -66,7 +72,7 @@ func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason string)
}
}
return NoMatch, ""
return NoMatch, nil
}
func (e Endpoints) String() string {

View file

@ -0,0 +1,34 @@
package endpoints
// Reason describes the reason why an endpoint has been
// permitted or blocked.
type Reason interface {
// String should return a human readable string
// describing the decision reason.
String() string
// Context returns the context that was used
// for the decision.
Context() interface{}
}
type reason struct {
description string
Filter string
Value string
Permitted bool
Extra map[string]interface{}
}
func (r *reason) String() string {
prefix := "endpoint in blocklist: "
if r.Permitted {
prefix = "endpoint in whitelist: "
}
return prefix + r.description + " " + r.Value
}
func (r *reason) Context() interface{} {
return r
}

View file

@ -204,12 +204,12 @@ func (lp *LayeredProfile) DefaultAction() uint8 {
}
// MatchEndpoint checks if the given endpoint matches an entry in any of the profiles.
func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (result endpoints.EPResult, reason string) {
func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
for _, layer := range lp.layers {
if layer.endpoints.IsSet() {
result, reason = layer.endpoints.Match(entity)
if result != endpoints.NoMatch {
return
result, reason := layer.endpoints.Match(entity)
if endpoints.IsDecision(result) {
return result, reason
}
}
}
@ -220,14 +220,14 @@ func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (result endpoints.
}
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles.
func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result endpoints.EPResult, reason string) {
func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
entity.EnableReverseResolving()
for _, layer := range lp.layers {
if layer.serviceEndpoints.IsSet() {
result, reason = layer.serviceEndpoints.Match(entity)
if result != endpoints.NoMatch {
return
result, reason := layer.serviceEndpoints.Match(entity)
if endpoints.IsDecision(result) {
return result, reason
}
}
}
@ -239,7 +239,7 @@ func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result end
// MatchFilterLists matches the entity against the set of filter
// lists.
func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPResult, string) {
func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
entity.ResolveSubDomainLists(lp.FilterSubDomains())
entity.EnableCNAMECheck(lp.FilterCNAMEs())
@ -249,10 +249,10 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPRe
entity.LoadLists()
if entity.MatchLists(layer.filterListIDs) {
return endpoints.Denied, entity.ListBlockReason().String()
return endpoints.Denied, entity.ListBlockReason()
}
return endpoints.NoMatch, ""
return endpoints.NoMatch, nil
}
}
@ -262,11 +262,11 @@ func (lp *LayeredProfile) MatchFilterLists(entity *intel.Entity) (endpoints.EPRe
entity.LoadLists()
if entity.MatchLists(cfgFilterLists) {
return endpoints.Denied, entity.ListBlockReason().String()
return endpoints.Denied, entity.ListBlockReason()
}
}
return endpoints.NoMatch, ""
return endpoints.NoMatch, nil
}
// AddEndpoint adds an endpoint to the local endpoint list, saves the local profile and reloads the configuration.