mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
319 lines
7.2 KiB
Go
319 lines
7.2 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/safing/portmaster/intel"
|
|
)
|
|
|
|
// Endpoints is a list of permitted or denied endpoints.
|
|
type Endpoints []*EndpointPermission
|
|
|
|
// EndpointPermission holds a decision about an endpoint.
|
|
type EndpointPermission struct {
|
|
Value string
|
|
Type EPType
|
|
|
|
Protocol uint8
|
|
StartPort uint16
|
|
EndPort uint16
|
|
|
|
Permit bool
|
|
Created int64
|
|
}
|
|
|
|
// EPType represents the type of an EndpointPermission
|
|
type EPType uint8
|
|
|
|
// EPType values
|
|
const (
|
|
EptUnknown EPType = 0
|
|
EptAny EPType = 1
|
|
EptDomain EPType = 2
|
|
EptIPv4 EPType = 3
|
|
EptIPv6 EPType = 4
|
|
EptIPv4Range EPType = 5
|
|
EptIPv6Range EPType = 6
|
|
EptASN EPType = 7
|
|
EptCountry EPType = 8
|
|
)
|
|
|
|
// EPResult represents the result of a check against an EndpointPermission
|
|
type EPResult uint8
|
|
|
|
// EndpointPermission return values
|
|
const (
|
|
NoMatch EPResult = iota
|
|
Undeterminable
|
|
Denied
|
|
Permitted
|
|
)
|
|
|
|
// IsSet returns whether the Endpoints object is "set".
|
|
func (e Endpoints) IsSet() bool {
|
|
return len(e) > 0
|
|
}
|
|
|
|
// CheckDomain checks the if the given endpoint matches a EndpointPermission in the list.
|
|
func (e Endpoints) CheckDomain(domain string) (result EPResult, reason string) {
|
|
if domain == "" {
|
|
return Denied, "internal error"
|
|
}
|
|
|
|
for _, entry := range e {
|
|
if entry != nil {
|
|
if result, reason = entry.MatchesDomain(domain); result != NoMatch {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
return NoMatch, ""
|
|
}
|
|
|
|
// CheckIP checks the if the given endpoint matches a EndpointPermission in the list. If _checkReverseIP_ and no domain is given, the IP will be resolved to a domain, if necessary.
|
|
func (e Endpoints) CheckIP(domain string, ip net.IP, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (result EPResult, reason string) {
|
|
if ip == nil {
|
|
return Denied, "internal error"
|
|
}
|
|
|
|
// ip resolving
|
|
var cachedGetDomainOfIP func() string
|
|
if checkReverseIP {
|
|
var ipResolved bool
|
|
var ipName string
|
|
// setup caching wrapper
|
|
cachedGetDomainOfIP = func() string {
|
|
if !ipResolved {
|
|
result, err := intel.ResolveIPAndValidate(context.TODO(), ip.String(), securityLevel)
|
|
if err != nil {
|
|
// log.Debug()
|
|
ipName = result
|
|
}
|
|
ipResolved = true
|
|
}
|
|
return ipName
|
|
}
|
|
}
|
|
|
|
for _, entry := range e {
|
|
if entry != nil {
|
|
if result, reason := entry.MatchesIP(domain, ip, protocol, port, cachedGetDomainOfIP); result != NoMatch {
|
|
return result, reason
|
|
}
|
|
}
|
|
}
|
|
|
|
return NoMatch, ""
|
|
}
|
|
|
|
func (ep EndpointPermission) matchesDomainOnly(domain string) (matches bool, reason string) {
|
|
dotInFront := strings.HasPrefix(ep.Value, ".")
|
|
wildcardInFront := strings.HasPrefix(ep.Value, "*")
|
|
wildcardInBack := strings.HasSuffix(ep.Value, "*")
|
|
|
|
switch {
|
|
case dotInFront && !wildcardInFront && !wildcardInBack:
|
|
// subdomain or domain
|
|
if strings.HasSuffix(domain, ep.Value) || domain == strings.TrimPrefix(ep.Value, ".") {
|
|
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
|
}
|
|
case wildcardInFront && wildcardInBack:
|
|
if strings.Contains(domain, strings.Trim(ep.Value, "*")) {
|
|
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
|
}
|
|
case wildcardInFront:
|
|
if strings.HasSuffix(domain, strings.TrimLeft(ep.Value, "*")) {
|
|
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
|
}
|
|
case wildcardInBack:
|
|
if strings.HasPrefix(domain, strings.TrimRight(ep.Value, "*")) {
|
|
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
|
}
|
|
default:
|
|
if domain == ep.Value {
|
|
return true, ""
|
|
}
|
|
}
|
|
|
|
return false, ""
|
|
}
|
|
|
|
func (ep EndpointPermission) matchProtocolAndPortsAndReturn(protocol uint8, port uint16) (result EPResult) {
|
|
// only check if protocol is defined
|
|
if ep.Protocol > 0 {
|
|
// if protocol is unknown, return Undeterminable
|
|
if protocol == 0 {
|
|
return Undeterminable
|
|
}
|
|
// if protocol does not match, return NoMatch
|
|
if protocol != ep.Protocol {
|
|
return NoMatch
|
|
}
|
|
}
|
|
|
|
// only check if port is defined
|
|
if ep.StartPort > 0 {
|
|
// if port is unknown, return Undeterminable
|
|
if port == 0 {
|
|
return Undeterminable
|
|
}
|
|
// if port does not match, return NoMatch
|
|
if port < ep.StartPort || port > ep.EndPort {
|
|
return NoMatch
|
|
}
|
|
}
|
|
|
|
// protocol and port matched or were defined as any
|
|
if ep.Permit {
|
|
return Permitted
|
|
}
|
|
return Denied
|
|
}
|
|
|
|
// MatchesDomain checks if the given endpoint matches the EndpointPermission.
|
|
func (ep EndpointPermission) MatchesDomain(domain string) (result EPResult, reason string) {
|
|
switch ep.Type {
|
|
case EptAny:
|
|
// always matches
|
|
case EptDomain:
|
|
var matched bool
|
|
matched, reason = ep.matchesDomainOnly(domain)
|
|
if !matched {
|
|
return NoMatch, ""
|
|
}
|
|
case EptIPv4:
|
|
return Undeterminable, ""
|
|
case EptIPv6:
|
|
return Undeterminable, ""
|
|
case EptIPv4Range:
|
|
return Undeterminable, ""
|
|
case EptIPv6Range:
|
|
return Undeterminable, ""
|
|
case EptASN:
|
|
return Undeterminable, ""
|
|
case EptCountry:
|
|
return Undeterminable, ""
|
|
default:
|
|
return Denied, "encountered unknown enpoint permission type"
|
|
}
|
|
|
|
return ep.matchProtocolAndPortsAndReturn(0, 0), reason
|
|
}
|
|
|
|
// MatchesIP checks if the given endpoint matches the EndpointPermission. _getDomainOfIP_, if given, will be used to get the domain if not given.
|
|
func (ep EndpointPermission) MatchesIP(domain string, ip net.IP, protocol uint8, port uint16, getDomainOfIP func() string) (result EPResult, reason string) {
|
|
switch ep.Type {
|
|
case EptAny:
|
|
// always matches
|
|
case EptDomain:
|
|
if domain == "" {
|
|
if getDomainOfIP == nil {
|
|
return NoMatch, ""
|
|
}
|
|
domain = getDomainOfIP()
|
|
}
|
|
|
|
var matched bool
|
|
matched, reason = ep.matchesDomainOnly(domain)
|
|
if !matched {
|
|
return NoMatch, ""
|
|
}
|
|
case EptIPv4, EptIPv6:
|
|
if ep.Value != ip.String() {
|
|
return NoMatch, ""
|
|
}
|
|
case EptIPv4Range:
|
|
return Denied, "endpoint type IP Range not yet implemented"
|
|
case EptIPv6Range:
|
|
return Denied, "endpoint type IP Range not yet implemented"
|
|
case EptASN:
|
|
return Denied, "endpoint type ASN not yet implemented"
|
|
case EptCountry:
|
|
return Denied, "endpoint type country not yet implemented"
|
|
default:
|
|
return Denied, "encountered unknown enpoint permission type"
|
|
}
|
|
|
|
return ep.matchProtocolAndPortsAndReturn(protocol, port), reason
|
|
}
|
|
|
|
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 (ept EPType) String() string {
|
|
switch ept {
|
|
case EptAny:
|
|
return "Any"
|
|
case EptDomain:
|
|
return "Domain"
|
|
case EptIPv4:
|
|
return "IPv4"
|
|
case EptIPv6:
|
|
return "IPv6"
|
|
case EptIPv4Range:
|
|
return "IPv4-Range"
|
|
case EptIPv6Range:
|
|
return "IPv6-Range"
|
|
case EptASN:
|
|
return "ASN"
|
|
case EptCountry:
|
|
return "Country"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
func (ep EndpointPermission) String() string {
|
|
s := ep.Type.String()
|
|
|
|
if ep.Type != EptAny {
|
|
s += ":"
|
|
s += ep.Value
|
|
}
|
|
s += " "
|
|
|
|
if ep.Protocol > 0 {
|
|
s += strconv.Itoa(int(ep.Protocol))
|
|
} else {
|
|
s += "*"
|
|
}
|
|
|
|
s += "/"
|
|
|
|
if ep.StartPort > 0 {
|
|
if ep.StartPort == ep.EndPort {
|
|
s += strconv.Itoa(int(ep.StartPort))
|
|
} else {
|
|
s += fmt.Sprintf("%d-%d", ep.StartPort, ep.EndPort)
|
|
}
|
|
} else {
|
|
s += "*"
|
|
}
|
|
|
|
return 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"
|
|
}
|
|
}
|