wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
13
service/profile/endpoints/annotations.go
Normal file
13
service/profile/endpoints/annotations.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package endpoints
|
||||
|
||||
// DisplayHintEndpointList marks an option as an endpoint
|
||||
// list option. It's meant to be used with DisplayHintAnnotation.
|
||||
const DisplayHintEndpointList = "endpoint list"
|
||||
|
||||
// EndpointListVerdictNamesAnnotation is the annotation identifier used in
|
||||
// configuration options to hint the UI on names to be used for endpoint list
|
||||
// verdicts.
|
||||
// If configured, it must be of type map[string]string, mapping the verdict
|
||||
// symbol to a name to be displayed in the UI.
|
||||
// May only used when config.DisplayHintAnnotation is set to DisplayHintEndpointList.
|
||||
const EndpointListVerdictNamesAnnotation = "safing/portmaster:ui:endpoint-list:verdict-names"
|
||||
29
service/profile/endpoints/endpoint-any.go
Normal file
29
service/profile/endpoints/endpoint-any.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
// EndpointAny matches anything.
|
||||
type EndpointAny struct {
|
||||
EndpointBase
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointAny) Matches(_ context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
return ep.match(ep, entity, "*", "matches")
|
||||
}
|
||||
|
||||
func (ep *EndpointAny) String() string {
|
||||
return ep.renderPPP("*")
|
||||
}
|
||||
|
||||
func parseTypeAny(fields []string) (Endpoint, error) {
|
||||
if fields[1] == "*" {
|
||||
ep := &EndpointAny{}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
65
service/profile/endpoints/endpoint-asn.go
Normal file
65
service/profile/endpoints/endpoint-asn.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
var asnRegex = regexp.MustCompile("^AS[0-9]+$")
|
||||
|
||||
// EndpointASN matches ASNs.
|
||||
type EndpointASN struct {
|
||||
EndpointBase
|
||||
|
||||
ASN uint
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointASN) Matches(ctx context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
if entity.IP == nil {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
if !entity.IPScope.IsGlobal() {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
asn, ok := entity.GetASN(ctx)
|
||||
if !ok {
|
||||
asnStr := strconv.Itoa(int(ep.ASN))
|
||||
return MatchError, ep.makeReason(ep, asnStr, "ASN data not available to match")
|
||||
}
|
||||
|
||||
if asn == ep.ASN {
|
||||
asnStr := strconv.Itoa(int(ep.ASN))
|
||||
return ep.match(ep, entity, asnStr, "IP is part of AS")
|
||||
}
|
||||
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (ep *EndpointASN) String() string {
|
||||
return ep.renderPPP("AS" + strconv.FormatInt(int64(ep.ASN), 10))
|
||||
}
|
||||
|
||||
func parseTypeASN(fields []string) (Endpoint, error) {
|
||||
if asnRegex.MatchString(fields[1]) {
|
||||
asnString := strings.TrimPrefix(fields[1], "AS")
|
||||
asn, err := strconv.ParseUint(asnString, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse AS number %s", asnString)
|
||||
}
|
||||
|
||||
ep := &EndpointASN{
|
||||
ASN: uint(asn),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
63
service/profile/endpoints/endpoint-continent.go
Normal file
63
service/profile/endpoints/endpoint-continent.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
var (
|
||||
continentCodePrefix = "C:"
|
||||
continentRegex = regexp.MustCompile(`^C:[A-Z]{2}$`)
|
||||
)
|
||||
|
||||
// EndpointContinent matches countries.
|
||||
type EndpointContinent struct {
|
||||
EndpointBase
|
||||
|
||||
ContinentCode string
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointContinent) Matches(ctx context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
if entity.IP == nil {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
if !entity.IPScope.IsGlobal() {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
countryInfo := entity.GetCountryInfo(ctx)
|
||||
if countryInfo == nil {
|
||||
return MatchError, ep.makeReason(ep, "", "country data not available to match")
|
||||
}
|
||||
|
||||
if ep.ContinentCode == countryInfo.Continent.Code {
|
||||
return ep.match(
|
||||
ep, entity,
|
||||
fmt.Sprintf("%s (%s)", countryInfo.Continent.Name, countryInfo.Continent.Code),
|
||||
"IP is located in",
|
||||
)
|
||||
}
|
||||
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (ep *EndpointContinent) String() string {
|
||||
return ep.renderPPP(continentCodePrefix + ep.ContinentCode)
|
||||
}
|
||||
|
||||
func parseTypeContinent(fields []string) (Endpoint, error) {
|
||||
if continentRegex.MatchString(fields[1]) {
|
||||
ep := &EndpointContinent{
|
||||
ContinentCode: strings.TrimPrefix(strings.ToUpper(fields[1]), continentCodePrefix),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
59
service/profile/endpoints/endpoint-country.go
Normal file
59
service/profile/endpoints/endpoint-country.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
var countryRegex = regexp.MustCompile(`^[A-Z]{2}$`)
|
||||
|
||||
// EndpointCountry matches countries.
|
||||
type EndpointCountry struct {
|
||||
EndpointBase
|
||||
|
||||
CountryCode string
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointCountry) Matches(ctx context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
if entity.IP == nil {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
if !entity.IPScope.IsGlobal() {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
countryInfo := entity.GetCountryInfo(ctx)
|
||||
if countryInfo == nil {
|
||||
return MatchError, ep.makeReason(ep, "", "country data not available to match")
|
||||
}
|
||||
|
||||
if ep.CountryCode == countryInfo.Code {
|
||||
return ep.match(
|
||||
ep, entity,
|
||||
fmt.Sprintf("%s (%s)", countryInfo.Name, countryInfo.Code),
|
||||
"IP is located in",
|
||||
)
|
||||
}
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (ep *EndpointCountry) String() string {
|
||||
return ep.renderPPP(ep.CountryCode)
|
||||
}
|
||||
|
||||
func parseTypeCountry(fields []string) (Endpoint, error) {
|
||||
if countryRegex.MatchString(fields[1]) {
|
||||
ep := &EndpointCountry{
|
||||
CountryCode: strings.ToUpper(fields[1]),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
170
service/profile/endpoints/endpoint-domain.go
Normal file
170
service/profile/endpoints/endpoint-domain.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
const (
|
||||
domainMatchTypeExact uint8 = iota
|
||||
domainMatchTypeZone
|
||||
domainMatchTypeSuffix
|
||||
domainMatchTypePrefix
|
||||
domainMatchTypeContains
|
||||
)
|
||||
|
||||
var (
|
||||
allowedDomainChars = regexp.MustCompile(`^[a-z0-9\.-]+$`)
|
||||
|
||||
// looksLikeAnIP matches domains that look like an IP address.
|
||||
looksLikeAnIP = regexp.MustCompile(`^[0-9\.:]+$`)
|
||||
)
|
||||
|
||||
// EndpointDomain matches domains.
|
||||
type EndpointDomain struct {
|
||||
EndpointBase
|
||||
|
||||
OriginalValue string
|
||||
Domain string
|
||||
DomainZone string
|
||||
MatchType uint8
|
||||
}
|
||||
|
||||
func (ep *EndpointDomain) check(entity *intel.Entity, domain string) (EPResult, Reason) {
|
||||
result, reason := ep.match(ep, entity, ep.OriginalValue, "domain matches")
|
||||
|
||||
switch ep.MatchType {
|
||||
case domainMatchTypeExact:
|
||||
if domain == ep.Domain {
|
||||
return result, reason
|
||||
}
|
||||
case domainMatchTypeZone:
|
||||
if domain == ep.Domain {
|
||||
return result, reason
|
||||
}
|
||||
if strings.HasSuffix(domain, ep.DomainZone) {
|
||||
return result, reason
|
||||
}
|
||||
case domainMatchTypeSuffix:
|
||||
if strings.HasSuffix(domain, ep.Domain) {
|
||||
return result, reason
|
||||
}
|
||||
case domainMatchTypePrefix:
|
||||
if strings.HasPrefix(domain, ep.Domain) {
|
||||
return result, reason
|
||||
}
|
||||
case domainMatchTypeContains:
|
||||
if strings.Contains(domain, ep.Domain) {
|
||||
return result, reason
|
||||
}
|
||||
}
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointDomain) Matches(ctx context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
domain, ok := entity.GetDomain(ctx, true /* mayUseReverseDomain */)
|
||||
if !ok {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
result, reason := ep.check(entity, domain)
|
||||
if result != NoMatch {
|
||||
return result, reason
|
||||
}
|
||||
|
||||
if entity.CNAMECheckEnabled() {
|
||||
for _, cname := range entity.CNAME {
|
||||
result, reason = ep.check(entity, cname)
|
||||
if result == Denied {
|
||||
return result, reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (ep *EndpointDomain) String() string {
|
||||
return ep.renderPPP(ep.OriginalValue)
|
||||
}
|
||||
|
||||
func parseTypeDomain(fields []string) (Endpoint, error) {
|
||||
domain := fields[1]
|
||||
ep := &EndpointDomain{
|
||||
OriginalValue: domain,
|
||||
}
|
||||
|
||||
// Fix domain ending.
|
||||
switch domain[len(domain)-1] {
|
||||
case '.', '*':
|
||||
default:
|
||||
domain += "."
|
||||
}
|
||||
|
||||
// Check if this looks like an IP address.
|
||||
// At least the TLDs has characters.
|
||||
if looksLikeAnIP.MatchString(domain) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Fix domain case.
|
||||
domain = strings.ToLower(domain)
|
||||
needValidFQDN := true
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "*") && strings.HasSuffix(domain, "*"):
|
||||
ep.MatchType = domainMatchTypeContains
|
||||
ep.Domain = strings.TrimPrefix(domain, "*")
|
||||
ep.Domain = strings.TrimSuffix(ep.Domain, "*")
|
||||
needValidFQDN = false
|
||||
|
||||
case strings.HasSuffix(domain, "*"):
|
||||
ep.MatchType = domainMatchTypePrefix
|
||||
ep.Domain = strings.TrimSuffix(domain, "*")
|
||||
needValidFQDN = false
|
||||
|
||||
// Prefix matching cannot be combined with zone matching
|
||||
if strings.HasPrefix(ep.Domain, ".") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Do not accept domains that look like an IP address and have a suffix wildcard.
|
||||
// This is confusing, because it looks like an IP Netmask matching rule.
|
||||
if looksLikeAnIP.MatchString(ep.Domain) {
|
||||
return nil, errors.New("use CIDR notation (eg. 10.0.0.0/24) for matching ip address ranges")
|
||||
}
|
||||
|
||||
case strings.HasPrefix(domain, "*"):
|
||||
ep.MatchType = domainMatchTypeSuffix
|
||||
ep.Domain = strings.TrimPrefix(domain, "*")
|
||||
needValidFQDN = false
|
||||
|
||||
case strings.HasPrefix(domain, "."):
|
||||
ep.MatchType = domainMatchTypeZone
|
||||
ep.Domain = strings.TrimPrefix(domain, ".")
|
||||
ep.DomainZone = "." + ep.Domain
|
||||
|
||||
default:
|
||||
ep.MatchType = domainMatchTypeExact
|
||||
ep.Domain = domain
|
||||
}
|
||||
|
||||
// Validate domain "content".
|
||||
switch {
|
||||
case needValidFQDN && !netutils.IsValidFqdn(ep.Domain):
|
||||
return nil, nil
|
||||
case !needValidFQDN && !allowedDomainChars.MatchString(ep.Domain):
|
||||
return nil, nil
|
||||
case strings.Contains(ep.Domain, ".."):
|
||||
// The above regex does not catch double dots.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
42
service/profile/endpoints/endpoint-ip.go
Normal file
42
service/profile/endpoints/endpoint-ip.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
// EndpointIP matches IPs.
|
||||
type EndpointIP struct {
|
||||
EndpointBase
|
||||
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointIP) Matches(_ context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
if entity.IP == nil {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
if ep.IP.Equal(entity.IP) {
|
||||
return ep.match(ep, entity, ep.IP.String(), "IP matches")
|
||||
}
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (ep *EndpointIP) String() string {
|
||||
return ep.renderPPP(ep.IP.String())
|
||||
}
|
||||
|
||||
func parseTypeIP(fields []string) (Endpoint, error) {
|
||||
ip := net.ParseIP(fields[1])
|
||||
if ip != nil {
|
||||
ep := &EndpointIP{
|
||||
IP: ip,
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
42
service/profile/endpoints/endpoint-iprange.go
Normal file
42
service/profile/endpoints/endpoint-iprange.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
// EndpointIPRange matches IP ranges.
|
||||
type EndpointIPRange struct {
|
||||
EndpointBase
|
||||
|
||||
Net *net.IPNet
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointIPRange) Matches(_ context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
if entity.IP == nil {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
if ep.Net.Contains(entity.IP) {
|
||||
return ep.match(ep, entity, ep.Net.String(), "IP is in")
|
||||
}
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (ep *EndpointIPRange) String() string {
|
||||
return ep.renderPPP(ep.Net.String())
|
||||
}
|
||||
|
||||
func parseTypeIPRange(fields []string) (Endpoint, error) {
|
||||
_, net, err := net.ParseCIDR(fields[1])
|
||||
if err == nil {
|
||||
ep := &EndpointIPRange{
|
||||
Net: net,
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
41
service/profile/endpoints/endpoint-lists.go
Normal file
41
service/profile/endpoints/endpoint-lists.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
// EndpointLists matches endpoint lists.
|
||||
type EndpointLists struct {
|
||||
EndpointBase
|
||||
|
||||
ListSet []string
|
||||
Lists string
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointLists) Matches(ctx context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
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 {
|
||||
return ep.renderPPP(ep.Lists)
|
||||
}
|
||||
|
||||
func parseTypeList(fields []string) (Endpoint, error) {
|
||||
if strings.HasPrefix(fields[1], "L:") {
|
||||
lists := strings.Split(strings.TrimPrefix(fields[1], "L:"), ",")
|
||||
ep := &EndpointLists{
|
||||
ListSet: lists,
|
||||
Lists: "L:" + strings.Join(lists, ","),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
107
service/profile/endpoints/endpoint-scopes.go
Normal file
107
service/profile/endpoints/endpoint-scopes.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
const (
|
||||
scopeLocalhost = 1
|
||||
scopeLocalhostName = "Localhost"
|
||||
scopeLocalhostMatcher = "localhost"
|
||||
|
||||
scopeLAN = 2
|
||||
scopeLANName = "LAN"
|
||||
scopeLANMatcher = "lan"
|
||||
|
||||
scopeInternet = 4
|
||||
scopeInternetName = "Internet"
|
||||
scopeInternetMatcher = "internet"
|
||||
)
|
||||
|
||||
// EndpointScope matches network scopes.
|
||||
type EndpointScope struct {
|
||||
EndpointBase
|
||||
|
||||
scopes uint8
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointScope) Matches(_ context.Context, entity *intel.Entity) (EPResult, Reason) {
|
||||
if entity.IP == nil {
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
var scope uint8
|
||||
switch entity.IPScope {
|
||||
case netutils.HostLocal:
|
||||
scope = scopeLocalhost
|
||||
case netutils.LinkLocal:
|
||||
scope = scopeLAN
|
||||
case netutils.SiteLocal:
|
||||
scope = scopeLAN
|
||||
case netutils.Global:
|
||||
scope = scopeInternet
|
||||
case netutils.LocalMulticast:
|
||||
scope = scopeLAN
|
||||
case netutils.GlobalMulticast:
|
||||
scope = scopeInternet
|
||||
case netutils.Undefined, netutils.Invalid:
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
if ep.scopes&scope > 0 {
|
||||
return ep.match(ep, entity, ep.Scopes(), "scope matches")
|
||||
}
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
// Scopes returns the string representation of all scopes.
|
||||
func (ep *EndpointScope) Scopes() string {
|
||||
// single scope
|
||||
switch ep.scopes {
|
||||
case scopeLocalhost:
|
||||
return scopeLocalhostName
|
||||
case scopeLAN:
|
||||
return scopeLANName
|
||||
case scopeInternet:
|
||||
return scopeInternetName
|
||||
}
|
||||
|
||||
// multiple scopes
|
||||
var s []string
|
||||
if ep.scopes&scopeLocalhost > 0 {
|
||||
s = append(s, scopeLocalhostName)
|
||||
}
|
||||
if ep.scopes&scopeLAN > 0 {
|
||||
s = append(s, scopeLANName)
|
||||
}
|
||||
if ep.scopes&scopeInternet > 0 {
|
||||
s = append(s, scopeInternetName)
|
||||
}
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
||||
func (ep *EndpointScope) String() string {
|
||||
return ep.renderPPP(ep.Scopes())
|
||||
}
|
||||
|
||||
func parseTypeScope(fields []string) (Endpoint, error) {
|
||||
ep := &EndpointScope{}
|
||||
for _, val := range strings.Split(strings.ToLower(fields[1]), ",") {
|
||||
switch val {
|
||||
case scopeLocalhostMatcher:
|
||||
ep.scopes ^= scopeLocalhost
|
||||
case scopeLANMatcher:
|
||||
ep.scopes ^= scopeLAN
|
||||
case scopeInternetMatcher:
|
||||
ep.scopes ^= scopeInternet
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
258
service/profile/endpoints/endpoint.go
Normal file
258
service/profile/endpoints/endpoint.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/network/reference"
|
||||
)
|
||||
|
||||
// Endpoint describes an Endpoint Matcher.
|
||||
type Endpoint interface {
|
||||
Matches(ctx context.Context, entity *intel.Entity) (EPResult, Reason)
|
||||
String() string
|
||||
}
|
||||
|
||||
// EndpointBase provides general functions for implementing an Endpoint to reduce boilerplate.
|
||||
type EndpointBase struct { //nolint:maligned // TODO
|
||||
Protocol uint8
|
||||
StartPort uint16
|
||||
EndPort uint16
|
||||
|
||||
Permitted bool
|
||||
}
|
||||
|
||||
func (ep *EndpointBase) match(s fmt.Stringer, entity *intel.Entity, value, desc string, keyval ...interface{}) (EPResult, Reason) {
|
||||
result := ep.matchesPPP(entity)
|
||||
if result == NoMatch {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return result, ep.makeReason(s, value, desc, keyval...)
|
||||
}
|
||||
|
||||
func (ep *EndpointBase) makeReason(s fmt.Stringer, value, desc string, keyval ...interface{}) Reason {
|
||||
r := &reason{
|
||||
description: desc,
|
||||
Filter: s.String(),
|
||||
Permitted: ep.Permitted,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
r.Extra = make(map[string]interface{})
|
||||
|
||||
for idx := 0; idx < len(keyval)/2; idx += 2 {
|
||||
key := keyval[idx]
|
||||
val := keyval[idx+1]
|
||||
|
||||
if keyName, ok := key.(string); ok {
|
||||
r.Extra[keyName] = val
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (ep *EndpointBase) matchesPPP(entity *intel.Entity) (result EPResult) {
|
||||
// only check if protocol is defined
|
||||
if ep.Protocol > 0 {
|
||||
// if protocol does not match, return NoMatch
|
||||
if entity.Protocol != ep.Protocol {
|
||||
return NoMatch
|
||||
}
|
||||
}
|
||||
|
||||
// only check if port is defined
|
||||
if ep.StartPort > 0 {
|
||||
// if port does not match, return NoMatch
|
||||
if entity.DstPort() < ep.StartPort || entity.DstPort() > ep.EndPort {
|
||||
return NoMatch
|
||||
}
|
||||
}
|
||||
|
||||
// protocol and port matched or were defined as any
|
||||
if ep.Permitted {
|
||||
return Permitted
|
||||
}
|
||||
return Denied
|
||||
}
|
||||
|
||||
func (ep *EndpointBase) renderPPP(s string) string {
|
||||
var rendered string
|
||||
if ep.Permitted {
|
||||
rendered = "+ " + s
|
||||
} else {
|
||||
rendered = "- " + s
|
||||
}
|
||||
|
||||
if ep.Protocol > 0 || ep.StartPort > 0 {
|
||||
if ep.Protocol > 0 {
|
||||
rendered += " " + reference.GetProtocolName(ep.Protocol)
|
||||
} else {
|
||||
rendered += " *"
|
||||
}
|
||||
|
||||
if ep.StartPort > 0 {
|
||||
if ep.StartPort == ep.EndPort {
|
||||
rendered += "/" + reference.GetPortName(ep.StartPort)
|
||||
} else {
|
||||
rendered += "/" + strconv.Itoa(int(ep.StartPort)) + "-" + strconv.Itoa(int(ep.EndPort))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
func (ep *EndpointBase) parsePPP(typedEp Endpoint, fields []string) (Endpoint, error) { //nolint:gocognit // TODO
|
||||
switch len(fields) {
|
||||
case 2:
|
||||
// nothing else to do here
|
||||
case 3:
|
||||
// parse protocol and port(s)
|
||||
var ok bool
|
||||
splitted := strings.Split(fields[2], "/")
|
||||
if len(splitted) > 2 {
|
||||
return nil, invalidDefinitionError(fields, "protocol and port must be in format <protocol>/<port>")
|
||||
}
|
||||
// protocol
|
||||
switch splitted[0] {
|
||||
case "":
|
||||
return nil, invalidDefinitionError(fields, "protocol can't be empty")
|
||||
case "*":
|
||||
// any protocol that supports ports
|
||||
default:
|
||||
n, err := strconv.ParseUint(splitted[0], 10, 8)
|
||||
n8 := uint8(n)
|
||||
if err != nil {
|
||||
// maybe it's a name?
|
||||
n8, ok = reference.GetProtocolNumber(splitted[0])
|
||||
if !ok {
|
||||
return nil, invalidDefinitionError(fields, "protocol number parsing error")
|
||||
}
|
||||
}
|
||||
ep.Protocol = n8
|
||||
}
|
||||
// port(s)
|
||||
if len(splitted) > 1 {
|
||||
switch splitted[1] {
|
||||
case "", "*":
|
||||
return nil, invalidDefinitionError(fields, "omit port if should match any")
|
||||
default:
|
||||
portSplitted := strings.Split(splitted[1], "-")
|
||||
if len(portSplitted) > 2 {
|
||||
return nil, invalidDefinitionError(fields, "ports must be in format from-to")
|
||||
}
|
||||
// parse start port
|
||||
n, err := strconv.ParseUint(portSplitted[0], 10, 16)
|
||||
n16 := uint16(n)
|
||||
if err != nil {
|
||||
// maybe it's a name?
|
||||
n16, ok = reference.GetPortNumber(portSplitted[0])
|
||||
if !ok {
|
||||
return nil, invalidDefinitionError(fields, "port number parsing error")
|
||||
}
|
||||
}
|
||||
if n16 == 0 {
|
||||
return nil, invalidDefinitionError(fields, "port number cannot be 0")
|
||||
}
|
||||
ep.StartPort = n16
|
||||
// parse end port
|
||||
if len(portSplitted) > 1 {
|
||||
n, err = strconv.ParseUint(portSplitted[1], 10, 16)
|
||||
n16 = uint16(n)
|
||||
if err != nil {
|
||||
// maybe it's a name?
|
||||
n16, ok = reference.GetPortNumber(portSplitted[1])
|
||||
if !ok {
|
||||
return nil, invalidDefinitionError(fields, "port number parsing error")
|
||||
}
|
||||
}
|
||||
}
|
||||
if n16 == 0 {
|
||||
return nil, invalidDefinitionError(fields, "port number cannot be 0")
|
||||
}
|
||||
ep.EndPort = n16
|
||||
}
|
||||
}
|
||||
// check if anything was parsed
|
||||
if ep.Protocol == 0 && ep.StartPort == 0 {
|
||||
return nil, invalidDefinitionError(fields, "omit protocol/port if should match any")
|
||||
}
|
||||
default:
|
||||
return nil, invalidDefinitionError(fields, "there should be only 2 or 3 segments")
|
||||
}
|
||||
|
||||
switch fields[0] {
|
||||
case "+":
|
||||
ep.Permitted = true
|
||||
case "-":
|
||||
ep.Permitted = false
|
||||
default:
|
||||
return nil, invalidDefinitionError(fields, "invalid permission prefix")
|
||||
}
|
||||
|
||||
return typedEp, nil
|
||||
}
|
||||
|
||||
func invalidDefinitionError(fields []string, msg string) error {
|
||||
return fmt.Errorf(`invalid endpoint definition: "%s" - %s`, strings.Join(fields, " "), msg)
|
||||
}
|
||||
|
||||
//nolint:gocognit,nakedret
|
||||
func parseEndpoint(value string) (endpoint Endpoint, err error) {
|
||||
fields := strings.Fields(value)
|
||||
if len(fields) < 2 {
|
||||
return nil, fmt.Errorf(`invalid endpoint definition: "%s"`, value)
|
||||
}
|
||||
|
||||
// Remove comment.
|
||||
for i, field := range fields {
|
||||
if strings.HasPrefix(field, "#") {
|
||||
fields = fields[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// any
|
||||
if endpoint, err = parseTypeAny(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// ip
|
||||
if endpoint, err = parseTypeIP(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// ip range
|
||||
if endpoint, err = parseTypeIPRange(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// country
|
||||
if endpoint, err = parseTypeCountry(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// continent
|
||||
if endpoint, err = parseTypeContinent(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// asn
|
||||
if endpoint, err = parseTypeASN(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// scopes
|
||||
if endpoint, err = parseTypeScope(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// lists
|
||||
if endpoint, err = parseTypeList(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// domain
|
||||
if endpoint, err = parseTypeDomain(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(`unknown endpoint definition: "%s"`, value)
|
||||
}
|
||||
99
service/profile/endpoints/endpoint_test.go
Normal file
99
service/profile/endpoints/endpoint_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEndpointParsing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// any (basics)
|
||||
testParsing(t, "- *")
|
||||
testParsing(t, "+ *")
|
||||
|
||||
// domain
|
||||
testDomainParsing(t, "- *bad*", domainMatchTypeContains, "bad")
|
||||
testDomainParsing(t, "- bad*", domainMatchTypePrefix, "bad")
|
||||
testDomainParsing(t, "- *bad.com", domainMatchTypeSuffix, "bad.com.")
|
||||
testDomainParsing(t, "- .bad.com", domainMatchTypeZone, "bad.com.")
|
||||
testDomainParsing(t, "- bad.com", domainMatchTypeExact, "bad.com.")
|
||||
testDomainParsing(t, "- www.bad.com.", domainMatchTypeExact, "www.bad.com.")
|
||||
testDomainParsing(t, "- www.bad.com", domainMatchTypeExact, "www.bad.com.")
|
||||
|
||||
// ip
|
||||
testParsing(t, "+ 127.0.0.1")
|
||||
testParsing(t, "+ 192.168.0.1")
|
||||
testParsing(t, "+ ::1")
|
||||
testParsing(t, "+ 2606:4700:4700::1111")
|
||||
|
||||
// ip
|
||||
testParsing(t, "+ 127.0.0.0/8")
|
||||
testParsing(t, "+ 192.168.0.0/24")
|
||||
testParsing(t, "+ 2606:4700:4700::/48")
|
||||
|
||||
// country
|
||||
testParsing(t, "+ DE")
|
||||
testParsing(t, "+ AT")
|
||||
testParsing(t, "+ CH")
|
||||
testParsing(t, "+ AS")
|
||||
|
||||
// asn
|
||||
testParsing(t, "+ AS1")
|
||||
testParsing(t, "+ AS12")
|
||||
testParsing(t, "+ AS123")
|
||||
testParsing(t, "+ AS1234")
|
||||
testParsing(t, "+ AS12345")
|
||||
|
||||
// network scope
|
||||
testParsing(t, "+ Localhost")
|
||||
testParsing(t, "+ LAN")
|
||||
testParsing(t, "+ Internet")
|
||||
testParsing(t, "+ Localhost,LAN,Internet")
|
||||
|
||||
// protocol and ports
|
||||
testParsing(t, "+ * TCP/1-1024")
|
||||
testParsing(t, "+ * */DNS")
|
||||
testParsing(t, "+ * ICMP")
|
||||
testParsing(t, "+ * 127")
|
||||
testParsing(t, "+ * UDP/1234")
|
||||
testParsing(t, "+ * TCP/HTTP")
|
||||
testParsing(t, "+ * TCP/80-443")
|
||||
|
||||
// TODO: Test fails:
|
||||
// testParsing(t, "+ 1234")
|
||||
}
|
||||
|
||||
func testParsing(t *testing.T, value string) {
|
||||
t.Helper()
|
||||
|
||||
ep, err := parseEndpoint(value)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
// t.Logf("%T: %+v", ep, ep)
|
||||
if value != ep.String() {
|
||||
t.Errorf(`stringified endpoint mismatch: original was "%s", parsed is "%s"`, value, ep.String())
|
||||
}
|
||||
}
|
||||
|
||||
func testDomainParsing(t *testing.T, value string, matchType uint8, matchValue string) {
|
||||
t.Helper()
|
||||
|
||||
testParsing(t, value)
|
||||
|
||||
epGeneric, err := parseTypeDomain(strings.Fields(value))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
ep := epGeneric.(*EndpointDomain) //nolint:forcetypeassert
|
||||
|
||||
if ep.MatchType != matchType {
|
||||
t.Errorf(`error parsing domain endpoint "%s": match type should be %d, was %d`, value, matchType, ep.MatchType)
|
||||
}
|
||||
if ep.Domain != matchValue {
|
||||
t.Errorf(`error parsing domain endpoint "%s": match domain value should be %s, was %s`, value, matchValue, ep.Domain)
|
||||
}
|
||||
}
|
||||
149
service/profile/endpoints/endpoints.go
Normal file
149
service/profile/endpoints/endpoints.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
// Endpoints is a list of permitted or denied endpoints.
|
||||
type Endpoints []Endpoint
|
||||
|
||||
// EPResult represents the result of a check against an EndpointPermission.
|
||||
type EPResult uint8
|
||||
|
||||
// Endpoint matching return values.
|
||||
const (
|
||||
NoMatch EPResult = iota
|
||||
MatchError
|
||||
Denied
|
||||
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 || result == MatchError
|
||||
}
|
||||
|
||||
// ParseEndpoints parses a list of endpoints and returns a list of Endpoints for matching.
|
||||
func ParseEndpoints(entries []string) (Endpoints, error) {
|
||||
var firstErr error
|
||||
var errCnt int
|
||||
endpoints := make(Endpoints, 0, len(entries))
|
||||
|
||||
entriesLoop:
|
||||
for _, entry := range entries {
|
||||
ep, err := parseEndpoint(entry)
|
||||
if err != nil {
|
||||
errCnt++
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue entriesLoop
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
|
||||
if firstErr != nil {
|
||||
if errCnt > 0 {
|
||||
return endpoints, fmt.Errorf("encountered %d errors, first was: %w", errCnt, firstErr)
|
||||
}
|
||||
return endpoints, firstErr
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ListEntryValidationRegex is a regex to bullshit check endpoint list entries.
|
||||
var ListEntryValidationRegex = strings.Join([]string{
|
||||
`^(\+|\-) `, // Rule verdict.
|
||||
`(! +)?`, // Invert matching.
|
||||
`[A-z0-9\.:\-*/]+`, // Entity matching.
|
||||
`( `, // Start of optional matching.
|
||||
`[A-z0-9*]+`, // Protocol matching.
|
||||
`(/[A-z0-9]+(\-[A-z0-9]+)?)?`, // Port and port range matching.
|
||||
`)?`, // End of optional matching.
|
||||
`( +#.*)?`, // Optional comment.
|
||||
}, "")
|
||||
|
||||
// ValidateEndpointListConfigOption validates the given value.
|
||||
func ValidateEndpointListConfigOption(value interface{}) error {
|
||||
list, ok := value.([]string)
|
||||
if !ok {
|
||||
return errors.New("invalid type")
|
||||
}
|
||||
|
||||
_, err := ParseEndpoints(list)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsSet returns whether the Endpoints object is "set".
|
||||
func (e Endpoints) IsSet() bool {
|
||||
return len(e) > 0
|
||||
}
|
||||
|
||||
// Match checks whether the given entity matches any of the endpoint definitions in the list.
|
||||
func (e Endpoints) Match(ctx context.Context, entity *intel.Entity) (result EPResult, reason Reason) {
|
||||
for _, entry := range e {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if result, reason = entry.Matches(ctx, entity); result != NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
// MatchMulti checks whether the given entities match any of the endpoint
|
||||
// definitions in the list. Every rule is evaluated against all given entities
|
||||
// and only if not match was registered, the next rule is evaluated.
|
||||
func (e Endpoints) MatchMulti(ctx context.Context, entities ...*intel.Entity) (result EPResult, reason Reason) {
|
||||
for _, entry := range e {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entity := range entities {
|
||||
if entity == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if result, reason = entry.Matches(ctx, entity); result != NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, nil
|
||||
}
|
||||
|
||||
func (e Endpoints) String() string {
|
||||
s := make([]string, 0, len(e))
|
||||
for _, entry := range e {
|
||||
s = append(s, entry.String())
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
|
||||
}
|
||||
|
||||
func (epr EPResult) String() string {
|
||||
switch epr {
|
||||
case NoMatch:
|
||||
return "No Match"
|
||||
case MatchError:
|
||||
return "Match Error"
|
||||
case Denied:
|
||||
return "Denied"
|
||||
case Permitted:
|
||||
return "Permitted"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
432
service/profile/endpoints/endpoints_test.go
Normal file
432
service/profile/endpoints/endpoints_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pmtesting.TestMain(m, intel.Module)
|
||||
}
|
||||
|
||||
func testEndpointMatch(t *testing.T, ep Endpoint, entity *intel.Entity, expectedResult EPResult) {
|
||||
t.Helper()
|
||||
|
||||
result, _ := ep.Matches(context.TODO(), entity)
|
||||
if result != expectedResult {
|
||||
t.Errorf(
|
||||
"line %d: unexpected result for endpoint %s and entity %+v: result=%s, expected=%s",
|
||||
getLineNumberOfCaller(1),
|
||||
ep,
|
||||
entity,
|
||||
result,
|
||||
expectedResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testFormat(t *testing.T, endpoint string, shouldSucceed bool) {
|
||||
t.Helper()
|
||||
|
||||
_, err := parseEndpoint(endpoint)
|
||||
if shouldSucceed {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testFormat(t, "+ .", false)
|
||||
testFormat(t, "+ .at", true)
|
||||
testFormat(t, "+ .at.", true)
|
||||
testFormat(t, "+ 1.at", true)
|
||||
testFormat(t, "+ 1.at.", true)
|
||||
testFormat(t, "+ 1.f.ix.de.", true)
|
||||
testFormat(t, "+ *contains*", true)
|
||||
testFormat(t, "+ *has.suffix", true)
|
||||
testFormat(t, "+ *.has.suffix", true)
|
||||
testFormat(t, "+ *has.prefix*", true)
|
||||
testFormat(t, "+ *has.prefix.*", true)
|
||||
testFormat(t, "+ .sub.and.prefix.*", false)
|
||||
testFormat(t, "+ *.sub..and.prefix.*", false)
|
||||
}
|
||||
|
||||
func TestEndpointMatching(t *testing.T) { //nolint:maintidx // TODO
|
||||
t.Parallel()
|
||||
|
||||
// ANY
|
||||
|
||||
ep, err := parseEndpoint("+ *")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
|
||||
// DOMAIN
|
||||
|
||||
// wildcard domains
|
||||
ep, err = parseEndpoint("+ *example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
|
||||
ep, err = parseEndpoint("+ *.example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
ep, err = parseEndpoint("+ .example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
ep, err = parseEndpoint("+ example.*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
ep, err = parseEndpoint("+ *.exampl*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
|
||||
ep, err = parseEndpoint("+ *.com.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.org.",
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
// protocol
|
||||
|
||||
ep, err = parseEndpoint("+ example.com UDP")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 17,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
// ports
|
||||
|
||||
ep, err = parseEndpoint("+ example.com 17/442-444")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entity := (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 17,
|
||||
Port: 441,
|
||||
}).Init(0)
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
entity.Port = 442
|
||||
entity.Init(0)
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity.Port = 443
|
||||
entity.Init(0)
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity.Port = 444
|
||||
entity.Init(0)
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity.Port = 445
|
||||
entity.Init(0)
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
// IP
|
||||
|
||||
ep, err = parseEndpoint("+ 10.2.3.4")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 17,
|
||||
Port: 443,
|
||||
}).Init(0), Permitted)
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "",
|
||||
IP: net.ParseIP("10.2.3.3"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.5"),
|
||||
Protocol: 17,
|
||||
Port: 443,
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
// IP Range
|
||||
|
||||
ep, err = parseEndpoint("+ 10.2.3.0/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("10.2.2.4"),
|
||||
}).Init(0), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
}).Init(0), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("10.2.4.4"),
|
||||
}).Init(0), NoMatch)
|
||||
|
||||
// Skip test that need the geoip database in CI.
|
||||
if !testing.Short() {
|
||||
|
||||
// ASN
|
||||
|
||||
ep, err = parseEndpoint("+ AS15169")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entity = (&intel.Entity{IP: net.IPv4(8, 8, 8, 8)}).Init(0)
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity = (&intel.Entity{IP: net.IPv4(1, 1, 1, 1)}).Init(0)
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
// Country
|
||||
|
||||
ep, err = parseEndpoint("+ AT")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entity = (&intel.Entity{IP: net.IPv4(194, 232, 104, 1)}).Init(0) // orf.at
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity = (&intel.Entity{IP: net.IPv4(151, 101, 1, 164)}).Init(0) // nytimes.com
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
}
|
||||
|
||||
// Scope
|
||||
|
||||
ep, err = parseEndpoint("+ Localhost,LAN")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entity = (&intel.Entity{IP: net.IPv4(192, 168, 0, 1)}).Init(0)
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity = (&intel.Entity{IP: net.IPv4(151, 101, 1, 164)}).Init(0) // nytimes.com
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
// Port with protocol wildcard
|
||||
|
||||
ep, err = parseEndpoint("+ * */443")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entity = &intel.Entity{
|
||||
Domain: "",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}
|
||||
entity.Init(0)
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
// Lists
|
||||
|
||||
// Skip test that need the filter lists in CI.
|
||||
if !testing.Short() {
|
||||
_, err = parseEndpoint("+ L:A,B,C")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: write test for lists matcher
|
||||
}
|
||||
|
||||
func getLineNumberOfCaller(levels int) int {
|
||||
_, _, line, _ := runtime.Caller(levels + 1) //nolint:dogsled
|
||||
return line
|
||||
}
|
||||
34
service/profile/endpoints/reason.go
Normal file
34
service/profile/endpoints/reason.go
Normal 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 := "denied by rule: "
|
||||
if r.Permitted {
|
||||
prefix = "allowed by rule: "
|
||||
}
|
||||
|
||||
return prefix + r.description + " " + r.Filter[2:]
|
||||
}
|
||||
|
||||
func (r *reason) Context() interface{} {
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user