mirror of
https://github.com/safing/portmaster
synced 2025-09-04 19:49:15 +00:00
Revamp Profile Domains and Ports to Endpoints and ServiceEndpoints
This commit is contained in:
parent
4017de7dac
commit
bde81d815d
13 changed files with 417 additions and 249 deletions
38
intel/main_test.go
Normal file
38
intel/main_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package intel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/database/dbmodule"
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
"github.com/Safing/portbase/modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// setup
|
||||||
|
testDir := os.TempDir()
|
||||||
|
dbmodule.SetDatabaseLocation(testDir)
|
||||||
|
err := modules.Start()
|
||||||
|
if err != nil {
|
||||||
|
if err == modules.ErrCleanExit {
|
||||||
|
os.Exit(0)
|
||||||
|
} else {
|
||||||
|
err = modules.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
log.Shutdown()
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run tests
|
||||||
|
rv := m.Run()
|
||||||
|
|
||||||
|
// teardown
|
||||||
|
modules.Shutdown()
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
// exit with test run return value
|
||||||
|
os.Exit(rv)
|
||||||
|
}
|
72
intel/reverse.go
Normal file
72
intel/reverse.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package intel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveIPAndValidate finds (reverse DNS), validates (forward DNS) and returns the domain name assigned to the given IP.
|
||||||
|
func ResolveIPAndValidate(ip string, securityLevel uint8) (domain string, err error) {
|
||||||
|
// get reversed DNS address
|
||||||
|
rQ, err := dns.ReverseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("intel: failed to get reverse address of %s: %s", ip, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get PTR record
|
||||||
|
rrCache := Resolve(rQ, dns.Type(dns.TypePTR), securityLevel)
|
||||||
|
if rrCache == nil {
|
||||||
|
return "", errors.New("querying for PTR record failed (may be NXDomain)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get result from record
|
||||||
|
var ptrName string
|
||||||
|
for _, rr := range rrCache.Answer {
|
||||||
|
ptrRec, ok := rr.(*dns.PTR)
|
||||||
|
if ok {
|
||||||
|
ptrName = ptrRec.Ptr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for nxDomain
|
||||||
|
if ptrName == "" {
|
||||||
|
return "", errors.New("no PTR record for IP (nxDomain)")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("ptrName: %s", ptrName)
|
||||||
|
|
||||||
|
// get forward record
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
rrCache = Resolve(ptrName, dns.Type(dns.TypeAAAA), securityLevel)
|
||||||
|
} else {
|
||||||
|
rrCache = Resolve(ptrName, dns.Type(dns.TypeA), securityLevel)
|
||||||
|
}
|
||||||
|
if rrCache == nil {
|
||||||
|
return "", errors.New("querying for A/AAAA record failed (may be NXDomain)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for matching A/AAAA record
|
||||||
|
log.Infof("rr: %s", rrCache)
|
||||||
|
for _, rr := range rrCache.Answer {
|
||||||
|
switch v := rr.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
log.Infof("A: %s", v.A.String())
|
||||||
|
if ip == v.A.String() {
|
||||||
|
return ptrName, nil
|
||||||
|
}
|
||||||
|
case *dns.AAAA:
|
||||||
|
log.Infof("AAAA: %s", v.AAAA.String())
|
||||||
|
if ip == v.AAAA.String() {
|
||||||
|
return ptrName, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no match
|
||||||
|
return "", errors.New("validation failed")
|
||||||
|
}
|
28
intel/reverse_test.go
Normal file
28
intel/reverse_test.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package intel
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func testReverse(t *testing.T, ip, result, expectedErr string) {
|
||||||
|
domain, err := ResolveIPAndValidate(ip, 0)
|
||||||
|
if err != nil {
|
||||||
|
if expectedErr == "" || err.Error() != expectedErr {
|
||||||
|
t.Errorf("reverse-validating %s: unexpected error: %s", ip, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if domain != result {
|
||||||
|
t.Errorf("reverse-validating %s: unexpected result: %s", ip, domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveIPAndValidate(t *testing.T) {
|
||||||
|
testReverse(t, "198.41.0.4", "a.root-servers.net.", "")
|
||||||
|
testReverse(t, "9.9.9.9", "dns.quad9.net.", "")
|
||||||
|
testReverse(t, "2620:fe::fe", "dns.quad9.net.", "")
|
||||||
|
testReverse(t, "1.1.1.1", "one.one.one.one.", "")
|
||||||
|
testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "")
|
||||||
|
|
||||||
|
testReverse(t, "93.184.216.34", "example.com.", "no PTR record for IP (nxDomain)")
|
||||||
|
testReverse(t, "185.199.109.153", "sites.github.io.", "no PTR record for IP (nxDomain)")
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Safing/portmaster/status"
|
"github.com/Safing/portmaster/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,50 +30,14 @@ func makeDefaultFallbackProfile() *Profile {
|
||||||
Related: status.SecurityLevelDynamic,
|
Related: status.SecurityLevelDynamic,
|
||||||
PeerToPeer: status.SecurityLevelDynamic,
|
PeerToPeer: status.SecurityLevelDynamic,
|
||||||
},
|
},
|
||||||
Ports: map[int16][]*Port{
|
ServiceEndpoints: []*EndpointPermission{
|
||||||
6: []*Port{
|
&EndpointPermission{
|
||||||
&Port{ // SSH
|
DomainOrIP: "",
|
||||||
Permit: true,
|
Wildcard: true,
|
||||||
Created: time.Now().Unix(),
|
Protocol: 0,
|
||||||
Start: 22,
|
StartPort: 0,
|
||||||
End: 22,
|
EndPort: 0,
|
||||||
},
|
Permit: false,
|
||||||
&Port{ // HTTP
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Start: 80,
|
|
||||||
End: 80,
|
|
||||||
},
|
|
||||||
&Port{ // HTTPS
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Start: 443,
|
|
||||||
End: 443,
|
|
||||||
},
|
|
||||||
&Port{ // SMTP (TLS)
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Start: 465,
|
|
||||||
End: 465,
|
|
||||||
},
|
|
||||||
&Port{ // SMTP (STARTTLS)
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Start: 587,
|
|
||||||
End: 587,
|
|
||||||
},
|
|
||||||
&Port{ // IMAP (TLS)
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Start: 993,
|
|
||||||
End: 993,
|
|
||||||
},
|
|
||||||
&Port{ // IMAP (STARTTLS)
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Start: 143,
|
|
||||||
End: 143,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ package profile
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Safing/portmaster/intel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Endpoints is a list of permitted or denied endpoints.
|
// Endpoints is a list of permitted or denied endpoints.
|
||||||
|
@ -10,13 +13,13 @@ type Endpoints []*EndpointPermission
|
||||||
|
|
||||||
// EndpointPermission holds a decision about an endpoint.
|
// EndpointPermission holds a decision about an endpoint.
|
||||||
type EndpointPermission struct {
|
type EndpointPermission struct {
|
||||||
DomainOrIP string
|
DomainOrIP string
|
||||||
IncludeSubdomains bool
|
Wildcard bool
|
||||||
Protocol uint8
|
Protocol uint8
|
||||||
PortStart uint16
|
StartPort uint16
|
||||||
PortEnd uint16
|
EndPort uint16
|
||||||
Permit bool
|
Permit bool
|
||||||
Created int64
|
Created int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSet returns whether the Endpoints object is "set".
|
// IsSet returns whether the Endpoints object is "set".
|
||||||
|
@ -28,59 +31,105 @@ func (e Endpoints) IsSet() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check checks if the given domain is governed in the list of domains and returns whether it is permitted.
|
// Check checks if the given domain is governed in the list of domains and returns whether it is permitted.
|
||||||
func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16) (permit, ok bool) {
|
// If getDomainOfIP (returns reverse and forward dns matching domain name) is supplied, an IP will be resolved to a domain, if necessary.
|
||||||
// check for exact domain
|
func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (permit bool, reason string, ok bool) {
|
||||||
ed, ok := d[domain]
|
|
||||||
if ok {
|
|
||||||
return ed.Permit, true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range e {
|
// ip resolving
|
||||||
if entry.Matches(domainOrIP, protocol, port) {
|
var cachedGetDomainOfIP func() string
|
||||||
return entry.Permit, true
|
if checkReverseIP {
|
||||||
|
var ipResolved bool
|
||||||
|
var ipName string
|
||||||
|
// setup caching wrapper
|
||||||
|
cachedGetDomainOfIP = func() string {
|
||||||
|
if !ipResolved {
|
||||||
|
result, err := intel.ResolveIPAndValidate(domainOrIP, securityLevel)
|
||||||
|
if err != nil {
|
||||||
|
// log.Debug()
|
||||||
|
ipName = result
|
||||||
|
}
|
||||||
|
ipResolved = true
|
||||||
|
}
|
||||||
|
return ipName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, false
|
isDomain := strings.HasSuffix(domainOrIP, ".")
|
||||||
|
|
||||||
|
for _, entry := range e {
|
||||||
|
if ok, reason := entry.Matches(domainOrIP, protocol, port, isDomain, cachedGetDomainOfIP); ok {
|
||||||
|
return entry.Permit, reason, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches checks whether a port object matches the given port.
|
func isSubdomainOf(domain, subdomain string) bool {
|
||||||
func (ep EndpointPermission) Matches(domainOrIP string, protocol uint8, port uint16) bool {
|
dotPrefixedDomain := "." + domain
|
||||||
if domainOrIP != ep.DomainOrIP {
|
return strings.HasSuffix(subdomain, dotPrefixedDomain)
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Matches checks whether the given endpoint has a managed permission. If getDomainOfIP (returns reverse and forward dns matching domain name) is supplied, this declares an incoming connection.
|
||||||
|
func (ep EndpointPermission) Matches(domainOrIP string, protocol uint8, port uint16, isDomain bool, getDomainOfIP func() string) (match bool, reason string) {
|
||||||
if ep.Protocol > 0 && protocol != ep.Protocol {
|
if ep.Protocol > 0 && protocol != ep.Protocol {
|
||||||
return false
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if ep.PortStart > 0 && (port < ep.PortStart || port > ep.PortEnd) {
|
if ep.StartPort > 0 && (port < ep.StartPort || port > ep.EndPort) {
|
||||||
return false
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
switch {
|
||||||
|
case ep.Wildcard && len(ep.DomainOrIP) == 0:
|
||||||
|
// host wildcard
|
||||||
|
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep)
|
||||||
|
case domainOrIP == ep.DomainOrIP:
|
||||||
|
// host match
|
||||||
|
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep)
|
||||||
|
case isDomain && ep.Wildcard && isSubdomainOf(ep.DomainOrIP, domainOrIP):
|
||||||
|
// subdomain match
|
||||||
|
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep)
|
||||||
|
case !isDomain && getDomainOfIP != nil && getDomainOfIP() == ep.DomainOrIP:
|
||||||
|
// resolved IP match
|
||||||
|
return true, fmt.Sprintf("%s->%s matches %s", domainOrIP, getDomainOfIP(), ep)
|
||||||
|
case !isDomain && getDomainOfIP != nil && ep.Wildcard && isSubdomainOf(ep.DomainOrIP, getDomainOfIP()):
|
||||||
|
// resolved IP subdomain match
|
||||||
|
return true, fmt.Sprintf("%s->%s matches %s", domainOrIP, getDomainOfIP(), ep)
|
||||||
|
default:
|
||||||
|
// no match
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Endpoints) String() string {
|
||||||
|
var s []string
|
||||||
|
for _, entry := range e {
|
||||||
|
s = append(s, entry.String())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep EndpointPermission) String() string {
|
func (ep EndpointPermission) String() string {
|
||||||
s := ep.DomainOrIP
|
s := ep.DomainOrIP
|
||||||
|
|
||||||
if ep.Protocol > 0 || ep.Start {
|
s += " "
|
||||||
s += " "
|
|
||||||
}
|
|
||||||
|
|
||||||
if ep.Protocol > 0 {
|
if ep.Protocol > 0 {
|
||||||
s += strconv.Itoa(int(ep.Protocol))
|
s += strconv.Itoa(int(ep.Protocol))
|
||||||
if ep.Start > 0 {
|
} else {
|
||||||
s += "/"
|
s += "*"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ep.Start > 0 {
|
s += "/"
|
||||||
if p.Start == p.End {
|
|
||||||
s += strconv.Itoa(int(ep.Start))
|
if ep.StartPort > 0 {
|
||||||
|
if ep.StartPort == ep.EndPort {
|
||||||
|
s += strconv.Itoa(int(ep.StartPort))
|
||||||
} else {
|
} else {
|
||||||
s += fmt.Sprintf("%d-%d", ep.Start, ep.End)
|
s += fmt.Sprintf("%d-%d", ep.StartPort, ep.EndPort)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
s += "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|
|
@ -2,47 +2,60 @@ package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPorts(t *testing.T) {
|
// TODO: RETIRED
|
||||||
var ports Ports
|
// func testdeMatcher(t *testing.T, value string, expectedResult bool) {
|
||||||
ports = map[int16][]*Port{
|
// if domainEndingMatcher.MatchString(value) != expectedResult {
|
||||||
6: []*Port{
|
// if expectedResult {
|
||||||
&Port{ // SSH
|
// t.Errorf("domainEndingMatcher should match %s", value)
|
||||||
Permit: true,
|
// } else {
|
||||||
Created: time.Now().Unix(),
|
// t.Errorf("domainEndingMatcher should not match %s", value)
|
||||||
Start: 22,
|
// }
|
||||||
End: 22,
|
// }
|
||||||
},
|
// }
|
||||||
|
//
|
||||||
|
// func TestdomainEndingMatcher(t *testing.T) {
|
||||||
|
// testdeMatcher(t, "example.com", true)
|
||||||
|
// testdeMatcher(t, "com", true)
|
||||||
|
// testdeMatcher(t, "example.xn--lgbbat1ad8j", true)
|
||||||
|
// testdeMatcher(t, "xn--lgbbat1ad8j", true)
|
||||||
|
// testdeMatcher(t, "fe80::beef", false)
|
||||||
|
// testdeMatcher(t, "fe80::dead:beef", false)
|
||||||
|
// testdeMatcher(t, "10.2.3.4", false)
|
||||||
|
// testdeMatcher(t, "4", false)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func TestEPString(t *testing.T) {
|
||||||
|
var endpoints Endpoints
|
||||||
|
endpoints = []*EndpointPermission{
|
||||||
|
&EndpointPermission{
|
||||||
|
DomainOrIP: "example.com",
|
||||||
|
Wildcard: false,
|
||||||
|
Protocol: 6,
|
||||||
|
Permit: true,
|
||||||
},
|
},
|
||||||
-17: []*Port{
|
&EndpointPermission{
|
||||||
&Port{ // HTTP
|
DomainOrIP: "8.8.8.8",
|
||||||
Permit: false,
|
Protocol: 17, // TCP
|
||||||
Created: time.Now().Unix(),
|
StartPort: 53, // DNS
|
||||||
Start: 80,
|
EndPort: 53,
|
||||||
End: 81,
|
Permit: false,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
93: []*Port{
|
&EndpointPermission{
|
||||||
&Port{ // HTTP
|
DomainOrIP: "google.com",
|
||||||
Permit: true,
|
Wildcard: true,
|
||||||
Created: time.Now().Unix(),
|
Permit: false,
|
||||||
Start: 93,
|
|
||||||
End: 93,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if ports.String() != "TCP:[permit:22], <UDP:[deny:80-81], 93:[permit:93]" &&
|
if endpoints.String() != "[example.com 6/*, 8.8.8.8 17/53, google.com */*]" {
|
||||||
ports.String() != "93:[permit:93], TCP:[permit:22], <UDP:[deny:80-81]" &&
|
t.Errorf("unexpected result: %s", endpoints.String())
|
||||||
ports.String() != "<UDP:[deny:80-81], 93:[permit:93], TCP:[permit:22]" {
|
|
||||||
t.Errorf("unexpected result: %s", ports.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var noPorts Ports
|
var noEndpoints Endpoints
|
||||||
noPorts = map[int16][]*Port{}
|
noEndpoints = []*EndpointPermission{}
|
||||||
if noPorts.String() != "None" {
|
if noEndpoints.String() != "[]" {
|
||||||
t.Errorf("unexpected result: %s", ports.String())
|
t.Errorf("unexpected result: %s", noEndpoints.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Safing/portmaster/status"
|
"github.com/Safing/portmaster/status"
|
||||||
|
@ -115,7 +116,7 @@ func (flags Flags) String() string {
|
||||||
markedFlags = append(markedFlags, s)
|
markedFlags = append(markedFlags, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(markedFlags, ", ")
|
return fmt.Sprintf("[%s]", strings.Join(markedFlags, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adds a flag to the Flags with the given level.
|
// Add adds a flag to the Flags with the given level.
|
||||||
|
|
|
@ -31,7 +31,7 @@ func TestProfileFlags(t *testing.T) {
|
||||||
RequireGate17: status.SecurityLevelsSecureAndFortress,
|
RequireGate17: status.SecurityLevelsSecureAndFortress,
|
||||||
}
|
}
|
||||||
|
|
||||||
if testFlags.String() != "Prompt, Internet++-, LAN++-, Localhost, Related+--, RequireGate17-++" {
|
if testFlags.String() != "[Prompt, Internet++-, LAN++-, Localhost, Related+--, RequireGate17-++]" {
|
||||||
t.Errorf("unexpected output: %s", testFlags.String())
|
t.Errorf("unexpected output: %s", testFlags.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,10 +37,10 @@ type Profile struct {
|
||||||
Fingerprints []*Fingerprint
|
Fingerprints []*Fingerprint
|
||||||
|
|
||||||
// The mininum security level to apply to connections made with this profile
|
// The mininum security level to apply to connections made with this profile
|
||||||
SecurityLevel uint8
|
SecurityLevel uint8
|
||||||
Flags Flags
|
Flags Flags
|
||||||
Domains Domains
|
Endpoints Endpoints
|
||||||
Ports Ports
|
ServiceEndpoints Endpoints
|
||||||
|
|
||||||
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
|
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
|
||||||
// Framework *Framework `json:",omitempty bson:",omitempty"`
|
// Framework *Framework `json:",omitempty bson:",omitempty"`
|
||||||
|
@ -97,7 +97,7 @@ func (profile *Profile) String() string {
|
||||||
|
|
||||||
// DetailedString returns a more detailed string representation of theProfile.
|
// DetailedString returns a more detailed string representation of theProfile.
|
||||||
func (profile *Profile) DetailedString() string {
|
func (profile *Profile) DetailedString() string {
|
||||||
return fmt.Sprintf("%s(SL=%s Flags=[%s] Ports=[%s] #Domains=%d)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Ports.String(), len(profile.Domains))
|
return fmt.Sprintf("%s(SL=%s Flags=%s Endpoints=%s)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Endpoints.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserProfile loads a profile from the database.
|
// GetUserProfile loads a profile from the database.
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
emptyFlags = Flags{}
|
emptyFlags = Flags{}
|
||||||
emptyPorts = Ports{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set handles Profile chaining.
|
// Set handles Profile chaining.
|
||||||
|
@ -120,8 +119,8 @@ func (set *Set) CheckFlag(flag uint8) (active bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckDomain checks if the given domain is governed in any the lists of domains and returns whether it is permitted.
|
// CheckEndpoint checks if the given protocol and port are governed in any the lists of ports and returns whether it is permitted.
|
||||||
func (set *Set) CheckDomain(domain string) (permit, ok bool) {
|
func (set *Set) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, inbound bool) (permit bool, reason string, ok bool) {
|
||||||
set.Lock()
|
set.Lock()
|
||||||
defer set.Unlock()
|
defer set.Unlock()
|
||||||
|
|
||||||
|
@ -131,39 +130,19 @@ func (set *Set) CheckDomain(domain string) (permit, ok bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
permit, ok = profile.Domains.Check(domain)
|
if inbound {
|
||||||
if ok {
|
if permit, reason, ok = profile.ServiceEndpoints.Check(domainOrIP, protocol, port, inbound, set.combinedSecurityLevel); ok {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if permit, reason, ok = profile.Endpoints.Check(domainOrIP, protocol, port, inbound, set.combinedSecurityLevel); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, false
|
return false, "", false
|
||||||
}
|
|
||||||
|
|
||||||
// CheckPort checks if the given protocol and port are governed in any the lists of ports and returns whether it is permitted.
|
|
||||||
func (set *Set) CheckPort(listen bool, protocol uint8, port uint16) (permit, ok bool) {
|
|
||||||
set.Lock()
|
|
||||||
defer set.Unlock()
|
|
||||||
|
|
||||||
signedProtocol := int16(protocol)
|
|
||||||
if listen {
|
|
||||||
signedProtocol = -1 * signedProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, profile := range set.profiles {
|
|
||||||
if i == 2 && set.independent {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile != nil {
|
|
||||||
if permit, ok = profile.Ports.Check(signedProtocol, port); ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSecurityLevel returns the highest prioritized security level.
|
// getSecurityLevel returns the highest prioritized security level.
|
||||||
|
|
|
@ -26,26 +26,33 @@ func init() {
|
||||||
Flags: map[uint8]uint8{
|
Flags: map[uint8]uint8{
|
||||||
Independent: status.SecurityLevelFortress,
|
Independent: status.SecurityLevelFortress,
|
||||||
},
|
},
|
||||||
Domains: map[string]*DomainDecision{
|
Endpoints: []*EndpointPermission{
|
||||||
"example.com": &DomainDecision{
|
&EndpointPermission{
|
||||||
Permit: true,
|
DomainOrIP: "good.bad.example.com.",
|
||||||
Created: time.Now().Unix(),
|
Wildcard: false,
|
||||||
IncludeSubdomains: false,
|
Permit: true,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
"bad.example.com": &DomainDecision{
|
&EndpointPermission{
|
||||||
Permit: false,
|
DomainOrIP: "bad.example.com.",
|
||||||
Created: time.Now().Unix(),
|
Wildcard: true,
|
||||||
IncludeSubdomains: true,
|
Permit: false,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
},
|
&EndpointPermission{
|
||||||
Ports: map[int16][]*Port{
|
DomainOrIP: "example.com.",
|
||||||
6: []*Port{
|
Wildcard: false,
|
||||||
&Port{
|
Permit: true,
|
||||||
Permit: true,
|
Created: time.Now().Unix(),
|
||||||
Created: time.Now().Unix(),
|
},
|
||||||
Start: 22000,
|
&EndpointPermission{
|
||||||
End: 22000,
|
DomainOrIP: "",
|
||||||
},
|
Wildcard: true,
|
||||||
|
Permit: true,
|
||||||
|
Protocol: 6,
|
||||||
|
StartPort: 22000,
|
||||||
|
EndPort: 22000,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -54,37 +61,41 @@ func init() {
|
||||||
ID: "unit-test-stamp",
|
ID: "unit-test-stamp",
|
||||||
Name: "Unit Test Stamp Profile",
|
Name: "Unit Test Stamp Profile",
|
||||||
SecurityLevel: status.SecurityLevelFortress,
|
SecurityLevel: status.SecurityLevelFortress,
|
||||||
Flags: map[uint8]uint8{
|
// Flags: map[uint8]uint8{
|
||||||
Internet: status.SecurityLevelsAll,
|
// Internet: status.SecurityLevelsAll,
|
||||||
},
|
// },
|
||||||
Domains: map[string]*DomainDecision{
|
Endpoints: []*EndpointPermission{
|
||||||
"bad2.example.com": &DomainDecision{
|
&EndpointPermission{
|
||||||
Permit: false,
|
DomainOrIP: "bad2.example.com.",
|
||||||
Created: time.Now().Unix(),
|
Wildcard: true,
|
||||||
IncludeSubdomains: true,
|
Permit: false,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
"good.bad.example.com": &DomainDecision{
|
&EndpointPermission{
|
||||||
Permit: true,
|
DomainOrIP: "",
|
||||||
Created: time.Now().Unix(),
|
Wildcard: true,
|
||||||
IncludeSubdomains: false,
|
Permit: true,
|
||||||
|
Protocol: 6,
|
||||||
|
StartPort: 80,
|
||||||
|
EndPort: 80,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Ports: map[int16][]*Port{
|
ServiceEndpoints: []*EndpointPermission{
|
||||||
6: []*Port{
|
&EndpointPermission{
|
||||||
&Port{
|
DomainOrIP: "",
|
||||||
Permit: false,
|
Wildcard: true,
|
||||||
Created: time.Now().Unix(),
|
Permit: true,
|
||||||
Start: 80,
|
Protocol: 17,
|
||||||
End: 80,
|
StartPort: 12345,
|
||||||
},
|
EndPort: 12347,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
-17: []*Port{
|
&EndpointPermission{ // default deny
|
||||||
&Port{
|
DomainOrIP: "",
|
||||||
Permit: true,
|
Wildcard: true,
|
||||||
Created: time.Now().Unix(),
|
Permit: false,
|
||||||
Start: 12345,
|
Created: time.Now().Unix(),
|
||||||
End: 12347,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -97,37 +108,21 @@ func testFlag(t *testing.T, set *Set, flag uint8, shouldBeActive bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDomain(t *testing.T, set *Set, domain string, shouldBePermitted bool) {
|
func testEndpoint(t *testing.T, set *Set, domainOrIP string, protocol uint8, port uint16, inbound bool, shouldBePermitted bool) {
|
||||||
permitted, ok := set.CheckDomain(domain)
|
var permitted, ok bool
|
||||||
|
permitted, _, ok = set.CheckEndpoint(domainOrIP, protocol, port, inbound)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("domain %s should be in test profile set", domain)
|
t.Errorf("endpoint %s/%d/%d/%v should be in test profile set", domainOrIP, protocol, port, inbound)
|
||||||
}
|
}
|
||||||
if permitted != shouldBePermitted {
|
if permitted != shouldBePermitted {
|
||||||
t.Errorf("unexpected result: domain %s: permitted=%v, expected=%v", domain, permitted, shouldBePermitted)
|
t.Errorf("unexpected result for endpoint %s/%d/%d/%v: permitted=%v, expected=%v", domainOrIP, protocol, port, inbound, permitted, shouldBePermitted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUnregulatedDomain(t *testing.T, set *Set, domain string) {
|
func testUnregulatedEndpoint(t *testing.T, set *Set, domainOrIP string, protocol uint8, port uint16, inbound bool) {
|
||||||
_, ok := set.CheckDomain(domain)
|
_, _, ok := set.CheckEndpoint(domainOrIP, protocol, port, inbound)
|
||||||
if ok {
|
if ok {
|
||||||
t.Errorf("domain %s should not be in test profile set", domain)
|
t.Errorf("endpoint %s/%d/%d/%v should not be in test profile set", domainOrIP, protocol, port, inbound)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPort(t *testing.T, set *Set, listen bool, protocol uint8, port uint16, shouldBePermitted bool) {
|
|
||||||
permitted, ok := set.CheckPort(listen, protocol, port)
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("port [%v %d %d] should be in test profile set", listen, protocol, port)
|
|
||||||
}
|
|
||||||
if permitted != shouldBePermitted {
|
|
||||||
t.Errorf("unexpected result: port [%v %d %d]: permitted=%v, expected=%v", listen, protocol, port, permitted, shouldBePermitted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUnregulatedPort(t *testing.T, set *Set, listen bool, protocol uint8, port uint16) {
|
|
||||||
_, ok := set.CheckPort(listen, protocol, port)
|
|
||||||
if ok {
|
|
||||||
t.Errorf("port [%v %d %d] should not be in test profile set", listen, protocol, port)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,31 +132,29 @@ func TestProfileSet(t *testing.T) {
|
||||||
|
|
||||||
set.Update(status.SecurityLevelDynamic)
|
set.Update(status.SecurityLevelDynamic)
|
||||||
testFlag(t, set, Whitelist, false)
|
testFlag(t, set, Whitelist, false)
|
||||||
testFlag(t, set, Internet, true)
|
// testFlag(t, set, Internet, true)
|
||||||
testDomain(t, set, "example.com", true)
|
testEndpoint(t, set, "example.com.", 0, 0, false, true)
|
||||||
testDomain(t, set, "bad.example.com", false)
|
testEndpoint(t, set, "bad.example.com.", 0, 0, false, false)
|
||||||
testDomain(t, set, "other.bad.example.com", false)
|
testEndpoint(t, set, "other.bad.example.com.", 0, 0, false, false)
|
||||||
testDomain(t, set, "good.bad.example.com", false)
|
testEndpoint(t, set, "good.bad.example.com.", 0, 0, false, true)
|
||||||
testDomain(t, set, "bad2.example.com", false)
|
testEndpoint(t, set, "bad2.example.com.", 0, 0, false, false)
|
||||||
testPort(t, set, false, 6, 443, true)
|
testEndpoint(t, set, "10.2.3.4", 6, 22000, false, true)
|
||||||
testPort(t, set, false, 6, 143, true)
|
testEndpoint(t, set, "fd00::1", 6, 22000, false, true)
|
||||||
testPort(t, set, false, 6, 22, true)
|
testEndpoint(t, set, "test.local.", 6, 22000, false, true)
|
||||||
testPort(t, set, false, 6, 80, false)
|
testUnregulatedEndpoint(t, set, "other.example.com.", 0, 0, false)
|
||||||
testPort(t, set, false, 6, 80, false)
|
testUnregulatedEndpoint(t, set, "10.2.3.4", 17, 53, false)
|
||||||
testPort(t, set, true, 17, 12345, true)
|
testUnregulatedEndpoint(t, set, "10.2.3.4", 17, 443, false)
|
||||||
testPort(t, set, true, 17, 12346, true)
|
testUnregulatedEndpoint(t, set, "10.2.3.4", 6, 12346, false)
|
||||||
testPort(t, set, true, 17, 12347, true)
|
testEndpoint(t, set, "10.2.3.4", 17, 12345, true, true)
|
||||||
testUnregulatedDomain(t, set, "other.example.com")
|
testEndpoint(t, set, "fd00::1", 17, 12347, true, true)
|
||||||
testUnregulatedPort(t, set, false, 17, 53)
|
|
||||||
testUnregulatedPort(t, set, false, 17, 443)
|
|
||||||
testUnregulatedPort(t, set, true, 6, 443)
|
|
||||||
|
|
||||||
set.Update(status.SecurityLevelSecure)
|
set.Update(status.SecurityLevelSecure)
|
||||||
testFlag(t, set, Internet, true)
|
// testFlag(t, set, Internet, true)
|
||||||
|
|
||||||
set.Update(status.SecurityLevelFortress) // Independent!
|
set.Update(status.SecurityLevelFortress) // Independent!
|
||||||
testFlag(t, set, Internet, false)
|
testFlag(t, set, Whitelist, true)
|
||||||
testPort(t, set, false, 6, 80, true)
|
testEndpoint(t, set, "10.2.3.4", 17, 12345, true, false)
|
||||||
testUnregulatedDomain(t, set, "bad2.example.com")
|
testEndpoint(t, set, "fd00::1", 17, 12347, true, false)
|
||||||
testUnregulatedPort(t, set, true, 17, 12346)
|
testUnregulatedEndpoint(t, set, "10.2.3.4", 6, 80, false)
|
||||||
|
testUnregulatedEndpoint(t, set, "bad2.example.com.", 0, 0, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,10 @@ func initSpecialProfiles() (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallbackProfile = makeDefaultFallbackProfile()
|
fallbackProfile = makeDefaultFallbackProfile()
|
||||||
|
ensureServiceEndpointsDenyAll(fallbackProfile)
|
||||||
fallbackProfile.Save(SpecialNamespace)
|
fallbackProfile.Save(SpecialNamespace)
|
||||||
}
|
}
|
||||||
|
ensureServiceEndpointsDenyAll(fallbackProfile)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -42,3 +44,26 @@ func initSpecialProfiles() (err error) {
|
||||||
func getSpecialProfile(ID string) (*Profile, error) {
|
func getSpecialProfile(ID string) (*Profile, error) {
|
||||||
return getProfile(SpecialNamespace, ID)
|
return getProfile(SpecialNamespace, ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
|
||||||
|
for _, ep := range p.ServiceEndpoints {
|
||||||
|
if ep.DomainOrIP == "" &&
|
||||||
|
ep.Wildcard == true &&
|
||||||
|
ep.Protocol == 0 &&
|
||||||
|
ep.StartPort == 0 &&
|
||||||
|
ep.EndPort == 0 &&
|
||||||
|
ep.Permit == false {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ServiceEndpoints = append(p.ServiceEndpoints, &EndpointPermission{
|
||||||
|
DomainOrIP: "",
|
||||||
|
Wildcard: true,
|
||||||
|
Protocol: 0,
|
||||||
|
StartPort: 0,
|
||||||
|
EndPort: 0,
|
||||||
|
Permit: false,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -35,12 +35,20 @@ func updateListener(sub *database.Subscription) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch profile.ID {
|
switch profile.DatabaseKey() {
|
||||||
case "global":
|
case "profiles/special/global":
|
||||||
specialProfileLock.Lock()
|
specialProfileLock.Lock()
|
||||||
globalProfile = profile
|
globalProfile = profile
|
||||||
specialProfileLock.Unlock()
|
specialProfileLock.Unlock()
|
||||||
case "fallback":
|
case "profiles/special/fallback":
|
||||||
|
profile.Lock()
|
||||||
|
if ensureServiceEndpointsDenyAll(profile) {
|
||||||
|
profile.Unlock()
|
||||||
|
profile.Save(SpecialNamespace)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
profile.Unlock()
|
||||||
|
|
||||||
specialProfileLock.Lock()
|
specialProfileLock.Lock()
|
||||||
fallbackProfile = profile
|
fallbackProfile = profile
|
||||||
specialProfileLock.Unlock()
|
specialProfileLock.Unlock()
|
||||||
|
|
Loading…
Add table
Reference in a new issue