From 046dd9b5adbb3e1f40271b5cb48c9f6dc1fd7a1d Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 18 Mar 2019 16:29:00 +0100 Subject: [PATCH 1/4] Fix immediate profile application, update endpoint domain syntax --- network/communication.go | 10 +++++-- process/database.go | 5 +++- process/matching.go | 4 ++- profile/active.go | 63 ++++++++++++++++++++++++++------------- profile/endpoints.go | 7 +++++ profile/endpoints_test.go | 30 +++++++++++++++++-- profile/set.go | 4 ++- profile/set_test.go | 2 +- profile/updates.go | 24 +++++++++------ 9 files changed, 110 insertions(+), 39 deletions(-) diff --git a/network/communication.go b/network/communication.go index 685e36cf..85bfca1a 100644 --- a/network/communication.go +++ b/network/communication.go @@ -131,9 +131,13 @@ func (comm *Communication) NeedsReevaluation() bool { comm.Lock() defer comm.Unlock() - updateVersion := profile.GetUpdateVersion() - if comm.profileUpdateVersion != updateVersion { - comm.profileUpdateVersion = updateVersion + oldVersion := comm.profileUpdateVersion + comm.profileUpdateVersion = profile.GetUpdateVersion() + + if oldVersion == 0 { + return false + } + if oldVersion != comm.profileUpdateVersion { return true } return false diff --git a/process/database.go b/process/database.go index 6c4a2e43..4f43785f 100644 --- a/process/database.go +++ b/process/database.go @@ -70,16 +70,19 @@ func (p *Process) Delete() { p.Lock() defer p.Unlock() + // delete from internal storage processesLock.Lock() delete(processes, p.Pid) processesLock.Unlock() + // propagate delete p.Meta().Delete() if dbControllerFlag.IsSet() { go dbController.PushUpdate(p) } - // TODO: this should not be necessary, as processes should always have a profileSet. + // deactivate profile + // TODO: check if there is another process using the same profile set if p.profileSet != nil { profile.DeactivateProfileSet(p.profileSet) } diff --git a/process/matching.go b/process/matching.go index bce476a3..44b62541 100644 --- a/process/matching.go +++ b/process/matching.go @@ -1,6 +1,8 @@ package process import ( + "fmt" + "github.com/Safing/portbase/database" "github.com/Safing/portbase/database/query" "github.com/Safing/portbase/log" @@ -64,7 +66,7 @@ func (p *Process) FindProfiles() error { // FIXME: implement! p.UserProfileKey = userProfile.Key() - p.profileSet = profile.NewSet(userProfile, nil) + p.profileSet = profile.NewSet(fmt.Sprintf("%d-%s", p.Pid, p.Path), userProfile, nil) go p.Save() return nil diff --git a/profile/active.go b/profile/active.go index fdf1f057..e6b05c8a 100644 --- a/profile/active.go +++ b/profile/active.go @@ -1,6 +1,10 @@ package profile -import "sync" +import ( + "sync" + + "github.com/Safing/portbase/log" +) var ( activeProfileSets = make(map[string]*Set) @@ -8,47 +12,64 @@ var ( ) func activateProfileSet(set *Set) { - set.Lock() - defer set.Unlock() activeProfileSetsLock.Lock() defer activeProfileSetsLock.Unlock() - activeProfileSets[set.profiles[0].ID] = set + set.Lock() + defer set.Unlock() + activeProfileSets[set.id] = set + log.Tracef("profile: activated profile set %s", set.id) } // DeactivateProfileSet marks a profile set as not active. func DeactivateProfileSet(set *Set) { - set.Lock() - defer set.Unlock() activeProfileSetsLock.Lock() defer activeProfileSetsLock.Unlock() - delete(activeProfileSets, set.profiles[0].ID) + set.Lock() + defer set.Unlock() + delete(activeProfileSets, set.id) + log.Tracef("profile: deactivated profile set %s", set.id) } -func updateActiveUserProfile(profile *Profile) { - activeProfileSetsLock.RLock() - defer activeProfileSetsLock.RUnlock() - activeSet, ok := activeProfileSets[profile.ID] - if ok { - activeSet.Lock() - defer activeSet.Unlock() - activeSet.profiles[0] = profile - } -} - -func updateActiveStampProfile(profile *Profile) { +func updateActiveProfile(profile *Profile, userProfile bool) { activeProfileSetsLock.RLock() defer activeProfileSetsLock.RUnlock() + var activeProfile *Profile + var profilesUpdated bool + + // iterate all active profile sets for _, activeSet := range activeProfileSets { activeSet.Lock() - activeProfile := activeSet.profiles[2] + + if userProfile { + activeProfile = activeSet.profiles[0] + } else { + activeProfile = activeSet.profiles[2] + } + + // check if profile exists (for stamp profiles) if activeProfile != nil { activeProfile.Lock() + + // check if the stamp profile has the same ID if activeProfile.ID == profile.ID { - activeSet.profiles[2] = profile + if userProfile { + activeSet.profiles[0] = profile + log.Infof("profile: updated active user profile %s (%s)", profile.ID, profile.LinkedPath) + } else { + activeSet.profiles[2] = profile + log.Infof("profile: updated active stamp profile %s", profile.ID) + } + profilesUpdated = true } + activeProfile.Unlock() } + activeSet.Unlock() } + + if profilesUpdated { + increaseUpdateVersion() + } } diff --git a/profile/endpoints.go b/profile/endpoints.go index 25431361..ad0e9dbc 100644 --- a/profile/endpoints.go +++ b/profile/endpoints.go @@ -114,9 +114,16 @@ func (e Endpoints) CheckIP(domain string, ip net.IP, protocol uint8, port uint16 } 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) diff --git a/profile/endpoints_test.go b/profile/endpoints_test.go index ff67d961..56e1c472 100644 --- a/profile/endpoints_test.go +++ b/profile/endpoints_test.go @@ -59,13 +59,39 @@ func TestEndpointMatching(t *testing.T) { ep.Value = "*example.com." testEndpointDomainMatch(t, ep, "example.com.", Permitted) testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted) + testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + testEndpointDomainMatch(t, ep, "abc-example.com.", Permitted) + testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + + ep.Value = "*.example.com." + testEndpointDomainMatch(t, ep, "example.com.", NoMatch) + testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) + testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted) + testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + testEndpointDomainMatch(t, ep, "abc-example.com.", NoMatch) + testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) + + ep.Value = ".example.com." + testEndpointDomainMatch(t, ep, "example.com.", Permitted) + testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted) + testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + testEndpointDomainMatch(t, ep, "abc-example.com.", NoMatch) + testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) - ep.Type = EptDomain ep.Value = "example.*" testEndpointDomainMatch(t, ep, "example.com.", Permitted) testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + testEndpointDomainMatch(t, ep, "abc.example.com.", NoMatch) + testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) + + ep.Value = ".example.*" + testEndpointDomainMatch(t, ep, "example.com.", NoMatch) + testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) + testEndpointDomainMatch(t, ep, "abc.example.com.", NoMatch) + testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) - ep.Type = EptDomain ep.Value = "*.exampl*" testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted) testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) diff --git a/profile/set.go b/profile/set.go index 04d63a85..a1a5120c 100644 --- a/profile/set.go +++ b/profile/set.go @@ -15,6 +15,7 @@ var ( type Set struct { sync.Mutex + id string profiles [4]*Profile // Application // Global @@ -26,8 +27,9 @@ type Set struct { } // NewSet returns a new profile set with given the profiles. -func NewSet(user, stamp *Profile) *Set { +func NewSet(id string, user, stamp *Profile) *Set { new := &Set{ + id: id, profiles: [4]*Profile{ user, // Application nil, // Global diff --git a/profile/set_test.go b/profile/set_test.go index 984b1f3a..a5a1df4a 100644 --- a/profile/set_test.go +++ b/profile/set_test.go @@ -140,7 +140,7 @@ func testEndpointIP(t *testing.T, set *Set, domain string, ip net.IP, protocol u func TestProfileSet(t *testing.T) { - set := NewSet(testUserProfile, testStampProfile) + set := NewSet("[pid]-/path/to/bin", testUserProfile, testStampProfile) set.Update(status.SecurityLevelDynamic) testFlag(t, set, Whitelist, false) diff --git a/profile/updates.go b/profile/updates.go index b8bc603f..cd923c10 100644 --- a/profile/updates.go +++ b/profile/updates.go @@ -10,7 +10,7 @@ import ( ) func initUpdateListener() error { - sub, err := profileDB.Subscribe(query.New(MakeProfileKey(SpecialNamespace, ""))) + sub, err := profileDB.Subscribe(query.New("core:profiles/")) if err != nil { return err } @@ -36,34 +36,40 @@ func updateListener(sub *database.Subscription) { continue } + log.Infof("profile: updated %s", profile.ID) + switch profile.DatabaseKey() { case "profiles/special/global": + specialProfileLock.Lock() globalProfile = profile specialProfileLock.Unlock() + case "profiles/special/fallback": + profile.Lock() - if ensureServiceEndpointsDenyAll(profile) { - profile.Unlock() + profileChanged := ensureServiceEndpointsDenyAll(profile) + profile.Unlock() + + if profileChanged { profile.Save(SpecialNamespace) continue } - profile.Unlock() specialProfileLock.Lock() fallbackProfile = profile specialProfileLock.Unlock() + default: + switch { case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")): - updateActiveUserProfile(profile) - increaseUpdateVersion() + updateActiveProfile(profile, true /* User Profile */) case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")): - updateActiveStampProfile(profile) - increaseUpdateVersion() + updateActiveProfile(profile, false /* Stamp Profile */) } - } + } } } } From 27881bf59a18ea03d832495f0d81f716950f7dc5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 18 Mar 2019 16:29:15 +0100 Subject: [PATCH 2/4] Fix prompt notifications --- firewall/firewall.go | 2 -- firewall/master.go | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/firewall/firewall.go b/firewall/firewall.go index 6468ee3f..1dfafa03 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -28,8 +28,6 @@ var ( packetsDropped *uint64 localNet4 *net.IPNet - // Yes, this would normally be 127.0.0.0/8 - // TODO: figure out any side effects localhost4 = net.IPv4(127, 0, 0, 1) localhost6 = net.IPv6loopback diff --git a/firewall/master.go b/firewall/master.go index 506e7ebb..117368c2 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -36,6 +36,7 @@ func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) // check if communication needs reevaluation if comm.NeedsReevaluation() { + log.Infof("firewall: re-evaluating verdict on %s", comm) comm.ResetVerdict() } @@ -89,6 +90,7 @@ func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) // DecideOnCommunicationAfterIntel makes a decision about a communication after the dns query is resolved and intel is gathered. func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, rrCache *intel.RRCache) { + // rrCache may be nil, when function is called for re-evaluation by DecideOnCommunication // check if need to run if comm.GetVerdict() != network.VerdictUndecided { @@ -353,9 +355,16 @@ func FilterDNSResponse(comm *network.Communication, fqdn string, rrCache *intel. // DecideOnCommunication makes a decision about a communication with its first packet. func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { - // check if communication needs reevaluation + // check if communication needs reevaluation, if it's not with a domain if comm.NeedsReevaluation() { + log.Infof("firewall: re-evaluating verdict on %s", comm) comm.ResetVerdict() + + // if communicating with a domain entity, re-evaluate with Before/AfterIntel + if strings.HasSuffix(comm.Domain, ".") { + DecideOnCommunicationBeforeIntel(comm, comm.Domain) + DecideOnCommunicationAfterIntel(comm, comm.Domain, nil) + } } // check if need to run From 5d0665bd57c4251b6626afb2b406b4dfd2778172 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 18 Mar 2019 16:31:06 +0100 Subject: [PATCH 3/4] Bump version to 0.2.4 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d02de592..90809691 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,7 @@ func init() { func main() { // Set Info - info.Set("Portmaster", "0.2.3", "AGPLv3", true) + info.Set("Portmaster", "0.2.4", "AGPLv3", true) // Start err := modules.Start() From af2f6c2ebe7a5566c4ad4243fc55c05926d9a437 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 1 Apr 2019 14:45:31 +0200 Subject: [PATCH 4/4] Add section about used ports to README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 929926b8..a05f632d 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,15 @@ Documentation _in progress_ can be found here: [docs.safing.io](http://docs.safi - debian/ubuntu: `sudo apt-get install libnetfilter-queue-dev` - fedora: `?` - arch: `?` + +## TCP/UDP Ports + +The Portmaster (with Gate17) uses the following ports: +- ` 17` Gate17 port for connecting to Gate17 nodes +- ` 53` DNS server (local only) +- `717` Gate17 entrypoint as the local endpoint for tunneled connections (local only) +- `817` Portmaster API for integration with UI elements and other helpers (local only) + +Learn more about [why we chose these ports](https://docs.safing.io/docs/portmaster/os-integration.html). + +Gate17 nodes additionally uses other common ports like `80` and `443` to provide access in restricted network environments.