Fix handling of connectivity / captive portal domains

Also, improve handling of queries during being captive.
This commit is contained in:
Daniel 2020-07-17 16:09:46 +02:00
parent a6e161e0a1
commit 68c2d23c1b
15 changed files with 223 additions and 63 deletions

View file

@ -1,9 +1,11 @@
package firewall package firewall
import ( import (
"context"
"net" "net"
"os" "os"
"strings" "strings"
"time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
@ -77,18 +79,13 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
} }
func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache { func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache {
p := conn.Process().Profile()
// do not modify own queries // do not modify own queries
if conn.Process().Pid == os.Getpid() { if conn.Process().Pid == os.Getpid() {
return rrCache return rrCache
} }
// get profile
p := conn.Process().Profile()
if p == nil {
conn.Block("no profile")
return nil
}
// check if DNS response filtering is completely turned off // check if DNS response filtering is completely turned off
if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() { if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() {
return rrCache return rrCache
@ -112,6 +109,31 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res
rrCache.Filtered = true rrCache.Filtered = true
if validIPs == 0 { if validIPs == 0 {
conn.Block("no addresses returned for this domain are permitted") conn.Block("no addresses returned for this domain are permitted")
// If all entries are filtered, this could mean that these are broken/bogus resource records.
if rrCache.Expired() {
// If the entry is expired, force delete it.
err := resolver.DeleteNameRecord(rrCache.Domain, rrCache.Question.String())
if err != nil && err != database.ErrNotFound {
log.Warningf(
"filter: failed to delete fully filtered name cache for %s: %s",
rrCache.ID(),
err,
)
}
} else if rrCache.TTL > time.Now().Add(10*time.Second).Unix() {
// Set a low TTL of 10 seconds if TTL is higher than that.
rrCache.TTL = time.Now().Add(10 * time.Second).Unix()
err := rrCache.Save()
if err != nil {
log.Debugf(
"filter: failed to set shorter TTL on fully filtered name cache for %s: %s",
rrCache.ID(),
err,
)
}
}
return nil return nil
} }
@ -122,7 +144,25 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res
} }
// DecideOnResolvedDNS filters a dns response according to the application profile and settings. // DecideOnResolvedDNS filters a dns response according to the application profile and settings.
func DecideOnResolvedDNS(conn *network.Connection, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { func DecideOnResolvedDNS(
ctx context.Context,
conn *network.Connection,
q *resolver.Query,
rrCache *resolver.RRCache,
) *resolver.RRCache {
// check profile
if checkProfileExists(ctx, conn, nil) {
// returns true if check triggered
return nil
}
// special grant for connectivity domains
if checkConnectivityDomain(ctx, conn, nil) {
// returns true if check triggered
return rrCache
}
updatedRR := filterDNSResponse(conn, rrCache) updatedRR := filterDNSResponse(conn, rrCache)
if updatedRR == nil { if updatedRR == nil {
return nil return nil

View file

@ -52,7 +52,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
checkSelfCommunication, checkSelfCommunication,
checkProfileExists, checkProfileExists,
checkConnectionType, checkConnectionType,
checkCaptivePortal, checkConnectivityDomain,
checkConnectionScope, checkConnectionScope,
checkEndpointLists, checkEndpointLists,
checkBypassPrevention, checkBypassPrevention,
@ -181,10 +181,17 @@ func checkConnectionType(ctx context.Context, conn *network.Connection, _ packet
return false return false
} }
func checkCaptivePortal(_ context.Context, conn *network.Connection, _ packet.Packet) bool { func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
if netenv.GetOnlineStatus() == netenv.StatusPortal && p := conn.Process().Profile()
conn.Entity.Domain == netenv.GetCaptivePortal().Domain {
conn.Accept("captive portal access permitted") if !p.BlockScopeInternet() {
// Special grant only applies if application is allowed to connect to the Internet.
return false
}
if netenv.GetOnlineStatus() <= netenv.StatusPortal &&
netenv.IsConnectivityDomain(conn.Entity.Domain) {
conn.Accept("special grant for connectivity domain during network bootstrap")
return true return true
} }

View file

@ -274,7 +274,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
} }
tracer.Trace("nameserver: deciding on resolved dns") tracer.Trace("nameserver: deciding on resolved dns")
rrCache = firewall.DecideOnResolvedDNS(conn, q, rrCache) rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache)
if rrCache == nil { if rrCache == nil {
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext) sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
return nil return nil

View file

@ -11,6 +11,10 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/safing/portbase/database"
"github.com/safing/portbase/notifications"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
@ -67,8 +71,41 @@ func init() {
// IsConnectivityDomain checks whether the given domain (fqdn) is used for any connectivity related network connections and should always be resolved using the network assigned DNS server. // IsConnectivityDomain checks whether the given domain (fqdn) is used for any connectivity related network connections and should always be resolved using the network assigned DNS server.
func IsConnectivityDomain(domain string) bool { func IsConnectivityDomain(domain string) bool {
switch domain { switch domain {
case "detectportal.firefox.com.", case "one.one.one.one.", // Internal DNS Check
"one.one.one.one.",
// Windows
"dns.msftncsi.com.", // DNS Check
"msftncsi.com.", // Older
"www.msftncsi.com.",
"microsoftconnecttest.com.", // Newer
"www.microsoftconnecttest.com.",
"ipv6.microsoftconnecttest.com.",
// https://de.wikipedia.org/wiki/Captive_Portal
// https://docs.microsoft.com/en-us/windows-hardware/drivers/mobilebroadband/captive-portals
// TODO: read value from registry: HKLM:\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet
// Apple
"captive.apple.com.",
// https://de.wikipedia.org/wiki/Captive_Portal
// Linux
"connectivity-check.ubuntu.com.", // Ubuntu
"nmcheck.gnome.org.", // Gnome DE
"network-test.debian.org.", // Debian
// There are probably a lot more domains for all the Linux Distro/DE Variants. Please raise issues and/or submit PRs!
// https://github.com/solus-project/budgie-desktop/issues/807
// https://www.lguruprasad.in/blog/2015/07/21/enabling-captive-portal-detection-in-gnome-3-14-on-debian-jessie/
// TODO: read value from NetworkManager config: /etc/NetworkManager/conf.d/*.conf
// Android
"connectivitycheck.gstatic.com.",
// https://de.wikipedia.org/wiki/Captive_Portal
// Other
"neverssl.com.", // Common Community Service
"detectportal.firefox.com.", // Firefox
// Redirected Domain
GetCaptivePortal().Domain: GetCaptivePortal().Domain:
return true return true
} }
@ -108,6 +145,7 @@ var (
captivePortal = &CaptivePortal{} captivePortal = &CaptivePortal{}
captivePortalLock sync.Mutex captivePortalLock sync.Mutex
captivePortalNotification *notifications.Notification
) )
// CaptivePortal holds information about a detected captive portal. // CaptivePortal holds information about a detected captive portal.
@ -180,9 +218,27 @@ func setCaptivePortal(portalURL string) {
// delete // delete
if portalURL == "" { if portalURL == "" {
captivePortal = &CaptivePortal{} captivePortal = &CaptivePortal{}
if captivePortalNotification != nil {
err := captivePortalNotification.Delete()
if err != nil && err != database.ErrNotFound {
log.Warningf("netenv: failed to delete old captive portal notification: %s", err)
}
captivePortalNotification = nil
}
return return
} }
// return if unchanged
if portalURL == captivePortal.URL {
return
}
// notify
defer notifications.NotifyInfo(
"netenv:captive-portal:"+captivePortal.Domain,
"Portmaster detected a captive portal at "+captivePortal.Domain,
)
// set // set
captivePortal = &CaptivePortal{ captivePortal = &CaptivePortal{
URL: portalURL, URL: portalURL,
@ -378,17 +434,17 @@ func checkOnlineStatus(ctx context.Context) {
if err != nil { if err != nil {
log.Warningf("network: failed to read http body of captive portal testing response: %s", err) log.Warningf("network: failed to read http body of captive portal testing response: %s", err)
// assume we are online nonetheless // assume we are online nonetheless
// TODO: improve handling this case
updateOnlineStatus(StatusOnline, "", "http request succeeded, albeit failing later") updateOnlineStatus(StatusOnline, "", "http request succeeded, albeit failing later")
return return
} }
// check body contents // check body contents
if strings.TrimSpace(string(data)) == HTTPExpectedContent { if strings.TrimSpace(string(data)) != HTTPExpectedContent {
updateOnlineStatus(StatusOnline, "", "http request succeeded") // Something is interfering with the website content.
} else { // This probably is a captive portal, just direct the user there.
// something is interfering with the website content
// this might be a weird captive portal, just direct the user there
updateOnlineStatus(StatusPortal, "detectportal.firefox.com", "http request succeeded, response content not as expected") updateOnlineStatus(StatusPortal, "detectportal.firefox.com", "http request succeeded, response content not as expected")
return
} }
// close the body now as we plan to reuse the http.Client // close the body now as we plan to reuse the http.Client
response.Body.Close() response.Body.Close()

View file

@ -180,6 +180,11 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
// check if expired // check if expired
if rrCache.Expired() { if rrCache.Expired() {
if netenv.IsConnectivityDomain(rrCache.Domain) {
// do not use cache, resolve immediately
return nil
}
rrCache.Lock() rrCache.Lock()
rrCache.requestingNew = true rrCache.requestingNew = true
rrCache.Unlock() rrCache.Unlock()

View file

@ -7,6 +7,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/safing/portmaster/netenv"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -75,9 +77,17 @@ func (rrCache *RRCache) Clean(minExpires uint32) {
lowestTTL = minExpires lowestTTL = minExpires
} }
// shorten NXDomain caching // shorten caching
if len(rrCache.Answer) == 0 { switch {
case rrCache.IsNXDomain():
// NXDomain
lowestTTL = 10 lowestTTL = 10
case netenv.IsConnectivityDomain(rrCache.Domain):
// Responses from these domains might change very quickly depending on the environment.
lowestTTL = 3
case !netenv.Online():
// Not being fully online could mean that we get funny responses.
lowestTTL = 60
} }
// log.Tracef("lowest TTL is %d", lowestTTL) // log.Tracef("lowest TTL is %d", lowestTTL)

View file

@ -1,6 +1,6 @@
package status package status
// Definitions of Security and Status Levels // Definitions of Security and Status Levels.
const ( const (
SecurityLevelOff uint8 = 0 SecurityLevelOff uint8 = 0

View file

@ -1,6 +1,8 @@
package status package status
import ( import (
"context"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/database/query" "github.com/safing/portbase/database/query"
"github.com/safing/portbase/database/record" "github.com/safing/portbase/database/record"
@ -35,7 +37,10 @@ func (sh *statusHook) PrePut(r record.Record) (record.Record, error) {
// apply applicable settings // apply applicable settings
if SelectedSecurityLevel() != newStatus.SelectedSecurityLevel { if SelectedSecurityLevel() != newStatus.SelectedSecurityLevel {
go setSelectedSecurityLevel(newStatus.SelectedSecurityLevel) module.StartWorker("set selected security level", func(_ context.Context) error {
setSelectedSecurityLevel(newStatus.SelectedSecurityLevel)
return nil
})
} }
// TODO: allow setting of Gate17 status (on/off) // TODO: allow setting of Gate17 status (on/off)

View file

@ -7,22 +7,16 @@ import (
var ( var (
activeSecurityLevel *uint32 activeSecurityLevel *uint32
selectedSecurityLevel *uint32 selectedSecurityLevel *uint32
portmasterStatus *uint32
gate17Status *uint32
) )
func init() { func init() {
var ( var (
activeSecurityLevelValue uint32 activeSecurityLevelValue uint32
selectedSecurityLevelValue uint32 selectedSecurityLevelValue uint32
portmasterStatusValue uint32
gate17StatusValue uint32
) )
activeSecurityLevel = &activeSecurityLevelValue activeSecurityLevel = &activeSecurityLevelValue
selectedSecurityLevel = &selectedSecurityLevelValue selectedSecurityLevel = &selectedSecurityLevelValue
portmasterStatus = &portmasterStatusValue
gate17Status = &gate17StatusValue
} }
// ActiveSecurityLevel returns the current security level. // ActiveSecurityLevel returns the current security level.
@ -34,13 +28,3 @@ func ActiveSecurityLevel() uint8 {
func SelectedSecurityLevel() uint8 { func SelectedSecurityLevel() uint8 {
return uint8(atomic.LoadUint32(selectedSecurityLevel)) return uint8(atomic.LoadUint32(selectedSecurityLevel))
} }
// PortmasterStatus returns the current Portmaster status.
func PortmasterStatus() uint8 {
return uint8(atomic.LoadUint32(portmasterStatus))
}
// Gate17Status returns the current Gate17 status.
func Gate17Status() uint8 {
return uint8(atomic.LoadUint32(gate17Status))
}

View file

@ -8,8 +8,6 @@ func TestGet(t *testing.T) {
// TODO: write real tests // TODO: write real tests
ActiveSecurityLevel() ActiveSecurityLevel()
SelectedSecurityLevel() SelectedSecurityLevel()
PortmasterStatus()
Gate17Status()
option := ConfigIsActive("invalid") option := ConfigIsActive("invalid")
option(0) option(0)
option = ConfigIsActiveConcurrent("invalid") option = ConfigIsActiveConcurrent("invalid")

View file

@ -6,21 +6,40 @@ import (
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
) )
var (
module *modules.Module
)
func init() { func init() {
modules.Register("status", nil, start, stop, "base") module = modules.Register("status", nil, start, stop, "base")
} }
func start() error { func start() error {
var loadedStatus *SystemStatus err := initSystemStatus()
if err != nil {
return err
}
err = startNetEnvHooking()
if err != nil {
return err
}
status.Save()
return initStatusHook()
}
func initSystemStatus() error {
// load status from database // load status from database
r, err := statusDB.Get(statusDBKey) r, err := statusDB.Get(statusDBKey)
switch err { switch err {
case nil: case nil:
loadedStatus, err = EnsureSystemStatus(r) loadedStatus, err := EnsureSystemStatus(r)
if err != nil { if err != nil {
log.Criticalf("status: failed to unwrap system status: %s", err) log.Criticalf("status: failed to unwrap system status: %s", err)
loadedStatus = nil } else {
status = loadedStatus
} }
case database.ErrNotFound: case database.ErrNotFound:
// create new status // create new status
@ -28,10 +47,6 @@ func start() error {
log.Criticalf("status: failed to load system status: %s", err) log.Criticalf("status: failed to load system status: %s", err)
} }
// activate loaded status, if available
if loadedStatus != nil {
status = loadedStatus
}
status.Lock() status.Lock()
defer status.Unlock() defer status.Unlock()
@ -41,10 +56,9 @@ func start() error {
// update status // update status
status.updateThreatMitigationLevel() status.updateThreatMitigationLevel()
status.autopilot() status.autopilot()
status.updateOnlineStatus()
go status.Save() return nil
return initStatusHook()
} }
func stop() error { func stop() error {

28
status/netenv.go Normal file
View file

@ -0,0 +1,28 @@
package status
import (
"context"
"github.com/safing/portmaster/netenv"
)
// startNetEnvHooking starts the listener for online status changes.
func startNetEnvHooking() error {
return module.RegisterEventHook(
"netenv",
netenv.OnlineStatusChangedEvent,
"update online status in system status",
func(_ context.Context, _ interface{}) error {
status.Lock()
status.updateOnlineStatus()
status.Unlock()
status.Save()
return nil
},
)
}
func (s *SystemStatus) updateOnlineStatus() {
s.OnlineStatus = netenv.GetOnlineStatus()
s.CaptivePortal = netenv.GetCaptivePortal()
}

View file

@ -6,7 +6,7 @@ import (
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
) )
// autopilot automatically adjusts the security level as needed // autopilot automatically adjusts the security level as needed.
func (s *SystemStatus) autopilot() { func (s *SystemStatus) autopilot() {
// check if users is overruling // check if users is overruling
if s.SelectedSecurityLevel > SecurityLevelOff { if s.SelectedSecurityLevel > SecurityLevelOff {
@ -33,19 +33,18 @@ func setSelectedSecurityLevel(level uint8) {
switch level { switch level {
case SecurityLevelOff, SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: case SecurityLevelOff, SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme:
status.Lock() status.Lock()
defer status.Unlock()
status.SelectedSecurityLevel = level status.SelectedSecurityLevel = level
atomicUpdateSelectedSecurityLevel(level) atomicUpdateSelectedSecurityLevel(level)
status.autopilot() status.autopilot()
go status.Save() status.Unlock()
status.Save()
default: default:
log.Errorf("status: tried to set selected security level to invalid value: %d", level) log.Errorf("status: tried to set selected security level to invalid value: %d", level)
} }
} }
// update functions for atomic stuff
func atomicUpdateActiveSecurityLevel(level uint8) { func atomicUpdateActiveSecurityLevel(level uint8) {
atomic.StoreUint32(activeSecurityLevel, uint32(level)) atomic.StoreUint32(activeSecurityLevel, uint32(level))
} }

View file

@ -1,9 +1,12 @@
package status package status
import ( import (
"context"
"fmt" "fmt"
"sync" "sync"
"github.com/safing/portmaster/netenv"
"github.com/safing/portbase/database/record" "github.com/safing/portbase/database/record"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
) )
@ -28,11 +31,22 @@ type SystemStatus struct {
ActiveSecurityLevel uint8 ActiveSecurityLevel uint8
SelectedSecurityLevel uint8 SelectedSecurityLevel uint8
OnlineStatus netenv.OnlineStatus
CaptivePortal *netenv.CaptivePortal
ThreatMitigationLevel uint8 ThreatMitigationLevel uint8
Threats map[string]*Threat Threats map[string]*Threat
} }
// Save saves the SystemStatus to the database // SaveAsync saves the SystemStatus to the database asynchronously.
func (s *SystemStatus) SaveAsync() {
module.StartWorker("save system status", func(_ context.Context) error {
s.Save()
return nil
})
}
// Save saves the SystemStatus to the database.
func (s *SystemStatus) Save() { func (s *SystemStatus) Save() {
err := statusDB.Put(s) err := statusDB.Put(s)
if err != nil { if err != nil {

View file

@ -26,7 +26,7 @@ func AddOrUpdateThreat(new *Threat) {
status.updateThreatMitigationLevel() status.updateThreatMitigationLevel()
status.autopilot() status.autopilot()
go status.Save() status.SaveAsync()
} }
// DeleteThreat deletes a threat from the system status. // DeleteThreat deletes a threat from the system status.
@ -38,7 +38,7 @@ func DeleteThreat(id string) {
status.updateThreatMitigationLevel() status.updateThreatMitigationLevel()
status.autopilot() status.autopilot()
go status.Save() status.SaveAsync()
} }
// GetThreats returns all threats who's IDs are prefixed by the given string, and also a locker for editing them. // GetThreats returns all threats who's IDs are prefixed by the given string, and also a locker for editing them.