diff --git a/global/databases.go b/core/databases.go similarity index 88% rename from global/databases.go rename to core/databases.go index 624370b6d..1fc81a93e 100644 --- a/global/databases.go +++ b/core/databases.go @@ -1,4 +1,4 @@ -package global +package core import ( "github.com/Safing/portbase/database" @@ -7,11 +7,10 @@ import ( // module dependencies _ "github.com/Safing/portbase/database/dbmodule" _ "github.com/Safing/portbase/database/storage/badger" - _ "github.com/Safing/portmaster/status" ) func init() { - modules.Register("global", nil, start, nil, "database", "status") + modules.Register("core", nil, start, nil, "database") } func start() error { diff --git a/firewall/config.go b/firewall/config.go index b900a6578..080ea7fdc 100644 --- a/firewall/config.go +++ b/firewall/config.go @@ -2,10 +2,13 @@ package firewall import ( "github.com/Safing/portbase/config" + "github.com/Safing/portmaster/status" ) var ( - permanentVerdicts config.BoolOption + permanentVerdicts config.BoolOption + filterDNSByScope status.SecurityLevelOption + filterDNSByProfile status.SecurityLevelOption ) func registerConfig() error { @@ -22,5 +25,35 @@ func registerConfig() error { } permanentVerdicts = config.Concurrent.GetAsBool("firewall/permanentVerdicts", true) + err = config.Register(&config.Option{ + Name: "Filter DNS Responses by Server Scope", + Key: "firewall/filterDNSByScope", + Description: "This option will filter out DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.", + ExpertiseLevel: config.ExpertiseLevelExpert, + OptType: config.OptTypeInt, + ExternalOptType: "security level", + DefaultValue: 7, + ValidationRegex: "^(7|6|4)$", + }) + if err != nil { + return err + } + filterDNSByScope = status.ConfigIsActiveConcurrent("firewall/filterDNSByScope") + + err = config.Register(&config.Option{ + Name: "Filter DNS Responses by Application Profile", + Key: "firewall/filterDNSByProfile", + Description: "This option will filter out DNS answers that an application would not be allowed to connect, based on its profile.", + ExpertiseLevel: config.ExpertiseLevelExpert, + OptType: config.OptTypeInt, + ExternalOptType: "security level", + DefaultValue: 7, + ValidationRegex: "^(7|6|4)$", + }) + if err != nil { + return err + } + filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile") + return nil } diff --git a/firewall/firewall.go b/firewall/firewall.go index 63d46c632..d6792adf0 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -13,6 +13,10 @@ import ( "github.com/Safing/portmaster/firewall/interception" "github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network/packet" + + // module dependencies + _ "github.com/Safing/portmaster/core" + _ "github.com/Safing/portmaster/profile" ) var ( @@ -37,7 +41,7 @@ var ( ) func init() { - modules.Register("firewall", prep, start, stop, "global", "network", "nameserver", "profile") + modules.Register("firewall", prep, start, stop, "core", "network", "nameserver", "profile") } func prep() (err error) { @@ -111,7 +115,7 @@ func handlePacket(pkt packet.Packet) { return } - // log.Debugf("firewall: pkt %s has ID %s", pkt, pkt.GetConnectionID()) + // log.Debugf("firewall: pkt %s has ID %s", pkt, pkt.GetLinkID()) // use this to time how long it takes process packet // timed := time.Now() @@ -146,43 +150,51 @@ func handlePacket(pkt packet.Packet) { func initialHandler(pkt packet.Packet, link *network.Link) { - // get Connection - connection, err := network.GetConnectionByFirstPacket(pkt) + // get Communication + comm, err := network.GetCommunicationByFirstPacket(pkt) if err != nil { - // get "unknown" connection + // get "unknown" comm link.Deny(fmt.Sprintf("could not get process: %s", err)) - connection, err = network.GetUnknownConnection(pkt) + comm, err = network.GetUnknownCommunication(pkt) if err != nil { // all failed - log.Errorf("firewall: could not get unknown connection (dropping %s): %s", pkt.String(), err) - link.UpdateVerdict(network.DROP) - verdict(pkt, network.DROP) + log.Errorf("firewall: could not get unknown comm (dropping %s): %s", pkt.String(), err) + link.UpdateVerdict(network.VerdictDrop) + verdict(pkt, network.VerdictDrop) link.StopFirewallHandler() return } } - // add new Link to Connection (and save both) - connection.AddLink(link) + // add new Link to Communication (and save both) + comm.AddLink(link) // reroute dns requests to nameserver - if connection.Process().Pid != os.Getpid() && pkt.IsOutbound() && pkt.GetTCPUDPHeader() != nil && !pkt.GetIPHeader().Dst.Equal(localhost) && pkt.GetTCPUDPHeader().DstPort == 53 { + if comm.Process().Pid != os.Getpid() && pkt.IsOutbound() && pkt.GetTCPUDPHeader() != nil && !pkt.GetIPHeader().Dst.Equal(localhost) && pkt.GetTCPUDPHeader().DstPort == 53 { link.RerouteToNameserver() verdict(pkt, link.GetVerdict()) link.StopFirewallHandler() return } + // check if communication needs reevaluation + if comm.NeedsReevaluation() { + comm.ResetVerdict() + } + // make a decision if not made already - if connection.GetVerdict() == network.UNDECIDED { - DecideOnConnection(connection, pkt) + switch comm.GetVerdict() { + case network.VerdictUndecided, network.VerdictUndeterminable: + DecideOnCommunication(comm, pkt) } - if connection.GetVerdict() == network.ACCEPT { - DecideOnLink(connection, link, pkt) - } else { - link.UpdateVerdict(connection.GetVerdict()) + + switch comm.GetVerdict() { + case network.VerdictUndecided, network.VerdictUndeterminable, network.VerdictAccept: + DecideOnLink(comm, link, pkt) + default: + link.UpdateVerdict(comm.GetVerdict()) } // log decision @@ -201,7 +213,7 @@ func initialHandler(pkt packet.Packet, link *network.Link) { // // tunnel link, don't inspect // link.Tunneled = true // link.StopFirewallHandler() - // permanentVerdict(pkt, network.ACCEPT) + // permanentVerdict(pkt, network.VerdictAccept) case link.Inspect: link.SetFirewallHandler(inspectThenVerdict) inspectThenVerdict(pkt, link) @@ -241,22 +253,22 @@ func inspectThenVerdict(pkt packet.Packet, link *network.Link) { func permanentVerdict(pkt packet.Packet, action network.Verdict) { switch action { - case network.ACCEPT: + case network.VerdictAccept: atomic.AddUint64(packetsAccepted, 1) pkt.PermanentAccept() return - case network.BLOCK: + case network.VerdictBlock: atomic.AddUint64(packetsBlocked, 1) pkt.PermanentBlock() return - case network.DROP: + case network.VerdictDrop: atomic.AddUint64(packetsDropped, 1) pkt.PermanentDrop() return - case network.RerouteToNameserver: + case network.VerdictRerouteToNameserver: pkt.RerouteToNameserver() return - case network.RerouteToTunnel: + case network.VerdictRerouteToTunnel: pkt.RerouteToTunnel() return } @@ -265,22 +277,22 @@ func permanentVerdict(pkt packet.Packet, action network.Verdict) { func verdict(pkt packet.Packet, action network.Verdict) { switch action { - case network.ACCEPT: + case network.VerdictAccept: atomic.AddUint64(packetsAccepted, 1) pkt.Accept() return - case network.BLOCK: + case network.VerdictBlock: atomic.AddUint64(packetsBlocked, 1) pkt.Block() return - case network.DROP: + case network.VerdictDrop: atomic.AddUint64(packetsDropped, 1) pkt.Drop() return - case network.RerouteToNameserver: + case network.VerdictRerouteToNameserver: pkt.RerouteToNameserver() return - case network.RerouteToTunnel: + case network.VerdictRerouteToTunnel: pkt.RerouteToTunnel() return } @@ -302,26 +314,26 @@ func verdict(pkt packet.Packet, action network.Verdict) { func logInitialVerdict(link *network.Link) { // switch link.GetVerdict() { - // case network.ACCEPT: + // case network.VerdictAccept: // log.Infof("firewall: accepting new link: %s", link.String()) - // case network.BLOCK: + // case network.VerdictBlock: // log.Infof("firewall: blocking new link: %s", link.String()) - // case network.DROP: + // case network.VerdictDrop: // log.Infof("firewall: dropping new link: %s", link.String()) - // case network.RerouteToNameserver: + // case network.VerdictRerouteToNameserver: // log.Infof("firewall: rerouting new link to nameserver: %s", link.String()) - // case network.RerouteToTunnel: + // case network.VerdictRerouteToTunnel: // log.Infof("firewall: rerouting new link to tunnel: %s", link.String()) // } } func logChangedVerdict(link *network.Link) { // switch link.GetVerdict() { - // case network.ACCEPT: + // case network.VerdictAccept: // log.Infof("firewall: change! - now accepting link: %s", link.String()) - // case network.BLOCK: + // case network.VerdictBlock: // log.Infof("firewall: change! - now blocking link: %s", link.String()) - // case network.DROP: + // case network.VerdictDrop: // log.Infof("firewall: change! - now dropping link: %s", link.String()) // } } diff --git a/firewall/inspection/inspection.go b/firewall/inspection/inspection.go index a00619255..8842f6250 100644 --- a/firewall/inspection/inspection.go +++ b/firewall/inspection/inspection.go @@ -54,7 +54,7 @@ func RunInspectors(pkt packet.Packet, link *network.Link) (network.Verdict, bool } continueInspection := false - verdict := network.UNDECIDED + verdict := network.VerdictUndecided for key, skip := range activeInspectors { @@ -69,28 +69,28 @@ func RunInspectors(pkt packet.Packet, link *network.Link) (network.Verdict, bool action := inspectors[key](pkt, link) switch action { case DO_NOTHING: - if verdict < network.ACCEPT { - verdict = network.ACCEPT + if verdict < network.VerdictAccept { + verdict = network.VerdictAccept } continueInspection = true case BLOCK_PACKET: - if verdict < network.BLOCK { - verdict = network.BLOCK + if verdict < network.VerdictBlock { + verdict = network.VerdictBlock } continueInspection = true case DROP_PACKET: - verdict = network.DROP + verdict = network.VerdictDrop continueInspection = true case BLOCK_LINK: - link.UpdateVerdict(network.BLOCK) + link.UpdateVerdict(network.VerdictBlock) activeInspectors[key] = true - if verdict < network.BLOCK { - verdict = network.BLOCK + if verdict < network.VerdictBlock { + verdict = network.VerdictBlock } case DROP_LINK: - link.UpdateVerdict(network.DROP) + link.UpdateVerdict(network.VerdictDrop) activeInspectors[key] = true - verdict = network.DROP + verdict = network.VerdictDrop case STOP_INSPECTING: activeInspectors[key] = true } diff --git a/firewall/master.go b/firewall/master.go index f44907251..74599b472 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -2,85 +2,89 @@ package firewall import ( "fmt" + "net" "os" "strings" "github.com/Safing/portbase/log" "github.com/Safing/portmaster/intel" "github.com/Safing/portmaster/network" + "github.com/Safing/portmaster/network/netutils" "github.com/Safing/portmaster/network/packet" "github.com/Safing/portmaster/profile" "github.com/Safing/portmaster/status" + "github.com/miekg/dns" "github.com/agext/levenshtein" ) // Call order: // -// 1. DecideOnConnectionBeforeIntel (if connecting to domain) +// 1. DecideOnCommunicationBeforeIntel (if connecting to domain) // is called when a DNS query is made, before the query is resolved -// 2. DecideOnConnectionAfterIntel (if connecting to domain) +// 2. DecideOnCommunicationAfterIntel (if connecting to domain) // is called when a DNS query is made, after the query is resolved -// 3. DecideOnConnection -// is called when the first packet of the first link of the connection arrives +// 3. DecideOnCommunication +// is called when the first packet of the first link of the communication arrives // 4. DecideOnLink -// is called when when the first packet of a link arrives only if connection has verdict UNDECIDED or CANTSAY +// is called when when the first packet of a link arrives only if communication has verdict UNDECIDED or CANTSAY -// DecideOnConnectionBeforeIntel makes a decision about a connection before the dns query is resolved and intel is gathered. -func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) { - // check: - // Profile.DomainWhitelist - // Profile.Flags - // - process specific: System, Admin, User - // - network specific: Internet, LocalNet +// DecideOnCommunicationBeforeIntel makes a decision about a communication before the dns query is resolved and intel is gathered. +func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) { // grant self - if connection.Process().Pid == os.Getpid() { - log.Infof("firewall: granting own connection %s", connection) - connection.Accept("") + if comm.Process().Pid == os.Getpid() { + log.Infof("firewall: granting own communication %s", comm) + comm.Accept("") return } - // check if there is a profile - profileSet := connection.Process().ProfileSet() + // get and check profile set + profileSet := comm.Process().ProfileSet() if profileSet == nil { - log.Errorf("firewall: denying connection %s, no Profile Set", connection) - connection.Deny("no Profile Set") + log.Errorf("firewall: denying communication %s, no Profile Set", comm) + comm.Deny("no Profile Set") return } profileSet.Update(status.ActiveSecurityLevel()) // check for any network access if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) { - log.Infof("firewall: denying connection %s, accessing Internet or LAN not allowed", connection) - connection.Deny("accessing Internet or LAN not allowed") + log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm) + comm.Deny("accessing Internet or LAN not permitted") return } - // check domain list - permitted, reason, ok := profileSet.CheckEndpoint(fqdn, 0, 0, false) - if ok { - if permitted { - log.Infof("firewall: accepting connection %s, endpoint is whitelisted: %s", connection, reason) - connection.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) - } else { - log.Infof("firewall: denying connection %s, endpoint is blacklisted: %s", connection, reason) - connection.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) - } + // check endpoint list + result, reason := profileSet.CheckEndpointDomain(fqdn) + switch result { + case profile.NoMatch: + comm.UpdateVerdict(network.VerdictUndecided) + case profile.Undeterminable: + comm.UpdateVerdict(network.VerdictUndeterminable) + return + case profile.Denied: + log.Infof("firewall: denying communication %s, endpoint is blacklisted: %s", comm, reason) + comm.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) + return + case profile.Permitted: + log.Infof("firewall: permitting communication %s, endpoint is whitelisted: %s", comm, reason) + comm.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) return } switch profileSet.GetProfileMode() { case profile.Whitelist: - log.Infof("firewall: denying connection %s, domain is not whitelisted", connection) - connection.Deny("domain is not whitelisted") + log.Infof("firewall: denying communication %s, domain is not whitelisted", comm) + comm.Deny("domain is not whitelisted") + return case profile.Prompt: // check Related flag // TODO: improve this! if profileSet.CheckFlag(profile.Related) { matched := false - pathElements := strings.Split(connection.Process().Path, "/") // FIXME: path seperator + pathElements := strings.Split(comm.Process().Path, "/") // FIXME: path seperator // only look at the last two path segments if len(pathElements) > 2 { pathElements = pathElements[len(pathElements)-2:] @@ -104,167 +108,285 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) processElement = profileSet.UserProfile().Name break matchLoop } - if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 { + if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 { matched = true - processElement = connection.Process().Name + processElement = comm.Process().Name break matchLoop } - if levenshtein.Match(domainElement, connection.Process().ExecName, nil) > 0.5 { + if levenshtein.Match(domainElement, comm.Process().ExecName, nil) > 0.5 { matched = true - processElement = connection.Process().ExecName + processElement = comm.Process().ExecName break matchLoop } } if matched { - log.Infof("firewall: accepting connection %s, match to domain was found: %s ~== %s", connection, domainElement, processElement) - connection.Accept("domain is related to process") + log.Infof("firewall: permitting communication %s, match to domain was found: %s ~== %s", comm, domainElement, processElement) + comm.Accept("domain is related to process") } } - if connection.GetVerdict() != network.ACCEPT { + if comm.GetVerdict() != network.VerdictAccept { // TODO - log.Infof("firewall: accepting connection %s, domain permitted (prompting is not yet implemented)", connection) - connection.Accept("domain permitted (prompting is not yet implemented)") + log.Infof("firewall: permitting communication %s, domain permitted (prompting is not yet implemented)", comm) + comm.Accept("domain permitted (prompting is not yet implemented)") } - + return case profile.Blacklist: - log.Infof("firewall: accepting connection %s, domain is not blacklisted", connection) - connection.Accept("domain is not blacklisted") + log.Infof("firewall: permitting communication %s, domain is not blacklisted", comm) + comm.Accept("domain is not blacklisted") + return } + log.Infof("firewall: denying communication %s, no profile mode set", comm) + comm.Deny("no profile mode set") } -// DecideOnConnectionAfterIntel makes a decision about a connection after the dns query is resolved and intel is gathered. -func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache { +// 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) { - // grant self - if connection.Process().Pid == os.Getpid() { - log.Infof("firewall: granting own connection %s", connection) - connection.Accept("") + // SUSPENDED until Stamp integration is finished + + // grant self - should not get here + // if comm.Process().Pid == os.Getpid() { + // log.Infof("firewall: granting own communication %s", comm) + // comm.Accept("") + // return + // } + + // check if there is a profile + // profileSet := comm.Process().ProfileSet() + // if profileSet == nil { + // log.Errorf("firewall: denying communication %s, no Profile Set", comm) + // comm.Deny("no Profile Set") + // return + // } + // profileSet.Update(status.ActiveSecurityLevel()) + + // TODO: Stamp integration + + return +} + +// FilterDNSResponse filters a dns response according to the application profile and settings. +func FilterDNSResponse(comm *network.Communication, fqdn string, rrCache *intel.RRCache) *intel.RRCache { + // do not modify own queries - this should not happen anyway + if comm.Process().Pid == os.Getpid() { return rrCache } // check if there is a profile - profileSet := connection.Process().ProfileSet() + profileSet := comm.Process().ProfileSet() if profileSet == nil { - log.Errorf("firewall: denying connection %s, no Profile Set", connection) - connection.Deny("no Profile Set") - return rrCache + log.Infof("firewall: blocking dns query of communication %s, no Profile Set", comm) + return nil } profileSet.Update(status.ActiveSecurityLevel()) - // TODO: Stamp integration + // save config for consistency during function call + secLevel := profileSet.SecurityLevel() + filterByScope := filterDNSByScope(secLevel) + filterByProfile := filterDNSByProfile(secLevel) - // TODO: Gate17 integration - // tunnelInfo, err := AssignTunnelIP(fqdn) + // check if DNS response filtering is completely turned off + if !filterByScope && !filterByProfile { + return rrCache + } + + // duplicate entry + rrCache = rrCache.ShallowCopy() + rrCache.FilteredEntries = make([]string, 0) + + // change information + var addressesRemoved int + var addressesOk int + + // loop vars + var classification int8 + var ip net.IP + var result profile.EPResult + + // filter function + filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) { + goodEntries = make([]dns.RR, 0, len(entries)) + + for _, rr := range entries { + + // get IP and classification + switch v := rr.(type) { + case *dns.A: + ip = v.A + case *dns.AAAA: + ip = v.AAAA + default: + // add non A/AAAA entries + goodEntries = append(goodEntries, rr) + continue + } + classification = netutils.ClassifyIP(ip) + + if filterByScope { + switch { + case classification == netutils.HostLocal: + // No DNS should return localhost addresses + addressesRemoved++ + rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) + continue + case rrCache.ServerScope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + // No global DNS should return LAN addresses + addressesRemoved++ + rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) + continue + } + } + + if filterByProfile { + // filter by flags + switch { + case !profileSet.CheckFlag(profile.Internet) && classification == netutils.Global: + addressesRemoved++ + rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) + continue + case !profileSet.CheckFlag(profile.LAN) && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + addressesRemoved++ + rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) + continue + case !profileSet.CheckFlag(profile.Localhost) && classification == netutils.HostLocal: + addressesRemoved++ + rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) + continue + } + + // filter by endpoints + result, _ = profileSet.CheckEndpointIP("", ip, 0, 0, false) + if result == profile.Denied { + addressesRemoved++ + rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) + continue + } + } + + // if survived, add to good entries + addressesOk++ + goodEntries = append(goodEntries, rr) + } + return + } + + rrCache.Answer = filterEntries(rrCache.Answer) + rrCache.Extra = filterEntries(rrCache.Extra) - rrCache.Duplicate().FilterEntries(profileSet.CheckFlag(profile.Internet), profileSet.CheckFlag(profile.LAN), false) - if len(rrCache.Answer) == 0 { - if profileSet.CheckFlag(profile.Internet) { - connection.Deny("server is located in the LAN, but LAN access is not permitted") - } else { - connection.Deny("server is located in the Internet, but Internet access is not permitted") + if addressesRemoved > 0 { + rrCache.Filtered = true + if addressesOk == 0 { + comm.Deny("no addresses returned for this domain are permitted") + log.Infof("firewall: fully dns responses for communication %s", comm) + return nil } } + if rrCache.Filtered { + log.Infof("firewall: filtered DNS replies for %s: %s", comm, strings.Join(rrCache.FilteredEntries, ", ")) + } + + // TODO: Gate17 integration + // tunnelInfo, err := AssignTunnelIP(fqdn) + return rrCache } -// DeciceOnConnection makes a decision about a connection with its first packet. -func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { +// DecideOnCommunication makes a decision about a communication with its first packet. +func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { // grant self - if connection.Process().Pid == os.Getpid() { - log.Infof("firewall: granting own connection %s", connection) - connection.Accept("") + if comm.Process().Pid == os.Getpid() { + log.Infof("firewall: granting own communication %s", comm) + comm.Accept("") return } // check if there is a profile - profileSet := connection.Process().ProfileSet() + profileSet := comm.Process().ProfileSet() if profileSet == nil { - log.Errorf("firewall: denying connection %s, no Profile Set", connection) - connection.Deny("no Profile Set") + log.Errorf("firewall: denying communication %s, no Profile Set", comm) + comm.Deny("no Profile Set") return } profileSet.Update(status.ActiveSecurityLevel()) - // check connection type - switch connection.Domain { + // check comm type + switch comm.Domain { case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid: if !profileSet.CheckFlag(profile.Service) { - log.Infof("firewall: denying connection %s, not a service", connection) - if connection.Domain == network.IncomingHost { - connection.Block("not a service") + log.Infof("firewall: denying communication %s, not a service", comm) + if comm.Domain == network.IncomingHost { + comm.Block("not a service") } else { - connection.Drop("not a service") + comm.Drop("not a service") } return } case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing! if !profileSet.CheckFlag(profile.PeerToPeer) { - log.Infof("firewall: denying connection %s, peer to peer connections (to an IP) not allowed", connection) - connection.Deny("peer to peer connections (to an IP) not allowed") + log.Infof("firewall: denying communication %s, peer to peer comms (to an IP) not allowed", comm) + comm.Deny("peer to peer comms (to an IP) not allowed") return } - default: } // check network scope - switch connection.Domain { + switch comm.Domain { case network.IncomingHost: if !profileSet.CheckFlag(profile.Localhost) { - log.Infof("firewall: denying connection %s, serving localhost not allowed", connection) - connection.Block("serving localhost not allowed") + log.Infof("firewall: denying communication %s, serving localhost not allowed", comm) + comm.Block("serving localhost not allowed") return } case network.IncomingLAN: if !profileSet.CheckFlag(profile.LAN) { - log.Infof("firewall: denying connection %s, serving LAN not allowed", connection) - connection.Deny("serving LAN not allowed") + log.Infof("firewall: denying communication %s, serving LAN not allowed", comm) + comm.Deny("serving LAN not allowed") return } case network.IncomingInternet: if !profileSet.CheckFlag(profile.Internet) { - log.Infof("firewall: denying connection %s, serving Internet not allowed", connection) - connection.Deny("serving Internet not allowed") + log.Infof("firewall: denying communication %s, serving Internet not allowed", comm) + comm.Deny("serving Internet not allowed") return } case network.IncomingInvalid: - log.Infof("firewall: denying connection %s, invalid IP address", connection) - connection.Drop("invalid IP address") + log.Infof("firewall: denying communication %s, invalid IP address", comm) + comm.Drop("invalid IP address") return case network.PeerHost: if !profileSet.CheckFlag(profile.Localhost) { - log.Infof("firewall: denying connection %s, accessing localhost not allowed", connection) - connection.Block("accessing localhost not allowed") + log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm) + comm.Block("accessing localhost not allowed") return } case network.PeerLAN: if !profileSet.CheckFlag(profile.LAN) { - log.Infof("firewall: denying connection %s, accessing the LAN not allowed", connection) - connection.Deny("accessing the LAN not allowed") + log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm) + comm.Deny("accessing the LAN not allowed") return } case network.PeerInternet: if !profileSet.CheckFlag(profile.Internet) { - log.Infof("firewall: denying connection %s, accessing the Internet not allowed", connection) - connection.Deny("accessing the Internet not allowed") + log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm) + comm.Deny("accessing the Internet not allowed") return } case network.PeerInvalid: - log.Infof("firewall: denying connection %s, invalid IP address", connection) - connection.Deny("invalid IP address") + log.Infof("firewall: denying communication %s, invalid IP address", comm) + comm.Deny("invalid IP address") return } - log.Infof("firewall: accepting connection %s", connection) - connection.Accept("") + log.Infof("firewall: undeterminable verdict for communication %s", comm) } // DecideOnLink makes a decision about a link with the first packet. -func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet.Packet) { +func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Packet) { // check: // Profile.Flags // - network specific: Internet, LocalNet @@ -272,34 +394,37 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet // Profile.ListenPorts // grant self - if connection.Process().Pid == os.Getpid() { - log.Infof("firewall: granting own link %s", connection) - connection.Accept("") + if comm.Process().Pid == os.Getpid() { + log.Infof("firewall: granting own link %s", comm) + link.Accept("") return } // check if there is a profile - profileSet := connection.Process().ProfileSet() + profileSet := comm.Process().ProfileSet() if profileSet == nil { log.Infof("firewall: no Profile Set, denying %s", link) - link.Block("no Profile Set") + link.Deny("no Profile Set") return } profileSet.Update(status.ActiveSecurityLevel()) - // get host - var domainOrIP string - switch { - case strings.HasSuffix(connection.Domain, "."): - domainOrIP = connection.Domain - case connection.Direction: - domainOrIP = pkt.GetIPHeader().Src.String() - default: - domainOrIP = pkt.GetIPHeader().Dst.String() + // get domain + var domain string + if strings.HasSuffix(comm.Domain, ".") { + domain = comm.Domain + } + + // remoteIP + var remoteIP net.IP + if comm.Direction { + remoteIP = pkt.GetIPHeader().Src + } else { + remoteIP = pkt.GetIPHeader().Dst } - // get protocol / destination port - protocol := pkt.GetIPHeader().Protocol + // protocol and destination port + protocol := uint8(pkt.GetIPHeader().Protocol) var dstPort uint16 tcpUDPHeader := pkt.GetTCPUDPHeader() if tcpUDPHeader != nil { @@ -307,15 +432,17 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet } // check endpoints list - permitted, reason, ok := profileSet.CheckEndpoint(domainOrIP, uint8(protocol), dstPort, connection.Direction) - if ok { - if permitted { - log.Infof("firewall: accepting link %s, endpoint is whitelisted: %s", link, reason) - link.Accept(fmt.Sprintf("port whitelisted: %s", reason)) - } else { - log.Infof("firewall: denying link %s: endpoint is blacklisted: %s", link, reason) - link.Deny("port blacklisted") - } + result, reason := profileSet.CheckEndpointIP(domain, remoteIP, protocol, dstPort, comm.Direction) + switch result { + // case profile.NoMatch, profile.Undeterminable: + // continue + case profile.Denied: + log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason) + link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) + return + case profile.Permitted: + log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason) + link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) return } @@ -325,15 +452,15 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet link.Deny("endpoint is not whitelisted") return case profile.Prompt: - log.Infof("firewall: accepting link %s: endpoint is not blacklisted (prompting is not yet implemented)", link) + log.Infof("firewall: permitting link %s: endpoint is not blacklisted (prompting is not yet implemented)", link) link.Accept("endpoint is not blacklisted (prompting is not yet implemented)") return case profile.Blacklist: - log.Infof("firewall: accepting link %s: endpoint is not blacklisted", link) + log.Infof("firewall: permitting link %s: endpoint is not blacklisted", link) link.Accept("endpoint is not blacklisted") return } - log.Infof("firewall: accepting link %s", link) - link.Accept("") + log.Infof("firewall: denying link %s, no profile mode set", link) + link.Deny("no profile mode set") } diff --git a/intel/config.go b/intel/config.go index d7a9525cf..faf40dbdd 100644 --- a/intel/config.go +++ b/intel/config.go @@ -58,8 +58,8 @@ func prep() error { ExpertiseLevel: config.ExpertiseLevelExpert, OptType: config.OptTypeInt, ExternalOptType: "security level", - DefaultValue: 3, - ValidationRegex: "^(1|2|3)$", + DefaultValue: 7, + ValidationRegex: "^(7|6|4)$", }) if err != nil { return err @@ -73,8 +73,8 @@ func prep() error { ExpertiseLevel: config.ExpertiseLevelExpert, OptType: config.OptTypeInt, ExternalOptType: "security level", - DefaultValue: 3, - ValidationRegex: "^(1|2|3)$", + DefaultValue: 7, + ValidationRegex: "^(7|6|4)$", }) if err != nil { return err @@ -88,8 +88,8 @@ func prep() error { ExpertiseLevel: config.ExpertiseLevelExpert, OptType: config.OptTypeInt, ExternalOptType: "security level", - DefaultValue: 3, - ValidationRegex: "^(1|2|3)$", + DefaultValue: 7, + ValidationRegex: "^(7|6|4)$", }) if err != nil { return err diff --git a/intel/main.go b/intel/main.go index fa0bbddb6..24e2b639d 100644 --- a/intel/main.go +++ b/intel/main.go @@ -7,11 +7,11 @@ import ( "github.com/Safing/portbase/modules" // module dependencies - _ "github.com/Safing/portmaster/global" + _ "github.com/Safing/portmaster/core" ) func init() { - modules.Register("intel", prep, start, nil, "global") + modules.Register("intel", prep, start, nil, "core") } func start() error { diff --git a/intel/namerecord.go b/intel/namerecord.go index 1424a5272..1b137a926 100644 --- a/intel/namerecord.go +++ b/intel/namerecord.go @@ -27,7 +27,9 @@ type NameRecord struct { Ns []string Extra []string TTL int64 - Filtered bool + + Server string + ServerScope int8 } func makeNameRecordKey(domain string, question string) string { diff --git a/intel/resolve.go b/intel/resolve.go index 87bf02490..6242a421d 100644 --- a/intel/resolve.go +++ b/intel/resolve.go @@ -15,7 +15,6 @@ import ( "github.com/Safing/portbase/database" "github.com/Safing/portbase/log" - "github.com/Safing/portmaster/network/netutils" "github.com/Safing/portmaster/status" ) @@ -304,13 +303,6 @@ func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype } resolver.Initialized.SetToIf(false, true) - // remove localhost entries, remove LAN entries if server is in global IP space. - if resolver.ServerIPScope == netutils.Global { - rrCache.FilterEntries(true, false, false) - } else { - rrCache.FilterEntries(true, true, false) - } - return rrCache, true } @@ -357,11 +349,13 @@ func query(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) { } new := &RRCache{ - Domain: fqdn, - Question: qtype, - Answer: reply.Answer, - Ns: reply.Ns, - Extra: reply.Extra, + Domain: fqdn, + Question: qtype, + Answer: reply.Answer, + Ns: reply.Ns, + Extra: reply.Extra, + Server: resolver.Server, + ServerScope: resolver.ServerIPScope, } // TODO: check if reply.Answer is valid diff --git a/intel/rrcache.go b/intel/rrcache.go index d5fd5009e..f33abbf05 100644 --- a/intel/rrcache.go +++ b/intel/rrcache.go @@ -5,11 +5,8 @@ package intel import ( "fmt" "net" - "strings" "time" - "github.com/Safing/portbase/log" - "github.com/Safing/portmaster/network/netutils" "github.com/miekg/dns" ) @@ -23,10 +20,14 @@ type RRCache struct { Extra []dns.RR TTL int64 + Server string + ServerScope int8 + updated int64 servedFromCache bool requestingNew bool Filtered bool + FilteredEntries []string } // Clean sets all TTLs to 17 and sets cache expiry with specified minimum. @@ -79,10 +80,11 @@ func (m *RRCache) ExportAllARecords() (ips []net.IP) { // ToNameRecord converts the RRCache to a NameRecord for cleaner persistence. func (m *RRCache) ToNameRecord() *NameRecord { new := &NameRecord{ - Domain: m.Domain, - Question: m.Question.String(), - TTL: m.TTL, - Filtered: m.Filtered, + Domain: m.Domain, + Question: m.Question.String(), + TTL: m.TTL, + Server: m.Server, + ServerScope: m.ServerScope, } // stringify RR entries @@ -136,7 +138,8 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) { } } - rrCache.Filtered = nameRecord.Filtered + rrCache.Server = nameRecord.Server + rrCache.ServerScope = nameRecord.ServerScope rrCache.servedFromCache = true return rrCache, nil } @@ -175,82 +178,23 @@ func (m *RRCache) IsNXDomain() bool { return len(m.Answer) == 0 } -// Duplicate returns a duplicate of the cache. slices are not copied, but referenced. -func (m *RRCache) Duplicate() *RRCache { +// ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced. +func (m *RRCache) ShallowCopy() *RRCache { return &RRCache{ - Domain: m.Domain, - Question: m.Question, - Answer: m.Answer, - Ns: m.Ns, - Extra: m.Extra, - TTL: m.TTL, + Domain: m.Domain, + Question: m.Question, + Answer: m.Answer, + Ns: m.Ns, + Extra: m.Extra, + TTL: m.TTL, + + Server: m.Server, + ServerScope: m.ServerScope, + updated: m.updated, servedFromCache: m.servedFromCache, requestingNew: m.requestingNew, Filtered: m.Filtered, + FilteredEntries: m.FilteredEntries, } } - -// FilterEntries filters resource records according to the given permission scope. -func (m *RRCache) FilterEntries(internet, lan, host bool) { - var filtered bool - - m.Answer, filtered = filterEntries(m, m.Answer, internet, lan, host) - if filtered { - m.Filtered = true - } - m.Extra, filtered = filterEntries(m, m.Extra, internet, lan, host) - if filtered { - m.Filtered = true - } -} - -func filterEntries(m *RRCache, entries []dns.RR, internet, lan, host bool) (filteredEntries []dns.RR, filtered bool) { - filteredEntries = make([]dns.RR, 0, len(entries)) - var classification int8 - var deletedEntries []string - -entryLoop: - for _, rr := range entries { - - classification = -1 - switch v := rr.(type) { - case *dns.A: - classification = netutils.ClassifyIP(v.A) - case *dns.AAAA: - classification = netutils.ClassifyIP(v.AAAA) - } - - if classification >= 0 { - switch { - case !internet && classification == netutils.Global: - filtered = true - deletedEntries = append(deletedEntries, rr.String()) - continue entryLoop - case !lan && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): - filtered = true - deletedEntries = append(deletedEntries, rr.String()) - continue entryLoop - case !host && classification == netutils.HostLocal: - filtered = true - deletedEntries = append(deletedEntries, rr.String()) - continue entryLoop - } - } - - filteredEntries = append(filteredEntries, rr) - } - - if len(deletedEntries) > 0 { - log.Infof("intel: filtered DNS replies for %s%s: %s (Settings: Int=%v LAN=%v Host=%v)", - m.Domain, - m.Question.String(), - strings.Join(deletedEntries, ", "), - internet, - lan, - host, - ) - } - - return -} diff --git a/main.go b/main.go index 9fbe4cd23..86e66db92 100644 --- a/main.go +++ b/main.go @@ -14,10 +14,7 @@ import ( "github.com/Safing/portbase/modules" // include packages here - - _ "github.com/Safing/portbase/api" - _ "github.com/Safing/portbase/database/dbmodule" - _ "github.com/Safing/portbase/database/storage/badger" + _ "github.com/Safing/portmaster/core" _ "github.com/Safing/portmaster/firewall" _ "github.com/Safing/portmaster/nameserver" _ "github.com/Safing/portmaster/ui" diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 3cbd49404..1656ff682 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -126,8 +126,8 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) { // get connection // start = time.Now() - connection, err := network.GetConnectionByDNSRequest(rAddr.IP, uint16(rAddr.Port), fqdn) - // log.Tracef("nameserver: took %s to get connection (and maybe process)", time.Since(start)) + comm, err := network.GetCommunicationByDNSRequest(rAddr.IP, uint16(rAddr.Port), fqdn) + // log.Tracef("nameserver: took %s to get comms (and maybe process)", time.Since(start)) if err != nil { log.Warningf("nameserver: someone is requesting %s, but could not identify process: %s, returning nxdomain", fqdn, err) nxDomain(w, query) @@ -137,39 +137,51 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) { // [2/2] use this to time how long it takes to get process info // log.Tracef("nameserver: took %s to get connection/process of %s request", time.Now().Sub(timed).String(), fqdn) + // check if communication needs reevaluation + if comm.NeedsReevaluation() { + comm.ResetVerdict() + } + // check profile before we even get intel and rr - if connection.GetVerdict() == network.UNDECIDED { + if comm.GetVerdict() == network.VerdictUndecided || comm.GetVerdict() == network.VerdictUndeterminable { // start = time.Now() - firewall.DecideOnConnectionBeforeIntel(connection, fqdn) + firewall.DecideOnCommunicationBeforeIntel(comm, fqdn) // log.Tracef("nameserver: took %s to make decision", time.Since(start)) } - if connection.GetVerdict() == network.BLOCK || connection.GetVerdict() == network.DROP { + if comm.GetVerdict() == network.VerdictBlock || comm.GetVerdict() == network.VerdictDrop { nxDomain(w, query) return } // get intel and RRs // start = time.Now() - domainIntel, rrCache := intel.GetIntelAndRRs(fqdn, qtype, connection.Process().ProfileSet().SecurityLevel()) + domainIntel, rrCache := intel.GetIntelAndRRs(fqdn, qtype, comm.Process().ProfileSet().SecurityLevel()) // log.Tracef("nameserver: took %s to get intel and RRs", time.Since(start)) if rrCache == nil { // TODO: analyze nxdomain requests, malware could be trying DGA-domains - log.Infof("nameserver: %s tried to query %s, but is nxdomain", connection.Process().String(), fqdn) + log.Infof("nameserver: %s tried to query %s, but is nxdomain", comm.Process().String(), fqdn) nxDomain(w, query) return } // set intel - connection.Lock() - connection.Intel = domainIntel - connection.Unlock() - connection.Save() + comm.Lock() + comm.Intel = domainIntel + comm.Unlock() + comm.Save() // do a full check with intel - if connection.GetVerdict() == network.UNDECIDED { - rrCache = firewall.DecideOnConnectionAfterIntel(connection, fqdn, rrCache) + if comm.GetVerdict() == network.VerdictUndecided || comm.GetVerdict() == network.VerdictUndeterminable { + firewall.DecideOnCommunicationAfterIntel(comm, fqdn, rrCache) } - if rrCache == nil || connection.GetVerdict() == network.BLOCK || connection.GetVerdict() == network.DROP { + if comm.GetVerdict() == network.VerdictBlock || comm.GetVerdict() == network.VerdictDrop { + nxDomain(w, query) + return + } + + // filter DNS response + rrCache = firewall.FilterDNSResponse(comm, fqdn, rrCache) + if rrCache == nil { nxDomain(w, query) return } diff --git a/network/clean.go b/network/clean.go index e7e671d23..adcc756dd 100644 --- a/network/clean.go +++ b/network/clean.go @@ -20,7 +20,7 @@ func cleaner() { cleanLinks() time.Sleep(2 * time.Second) - cleanConnections() + cleanComms() time.Sleep(2 * time.Second) cleanProcesses() } @@ -73,18 +73,18 @@ func cleanLinks() { } } -func cleanConnections() { - connectionsLock.RLock() - defer connectionsLock.RUnlock() +func cleanComms() { + commsLock.RLock() + defer commsLock.RUnlock() threshold := time.Now().Add(-thresholdDuration).Unix() - for _, conn := range connections { - conn.Lock() - if conn.FirstLinkEstablished < threshold && conn.LinkCount == 0 { - // log.Tracef("network.clean: deleted %s", conn.DatabaseKey()) - go conn.Delete() + for _, comm := range comms { + comm.Lock() + if comm.FirstLinkEstablished < threshold && comm.LinkCount == 0 { + // log.Tracef("network.clean: deleted %s", comm.DatabaseKey()) + go comm.Delete() } - conn.Unlock() + comm.Unlock() } } diff --git a/network/communication.go b/network/communication.go new file mode 100644 index 000000000..94901d682 --- /dev/null +++ b/network/communication.go @@ -0,0 +1,349 @@ +// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file. + +package network + +import ( + "errors" + "fmt" + "net" + "sync" + "time" + + "github.com/Safing/portbase/database/record" + "github.com/Safing/portmaster/intel" + "github.com/Safing/portmaster/network/netutils" + "github.com/Safing/portmaster/network/packet" + "github.com/Safing/portmaster/process" + "github.com/Safing/portmaster/profile" +) + +// Communication describes a logical connection between a process and a domain. +type Communication struct { + record.Base + sync.Mutex + + Domain string + Direction bool + Intel *intel.Intel + process *process.Process + Verdict Verdict + Reason string + Inspect bool + + FirstLinkEstablished int64 + LastLinkEstablished int64 + LinkCount uint + + profileUpdateVersion uint32 +} + +// Process returns the process that owns the connection. +func (comm *Communication) Process() *process.Process { + comm.Lock() + defer comm.Unlock() + + return comm.process +} + +// ResetVerdict resets the verdict to VerdictUndecided. +func (comm *Communication) ResetVerdict() { + comm.Lock() + defer comm.Unlock() + + comm.Verdict = VerdictUndecided +} + +// GetVerdict returns the current verdict. +func (comm *Communication) GetVerdict() Verdict { + comm.Lock() + defer comm.Unlock() + + return comm.Verdict +} + +// Accept accepts the communication and adds the given reason. +func (comm *Communication) Accept(reason string) { + comm.AddReason(reason) + comm.UpdateVerdict(VerdictAccept) +} + +// Deny blocks or drops the communication depending on the connection direction and adds the given reason. +func (comm *Communication) Deny(reason string) { + if comm.Direction { + comm.Drop(reason) + } else { + comm.Block(reason) + } +} + +// Block blocks the communication and adds the given reason. +func (comm *Communication) Block(reason string) { + comm.AddReason(reason) + comm.UpdateVerdict(VerdictBlock) +} + +// Drop drops the communication and adds the given reason. +func (comm *Communication) Drop(reason string) { + comm.AddReason(reason) + comm.UpdateVerdict(VerdictDrop) +} + +// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts. +func (comm *Communication) UpdateVerdict(newVerdict Verdict) { + comm.Lock() + defer comm.Unlock() + + if newVerdict > comm.Verdict { + comm.Verdict = newVerdict + go comm.Save() + } +} + +// AddReason adds a human readable string as to why a certain verdict was set in regard to this communication. +func (comm *Communication) AddReason(reason string) { + if reason == "" { + return + } + + comm.Lock() + defer comm.Unlock() + + if comm.Reason != "" { + comm.Reason += " | " + } + comm.Reason += reason +} + +// NeedsReevaluation returns whether the decision on this communication should be re-evaluated. +func (comm *Communication) NeedsReevaluation() bool { + comm.Lock() + defer comm.Unlock() + + updateVersion := profile.GetUpdateVersion() + if comm.profileUpdateVersion != updateVersion { + comm.profileUpdateVersion = updateVersion + return true + } + return false +} + +// GetCommunicationByFirstPacket returns the matching communication from the internal storage. +func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) { + // get Process + proc, direction, err := process.GetProcessByPacket(pkt) + if err != nil { + return nil, err + } + var domain string + + // Incoming + if direction { + switch netutils.ClassifyIP(pkt.GetIPHeader().Src) { + case netutils.HostLocal: + domain = IncomingHost + case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: + domain = IncomingLAN + case netutils.Global, netutils.GlobalMulticast: + domain = IncomingInternet + case netutils.Invalid: + domain = IncomingInvalid + } + + communication, ok := GetCommunication(proc.Pid, domain) + if !ok { + communication = &Communication{ + Domain: domain, + Direction: Inbound, + process: proc, + Inspect: true, + FirstLinkEstablished: time.Now().Unix(), + } + } + communication.process.AddCommunication() + return communication, nil + } + + // get domain + ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP()) + + // PeerToPeer + if err != nil { + // if no domain could be found, it must be a direct connection (ie. no DNS) + + switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) { + case netutils.HostLocal: + domain = PeerHost + case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: + domain = PeerLAN + case netutils.Global, netutils.GlobalMulticast: + domain = PeerInternet + case netutils.Invalid: + domain = PeerInvalid + } + + communication, ok := GetCommunication(proc.Pid, domain) + if !ok { + communication = &Communication{ + Domain: domain, + Direction: Outbound, + process: proc, + Inspect: true, + FirstLinkEstablished: time.Now().Unix(), + } + } + communication.process.AddCommunication() + return communication, nil + } + + // To Domain + // FIXME: how to handle multiple possible domains? + communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0]) + if !ok { + communication = &Communication{ + Domain: ipinfo.Domains[0], + Direction: Outbound, + process: proc, + Inspect: true, + FirstLinkEstablished: time.Now().Unix(), + } + } + communication.process.AddCommunication() + return communication, nil +} + +// var localhost = net.IPv4(127, 0, 0, 1) + +var ( + dnsAddress = net.IPv4(127, 0, 0, 1) + dnsPort uint16 = 53 +) + +// GetCommunicationByDNSRequest returns the matching communication from the internal storage. +func GetCommunicationByDNSRequest(ip net.IP, port uint16, fqdn string) (*Communication, error) { + // get Process + proc, err := process.GetProcessByEndpoints(ip, port, dnsAddress, dnsPort, packet.UDP) + if err != nil { + return nil, err + } + + communication, ok := GetCommunication(proc.Pid, fqdn) + if !ok { + communication = &Communication{ + Domain: fqdn, + process: proc, + Inspect: true, + } + communication.process.AddCommunication() + communication.Save() + } + return communication, nil +} + +// GetCommunication fetches a connection object from the internal storage. +func GetCommunication(pid int, domain string) (comm *Communication, ok bool) { + commsLock.RLock() + defer commsLock.RUnlock() + comm, ok = comms[fmt.Sprintf("%d/%s", pid, domain)] + return +} + +func (comm *Communication) makeKey() string { + return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Domain) +} + +// Save saves the connection object in the storage and propagates the change. +func (comm *Communication) Save() error { + comm.Lock() + defer comm.Unlock() + + if comm.process == nil { + return errors.New("cannot save connection without process") + } + + if !comm.KeyIsSet() { + comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Domain)) + comm.CreateMeta() + } + + key := comm.makeKey() + commsLock.RLock() + _, ok := comms[key] + commsLock.RUnlock() + + if !ok { + commsLock.Lock() + comms[key] = comm + commsLock.Unlock() + } + + go dbController.PushUpdate(comm) + return nil +} + +// Delete deletes a connection from the storage and propagates the change. +func (comm *Communication) Delete() { + comm.Lock() + defer comm.Unlock() + + commsLock.Lock() + delete(comms, comm.makeKey()) + commsLock.Unlock() + + comm.Meta().Delete() + go dbController.PushUpdate(comm) + comm.process.RemoveCommunication() + go comm.process.Save() +} + +// AddLink applies the Communication to the Link and increases sets counter and timestamps. +func (comm *Communication) AddLink(link *Link) { + link.Lock() + link.comm = comm + link.Verdict = comm.Verdict + link.Inspect = comm.Inspect + link.Unlock() + link.Save() + + comm.Lock() + comm.LinkCount++ + comm.LastLinkEstablished = time.Now().Unix() + if comm.FirstLinkEstablished == 0 { + comm.FirstLinkEstablished = comm.LastLinkEstablished + } + comm.Unlock() + comm.Save() +} + +// RemoveLink lowers the link counter by one. +func (comm *Communication) RemoveLink() { + comm.Lock() + defer comm.Unlock() + + if comm.LinkCount > 0 { + comm.LinkCount-- + } +} + +// String returns a string representation of Communication. +func (comm *Communication) String() string { + comm.Lock() + defer comm.Unlock() + + switch comm.Domain { + case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid: + if comm.process == nil { + return "? <- *" + } + return fmt.Sprintf("%s <- *", comm.process.String()) + case PeerHost, PeerLAN, PeerInternet, PeerInvalid: + if comm.process == nil { + return "? -> *" + } + return fmt.Sprintf("%s -> *", comm.process.String()) + default: + if comm.process == nil { + return fmt.Sprintf("? -> %s", comm.Domain) + } + return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Domain) + } +} diff --git a/network/connection.go b/network/connection.go deleted file mode 100644 index 418350eae..000000000 --- a/network/connection.go +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file. - -package network - -import ( - "errors" - "fmt" - "net" - "sync" - "time" - - "github.com/Safing/portbase/database/record" - "github.com/Safing/portmaster/intel" - "github.com/Safing/portmaster/network/netutils" - "github.com/Safing/portmaster/network/packet" - "github.com/Safing/portmaster/process" -) - -// Connection describes a connection between a process and a domain -type Connection struct { - record.Base - sync.Mutex - - Domain string - Direction bool - Intel *intel.Intel - process *process.Process - Verdict Verdict - Reason string - Inspect bool - - FirstLinkEstablished int64 - LastLinkEstablished int64 - LinkCount uint -} - -// Process returns the process that owns the connection. -func (conn *Connection) Process() *process.Process { - conn.Lock() - defer conn.Unlock() - - return conn.process -} - -// GetVerdict returns the current verdict. -func (conn *Connection) GetVerdict() Verdict { - conn.Lock() - defer conn.Unlock() - - return conn.Verdict -} - -// Accept accepts the connection and adds the given reason. -func (conn *Connection) Accept(reason string) { - conn.AddReason(reason) - conn.UpdateVerdict(ACCEPT) -} - -// Deny blocks or drops the connection depending on the connection direction and adds the given reason. -func (conn *Connection) Deny(reason string) { - if conn.Direction { - conn.Drop(reason) - } else { - conn.Block(reason) - } -} - -// Block blocks the connection and adds the given reason. -func (conn *Connection) Block(reason string) { - conn.AddReason(reason) - conn.UpdateVerdict(BLOCK) -} - -// Drop drops the connection and adds the given reason. -func (conn *Connection) Drop(reason string) { - conn.AddReason(reason) - conn.UpdateVerdict(DROP) -} - -// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts -func (conn *Connection) UpdateVerdict(newVerdict Verdict) { - conn.Lock() - defer conn.Unlock() - - if newVerdict > conn.Verdict { - conn.Verdict = newVerdict - go conn.Save() - } -} - -// AddReason adds a human readable string as to why a certain verdict was set in regard to this connection -func (conn *Connection) AddReason(reason string) { - if reason == "" { - return - } - - conn.Lock() - defer conn.Unlock() - - if conn.Reason != "" { - conn.Reason += " | " - } - conn.Reason += reason -} - -// GetConnectionByFirstPacket returns the matching connection from the internal storage. -func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) { - // get Process - proc, direction, err := process.GetProcessByPacket(pkt) - if err != nil { - return nil, err - } - var domain string - - // Incoming - if direction { - switch netutils.ClassifyIP(pkt.GetIPHeader().Src) { - case netutils.HostLocal: - domain = IncomingHost - case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - domain = IncomingLAN - case netutils.Global, netutils.GlobalMulticast: - domain = IncomingInternet - case netutils.Invalid: - domain = IncomingInvalid - } - - connection, ok := GetConnection(proc.Pid, domain) - if !ok { - connection = &Connection{ - Domain: domain, - Direction: Inbound, - process: proc, - Inspect: true, - FirstLinkEstablished: time.Now().Unix(), - } - } - connection.process.AddConnection() - return connection, nil - } - - // get domain - ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP()) - - // PeerToPeer - if err != nil { - // if no domain could be found, it must be a direct connection - - switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) { - case netutils.HostLocal: - domain = PeerHost - case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - domain = PeerLAN - case netutils.Global, netutils.GlobalMulticast: - domain = PeerInternet - case netutils.Invalid: - domain = PeerInvalid - } - - connection, ok := GetConnection(proc.Pid, domain) - if !ok { - connection = &Connection{ - Domain: domain, - Direction: Outbound, - process: proc, - Inspect: true, - FirstLinkEstablished: time.Now().Unix(), - } - } - connection.process.AddConnection() - return connection, nil - } - - // To Domain - // FIXME: how to handle multiple possible domains? - connection, ok := GetConnection(proc.Pid, ipinfo.Domains[0]) - if !ok { - connection = &Connection{ - Domain: ipinfo.Domains[0], - Direction: Outbound, - process: proc, - Inspect: true, - FirstLinkEstablished: time.Now().Unix(), - } - } - connection.process.AddConnection() - return connection, nil -} - -// var localhost = net.IPv4(127, 0, 0, 1) - -var ( - dnsAddress = net.IPv4(127, 0, 0, 1) - dnsPort uint16 = 53 -) - -// GetConnectionByDNSRequest returns the matching connection from the internal storage. -func GetConnectionByDNSRequest(ip net.IP, port uint16, fqdn string) (*Connection, error) { - // get Process - proc, err := process.GetProcessByEndpoints(ip, port, dnsAddress, dnsPort, packet.UDP) - if err != nil { - return nil, err - } - - connection, ok := GetConnection(proc.Pid, fqdn) - if !ok { - connection = &Connection{ - Domain: fqdn, - process: proc, - Inspect: true, - } - connection.process.AddConnection() - connection.Save() - } - return connection, nil -} - -// GetConnection fetches a connection object from the internal storage. -func GetConnection(pid int, domain string) (conn *Connection, ok bool) { - connectionsLock.RLock() - defer connectionsLock.RUnlock() - conn, ok = connections[fmt.Sprintf("%d/%s", pid, domain)] - return -} - -func (conn *Connection) makeKey() string { - return fmt.Sprintf("%d/%s", conn.process.Pid, conn.Domain) -} - -// Save saves the connection object in the storage and propagates the change. -func (conn *Connection) Save() error { - conn.Lock() - defer conn.Unlock() - - if conn.process == nil { - return errors.New("cannot save connection without process") - } - - if !conn.KeyIsSet() { - conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Domain)) - conn.CreateMeta() - } - - key := conn.makeKey() - connectionsLock.RLock() - _, ok := connections[key] - connectionsLock.RUnlock() - - if !ok { - connectionsLock.Lock() - connections[key] = conn - connectionsLock.Unlock() - } - - go dbController.PushUpdate(conn) - return nil -} - -// Delete deletes a connection from the storage and propagates the change. -func (conn *Connection) Delete() { - conn.Lock() - defer conn.Unlock() - - connectionsLock.Lock() - delete(connections, conn.makeKey()) - connectionsLock.Unlock() - - conn.Meta().Delete() - go dbController.PushUpdate(conn) - conn.process.RemoveConnection() - go conn.process.Save() -} - -// AddLink applies the connection to the link and increases sets counter and timestamps. -func (conn *Connection) AddLink(link *Link) { - link.Lock() - link.connection = conn - link.Verdict = conn.Verdict - link.Inspect = conn.Inspect - link.Unlock() - link.Save() - - conn.Lock() - conn.LinkCount++ - conn.LastLinkEstablished = time.Now().Unix() - if conn.FirstLinkEstablished == 0 { - conn.FirstLinkEstablished = conn.LastLinkEstablished - } - conn.Unlock() - conn.Save() -} - -// RemoveLink lowers the link counter by one. -func (conn *Connection) RemoveLink() { - conn.Lock() - defer conn.Unlock() - - if conn.LinkCount > 0 { - conn.LinkCount-- - } -} - -// String returns a string representation of Connection. -func (conn *Connection) String() string { - conn.Lock() - defer conn.Unlock() - - switch conn.Domain { - case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid: - if conn.process == nil { - return "? <- *" - } - return fmt.Sprintf("%s <- *", conn.process.String()) - case PeerHost, PeerLAN, PeerInternet, PeerInvalid: - if conn.process == nil { - return "? -> *" - } - return fmt.Sprintf("%s -> *", conn.process.String()) - default: - if conn.process == nil { - return fmt.Sprintf("? -> %s", conn.Domain) - } - return fmt.Sprintf("%s -> %s", conn.process.String(), conn.Domain) - } -} diff --git a/network/database.go b/network/database.go index 28147abfe..519d10c1a 100644 --- a/network/database.go +++ b/network/database.go @@ -14,10 +14,10 @@ import ( ) var ( - links = make(map[string]*Link) - linksLock sync.RWMutex - connections = make(map[string]*Connection) - connectionsLock sync.RWMutex + links = make(map[string]*Link) + linksLock sync.RWMutex + comms = make(map[string]*Communication) + commsLock sync.RWMutex dbController *database.Controller ) @@ -43,9 +43,9 @@ func (s *StorageInterface) Get(key string) (record.Record, error) { } } case 3: - connectionsLock.RLock() - defer connectionsLock.RUnlock() - conn, ok := connections[splitted[2]] + commsLock.RLock() + defer commsLock.RUnlock() + conn, ok := comms[splitted[2]] if ok { return conn, nil } @@ -79,14 +79,14 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) { } } - // connections - connectionsLock.RLock() - for _, conn := range connections { + // comms + commsLock.RLock() + for _, conn := range comms { if strings.HasPrefix(conn.DatabaseKey(), q.DatabaseKeyPrefix()) { it.Next <- conn } } - connectionsLock.RUnlock() + commsLock.RUnlock() // links linksLock.RLock() diff --git a/network/link.go b/network/link.go index a8deae6b9..ef377e37e 100644 --- a/network/link.go +++ b/network/link.go @@ -38,18 +38,18 @@ type Link struct { pktQueue chan packet.Packet firewallHandler FirewallHandler - connection *Connection + comm *Communication activeInspectors []bool inspectorData map[uint8]interface{} } -// Connection returns the Connection the Link is part of -func (link *Link) Connection() *Connection { +// Communication returns the Communication the Link is part of +func (link *Link) Communication() *Communication { link.Lock() defer link.Unlock() - return link.connection + return link.comm } // GetVerdict returns the current verdict. @@ -106,12 +106,12 @@ func (link *Link) HandlePacket(pkt packet.Packet) { // Accept accepts the link and adds the given reason. func (link *Link) Accept(reason string) { link.AddReason(reason) - link.UpdateVerdict(ACCEPT) + link.UpdateVerdict(VerdictAccept) } // Deny blocks or drops the link depending on the connection direction and adds the given reason. func (link *Link) Deny(reason string) { - if link.connection != nil && link.connection.Direction { + if link.comm != nil && link.comm.Direction { link.Drop(reason) } else { link.Block(reason) @@ -121,24 +121,24 @@ func (link *Link) Deny(reason string) { // Block blocks the link and adds the given reason. func (link *Link) Block(reason string) { link.AddReason(reason) - link.UpdateVerdict(BLOCK) + link.UpdateVerdict(VerdictBlock) } // Drop drops the link and adds the given reason. func (link *Link) Drop(reason string) { link.AddReason(reason) - link.UpdateVerdict(DROP) + link.UpdateVerdict(VerdictDrop) } // RerouteToNameserver reroutes the link to the portmaster nameserver. func (link *Link) RerouteToNameserver() { - link.UpdateVerdict(RerouteToNameserver) + link.UpdateVerdict(VerdictRerouteToNameserver) } // RerouteToTunnel reroutes the link to the tunnel entrypoint and adds the given reason for accepting the connection. func (link *Link) RerouteToTunnel(reason string) { link.AddReason(reason) - link.UpdateVerdict(RerouteToTunnel) + link.UpdateVerdict(VerdictRerouteToTunnel) } // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts @@ -192,30 +192,30 @@ func (link *Link) ApplyVerdict(pkt packet.Packet) { if link.VerdictPermanent { switch link.Verdict { - case ACCEPT: + case VerdictAccept: pkt.PermanentAccept() - case BLOCK: + case VerdictBlock: pkt.PermanentBlock() - case DROP: + case VerdictDrop: pkt.PermanentDrop() - case RerouteToNameserver: + case VerdictRerouteToNameserver: pkt.RerouteToNameserver() - case RerouteToTunnel: + case VerdictRerouteToTunnel: pkt.RerouteToTunnel() default: pkt.Drop() } } else { switch link.Verdict { - case ACCEPT: + case VerdictAccept: pkt.Accept() - case BLOCK: + case VerdictBlock: pkt.Block() - case DROP: + case VerdictDrop: pkt.Drop() - case RerouteToNameserver: + case VerdictRerouteToNameserver: pkt.RerouteToNameserver() - case RerouteToTunnel: + case VerdictRerouteToTunnel: pkt.RerouteToTunnel() default: pkt.Drop() @@ -228,12 +228,12 @@ func (link *Link) Save() error { link.Lock() defer link.Unlock() - if link.connection == nil { - return errors.New("cannot save link without connection") + if link.comm == nil { + return errors.New("cannot save link without comms") } if !link.KeyIsSet() { - link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.connection.Process().Pid, link.connection.Domain, link.ID)) + link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Domain, link.ID)) link.CreateMeta() } @@ -262,8 +262,8 @@ func (link *Link) Delete() { link.Meta().Delete() go dbController.PushUpdate(link) - link.connection.RemoveLink() - go link.connection.Save() + link.comm.RemoveLink() + go link.comm.Save() } // GetLink fetches a Link from the database from the default namespace for this object @@ -288,7 +288,7 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) { func CreateLinkFromPacket(pkt packet.Packet) *Link { link := &Link{ ID: pkt.GetLinkID(), - Verdict: UNDECIDED, + Verdict: VerdictUndecided, Started: time.Now().Unix(), RemoteAddress: pkt.FmtRemoteAddress(), } @@ -332,24 +332,24 @@ func (link *Link) String() string { link.Lock() defer link.Unlock() - if link.connection == nil { + if link.comm == nil { return fmt.Sprintf("? <-> %s", link.RemoteAddress) } - switch link.connection.Domain { + switch link.comm.Domain { case "I": - if link.connection.process == nil { + if link.comm.process == nil { return fmt.Sprintf("? <- %s", link.RemoteAddress) } - return fmt.Sprintf("%s <- %s", link.connection.process.String(), link.RemoteAddress) + return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.RemoteAddress) case "D": - if link.connection.process == nil { + if link.comm.process == nil { return fmt.Sprintf("? -> %s", link.RemoteAddress) } - return fmt.Sprintf("%s -> %s", link.connection.process.String(), link.RemoteAddress) + return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.RemoteAddress) default: - if link.connection.process == nil { - return fmt.Sprintf("? -> %s (%s)", link.connection.Domain, link.RemoteAddress) + if link.comm.process == nil { + return fmt.Sprintf("? -> %s (%s)", link.comm.Domain, link.RemoteAddress) } - return fmt.Sprintf("%s to %s (%s)", link.connection.process.String(), link.connection.Domain, link.RemoteAddress) + return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Domain, link.RemoteAddress) } } diff --git a/network/status.go b/network/status.go index a02756ae7..d25a28c57 100644 --- a/network/status.go +++ b/network/status.go @@ -8,12 +8,13 @@ type Verdict uint8 // List of values a Status can have const ( // UNDECIDED is the default status of new connections - UNDECIDED Verdict = iota - ACCEPT - BLOCK - DROP - RerouteToNameserver - RerouteToTunnel + VerdictUndecided Verdict = 0 + VerdictUndeterminable Verdict = 1 + VerdictAccept Verdict = 2 + VerdictBlock Verdict = 3 + VerdictDrop Verdict = 4 + VerdictRerouteToNameserver Verdict = 5 + VerdictRerouteToTunnel Verdict = 6 ) // Packer Directions diff --git a/network/unknown.go b/network/unknown.go index 6e8131bef..c81612089 100644 --- a/network/unknown.go +++ b/network/unknown.go @@ -13,52 +13,52 @@ const ( ReasonUnknownProcess = "unknown connection owner: process could not be found" ) -// GetUnknownConnection returns the connection to a packet of unknown owner. -func GetUnknownConnection(pkt packet.Packet) (*Connection, error) { +// GetUnknownCommunication returns the connection to a packet of unknown owner. +func GetUnknownCommunication(pkt packet.Packet) (*Communication, error) { if pkt.IsInbound() { switch netutils.ClassifyIP(pkt.GetIPHeader().Src) { case netutils.HostLocal: - return getOrCreateUnknownConnection(pkt, IncomingHost) + return getOrCreateUnknownCommunication(pkt, IncomingHost) case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - return getOrCreateUnknownConnection(pkt, IncomingLAN) + return getOrCreateUnknownCommunication(pkt, IncomingLAN) case netutils.Global, netutils.GlobalMulticast: - return getOrCreateUnknownConnection(pkt, IncomingInternet) + return getOrCreateUnknownCommunication(pkt, IncomingInternet) case netutils.Invalid: - return getOrCreateUnknownConnection(pkt, IncomingInvalid) + return getOrCreateUnknownCommunication(pkt, IncomingInvalid) } } switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) { case netutils.HostLocal: - return getOrCreateUnknownConnection(pkt, PeerHost) + return getOrCreateUnknownCommunication(pkt, PeerHost) case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - return getOrCreateUnknownConnection(pkt, PeerLAN) + return getOrCreateUnknownCommunication(pkt, PeerLAN) case netutils.Global, netutils.GlobalMulticast: - return getOrCreateUnknownConnection(pkt, PeerInternet) + return getOrCreateUnknownCommunication(pkt, PeerInternet) case netutils.Invalid: - return getOrCreateUnknownConnection(pkt, PeerInvalid) + return getOrCreateUnknownCommunication(pkt, PeerInvalid) } // this should never happen - return getOrCreateUnknownConnection(pkt, PeerInvalid) + return getOrCreateUnknownCommunication(pkt, PeerInvalid) } -func getOrCreateUnknownConnection(pkt packet.Packet, connClass string) (*Connection, error) { - connection, ok := GetConnection(process.UnknownProcess.Pid, connClass) +func getOrCreateUnknownCommunication(pkt packet.Packet, connClass string) (*Communication, error) { + connection, ok := GetCommunication(process.UnknownProcess.Pid, connClass) if !ok { - connection = &Connection{ + connection = &Communication{ Domain: connClass, Direction: pkt.IsInbound(), - Verdict: DROP, + Verdict: VerdictDrop, Reason: ReasonUnknownProcess, process: process.UnknownProcess, Inspect: true, FirstLinkEstablished: time.Now().Unix(), } if pkt.IsOutbound() { - connection.Verdict = BLOCK + connection.Verdict = VerdictBlock } } - connection.process.AddConnection() + connection.process.AddCommunication() return connection, nil } diff --git a/process/database.go b/process/database.go index 3cb87eede..6c4a2e434 100644 --- a/process/database.go +++ b/process/database.go @@ -93,7 +93,7 @@ func CleanProcessStorage(thresholdDuration time.Duration) { threshold := time.Now().Add(-thresholdDuration).Unix() for _, p := range processes { p.Lock() - if p.FirstConnectionEstablished < threshold && p.ConnectionCount == 0 { + if p.FirstCommEstablished < threshold && p.CommCount == 0 { go p.Delete() } p.Unlock() diff --git a/process/process.go b/process/process.go index 65757dbe8..5496d9115 100644 --- a/process/process.go +++ b/process/process.go @@ -42,9 +42,9 @@ type Process struct { Icon string // Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache. - FirstConnectionEstablished int64 - LastConnectionEstablished int64 - ConnectionCount uint + FirstCommEstablished int64 + LastCommEstablished int64 + CommCount uint } // ProfileSet returns the assigned profile set. @@ -66,25 +66,25 @@ func (p *Process) String() string { return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid) } -// AddConnection increases the connection counter and the last connection timestamp. -func (p *Process) AddConnection() { +// AddCommunication increases the connection counter and the last connection timestamp. +func (p *Process) AddCommunication() { p.Lock() defer p.Unlock() - p.ConnectionCount++ - p.LastConnectionEstablished = time.Now().Unix() - if p.FirstConnectionEstablished == 0 { - p.FirstConnectionEstablished = p.LastConnectionEstablished + p.CommCount++ + p.LastCommEstablished = time.Now().Unix() + if p.FirstCommEstablished == 0 { + p.FirstCommEstablished = p.LastCommEstablished } } -// RemoveConnection lowers the connection counter by one. -func (p *Process) RemoveConnection() { +// RemoveCommunication lowers the connection counter by one. +func (p *Process) RemoveCommunication() { p.Lock() defer p.Unlock() - if p.ConnectionCount > 0 { - p.ConnectionCount-- + if p.CommCount > 0 { + p.CommCount-- } } diff --git a/profile/defaults.go b/profile/defaults.go index e4551feee..ed12ae809 100644 --- a/profile/defaults.go +++ b/profile/defaults.go @@ -27,17 +27,15 @@ func makeDefaultFallbackProfile() *Profile { Localhost: status.SecurityLevelsAll, // Specials - Related: status.SecurityLevelDynamic, - PeerToPeer: status.SecurityLevelDynamic, + Related: status.SecurityLevelDynamic, }, ServiceEndpoints: []*EndpointPermission{ &EndpointPermission{ - DomainOrIP: "", - Wildcard: true, - Protocol: 0, - StartPort: 0, - EndPort: 0, - Permit: false, + Type: EptAny, + Protocol: 0, + StartPort: 0, + EndPort: 0, + Permit: false, }, }, } diff --git a/profile/endpoints.go b/profile/endpoints.go index 3716f8243..25431361f 100644 --- a/profile/endpoints.go +++ b/profile/endpoints.go @@ -2,6 +2,7 @@ package profile import ( "fmt" + "net" "strconv" "strings" @@ -13,15 +14,44 @@ type Endpoints []*EndpointPermission // EndpointPermission holds a decision about an endpoint. type EndpointPermission struct { - DomainOrIP string - Wildcard bool - Protocol uint8 - StartPort uint16 - EndPort uint16 - Permit bool - Created int64 + Type EPType + Value string + + 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 { if len(e) > 0 { @@ -30,9 +60,28 @@ func (e Endpoints) IsSet() bool { return false } -// Check checks if the given domain is governed in the list of domains and returns whether it is permitted. -// 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) { +// 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 @@ -42,7 +91,7 @@ func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkRe // setup caching wrapper cachedGetDomainOfIP = func() string { if !ipResolved { - result, err := intel.ResolveIPAndValidate(domainOrIP, securityLevel) + result, err := intel.ResolveIPAndValidate(ip.String(), securityLevel) if err != nil { // log.Debug() ipName = result @@ -53,54 +102,139 @@ func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkRe } } - isDomain := strings.HasSuffix(domainOrIP, ".") - for _, entry := range e { if entry != nil { - if ok, reason := entry.Matches(domainOrIP, protocol, port, isDomain, cachedGetDomainOfIP); ok { - return entry.Permit, reason, true + if result, reason := entry.MatchesIP(domain, ip, protocol, port, cachedGetDomainOfIP); result != NoMatch { + return result, reason } } } - return false, "", false + return NoMatch, "" } -func isSubdomainOf(domain, subdomain string) bool { - dotPrefixedDomain := "." + domain - return strings.HasSuffix(subdomain, dotPrefixedDomain) +func (ep EndpointPermission) matchesDomainOnly(domain string) (matches bool, reason string) { + wildcardInFront := strings.HasPrefix(ep.Value, "*") + wildcardInBack := strings.HasSuffix(ep.Value, "*") + switch { + 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, "" } -// 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, "" +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 + } } - if ep.StartPort > 0 && (port < ep.StartPort || port > ep.EndPort) { - return false, "" + // protocol and port matched or were defined as any + if ep.Permit { + return Permitted } + return Denied +} - 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) +// 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: - // no match - return false, "" + 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 { @@ -111,9 +245,36 @@ func (e Endpoints) String() 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.DomainOrIP + s := ep.Type.String() + if ep.Type != EptAny { + s += ":" + s += ep.Value + } s += " " if ep.Protocol > 0 { @@ -136,3 +297,18 @@ func (ep EndpointPermission) String() string { 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" + } +} diff --git a/profile/endpoints_test.go b/profile/endpoints_test.go index a87ab0da1..ff67d9614 100644 --- a/profile/endpoints_test.go +++ b/profile/endpoints_test.go @@ -1,54 +1,159 @@ package profile import ( + "net" "testing" + + "github.com/Safing/portbase/utils/testutils" ) -// 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 testEndpointDomainMatch(t *testing.T, ep *EndpointPermission, domain string, expectedResult EPResult) { + var result EPResult + result, _ = ep.MatchesDomain(domain) + if result != expectedResult { + t.Errorf( + "line %d: unexpected result for endpoint domain match %s: result=%s, expected=%s", + testutils.GetLineNumberOfCaller(1), + domain, + result, + expectedResult, + ) + } +} + +func testEndpointIPMatch(t *testing.T, ep *EndpointPermission, domain string, ip net.IP, protocol uint8, port uint16, expectedResult EPResult) { + var result EPResult + result, _ = ep.MatchesIP(domain, ip, protocol, port, nil) + if result != expectedResult { + t.Errorf( + "line %d: unexpected result for endpoint %s/%s/%d/%d: result=%s, expected=%s", + testutils.GetLineNumberOfCaller(1), + domain, + ip, + protocol, + port, + result, + expectedResult, + ) + } +} + +func TestEndpointMatching(t *testing.T) { + ep := &EndpointPermission{ + Type: EptAny, + Protocol: 0, + StartPort: 0, + EndPort: 0, + Permit: true, + } + + // ANY + + testEndpointDomainMatch(t, ep, "example.com.", Permitted) + testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + + // DOMAIN + + // wildcard domains + ep.Type = EptDomain + ep.Value = "*example.com." + testEndpointDomainMatch(t, ep, "example.com.", Permitted) + testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + + 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) + + 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) + + ep.Value = "*.com." + testEndpointDomainMatch(t, ep, "example.com.", Permitted) + testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted) + + // edge case + ep.Value = "" + testEndpointDomainMatch(t, ep, "example.com", NoMatch) + + // edge case + ep.Value = "*" + testEndpointDomainMatch(t, ep, "example.com", Permitted) + + // edge case + ep.Value = "**" + testEndpointDomainMatch(t, ep, "example.com", Permitted) + + // edge case + ep.Value = "***" + testEndpointDomainMatch(t, ep, "example.com", Permitted) + + // protocol + ep.Value = "example.com" + ep.Protocol = 17 + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 6, 443, NoMatch) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted) + testEndpointDomainMatch(t, ep, "example.com", Undeterminable) + + // ports + ep.StartPort = 442 + ep.EndPort = 444 + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted) + ep.StartPort = 442 + ep.StartPort = 443 + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted) + ep.StartPort = 443 + ep.EndPort = 444 + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted) + ep.StartPort = 443 + ep.EndPort = 443 + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted) + testEndpointDomainMatch(t, ep, "example.com", Undeterminable) + + // IP + + ep.Type = EptIPv4 + ep.Value = "10.2.3.4" + ep.Protocol = 0 + ep.StartPort = 0 + ep.EndPort = 0 + testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.4"), 6, 80, Permitted) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted) + testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.5"), 6, 80, NoMatch) + testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.5"), 17, 443, NoMatch) + testEndpointDomainMatch(t, ep, "example.com", Undeterminable) +} func TestEPString(t *testing.T) { var endpoints Endpoints endpoints = []*EndpointPermission{ &EndpointPermission{ - DomainOrIP: "example.com", - Wildcard: false, - Protocol: 6, - Permit: true, + Type: EptDomain, + Value: "example.com", + Protocol: 6, + Permit: true, }, &EndpointPermission{ - DomainOrIP: "8.8.8.8", - Protocol: 17, // TCP - StartPort: 53, // DNS - EndPort: 53, - Permit: false, + Type: EptIPv4, + Value: "1.1.1.1", + Protocol: 17, // TCP + StartPort: 53, // DNS + EndPort: 53, + Permit: false, }, &EndpointPermission{ - DomainOrIP: "google.com", - Wildcard: true, - Permit: false, + Type: EptDomain, + Value: "example.org", + Permit: false, }, } - if endpoints.String() != "[example.com 6/*, 8.8.8.8 17/53, google.com */*]" { + if endpoints.String() != "[Domain:example.com 6/*, IPv4:1.1.1.1 17/53, Domain:example.org */*]" { t.Errorf("unexpected result: %s", endpoints.String()) } @@ -57,5 +162,4 @@ func TestEPString(t *testing.T) { if noEndpoints.String() != "[]" { t.Errorf("unexpected result: %s", noEndpoints.String()) } - } diff --git a/profile/module.go b/profile/module.go index 37e6f7096..a3e526473 100644 --- a/profile/module.go +++ b/profile/module.go @@ -1,13 +1,18 @@ package profile -import "github.com/Safing/portbase/modules" +import ( + "github.com/Safing/portbase/modules" + + // module dependencies + _ "github.com/Safing/portmaster/core" +) var ( shutdownSignal = make(chan struct{}) ) func init() { - modules.Register("profile", nil, start, stop, "global", "database") + modules.Register("profile", nil, start, stop, "core") } func start() error { diff --git a/profile/set.go b/profile/set.go index 9cb53ff11..04d63a859 100644 --- a/profile/set.go +++ b/profile/set.go @@ -1,6 +1,7 @@ package profile import ( + "net" "sync" "github.com/Safing/portmaster/status" @@ -119,8 +120,28 @@ func (set *Set) CheckFlag(flag uint8) (active bool) { return false } -// 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) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, inbound bool) (permit bool, reason string, ok bool) { +// CheckEndpointDomain checks if the given endpoint matches an entry in the corresponding list. This is for outbound communication only. +func (set *Set) CheckEndpointDomain(domain string) (result EPResult, reason string) { + set.Lock() + defer set.Unlock() + + for i, profile := range set.profiles { + if i == 2 && set.independent { + continue + } + + if profile != nil { + if result, reason = profile.Endpoints.CheckDomain(domain); result != NoMatch { + return + } + } + } + + return NoMatch, "" +} + +// CheckEndpointIP checks if the given endpoint matches an entry in the corresponding list. +func (set *Set) CheckEndpointIP(domain string, ip net.IP, protocol uint8, port uint16, inbound bool) (result EPResult, reason string) { set.Lock() defer set.Unlock() @@ -131,18 +152,18 @@ func (set *Set) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, in if profile != nil { if inbound { - if permit, reason, ok = profile.ServiceEndpoints.Check(domainOrIP, protocol, port, inbound, set.combinedSecurityLevel); ok { + if result, reason = profile.ServiceEndpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch { return } } else { - if permit, reason, ok = profile.Endpoints.Check(domainOrIP, protocol, port, inbound, set.combinedSecurityLevel); ok { + if result, reason = profile.Endpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch { return } } } } - return false, "", false + return NoMatch, "" } // getSecurityLevel returns the highest prioritized security level. diff --git a/profile/set_test.go b/profile/set_test.go index 332ec35ca..984b1f3aa 100644 --- a/profile/set_test.go +++ b/profile/set_test.go @@ -1,9 +1,11 @@ package profile import ( + "net" "testing" "time" + "github.com/Safing/portbase/utils/testutils" "github.com/Safing/portmaster/status" ) @@ -28,31 +30,30 @@ func init() { }, Endpoints: []*EndpointPermission{ &EndpointPermission{ - DomainOrIP: "good.bad.example.com.", - Wildcard: false, - Permit: true, - Created: time.Now().Unix(), + Type: EptDomain, + Value: "good.bad.example.com.", + Permit: true, + Created: time.Now().Unix(), }, &EndpointPermission{ - DomainOrIP: "bad.example.com.", - Wildcard: true, - Permit: false, - Created: time.Now().Unix(), + Type: EptDomain, + Value: "*bad.example.com.", + Permit: false, + Created: time.Now().Unix(), }, &EndpointPermission{ - DomainOrIP: "example.com.", - Wildcard: false, - Permit: true, - Created: time.Now().Unix(), + Type: EptDomain, + Value: "example.com.", + Permit: true, + Created: time.Now().Unix(), }, &EndpointPermission{ - DomainOrIP: "", - Wildcard: true, - Permit: true, - Protocol: 6, - StartPort: 22000, - EndPort: 22000, - Created: time.Now().Unix(), + Type: EptAny, + Permit: true, + Protocol: 6, + StartPort: 22000, + EndPort: 22000, + Created: time.Now().Unix(), }, }, } @@ -66,36 +67,33 @@ func init() { // }, Endpoints: []*EndpointPermission{ &EndpointPermission{ - DomainOrIP: "bad2.example.com.", - Wildcard: true, - Permit: false, - Created: time.Now().Unix(), + Type: EptDomain, + Value: "*bad2.example.com.", + Permit: false, + Created: time.Now().Unix(), }, &EndpointPermission{ - DomainOrIP: "", - Wildcard: true, - Permit: true, - Protocol: 6, - StartPort: 80, - EndPort: 80, - Created: time.Now().Unix(), + Type: EptAny, + Permit: true, + Protocol: 6, + StartPort: 80, + EndPort: 80, + Created: time.Now().Unix(), }, }, ServiceEndpoints: []*EndpointPermission{ &EndpointPermission{ - DomainOrIP: "", - Wildcard: true, - Permit: true, - Protocol: 17, - StartPort: 12345, - EndPort: 12347, - Created: time.Now().Unix(), + Type: EptAny, + Permit: true, + Protocol: 17, + StartPort: 12345, + EndPort: 12347, + Created: time.Now().Unix(), }, &EndpointPermission{ // default deny - DomainOrIP: "", - Wildcard: true, - Permit: false, - Created: time.Now().Unix(), + Type: EptAny, + Permit: false, + Created: time.Now().Unix(), }, }, } @@ -104,25 +102,39 @@ func init() { func testFlag(t *testing.T, set *Set, flag uint8, shouldBeActive bool) { active := set.CheckFlag(flag) if active != shouldBeActive { - t.Errorf("unexpected result: flag %s: permitted=%v, expected=%v", flagNames[flag], active, shouldBeActive) + t.Errorf("unexpected result: flag %s: active=%v, expected=%v", flagNames[flag], active, shouldBeActive) } } -func testEndpoint(t *testing.T, set *Set, domainOrIP string, protocol uint8, port uint16, inbound bool, shouldBePermitted bool) { - var permitted, ok bool - permitted, _, ok = set.CheckEndpoint(domainOrIP, protocol, port, inbound) - if !ok { - t.Errorf("endpoint %s/%d/%d/%v should be in test profile set", domainOrIP, protocol, port, inbound) - } - if permitted != shouldBePermitted { - t.Errorf("unexpected result for endpoint %s/%d/%d/%v: permitted=%v, expected=%v", domainOrIP, protocol, port, inbound, permitted, shouldBePermitted) +func testEndpointDomain(t *testing.T, set *Set, domain string, expectedResult EPResult) { + var result EPResult + result, _ = set.CheckEndpointDomain(domain) + if result != expectedResult { + t.Errorf( + "line %d: unexpected result for endpoint domain %s: result=%s, expected=%s", + testutils.GetLineNumberOfCaller(1), + domain, + result, + expectedResult, + ) } } -func testUnregulatedEndpoint(t *testing.T, set *Set, domainOrIP string, protocol uint8, port uint16, inbound bool) { - _, _, ok := set.CheckEndpoint(domainOrIP, protocol, port, inbound) - if ok { - t.Errorf("endpoint %s/%d/%d/%v should not be in test profile set", domainOrIP, protocol, port, inbound) +func testEndpointIP(t *testing.T, set *Set, domain string, ip net.IP, protocol uint8, port uint16, inbound bool, expectedResult EPResult) { + var result EPResult + result, _ = set.CheckEndpointIP(domain, ip, protocol, port, inbound) + if result != expectedResult { + t.Errorf( + "line %d: unexpected result for endpoint %s/%s/%d/%d/%v: result=%s, expected=%s", + testutils.GetLineNumberOfCaller(1), + domain, + ip, + protocol, + port, + inbound, + result, + expectedResult, + ) } } @@ -133,28 +145,28 @@ func TestProfileSet(t *testing.T) { set.Update(status.SecurityLevelDynamic) testFlag(t, set, Whitelist, false) // testFlag(t, set, Internet, true) - testEndpoint(t, set, "example.com.", 0, 0, false, true) - testEndpoint(t, set, "bad.example.com.", 0, 0, false, false) - testEndpoint(t, set, "other.bad.example.com.", 0, 0, false, false) - testEndpoint(t, set, "good.bad.example.com.", 0, 0, false, true) - testEndpoint(t, set, "bad2.example.com.", 0, 0, false, false) - testEndpoint(t, set, "10.2.3.4", 6, 22000, false, true) - testEndpoint(t, set, "fd00::1", 6, 22000, false, true) - testEndpoint(t, set, "test.local.", 6, 22000, false, true) - testUnregulatedEndpoint(t, set, "other.example.com.", 0, 0, false) - testUnregulatedEndpoint(t, set, "10.2.3.4", 17, 53, false) - testUnregulatedEndpoint(t, set, "10.2.3.4", 17, 443, false) - testUnregulatedEndpoint(t, set, "10.2.3.4", 6, 12346, false) - testEndpoint(t, set, "10.2.3.4", 17, 12345, true, true) - testEndpoint(t, set, "fd00::1", 17, 12347, true, true) + testEndpointDomain(t, set, "example.com.", Permitted) + testEndpointDomain(t, set, "bad.example.com.", Denied) + testEndpointDomain(t, set, "other.bad.example.com.", Denied) + testEndpointDomain(t, set, "good.bad.example.com.", Permitted) + testEndpointDomain(t, set, "bad2.example.com.", Undeterminable) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 22000, false, Permitted) + testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 6, 22000, false, Permitted) + testEndpointDomain(t, set, "test.local.", Undeterminable) + testEndpointDomain(t, set, "other.example.com.", Undeterminable) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 53, false, NoMatch) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 443, false, NoMatch) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 12346, false, NoMatch) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Permitted) + testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Permitted) set.Update(status.SecurityLevelSecure) // testFlag(t, set, Internet, true) set.Update(status.SecurityLevelFortress) // Independent! testFlag(t, set, Whitelist, true) - testEndpoint(t, set, "10.2.3.4", 17, 12345, true, false) - testEndpoint(t, set, "fd00::1", 17, 12347, true, false) - testUnregulatedEndpoint(t, set, "10.2.3.4", 6, 80, false) - testUnregulatedEndpoint(t, set, "bad2.example.com.", 0, 0, false) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Denied) + testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Denied) + testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 80, false, NoMatch) + testEndpointDomain(t, set, "bad2.example.com.", Undeterminable) } diff --git a/profile/specialprofiles.go b/profile/specialprofiles.go index d2e5ceb66..5011c0619 100644 --- a/profile/specialprofiles.go +++ b/profile/specialprofiles.go @@ -48,8 +48,7 @@ func getSpecialProfile(ID string) (*Profile, error) { func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) { for _, ep := range p.ServiceEndpoints { if ep != nil { - if ep.DomainOrIP == "" && - ep.Wildcard == true && + if ep.Type == EptAny && ep.Protocol == 0 && ep.StartPort == 0 && ep.EndPort == 0 && @@ -60,12 +59,11 @@ func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) { } p.ServiceEndpoints = append(p.ServiceEndpoints, &EndpointPermission{ - DomainOrIP: "", - Wildcard: true, - Protocol: 0, - StartPort: 0, - EndPort: 0, - Permit: false, + Type: EptAny, + Protocol: 0, + StartPort: 0, + EndPort: 0, + Permit: false, }) return true } diff --git a/profile/updates.go b/profile/updates.go index 7d7670521..b8bc603fc 100644 --- a/profile/updates.go +++ b/profile/updates.go @@ -2,6 +2,7 @@ package profile import ( "strings" + "sync/atomic" "github.com/Safing/portbase/database" "github.com/Safing/portbase/database/query" @@ -56,11 +57,27 @@ func updateListener(sub *database.Subscription) { switch { case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")): updateActiveUserProfile(profile) + increaseUpdateVersion() case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")): updateActiveStampProfile(profile) + increaseUpdateVersion() } } } } } + +var ( + updateVersion uint32 +) + +// GetUpdateVersion returns the current profiles internal update version +func GetUpdateVersion() uint32 { + return atomic.LoadUint32(&updateVersion) +} + +func increaseUpdateVersion() { + // we intentially want to wrap + atomic.AddUint32(&updateVersion, 1) +} diff --git a/status/get-config.go b/status/get-config.go index 00349f739..c8641551e 100644 --- a/status/get-config.go +++ b/status/get-config.go @@ -18,16 +18,16 @@ func max(a, b uint8) uint8 { // ConfigIsActive returns whether the given security level dependent config option is on or off. func ConfigIsActive(name string) SecurityLevelOption { - activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic)) + activeAtLevel := config.GetAsInt(name, int64(SecurityLevelsAll)) return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel) + return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 } } // ConfigIsActiveConcurrent returns whether the given security level dependent config option is on or off and is concurrency safe. func ConfigIsActiveConcurrent(name string) SecurityLevelOption { - activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelDynamic)) + activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll)) return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel) + return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 } } diff --git a/status/module.go b/status/module.go index 96e0b093a..9096cc8c3 100644 --- a/status/module.go +++ b/status/module.go @@ -4,6 +4,9 @@ import ( "github.com/Safing/portbase/database" "github.com/Safing/portbase/log" "github.com/Safing/portbase/modules" + + // module dependencies + _ "github.com/Safing/portmaster/core" ) var ( @@ -11,7 +14,7 @@ var ( ) func init() { - modules.Register("status", nil, start, stop, "database") + modules.Register("status", nil, start, stop, "core") } func start() error { diff --git a/threats/all.go b/threats/all.go new file mode 100644 index 000000000..6ec88a42b --- /dev/null +++ b/threats/all.go @@ -0,0 +1,6 @@ +package threats + +import ( + _ "github.com/Safing/portmaster/threats/arp" + _ "github.com/Safing/portmaster/threats/portscan" +) diff --git a/threats/arp/arpentry.go b/threats/arp/arpentry.go new file mode 100644 index 000000000..68e1e8d6d --- /dev/null +++ b/threats/arp/arpentry.go @@ -0,0 +1,7 @@ +package arp + +type arpEntry struct { + IP string + MAC string + Interface string +} diff --git a/threats/arp/os_linux.go b/threats/arp/os_linux.go new file mode 100644 index 000000000..3512d54b9 --- /dev/null +++ b/threats/arp/os_linux.go @@ -0,0 +1,48 @@ +package arp + +import ( + "bufio" + "os" + "strings" + + "github.com/Safing/portbase/log" +) + +const ( + arpTableProcFile = "/proc/net/arp" +) + +func getArpTable() (table []*arpEntry, err error) { + // open file + arpData, err := os.Open(arpTableProcFile) + if err != nil { + log.Warningf("threats/arp: could not read %s: %s", arpTableProcFile, err) + return nil, err + } + defer arpData.Close() + + // file scanner + scanner := bufio.NewScanner(arpData) + scanner.Split(bufio.ScanLines) + + // parse + scanner.Scan() // skip first line + for scanner.Scan() { + line := strings.Fields(scanner.Text()) + if len(line) < 6 { + continue + } + + table = append(table, &arpEntry{ + IP: line[0], + MAC: line[3], + Interface: line[5], + }) + } + + return table, nil +} + +func clearArpTable() error { + return nil +} diff --git a/threats/portscan/detection.go b/threats/portscan/detection.go new file mode 100644 index 000000000..416aa5341 --- /dev/null +++ b/threats/portscan/detection.go @@ -0,0 +1 @@ +package portscan diff --git a/threats/portscan/module.go b/threats/portscan/module.go new file mode 100644 index 000000000..416aa5341 --- /dev/null +++ b/threats/portscan/module.go @@ -0,0 +1 @@ +package portscan diff --git a/ui/serve.go b/ui/serve.go index d6ef8af6c..6274389b7 100644 --- a/ui/serve.go +++ b/ui/serve.go @@ -103,6 +103,7 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri return } + // set content type _, ok := w.Header()["Content-Type"] if !ok { contentType := mime.TypeByExtension(filepath.Ext(path)) @@ -111,6 +112,9 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri } } + // set content security policy + w.Header().Set("Content-Security-Policy", "default-src 'self'") + w.WriteHeader(http.StatusOK) if r.Method != "HEAD" { _, err = io.Copy(w, readCloser) diff --git a/updates/main.go b/updates/main.go index 98ae95e25..0a168b618 100644 --- a/updates/main.go +++ b/updates/main.go @@ -7,7 +7,11 @@ import ( "runtime" "github.com/Safing/portbase/database" + "github.com/Safing/portbase/info" "github.com/Safing/portbase/modules" + + // module dependencies + _ "github.com/Safing/portmaster/core" ) var ( @@ -15,10 +19,11 @@ var ( ) func init() { - modules.Register("updates", prep, start, nil, "global", "database") + modules.Register("updates", prep, start, nil, "core") } func prep() error { + status.Core = info.GetInfo() updateStoragePath = filepath.Join(database.GetDatabaseRoot(), "updates") err := checkUpdateDirs() diff --git a/updates/status.go b/updates/status.go index 2333c66cc..7a61c555d 100644 --- a/updates/status.go +++ b/updates/status.go @@ -7,6 +7,7 @@ import ( "github.com/Safing/portbase/database" "github.com/Safing/portbase/database/query" "github.com/Safing/portbase/database/record" + "github.com/Safing/portbase/info" "github.com/Safing/portbase/log" "github.com/tevino/abool" ) @@ -36,7 +37,7 @@ var ( func init() { status = &versionStatus{ - Versions: make(map[string]*versionStatusEntry), + Modules: make(map[string]*versionStatusEntry), } status.SetKey(statusDBKey) } @@ -45,7 +46,9 @@ func init() { type versionStatus struct { record.Base sync.Mutex - Versions map[string]*versionStatusEntry + + Core *info.Info + Modules map[string]*versionStatusEntry } func (vs *versionStatus) save() { @@ -68,10 +71,10 @@ func updateUsedStatus(identifier string, version string) { status.Lock() defer status.Unlock() - entry, ok := status.Versions[identifier] + entry, ok := status.Modules[identifier] if !ok { entry = &versionStatusEntry{} - status.Versions[identifier] = entry + status.Modules[identifier] = entry } entry.LastVersionUsed = version @@ -87,10 +90,10 @@ func updateStatus(vClass versionClass, state map[string]string) { for identifier, version := range state { - entry, ok := status.Versions[identifier] + entry, ok := status.Modules[identifier] if !ok { entry = &versionStatusEntry{} - status.Versions[identifier] = entry + status.Modules[identifier] = entry } switch vClass {