mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Revamp endpoint matching system
This commit is contained in:
parent
5a2e0b84ff
commit
543a70422a
11 changed files with 1126 additions and 0 deletions
25
profile/endpoints/endpoint-any.go
Normal file
25
profile/endpoints/endpoint-any.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package endpoints
|
||||
|
||||
import "github.com/safing/portmaster/intel"
|
||||
|
||||
// EndpointAny matches anything.
|
||||
type EndpointAny struct {
|
||||
EndpointBase
|
||||
}
|
||||
|
||||
// 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) String() string {
|
||||
return ep.renderPPP("*")
|
||||
}
|
||||
|
||||
func parseTypeAny(fields []string) (Endpoint, error) {
|
||||
if fields[1] == "*" {
|
||||
ep := &EndpointAny{}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
58
profile/endpoints/endpoint-asn.go
Normal file
58
profile/endpoints/endpoint-asn.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
var (
|
||||
asnRegex = regexp.MustCompile("^(AS)?[0-9]+$")
|
||||
)
|
||||
|
||||
// EndpointASN matches ASNs.
|
||||
type EndpointASN struct {
|
||||
EndpointBase
|
||||
|
||||
ASN uint
|
||||
Reason string
|
||||
}
|
||||
|
||||
// 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, ""
|
||||
}
|
||||
|
||||
asn, ok := entity.GetASN()
|
||||
if !ok {
|
||||
return Undeterminable, ""
|
||||
}
|
||||
if asn == ep.ASN {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
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]) {
|
||||
asn, err := strconv.ParseUint(fields[1][2:], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse AS number %s", fields[1])
|
||||
}
|
||||
|
||||
ep := &EndpointASN{
|
||||
ASN: uint(asn),
|
||||
Reason: "IP is part of AS" + strconv.FormatInt(int64(asn), 10),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
50
profile/endpoints/endpoint-country.go
Normal file
50
profile/endpoints/endpoint-country.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
var (
|
||||
countryRegex = regexp.MustCompile(`^[A-Z]{2}$`)
|
||||
)
|
||||
|
||||
// EndpointCountry matches countries.
|
||||
type EndpointCountry struct {
|
||||
EndpointBase
|
||||
|
||||
Country string
|
||||
}
|
||||
|
||||
// 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, ""
|
||||
}
|
||||
|
||||
country, ok := entity.GetCountry()
|
||||
if !ok {
|
||||
return Undeterminable, ""
|
||||
}
|
||||
if country == ep.Country {
|
||||
return ep.matchesPPP(entity), "IP is located in " + country
|
||||
}
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
func (ep *EndpointCountry) String() string {
|
||||
return ep.renderPPP(ep.Country)
|
||||
}
|
||||
|
||||
func parseTypeCountry(fields []string) (Endpoint, error) {
|
||||
if countryRegex.MatchString(fields[1]) {
|
||||
ep := &EndpointCountry{
|
||||
Country: strings.ToUpper(fields[1]),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
123
profile/endpoints/endpoint-domain.go
Normal file
123
profile/endpoints/endpoint-domain.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
const (
|
||||
domainMatchTypeExact uint8 = iota
|
||||
domainMatchTypeZone
|
||||
domainMatchTypeSuffix
|
||||
domainMatchTypePrefix
|
||||
domainMatchTypeContains
|
||||
)
|
||||
|
||||
var (
|
||||
domainRegex = regexp.MustCompile(`^\*?(([a-z0-9][a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z]{2,}\.?$`)
|
||||
altDomainRegex = regexp.MustCompile(`^\*?[a-z0-9\.-]+\*$`)
|
||||
)
|
||||
|
||||
// EndpointDomain matches domains.
|
||||
type EndpointDomain struct {
|
||||
EndpointBase
|
||||
|
||||
OriginalValue string
|
||||
Domain string
|
||||
DomainZone string
|
||||
MatchType uint8
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason string) {
|
||||
if entity.Domain == "" {
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
switch ep.MatchType {
|
||||
case domainMatchTypeExact:
|
||||
if entity.Domain == ep.Domain {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
case domainMatchTypeZone:
|
||||
if entity.Domain == ep.Domain {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
if strings.HasSuffix(entity.Domain, ep.DomainZone) {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
case domainMatchTypeSuffix:
|
||||
if strings.HasSuffix(entity.Domain, ep.Domain) {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
case domainMatchTypePrefix:
|
||||
if strings.HasPrefix(entity.Domain, ep.Domain) {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
case domainMatchTypeContains:
|
||||
if strings.Contains(entity.Domain, ep.Domain) {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
func (ep *EndpointDomain) String() string {
|
||||
return ep.renderPPP(ep.OriginalValue)
|
||||
}
|
||||
|
||||
func parseTypeDomain(fields []string) (Endpoint, error) {
|
||||
domain := fields[1]
|
||||
|
||||
if domainRegex.MatchString(domain) || altDomainRegex.MatchString(domain) {
|
||||
ep := &EndpointDomain{
|
||||
OriginalValue: domain,
|
||||
Reason: "domain matches " + domain,
|
||||
}
|
||||
|
||||
// fix domain ending
|
||||
switch domain[len(domain)-1] {
|
||||
case '.':
|
||||
case '*':
|
||||
default:
|
||||
domain += "."
|
||||
}
|
||||
|
||||
// fix domain case
|
||||
domain = strings.ToLower(domain)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "*") && strings.HasSuffix(domain, "*"):
|
||||
ep.MatchType = domainMatchTypeContains
|
||||
ep.Domain = strings.Trim(domain, "*")
|
||||
return ep.parsePPP(ep, fields)
|
||||
|
||||
case strings.HasSuffix(domain, "*"):
|
||||
ep.MatchType = domainMatchTypePrefix
|
||||
ep.Domain = strings.Trim(domain, "*")
|
||||
return ep.parsePPP(ep, fields)
|
||||
|
||||
case strings.HasPrefix(domain, "*"):
|
||||
ep.MatchType = domainMatchTypeSuffix
|
||||
ep.Domain = strings.Trim(domain, "*")
|
||||
return ep.parsePPP(ep, fields)
|
||||
|
||||
case strings.HasPrefix(domain, "."):
|
||||
ep.MatchType = domainMatchTypeZone
|
||||
ep.Domain = strings.TrimLeft(domain, ".")
|
||||
ep.DomainZone = "." + ep.Domain
|
||||
return ep.parsePPP(ep, fields)
|
||||
|
||||
default:
|
||||
ep.MatchType = domainMatchTypeExact
|
||||
ep.Domain = domain
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
42
profile/endpoints/endpoint-ip.go
Normal file
42
profile/endpoints/endpoint-ip.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
// EndpointIP matches IPs.
|
||||
type EndpointIP struct {
|
||||
EndpointBase
|
||||
|
||||
IP net.IP
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointIP) Matches(entity *intel.Entity) (result EPResult, reason string) {
|
||||
if entity.IP == nil {
|
||||
return Undeterminable, ""
|
||||
}
|
||||
if ep.IP.Equal(entity.IP) {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
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,
|
||||
Reason: "IP is " + ip.String(),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
42
profile/endpoints/endpoint-iprange.go
Normal file
42
profile/endpoints/endpoint-iprange.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
// EndpointIPRange matches IP ranges.
|
||||
type EndpointIPRange struct {
|
||||
EndpointBase
|
||||
|
||||
Net *net.IPNet
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Matches checks whether the given entity matches this endpoint definition.
|
||||
func (ep *EndpointIPRange) Matches(entity *intel.Entity) (result EPResult, reason string) {
|
||||
if entity.IP == nil {
|
||||
return Undeterminable, ""
|
||||
}
|
||||
if ep.Net.Contains(entity.IP) {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
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,
|
||||
Reason: "IP is part of " + net.String(),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
46
profile/endpoints/endpoint-lists.go
Normal file
46
profile/endpoints/endpoint-lists.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
// EndpointLists matches endpoint lists.
|
||||
type EndpointLists struct {
|
||||
EndpointBase
|
||||
|
||||
ListSet *intel.ListSet
|
||||
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) {
|
||||
lists, ok := entity.GetLists()
|
||||
if !ok {
|
||||
return Undeterminable, ""
|
||||
}
|
||||
matched := ep.ListSet.MatchSet(lists)
|
||||
if len(matched) > 0 {
|
||||
return ep.matchesPPP(entity), ep.Reason
|
||||
}
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
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: intel.NewListSet(lists),
|
||||
Lists: "L:" + strings.Join(lists, ","),
|
||||
Reason: "matched lists " + strings.Join(lists, ","),
|
||||
}
|
||||
return ep.parsePPP(ep, fields)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
211
profile/endpoints/endpoint.go
Normal file
211
profile/endpoints/endpoint.go
Normal file
|
@ -0,0 +1,211 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/network/reference"
|
||||
)
|
||||
|
||||
// Endpoint describes an Endpoint Matcher
|
||||
type Endpoint interface {
|
||||
Matches(entity *intel.Entity) (result EPResult, reason string)
|
||||
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) matchesPPP(entity *intel.Entity) (result EPResult) {
|
||||
// only check if protocol is defined
|
||||
if ep.Protocol > 0 {
|
||||
// if protocol is unknown, return Undeterminable
|
||||
if entity.Protocol == 0 {
|
||||
return Undeterminable
|
||||
}
|
||||
// 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 is unknown, return Undeterminable
|
||||
if entity.Port == 0 {
|
||||
return Undeterminable
|
||||
}
|
||||
// if port does not match, return NoMatch
|
||||
if entity.Port < ep.StartPort || entity.Port > 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")
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// domain
|
||||
if endpoint, err = parseTypeDomain(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// country
|
||||
if endpoint, err = parseTypeCountry(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// asn
|
||||
if endpoint, err = parseTypeASN(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
// lists
|
||||
if endpoint, err = parseTypeList(fields); endpoint != nil || err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(`unknown endpoint definition: "%s"`, value)
|
||||
}
|
83
profile/endpoints/endpoint_test.go
Normal file
83
profile/endpoints/endpoint_test.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEndpointParsing(t *testing.T) {
|
||||
// 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")
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func testParsing(t *testing.T, value string) {
|
||||
ep, err := parseEndpoint(value)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
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) {
|
||||
testParsing(t, value)
|
||||
|
||||
epGeneric, err := parseTypeDomain(strings.Fields(value))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
ep := epGeneric.(*EndpointDomain)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
93
profile/endpoints/endpoints.go
Normal file
93
profile/endpoints/endpoints.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/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
|
||||
Undeterminable
|
||||
Denied
|
||||
Permitted
|
||||
)
|
||||
|
||||
// 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: %s", errCnt, firstErr)
|
||||
}
|
||||
return endpoints, firstErr
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// 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(entity *intel.Entity) (result EPResult, reason string) {
|
||||
for _, entry := range e {
|
||||
if entry != nil {
|
||||
if result, reason = entry.Matches(entity); result != NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
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 Undeterminable:
|
||||
return "Undeterminable"
|
||||
case Denied:
|
||||
return "Denied"
|
||||
case Permitted:
|
||||
return "Permitted"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
353
profile/endpoints/endpoints_test.go
Normal file
353
profile/endpoints/endpoints_test.go
Normal file
|
@ -0,0 +1,353 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/core/pmtesting"
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pmtesting.TestMain(m)
|
||||
}
|
||||
|
||||
func testEndpointMatch(t *testing.T, ep Endpoint, entity *intel.Entity, expectedResult EPResult) {
|
||||
result, _ := ep.Matches(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 TestEndpointMatching(t *testing.T) {
|
||||
// ANY
|
||||
|
||||
ep, err := parseEndpoint("+ *")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
|
||||
// DOMAIN
|
||||
|
||||
// wildcard domains
|
||||
ep, err = parseEndpoint("+ *example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
|
||||
ep, err = parseEndpoint("+ *.example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
|
||||
ep, err = parseEndpoint("+ .example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc-example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
|
||||
ep, err = parseEndpoint("+ example.*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
|
||||
ep, err = parseEndpoint("+ *.exampl*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "abc.example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
|
||||
ep, err = parseEndpoint("+ *.com.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.org.",
|
||||
}).Init(), 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(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 17,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Undeterminable)
|
||||
|
||||
// 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()
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
entity.Port = 442
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity.Port = 443
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity.Port = 444
|
||||
testEndpointMatch(t, ep, entity, Permitted)
|
||||
|
||||
entity.Port = 445
|
||||
testEndpointMatch(t, ep, entity, NoMatch)
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Undeterminable)
|
||||
|
||||
// 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(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
Protocol: 17,
|
||||
Port: 443,
|
||||
}).Init(), Permitted)
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "",
|
||||
IP: net.ParseIP("10.2.3.3"),
|
||||
Protocol: 6,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
IP: net.ParseIP("10.2.3.5"),
|
||||
Protocol: 17,
|
||||
Port: 443,
|
||||
}).Init(), NoMatch)
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
Domain: "example.com.",
|
||||
}).Init(), Undeterminable)
|
||||
|
||||
// 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(), NoMatch)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("10.2.3.4"),
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("10.2.4.4"),
|
||||
}).Init(), NoMatch)
|
||||
|
||||
// ASN
|
||||
|
||||
ep, err = parseEndpoint("+ AS13335")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("1.1.1.1"),
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("8.8.8.8"),
|
||||
}).Init(), NoMatch)
|
||||
|
||||
// Country
|
||||
|
||||
ep, err = parseEndpoint("+ AT")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("194.232.104.1"), // orf.at
|
||||
}).Init(), Permitted)
|
||||
testEndpointMatch(t, ep, (&intel.Entity{
|
||||
IP: net.ParseIP("151.101.1.164"), // nytimes.com
|
||||
}).Init(), NoMatch)
|
||||
|
||||
// Lists
|
||||
// TODO: write test for lists matcher
|
||||
|
||||
}
|
||||
|
||||
func getLineNumberOfCaller(levels int) int {
|
||||
_, _, line, _ := runtime.Caller(levels + 1) //nolint:dogsled
|
||||
return line
|
||||
}
|
Loading…
Add table
Reference in a new issue