diff --git a/firewall/dns.go b/firewall/dns.go index 32f63b70..953cfebe 100644 --- a/firewall/dns.go +++ b/firewall/dns.go @@ -16,7 +16,7 @@ import ( "github.com/safing/portmaster/resolver" ) -func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int, string) { +func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, resolverScope netutils.IPScope, sysResolver bool) ([]dns.RR, []string, int, string) { goodEntries := make([]dns.RR, 0, len(entries)) filteredRecords := make([]string, 0, len(entries)) @@ -38,16 +38,16 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ( goodEntries = append(goodEntries, rr) continue } - classification := netutils.ClassifyIP(ip) + ipScope := netutils.GetIPScope(ip) if p.RemoveOutOfScopeDNS() { switch { - case classification == netutils.HostLocal: + case ipScope.IsLocalhost(): // No DNS should return localhost addresses filteredRecords = append(filteredRecords, rr.String()) interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey continue - case scope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + case resolverScope.IsGlobal() && ipScope.IsLAN() && !sysResolver: // No global DNS should return LAN addresses filteredRecords = append(filteredRecords, rr.String()) interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey @@ -55,18 +55,18 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ( } } - if p.RemoveBlockedDNS() { + if p.RemoveBlockedDNS() && !sysResolver { // filter by flags switch { - case p.BlockScopeInternet() && classification == netutils.Global: + case p.BlockScopeInternet() && ipScope.IsGlobal(): filteredRecords = append(filteredRecords, rr.String()) interveningOptionKey = profile.CfgOptionBlockScopeInternetKey continue - case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + case p.BlockScopeLAN() && ipScope.IsLAN(): filteredRecords = append(filteredRecords, rr.String()) interveningOptionKey = profile.CfgOptionBlockScopeLANKey continue - case p.BlockScopeLocal() && classification == netutils.HostLocal: + case p.BlockScopeLocal() && ipScope.IsLocalhost(): filteredRecords = append(filteredRecords, rr.String()) interveningOptionKey = profile.CfgOptionBlockScopeLocalKey continue @@ -83,7 +83,7 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ( return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey } -func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache { +func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache, sysResolver bool) *resolver.RRCache { p := conn.Process().Profile() // do not modify own queries @@ -104,11 +104,11 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res var validIPs int var interveningOptionKey string - rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope) + rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.Resolver.IPScope, sysResolver) rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) // we don't count the valid IPs in the extra section - rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope) + rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.Resolver.IPScope, sysResolver) rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) if len(rrCache.FilteredEntries) > 0 { @@ -160,8 +160,9 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res return rrCache } -// DecideOnResolvedDNS filters a dns response according to the application profile and settings. -func DecideOnResolvedDNS( +// FilterResolvedDNS filters a dns response according to the application +// profile and settings. +func FilterResolvedDNS( ctx context.Context, conn *network.Connection, q *resolver.Query, @@ -174,14 +175,15 @@ func DecideOnResolvedDNS( return rrCache } - updatedRR := filterDNSResponse(conn, rrCache) + // Only filter criticial things if request comes from the system resolver. + sysResolver := conn.Process().IsSystemResolver() + + updatedRR := filterDNSResponse(conn, rrCache, sysResolver) if updatedRR == nil { return nil } - updateIPsAndCNAMEs(q, rrCache, conn) - - if mayBlockCNAMEs(ctx, conn) { + if !sysResolver && mayBlockCNAMEs(ctx, conn) { return nil } @@ -213,14 +215,23 @@ func mayBlockCNAMEs(ctx context.Context, conn *network.Connection) bool { return false } -// updateIPsAndCNAMEs saves all the IP->Name mappings to the cache database and +// UpdateIPsAndCNAMEs saves all the IP->Name mappings to the cache database and // updates the CNAMEs in the Connection's Entity. -func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) { +func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) { + // Sanity check input, as this is called from defer. + if q == nil || rrCache == nil { + return + } + // Get profileID for scoping IPInfo. var profileID string - proc := conn.Process() - if proc != nil { - profileID = proc.LocalProfileKey + localProfile := conn.Process().Profile().LocalProfile() + switch localProfile.ID { + case profile.UnidentifiedProfileID, + profile.SystemResolverProfileID: + profileID = resolver.IPInfoProfileScopeGlobal + default: + profileID = localProfile.ID } // Collect IPs and CNAMEs. @@ -249,8 +260,9 @@ func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw // Create new record for this IP. record := resolver.ResolvedDomain{ - Domain: q.FQDN, - Expires: rrCache.Expires, + Domain: q.FQDN, + Expires: rrCache.Expires, + Resolver: rrCache.Resolver, } // Resolve all CNAMEs in the correct order and add the to the record. diff --git a/firewall/master.go b/firewall/master.go index 6e92b7af..efe515d9 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -39,7 +39,7 @@ const noReasonOptionKey = "" type deciderFn func(context.Context, *network.Connection, packet.Packet) bool -var deciders = []deciderFn{ +var defaultDeciders = []deciderFn{ checkPortmasterConnection, checkSelfCommunication, checkConnectionType, @@ -53,6 +53,11 @@ var deciders = []deciderFn{ checkAutoPermitRelated, } +var dnsFromSystemResolverDeciders = []deciderFn{ + checkConnectivityDomain, + checkBypassPrevention, +} + // DecideOnConnection makes a decision about a connection. // When called, the connection and profile is already locked. func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) { @@ -79,8 +84,21 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe } } + // DNS request from the system resolver require a special decision process, + // because the original requesting process is not known. Here, we only check + // global-only and the most important per-app aspects. The resulting + // connection is then blocked when the original requesting process is known. + if conn.Type == network.DNSRequest && conn.Process().IsSystemResolver() { + // Run all deciders and return if they came to a conclusion. + done, _ := runDeciders(ctx, dnsFromSystemResolverDeciders, conn, pkt) + if !done { + conn.Accept("permitting system resolver dns request", noReasonOptionKey) + } + return + } + // Run all deciders and return if they came to a conclusion. - done, defaultAction := runDeciders(ctx, conn, pkt) + done, defaultAction := runDeciders(ctx, defaultDeciders, conn, pkt) if done { return } @@ -96,7 +114,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe } } -func runDeciders(ctx context.Context, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) { +func runDeciders(ctx context.Context, selectedDeciders []deciderFn, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) { layeredProfile := conn.Process().Profile() // Read-lock the all the profiles. @@ -104,7 +122,7 @@ func runDeciders(ctx context.Context, conn *network.Connection, pkt packet.Packe defer layeredProfile.UnlockForUsage() // Go though all deciders, return if one sets an action. - for _, decider := range deciders { + for _, decider := range selectedDeciders { if decider(ctx, conn, pkt) { return true, profile.DefaultActionNotSet } @@ -248,39 +266,58 @@ func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ pack func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool { p := conn.Process().Profile() - // check scopes - if conn.Entity.IP != nil { - classification := netutils.ClassifyIP(conn.Entity.IP) - - switch classification { - case netutils.Global, netutils.GlobalMulticast: - if p.BlockScopeInternet() { - conn.Deny("Internet access blocked", profile.CfgOptionBlockScopeInternetKey) // Block Outbound / Drop Inbound - return true - } - case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast: - if p.BlockScopeLAN() { - conn.Block("LAN access blocked", profile.CfgOptionBlockScopeLANKey) // Block Outbound / Drop Inbound - return true - } - case netutils.HostLocal: - if p.BlockScopeLocal() { - conn.Block("Localhost access blocked", profile.CfgOptionBlockScopeLocalKey) // Block Outbound / Drop Inbound - return true - } - default: // netutils.Invalid - conn.Deny("invalid IP", noReasonOptionKey) // Block Outbound / Drop Inbound - return true - } - } else if conn.Entity.Domain != "" { - // This is a DNS Request. + // If we are handling a DNS request, check if we can immediately block it. + if conn.Type == network.DNSRequest { // DNS is expected to resolve to LAN or Internet addresses. // Localhost queries are immediately responded to by the nameserver. if p.BlockScopeInternet() && p.BlockScopeLAN() { conn.Block("Internet and LAN access blocked", profile.CfgOptionBlockScopeInternetKey) return true } + + return false } + + // Check if the network scope is permitted. + switch conn.Entity.IPScope { + case netutils.Global, netutils.GlobalMulticast: + if p.BlockScopeInternet() { + conn.Deny("Internet access blocked", profile.CfgOptionBlockScopeInternetKey) // Block Outbound / Drop Inbound + return true + } + case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast: + if p.BlockScopeLAN() { + conn.Block("LAN access blocked", profile.CfgOptionBlockScopeLANKey) // Block Outbound / Drop Inbound + return true + } + case netutils.HostLocal: + if p.BlockScopeLocal() { + conn.Block("Localhost access blocked", profile.CfgOptionBlockScopeLocalKey) // Block Outbound / Drop Inbound + return true + } + default: // netutils.Unknown and netutils.Invalid + conn.Deny("invalid IP", noReasonOptionKey) // Block Outbound / Drop Inbound + return true + } + + // If the IP address was resolved, check the scope of the resolver. + switch { + case p.RemoveOutOfScopeDNS(): + // Out of scope checking is not active. + case conn.Resolver == nil: + // IP address of connection was not resolved. + case conn.Resolver.IPScope.IsGlobal() && + (conn.Entity.IPScope.IsLAN() || conn.Entity.IPScope.IsLocalhost()): + // Block global resolvers from returning LAN/Localhost IPs. + conn.Block("DNS server horizon violation: global DNS server returned local IP address", profile.CfgOptionRemoveOutOfScopeDNSKey) + return true + case conn.Resolver.IPScope.IsLAN() && + conn.Entity.IPScope.IsLocalhost(): + // Block LAN resolvers from returning Localhost IPs. + conn.Block("DNS server horizon violation: LAN DNS server returned localhost IP address", profile.CfgOptionRemoveOutOfScopeDNSKey) + return true + } + return false } diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index ad1cff37..6abf26bf 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -3,6 +3,7 @@ package nameserver import ( "context" "errors" + "fmt" "net" "strings" "time" @@ -57,6 +58,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) log.Warningf("nameserver: failed to get remote address of request for %s%s, ignoring", q.FQDN, q.QType) return nil } + // log.Errorf("DEBUG: nameserver: handling new request for %s from %s:%d", q.ID(), remoteAddr.IP, remoteAddr.Port) // Start context tracer for context-aware logging. ctx, tracer := log.AddTracer(ctx) @@ -133,20 +135,24 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) conn.Lock() defer conn.Unlock() + // Create reference for the rrCache. + var rrCache *resolver.RRCache + // Once we decided on the connection we might need to save it to the database, // so we defer that check for now. defer func() { switch conn.Verdict { // We immediately save blocked, dropped or failed verdicts so // they pop up in the UI. - case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: + case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel: conn.Save() // For undecided or accepted connections we don't save them yet, because // that will happen later anyway. - case network.VerdictUndecided, network.VerdictAccept, - network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel: - return + case network.VerdictUndecided, network.VerdictAccept: + // Save the request as open, as we don't know if there will be a connection or not. + network.SaveOpenDNSRequest(conn) + firewall.UpdateIPsAndCNAMEs(q, rrCache, conn) default: tracer.Warningf("nameserver: unexpected verdict %s for connection %s, not saving", conn.Verdict, conn) @@ -162,17 +168,19 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) // IP address in which case we "accept" it, but let the firewall handle // the resolving as it wishes. if responder, ok := conn.Reason.Context.(nsutil.Responder); ok { - // Save the request as open, as we don't know if there will be a connection or not. - network.SaveOpenDNSRequest(conn) - tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason.Msg) return reply(responder) } - // Check if there is Verdict to act upon. + // Check if there is a Verdict to act upon. switch conn.Verdict { case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: - tracer.Infof("nameserver: request for %s from %s %s", q.ID(), conn.Process(), conn.Verdict.Verb()) + tracer.Infof( + "nameserver: returning %s response for %s to %s", + conn.Verdict.Verb(), + q.ID(), + conn.Process(), + ) return reply(conn, conn) } @@ -180,7 +188,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) q.SecurityLevel = conn.Process().Profile().SecurityLevel() // Resolve request. - rrCache, err := resolver.Resolve(ctx, q) + rrCache, err = resolver.Resolve(ctx, q) if err != nil { // React to special errors. switch { @@ -212,13 +220,10 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) } tracer.Trace("nameserver: deciding on resolved dns") - rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache) + rrCache = firewall.FilterResolvedDNS(ctx, conn, q, rrCache) if rrCache == nil { // Check again if there is a responder from the firewall. if responder, ok := conn.Reason.Context.(nsutil.Responder); ok { - // Save the request as open, as we don't know if there will be a connection or not. - network.SaveOpenDNSRequest(conn) - tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason.Msg) return reply(responder) } @@ -236,9 +241,6 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) } } - // Save dns request as open. - defer network.SaveOpenDNSRequest(conn) - // Revert back to non-standard question format, if we had to convert. if nonStandardQuestionFormat { rrCache.ReplaceAnswerNames(originalQuestion.Name) diff --git a/process/config.go b/process/config.go index 19f239ce..3e91aa55 100644 --- a/process/config.go +++ b/process/config.go @@ -17,7 +17,7 @@ func registerConfiguration() error { err := config.Register(&config.Option{ Name: "Process Detection", Key: CfgOptionEnableProcessDetectionKey, - Description: "This option enables the attribution of network traffic to processes. This should always be enabled, and effectively disables app settings if disabled.", + Description: "This option enables the attribution of network traffic to processes. Without it, app settings are effectively disabled.", OptType: config.OptTypeBool, ExpertiseLevel: config.ExpertiseLevelDeveloper, DefaultValue: true, diff --git a/profile/config.go b/profile/config.go index 5a0d37bb..9b36062a 100644 --- a/profile/config.go +++ b/profile/config.go @@ -434,7 +434,7 @@ The lists are automatically updated every hour using incremental updates. err = config.Register(&config.Option{ Name: "Enforce Global/Private Split-View", Key: CfgOptionRemoveOutOfScopeDNSKey, - Description: "Reject private IP addresses (RFC1918 et al.) from public DNS responses.", + Description: "Reject private IP addresses (RFC1918 et al.) from public DNS responses. If the system resolver is in use, the resulting connection will be blocked instead of the DNS request.", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelDeveloper, DefaultValue: status.SecurityLevelsAll, @@ -455,7 +455,7 @@ The lists are automatically updated every hour using incremental updates. err = config.Register(&config.Option{ Name: "Reject Blocked IPs", Key: CfgOptionRemoveBlockedDNSKey, - Description: "Reject blocked IP addresses directly from the DNS response instead of handing them over to the app and blocking a resulting connection.", + Description: "Reject blocked IP addresses directly from the DNS response instead of handing them over to the app and blocking a resulting connection. This settings does not affect privacy and only takes effect when the system resolver is not in use.", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelDeveloper, DefaultValue: status.SecurityLevelsAll, @@ -491,6 +491,7 @@ The lists are automatically updated every hour using incremental updates. return err } cfgOptionDomainHeuristics = config.Concurrent.GetAsInt(CfgOptionDomainHeuristicsKey, int64(status.SecurityLevelsAll)) + cfgIntOptions[CfgOptionDomainHeuristicsKey] = cfgOptionDomainHeuristics // Bypass prevention err = config.Register(&config.Option{ @@ -499,7 +500,9 @@ The lists are automatically updated every hour using incremental updates. Description: `Prevent apps from bypassing the privacy filter. Current Features: - Disable Firefox' internal DNS-over-HTTPs resolver -- Block direct access to public DNS resolvers`, +- Block direct access to public DNS resolvers + +Please note that if you are using the system resolver, bypass attempts might be additionally blocked there too.`, OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelUser, ReleaseLevel: config.ReleaseLevelBeta,