From bde81d815d4aa531ec75c0427e9ee17bb5bc8cc9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 17 Jan 2019 10:55:06 +0100 Subject: [PATCH] Revamp Profile Domains and Ports to Endpoints and ServiceEndpoints --- intel/main_test.go | 38 ++++++++ intel/reverse.go | 72 +++++++++++++++ intel/reverse_test.go | 28 ++++++ profile/defaults.go | 54 ++--------- profile/endpoints.go | 121 +++++++++++++++++-------- profile/endpoints_test.go | 79 ++++++++++------- profile/flags.go | 3 +- profile/flags_test.go | 2 +- profile/profile.go | 10 +-- profile/set.go | 43 +++------ profile/set_test.go | 177 ++++++++++++++++++------------------- profile/specialprofiles.go | 25 ++++++ profile/updates.go | 14 ++- 13 files changed, 417 insertions(+), 249 deletions(-) create mode 100644 intel/main_test.go create mode 100644 intel/reverse.go create mode 100644 intel/reverse_test.go diff --git a/intel/main_test.go b/intel/main_test.go new file mode 100644 index 00000000..bcb8e407 --- /dev/null +++ b/intel/main_test.go @@ -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) +} diff --git a/intel/reverse.go b/intel/reverse.go new file mode 100644 index 00000000..21f811bc --- /dev/null +++ b/intel/reverse.go @@ -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") +} diff --git a/intel/reverse_test.go b/intel/reverse_test.go new file mode 100644 index 00000000..e53d1a6f --- /dev/null +++ b/intel/reverse_test.go @@ -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)") +} diff --git a/profile/defaults.go b/profile/defaults.go index 89afe4b0..e4551fee 100644 --- a/profile/defaults.go +++ b/profile/defaults.go @@ -1,8 +1,6 @@ package profile import ( - "time" - "github.com/Safing/portmaster/status" ) @@ -32,50 +30,14 @@ func makeDefaultFallbackProfile() *Profile { Related: status.SecurityLevelDynamic, PeerToPeer: status.SecurityLevelDynamic, }, - Ports: map[int16][]*Port{ - 6: []*Port{ - &Port{ // SSH - Permit: true, - Created: time.Now().Unix(), - Start: 22, - End: 22, - }, - &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, - }, + ServiceEndpoints: []*EndpointPermission{ + &EndpointPermission{ + DomainOrIP: "", + Wildcard: true, + Protocol: 0, + StartPort: 0, + EndPort: 0, + Permit: false, }, }, } diff --git a/profile/endpoints.go b/profile/endpoints.go index 0eee1a23..9e86eaf1 100644 --- a/profile/endpoints.go +++ b/profile/endpoints.go @@ -3,6 +3,9 @@ package profile import ( "fmt" "strconv" + "strings" + + "github.com/Safing/portmaster/intel" ) // Endpoints is a list of permitted or denied endpoints. @@ -10,13 +13,13 @@ type Endpoints []*EndpointPermission // EndpointPermission holds a decision about an endpoint. type EndpointPermission struct { - DomainOrIP string - IncludeSubdomains bool - Protocol uint8 - PortStart uint16 - PortEnd uint16 - Permit bool - Created int64 + DomainOrIP string + Wildcard bool + Protocol uint8 + StartPort uint16 + EndPort uint16 + Permit bool + Created int64 } // 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. -func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16) (permit, ok bool) { - // check for exact domain - ed, ok := d[domain] - if ok { - return ed.Permit, true - } +// If getDomainOfIP (returns reverse and forward dns matching domain name) is supplied, an IP will be resolved to a domain, if necessary. +func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (permit bool, reason string, ok bool) { - for _, entry := range e { - if entry.Matches(domainOrIP, protocol, port) { - return entry.Permit, true + // 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(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 (ep EndpointPermission) Matches(domainOrIP string, protocol uint8, port uint16) bool { - if domainOrIP != ep.DomainOrIP { - return false - } +func isSubdomainOf(domain, subdomain string) bool { + dotPrefixedDomain := "." + domain + return strings.HasSuffix(subdomain, dotPrefixedDomain) +} +// 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 { - return false + return false, "" } - if ep.PortStart > 0 && (port < ep.PortStart || port > ep.PortEnd) { - return false + if ep.StartPort > 0 && (port < ep.StartPort || port > ep.EndPort) { + 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 { s := ep.DomainOrIP - if ep.Protocol > 0 || ep.Start { - s += " " - } + s += " " if ep.Protocol > 0 { s += strconv.Itoa(int(ep.Protocol)) - if ep.Start > 0 { - s += "/" - } + } else { + s += "*" } - if ep.Start > 0 { - if p.Start == p.End { - s += strconv.Itoa(int(ep.Start)) + s += "/" + + if ep.StartPort > 0 { + if ep.StartPort == ep.EndPort { + s += strconv.Itoa(int(ep.StartPort)) } else { - s += fmt.Sprintf("%d-%d", ep.Start, ep.End) + s += fmt.Sprintf("%d-%d", ep.StartPort, ep.EndPort) } + } else { + s += "*" } return s diff --git a/profile/endpoints_test.go b/profile/endpoints_test.go index d40d3b7f..a87ab0da 100644 --- a/profile/endpoints_test.go +++ b/profile/endpoints_test.go @@ -2,47 +2,60 @@ package profile import ( "testing" - "time" ) -func TestPorts(t *testing.T) { - var ports Ports - ports = map[int16][]*Port{ - 6: []*Port{ - &Port{ // SSH - Permit: true, - Created: time.Now().Unix(), - Start: 22, - End: 22, - }, +// TODO: RETIRED +// func testdeMatcher(t *testing.T, value string, expectedResult bool) { +// if domainEndingMatcher.MatchString(value) != expectedResult { +// if expectedResult { +// t.Errorf("domainEndingMatcher should match %s", value) +// } else { +// t.Errorf("domainEndingMatcher should not match %s", value) +// } +// } +// } +// +// 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{ - &Port{ // HTTP - Permit: false, - Created: time.Now().Unix(), - Start: 80, - End: 81, - }, + &EndpointPermission{ + DomainOrIP: "8.8.8.8", + Protocol: 17, // TCP + StartPort: 53, // DNS + EndPort: 53, + Permit: false, }, - 93: []*Port{ - &Port{ // HTTP - Permit: true, - Created: time.Now().Unix(), - Start: 93, - End: 93, - }, + &EndpointPermission{ + DomainOrIP: "google.com", + Wildcard: true, + Permit: false, }, } - if ports.String() != "TCP:[permit:22],