Merge pull request #299 from safing/feature/patch-set-1

Improvements for notifications and events
This commit is contained in:
Daniel
2021-05-13 17:02:36 +02:00
committed by GitHub
29 changed files with 392 additions and 393 deletions

View File

@@ -1,7 +1,9 @@
package core
import (
"context"
"net/http"
"time"
"github.com/safing/portbase/api"
"github.com/safing/portbase/log"
@@ -11,19 +13,33 @@ import (
"github.com/safing/portmaster/updates"
)
const (
eventShutdown = "shutdown"
eventRestart = "restart"
)
func registerEvents() {
module.RegisterEvent(eventShutdown, true)
module.RegisterEvent(eventRestart, true)
}
func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: "core/shutdown",
Read: api.PermitSelf,
ActionFunc: shutdown,
Path: "core/shutdown",
Write: api.PermitSelf,
ActionFunc: shutdown,
Name: "Shut Down Portmaster",
Description: "Shut down the Portmaster Core Service and all UI components.",
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: "core/restart",
Read: api.PermitAdmin,
ActionFunc: restart,
Path: "core/restart",
Write: api.PermitAdmin,
ActionFunc: restart,
Name: "Restart Portmaster",
Description: "Restart the Portmaster Core Service.",
}); err != nil {
return err
}
@@ -50,15 +66,35 @@ func registerAPIEndpoints() error {
// shutdown shuts the Portmaster down.
func shutdown(_ *api.Request) (msg string, err error) {
log.Warning("core: user requested shutdown via action")
// Do not use a worker, as this would block itself here.
go modules.Shutdown() //nolint:errcheck
module.StartWorker("shutdown", func(context.Context) error {
// Notify everyone of the shutdown.
module.TriggerEvent(eventShutdown, nil)
// Wait a bit for the event to propagate.
time.Sleep(1 * time.Second)
// Do not run in worker, as this would block itself here.
go modules.Shutdown() //nolint:errcheck
return nil
})
return "shutdown initiated", nil
}
// restart restarts the Portmaster.
func restart(_ *api.Request) (msg string, err error) {
log.Info("core: user requested restart via action")
updates.RestartNow()
module.StartWorker("restart", func(context.Context) error {
// Notify everyone of the shutdown.
module.TriggerEvent(eventRestart, nil)
// Wait a bit for the event to propagate.
time.Sleep(1 * time.Second)
updates.RestartNow()
return nil
})
return "restart initiated", nil
}

View File

@@ -4,7 +4,6 @@ import (
"flag"
"github.com/safing/portbase/config"
"github.com/safing/portbase/log"
)
// Configuration Keys.
@@ -19,13 +18,12 @@ var (
)
func init() {
flag.BoolVar(&defaultNetworkServiceMode, "network-service", false, "force network service mode")
}
func logFlagOverrides() {
if defaultNetworkServiceMode {
log.Warningf("core: %s config is being forced by the -network-service flag", CfgNetworkServiceKey)
}
flag.BoolVar(
&defaultNetworkServiceMode,
"network-service",
false,
"set default network service mode; configuration is stronger",
)
}
func registerConfig() error {

View File

@@ -1,99 +0,0 @@
package core
import (
"fmt"
"strings"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/database/storage"
)
// StorageInterface provices a storage.Interface to the storage manager.
type StorageInterface struct {
storage.InjectBase
}
// Get returns a database record.
func (s *StorageInterface) Get(key string) (record.Record, error) {
msg := newMessage(key)
splittedKey := strings.Split(key, "/")
switch splittedKey[0] {
case "module":
return controlModule(msg, splittedKey)
default:
return nil, storage.ErrNotFound
}
}
func controlModule(msg *MessageRecord, splittedKey []string) (record.Record, error) {
// format: module/moduleName/action/param
var moduleName string
var action string
var param string
var err error
// parse elements
switch len(splittedKey) {
case 4:
param = splittedKey[3]
fallthrough
case 3:
moduleName = splittedKey[1]
action = splittedKey[2]
default:
return nil, storage.ErrNotFound
}
// execute
switch action {
case "trigger":
err = module.InjectEvent(fmt.Sprintf("user triggered the '%s/%s' event", moduleName, param), moduleName, param, nil)
default:
return nil, storage.ErrNotFound
}
if err != nil {
msg.Message = err.Error()
} else {
msg.Success = true
}
return msg, nil
}
func registerControlDatabase() error {
_, err := database.Register(&database.Database{
Name: "control",
Description: "Control Interface for the Portmaster",
StorageType: "injected",
})
if err != nil {
return err
}
_, err = database.InjectDatabase("control", &StorageInterface{})
if err != nil {
return err
}
return nil
}
// MessageRecord is a simple record used for control database feedback
type MessageRecord struct {
record.Base
sync.Mutex
Success bool
Message string
}
func newMessage(key string) *MessageRecord {
m := &MessageRecord{}
m.SetKey("control:" + key)
m.UpdateMeta()
return m
}

View File

@@ -33,12 +33,15 @@ func prep() error {
registerEvents()
// init config
logFlagOverrides()
err := registerConfig()
if err != nil {
return err
}
if err := registerAPIEndpoints(); err != nil {
return err
}
return nil
}
@@ -47,18 +50,6 @@ func start() error {
return fmt.Errorf("failed to start plattform-specific components: %s", err)
}
if err := registerEventHooks(); err != nil {
return err
}
if err := registerControlDatabase(); err != nil {
return err
}
if err := registerAPIEndpoints(); err != nil {
return err
}
registerLogCleaner()
return nil

View File

@@ -1,50 +0,0 @@
// DEPRECATED: remove in v0.7
package core
import (
"context"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/updates"
)
const (
eventShutdown = "shutdown"
eventRestart = "restart"
)
func registerEvents() {
module.RegisterEvent(eventShutdown)
module.RegisterEvent(eventRestart)
}
func registerEventHooks() error {
err := module.RegisterEventHook("core", eventShutdown, "execute shutdown", shutdownHook)
if err != nil {
return err
}
err = module.RegisterEventHook("core", eventRestart, "execute shutdown", restartHook)
if err != nil {
return err
}
return nil
}
// shutdownHook shuts the Portmaster down.
func shutdownHook(ctx context.Context, _ interface{}) error {
log.Warning("core: user requested shutdown")
// Do not use a worker, as this would block itself here.
go modules.Shutdown() //nolint:errcheck
return nil
}
// restartHook restarts the Portmaster.
func restartHook(ctx context.Context, data interface{}) error {
log.Info("core: user requested restart")
updates.RestartNow()
return nil
}

View File

@@ -15,7 +15,7 @@ var (
)
func init() {
flag.BoolVar(&disableInterception, "disable-interception", false, "disable packet interception - this breaks a lot of functionality")
flag.BoolVar(&disableInterception, "disable-interception", false, "disable packet interception; this breaks a lot of functionality")
}
// Start starts the interception.

View File

@@ -3,9 +3,6 @@ package interception
import (
"fmt"
"github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
"github.com/safing/portbase/utils/osdetail"
"github.com/safing/portmaster/firewall/interception/windowskext"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/updates"
@@ -33,7 +30,6 @@ func start(ch chan packet.Packet) error {
}
go windowskext.Handler(ch)
go checkWindowsDNSCache()
return nil
}
@@ -42,28 +38,3 @@ func start(ch chan packet.Packet) error {
func stop() error {
return windowskext.Stop()
}
func checkWindowsDNSCache() {
status, err := osdetail.GetServiceStatus("dnscache")
if err != nil {
log.Warningf("firewall/interception: failed to check status of Windows DNS-Client: %s", err)
}
if status == osdetail.StatusStopped {
err := osdetail.EnableDNSCache()
if err != nil {
log.Warningf("firewall/interception: failed to enable Windows Service \"DNS Client\" (dnscache): %s", err)
} else {
log.Warningf("firewall/interception: successfully enabled the dnscache")
notifyRebootRequired()
}
}
}
func notifyRebootRequired() {
(&notifications.Notification{
EventID: "interception:windows-dnscache-reboot-required",
Message: "Please restart your system to complete Portmaster integration.",
Type: notifications.Warning,
}).Save()
}

View File

@@ -18,7 +18,7 @@ var (
)
func init() {
flag.StringVar(&packetMetricsDestination, "write-packet-metrics", "", "Write packet metrics to the specified file")
flag.StringVar(&packetMetricsDestination, "write-packet-metrics", "", "write packet metrics to the specified file")
}
type (

View File

@@ -16,12 +16,10 @@ var (
)
const (
filterlistsDisabled = "filterlists:disabled"
filterlistsUpdateFailed = "filterlists:update-failed"
filterlistsStaleDataSurvived = "filterlists:staledata"
filterlistsStaleDataDescr = "Removing stale filter list records failed. Some connections may be overblocked."
filterlistsUpdateInProgress = "filterlists:update-in-progress"
filterlistsUpdateInProgressDescr = "Performance slightly degraded during list update."
filterlistsDisabled = "filterlists:disabled"
filterlistsUpdateFailed = "filterlists:update-failed"
filterlistsStaleDataSurvived = "filterlists:staledata"
filterlistsUpdateInProgress = "filterlists:update-in-progress"
)
// booleans mainly used to decouple the module
@@ -91,7 +89,7 @@ func start() error {
if err != nil {
log.Debugf("intel/filterlists: blocklists disabled, waiting for update (%s)", err)
module.Warning(filterlistsDisabled, "Blocklist features disabled, waiting for update")
warnAboutDisabledFilterLists()
} else {
log.Debugf("intel/filterlists: using cache database")
close(filterListsLoaded)
@@ -104,3 +102,11 @@ func stop() error {
filterListsLoaded = make(chan struct{})
return nil
}
func warnAboutDisabledFilterLists() {
module.Warning(
filterlistsDisabled,
"Filter Lists Are Initializing",
"Filter lists are being downloaded and set up in the background. Until this initialization is finished, the filter lists are disabled and will not block anything.",
)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/updater"
"github.com/tevino/abool"
)
@@ -22,18 +23,20 @@ func tryListUpdate(ctx context.Context) error {
err := performUpdate(ctx)
if err != nil {
if !isLoaded() {
module.Error(filterlistsDisabled, err.Error())
} else {
module.Warning(filterlistsUpdateFailed, err.Error())
// Check if the module already has a failure status set. If not, set a
// generic one with the returned error.
failureStatus, _, _ := module.FailureStatus()
if failureStatus < modules.FailureWarning {
module.Warning(
filterlistsUpdateFailed,
"Filter Lists Update Failed",
fmt.Sprintf("The Portmaster failed to process a filter lists update. Filtering capabilities are currently either impaired or not available at all. Error: %s", err.Error()),
)
}
return err
}
// if the module is in an error, warning or hint state resolve that right now.
module.Resolve(filterlistsDisabled)
module.Resolve(filterlistsStaleDataSurvived)
module.Resolve(filterlistsUpdateInProgress)
return nil
}
@@ -44,8 +47,6 @@ func performUpdate(ctx context.Context) error {
}
defer updateInProgress.UnSet()
module.Hint(filterlistsUpdateInProgress, filterlistsUpdateInProgressDescr)
// First, update the list index.
err := updateListIndex()
if err != nil {
@@ -119,7 +120,11 @@ func performUpdate(ctx context.Context) error {
// if we failed to remove all stale cache entries
// we abort now WITHOUT updating the database version. This means
// we'll try again during the next update.
module.Warning(filterlistsStaleDataSurvived, filterlistsStaleDataDescr)
module.Warning(
filterlistsStaleDataSurvived,
"Filter Lists May Overblock",
fmt.Sprintf("The Portmaster failed to delete outdated filter list data. Filtering capabilities are fully available, but overblocking may occur. Error: %s", err.Error()),
)
return fmt.Errorf("failed to cleanup stale cache records: %w", err)
}
}
@@ -132,6 +137,8 @@ func performUpdate(ctx context.Context) error {
log.Infof("intel/filterlists: successfully migrated cache database to %s", highestVersion.Version())
}
// The list update suceeded, resolve any states.
module.Resolve("")
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"runtime"
"github.com/safing/portbase/config"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/core"
)
@@ -15,10 +14,9 @@ const (
)
var (
nameserverAddressFlag string
nameserverAddressConfig config.StringOption
defaultNameserverAddress = "localhost:53"
nameserverAddress string
nameserverAddressConfig config.StringOption
networkServiceMode config.BoolOption
)
@@ -29,22 +27,12 @@ func init() {
defaultNameserverAddress = "0.0.0.0:53"
}
flag.StringVar(&nameserverAddressFlag, "nameserver-address", "", "override nameserver listen address")
}
func logFlagOverrides() {
if nameserverAddressFlag != "" {
log.Warning("nameserver: dns/listenAddress default config is being overridden by the -nameserver-address flag")
}
}
func getDefaultNameserverAddress() string {
// check if overridden
if nameserverAddressFlag != "" {
return nameserverAddressFlag
}
// return internal default
return defaultNameserverAddress
flag.StringVar(
&nameserverAddress,
"nameserver-address",
defaultNameserverAddress,
"set default nameserver address; configuration is stronger",
)
}
func registerConfig() error {
@@ -55,7 +43,7 @@ func registerConfig() error {
OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: getDefaultNameserverAddress(),
DefaultValue: nameserverAddress,
ValidationRegex: "^(localhost|[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}|\\[[:0-9A-Fa-f]+\\]):[0-9]{1,5}$",
RequiresRestart: true,
Annotations: config.Annotations{
@@ -66,7 +54,7 @@ func registerConfig() error {
if err != nil {
return err
}
nameserverAddressConfig = config.GetAsString(CfgDefaultNameserverAddressKey, getDefaultNameserverAddress())
nameserverAddressConfig = config.GetAsString(CfgDefaultNameserverAddressKey, nameserverAddress)
networkServiceMode = config.Concurrent.GetAsBool(core.CfgNetworkServiceKey, false)

View File

@@ -37,8 +37,6 @@ func prep() error {
}
func start() error {
logFlagOverrides()
if err := registerMetrics(); err != nil {
return err
}

View File

@@ -55,14 +55,25 @@ func checkForConflictingService(ip net.IP, port uint16) error {
// Notify the user that we killed something.
notifications.Notify(&notifications.Notification{
EventID: "namserver:stopped-conflicting-service",
Type: notifications.Info,
Title: "Conflicting DNS Service",
Category: "Secure DNS",
EventID: "namserver:stopped-conflicting-service",
Type: notifications.Info,
Title: "Stopped Conflicting DNS Client",
Message: fmt.Sprintf(
"The Portmaster stopped a conflicting name service (pid %d) to gain required system integration.",
"The Portmaster stopped a conflicting DNS client (pid %d) to gain required system integration. If you are running another DNS client on this device on purpose, you can the check the documentation if it is compatible with the Portmaster.",
killed,
),
ShowOnSystem: true,
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
{
Text: "Open Docs",
Type: notifications.ActionTypeOpenURL,
Payload: "https://docs.safing.io/portmaster/install/status/software-compatibility",
},
},
})
// Restart nameserver via service-worker logic.

View File

@@ -16,8 +16,8 @@ var (
func init() {
module = modules.Register("netenv", prep, start, nil)
module.RegisterEvent(NetworkChangedEvent)
module.RegisterEvent(OnlineStatusChangedEvent)
module.RegisterEvent(NetworkChangedEvent, true)
module.RegisterEvent(OnlineStatusChangedEvent, true)
}
func prep() error {

View File

@@ -10,8 +10,6 @@ import (
"sync/atomic"
"time"
"github.com/safing/portbase/database"
"github.com/safing/portbase/notifications"
"github.com/safing/portbase/log"
@@ -243,24 +241,29 @@ func setCaptivePortal(portalURL *url.URL) {
// notify
cleanUpPortalNotification()
captivePortalNotification = notifications.Notify(&notifications.Notification{
EventID: "netenv:captive-portal",
Type: notifications.Info,
Title: "Captive Portal",
Category: "Core",
Message: fmt.Sprintf(
"Portmaster detected a captive portal at %s",
captivePortal.Domain,
),
EventData: captivePortal,
EventID: "netenv:captive-portal",
Type: notifications.Info,
Title: "Captive Portal Detected",
Message: "The Portmaster detected a captive portal. You might experience limited network connectivity until the portal is handled.",
ShowOnSystem: true,
EventData: captivePortal,
AvailableActions: []*notifications.Action{
{
Text: "Open Portal",
Type: notifications.ActionTypeOpenURL,
Payload: captivePortal.URL,
},
{
ID: "ack",
Text: "Ignore",
},
},
})
}
func cleanUpPortalNotification() {
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.Delete()
captivePortalNotification = nil
}
}

View File

@@ -123,7 +123,8 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
// Add module warning to inform user.
module.Warning(
globalConfigProfileErrorID,
fmt.Sprintf("Failed to process global settings: %s", err),
"Internal Settings Failure",
fmt.Sprintf("Some global settings might not be applied correctly. You can try restarting the Portmaster to resolve this problem. Error: %s", err),
)
}

View File

@@ -51,6 +51,12 @@ func GetProfile(source profileSource, id, linkedPath string) ( //nolint:gocognit
// Get from database.
profile, err = getProfile(scopedID)
// Check if the request is for a special profile that may need a reset.
if err == nil && specialProfileNeedsReset(profile) {
// Trigger creation of special profile.
err = database.ErrNotFound
}
// If we cannot find a profile, check if the request is for a special
// profile we can create.
if errors.Is(err, database.ErrNotFound) {

View File

@@ -97,10 +97,15 @@ type Profile struct { //nolint:maligned // not worth the effort
// an object) need to be concatenated for the settings database
// path.
Config map[string]interface{}
// ApproxLastUsed holds a UTC timestamp in seconds of
// when this Profile was approximately last used.
// For performance reasons not every single usage is saved.
ApproxLastUsed int64
// LastEdited hols the UTC timestamp in seconds when the profile was last
// edited by the user. This is not set automatically, but has to be manually
// set by the user interface.
LastEdited int64
// Created holds the UTC timestamp in seconds when the
// profile has been created.
Created int64

View File

@@ -1,5 +1,14 @@
package profile
import (
"fmt"
"time"
"github.com/safing/portbase/notifications"
"github.com/safing/portbase/log"
)
const (
// UnidentifiedProfileID is the profile ID used for unidentified processes.
UnidentifiedProfileID = "_unidentified"
@@ -76,7 +85,19 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
return New(SourceLocal, SystemProfileID, linkedPath, nil)
case SystemResolverProfileID:
return New(SourceLocal, SystemResolverProfileID, linkedPath, nil)
return New(
SourceLocal,
SystemResolverProfileID,
linkedPath,
map[string]interface{}{
CfgOptionServiceEndpointsKey: []string{
"+ Localhost", // Allow everything from localhost.
"+ LAN UDP/5353", // Allow inbound mDNS requests and multicast replies.
"+ LAN UDP/5355", // Allow inbound LLMNR requests and multicast replies.
"+ LAN UDP/1900", // Allow inbound SSDP requests and multicast replies.
},
},
)
case PortmasterProfileID:
profile := New(SourceLocal, PortmasterProfileID, linkedPath, nil)
@@ -117,3 +138,63 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
return nil
}
}
// specialProfileNeedsReset is used as a workaround until we can properly use
// profile layering in a way that it is also correctly handled by the UI. We
// check if the special profile has not been changed by the user and if not,
// check if the profile is outdated and can be upgraded.
func specialProfileNeedsReset(profile *Profile) bool {
switch {
case profile.Source != SourceLocal:
// Special profiles live in the local scope only.
return false
case profile.LastEdited > 0:
// Profile was edited - don't override user settings.
return false
}
switch profile.ID {
case SystemResolverProfileID:
return canBeUpgraded(profile, "10.5.2021")
default:
// Not a special profile or no upgrade available yet.
return false
}
}
func canBeUpgraded(profile *Profile, upgradeDate string) bool {
// Parse upgrade date.
upgradeTime, err := time.Parse("2.1.2006", upgradeDate)
if err != nil {
log.Warningf("profile: failed to parse date %q: %s", upgradeDate, err)
return false
}
// Check if the upgrade is applicable.
if profile.Meta().Created < upgradeTime.Unix() {
log.Infof("profile: upgrading special profile %s", profile.ScopedID())
notifications.NotifyInfo(
"profiles:upgraded-special-profile-"+profile.ID,
profile.Name+" Settings Upgraded",
// TODO: Remove disclaimer.
fmt.Sprintf(
"The %s settings were automatically upgraded. The current app settings have been replaced, as the Portmaster did not detect any changes made by you. Please note that settings upgrades before June 2021 might not detect previous changes correctly and you might want to review the new settings.",
profile.Name,
),
notifications.Action{
ID: "ack",
Text: "OK",
},
notifications.Action{
Text: "Open Settings",
Type: notifications.ActionTypeOpenProfile,
Payload: profile.ScopedID(),
},
)
return true
}
return false
}

View File

@@ -10,7 +10,7 @@ import (
func registerAPI() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: "dns/clear",
Read: api.PermitUser,
Write: api.PermitUser,
ActionFunc: clearNameCache,
Name: "Clear cached DNS records",
Description: "Deletes all saved DNS records from the database.",
@@ -29,16 +29,16 @@ func registerAPI() error {
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: `dns/cache`,
Path: `dns/cache/{query:[a-z0-9\.-]{0,512}\.[A-Z]{1,32}}`,
Read: api.PermitUser,
RecordFunc: func(r *api.Request) (record.Record, error) {
return recordDatabase.Get(nameRecordsKeyPrefix + r.URL.Query().Get("q"))
return recordDatabase.Get(nameRecordsKeyPrefix + r.URLVars["query"])
},
Name: "Get DNS Record from Cache",
Description: "Returns cached dns records from the internal cache.",
Parameters: []api.Parameter{{
Method: http.MethodGet,
Field: "q",
Field: "query (in path)",
Value: "fqdn and query type",
Description: "Specify the query like this: `example.com.A`.",
}},

View File

@@ -15,15 +15,11 @@ import (
)
var (
// ClearNameCacheEvent is a triggerable event that clears the name record cache.
ClearNameCacheEvent = "clear name cache"
module *modules.Module
)
func init() {
module = modules.Register("resolver", prep, start, nil, "base", "netenv")
module.RegisterEvent(ClearNameCacheEvent)
}
func prep() error {
@@ -80,18 +76,6 @@ func start() error {
return err
}
// DEPRECATED: remove in v0.7
// cache clearing
err = module.RegisterEventHook(
"resolver",
ClearNameCacheEvent,
ClearNameCacheEvent,
clearNameCacheEventHandler,
)
if err != nil {
return err
}
module.StartServiceWorker(
"mdns handler",
5*time.Second,

View File

@@ -240,7 +240,8 @@ func (brc *BasicResolverConn) IsFailing() bool {
// Reset failure status if the network changed since the last query.
if brc.networkChangedFlag.IsSet() {
brc.networkChangedFlag.Refresh()
brc.ResetFailure()
brc.fails = 0
brc.failing.UnSet()
return false
}

View File

@@ -227,16 +227,22 @@ func loadResolvers() {
)
if len(newResolvers) == 0 {
msg := "no (valid) dns servers found in configuration or system, falling back to defaults"
log.Warningf("resolver: %s", msg)
module.Warning(missingResolversErrorID, msg)
log.Warning("resolver: no (valid) dns server found in config or system, falling back to global defaults")
module.Warning(
missingResolversErrorID,
"Using Factory Default DNS Servers",
"The Portmaster could not find any (valid) DNS servers in the settings or system. In order to prevent being disconnected, the factory defaults are being used instead.",
)
// load defaults directly, overriding config system
newResolvers = getConfiguredResolvers(defaultNameServers)
if len(newResolvers) == 0 {
msg = "no (valid) dns servers found in configuration or system"
log.Criticalf("resolver: %s", msg)
module.Error(missingResolversErrorID, msg)
log.Critical("resolver: no (valid) dns server found in config, system or global defaults")
module.Error(
missingResolversErrorID,
"No DNS Server Configured",
"The Portmaster could not find any (valid) DNS servers in the settings or system. You will experience severe connectivity problems until resolved.",
)
}
}

35
ui/api.go Normal file
View File

@@ -0,0 +1,35 @@
package ui
import (
resources "github.com/cookieo9/resources-go"
"github.com/safing/portbase/api"
"github.com/safing/portbase/log"
)
func registerAPIEndpoints() error {
return api.RegisterEndpoint(api.Endpoint{
Path: "ui/reload",
Write: api.PermitUser,
ActionFunc: reloadUI,
Name: "Reload UI Assets",
Description: "Removes all assets from the cache and reloads the current (possibly updated) version from disk when requested.",
})
}
func reloadUI(_ *api.Request) (msg string, err error) {
appsLock.Lock()
defer appsLock.Unlock()
// close all bundles.
for id, bundle := range apps {
err := bundle.Close()
if err != nil {
log.Warningf("ui: failed to close bundle %s: %s", id, err)
}
}
// Reset index.
apps = make(map[string]*resources.BundleSequence)
return "all ui bundles successfully reloaded", nil
}

View File

@@ -1,11 +1,8 @@
package ui
import (
"context"
"github.com/safing/portbase/dataroot"
resources "github.com/cookieo9/resources-go"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
)
@@ -23,7 +20,9 @@ func init() {
}
func prep() error {
module.RegisterEvent(eventReload)
if err := registerAPIEndpoints(); err != nil {
return err
}
return registerRoutes()
}
@@ -42,25 +41,5 @@ func start() error {
log.Warningf("ui: failed to create safe exec dir: %s", err)
}
return module.RegisterEventHook("ui", eventReload, "reload assets", reloadUI)
}
func reloadUI(ctx context.Context, _ interface{}) error {
log.Info("core: user/UI requested UI reload")
appsLock.Lock()
defer appsLock.Unlock()
// close all bundles
for id, bundle := range apps {
err := bundle.Close()
if err != nil {
log.Warningf("ui: failed to close bundle %s: %s", id, err)
}
}
// reset index
apps = make(map[string]*resources.BundleSequence)
return nil
}

24
updates/api.go Normal file
View File

@@ -0,0 +1,24 @@
package updates
import (
"github.com/safing/portbase/api"
)
const (
apiPathCheckForUpdates = "updates/check"
)
func registerAPIEndpoints() error {
return api.RegisterEndpoint(api.Endpoint{
Path: apiPathCheckForUpdates,
Write: api.PermitUser,
ActionFunc: func(_ *api.Request) (msg string, err error) {
if err := TriggerUpdate(); err != nil {
return "", err
}
return "triggered update check", nil
},
Name: "Check for Updates",
Description: "Triggers checking for updates.",
})
}

View File

@@ -3,12 +3,15 @@ package updates
import (
"context"
"github.com/safing/portbase/notifications"
"github.com/safing/portbase/config"
"github.com/safing/portbase/log"
)
const (
cfgDevModeKey = "core/devMode"
cfgDevModeKey = "core/devMode"
updatesDisabledNotificationID = "updates:disabled"
)
var (
@@ -85,7 +88,6 @@ func initConfig() {
func updateRegistryConfig(_ context.Context, _ interface{}) error {
changed := false
forceUpdate := false
if releaseChannel() != previousReleaseChannel {
registry.SetBeta(releaseChannel() == releaseChannelBeta)
@@ -102,20 +104,33 @@ func updateRegistryConfig(_ context.Context, _ interface{}) error {
if enableUpdates() != updatesCurrentlyEnabled {
updatesCurrentlyEnabled = enableUpdates()
changed = true
forceUpdate = updatesCurrentlyEnabled
}
if changed {
registry.SelectVersions()
module.TriggerEvent(VersionUpdateEvent, nil)
if forceUpdate {
module.Resolve(updateFailed)
_ = TriggerUpdate()
log.Infof("updates: automatic updates enabled again.")
} else if !updatesCurrentlyEnabled {
module.Warning(updateFailed, "Automatic updates are disabled! This also affects security updates and threat intelligence.")
log.Warningf("updates: automatic updates are now disabled.")
if updatesCurrentlyEnabled {
module.Resolve("")
if err := TriggerUpdate(); err != nil {
log.Warningf("updates: failed to trigger update: %s", err)
}
log.Infof("updates: automatic updates are now enabled")
} else {
notifications.NotifyWarn(
updatesDisabledNotificationID,
"Automatic Updates Disabled",
"Automatic updates are disabled through configuration. Please note that this is potentially dangerous, as this also affects security updates as well as the filter lists and threat intelligence feeds.",
notifications.Action{
ID: "change",
Text: "Change",
Type: notifications.ActionTypeOpenSetting,
Payload: &notifications.ActionTypeOpenSettingPayload{
Key: enableUpdatesKey,
},
},
).AttachToModule(module)
log.Warningf("updates: automatic updates are now disabled")
}
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications"
"github.com/safing/portbase/updater"
)
@@ -41,10 +42,6 @@ const (
// to check if new versions of their resources are
// available by checking File.UpgradeAvailable().
ResourceUpdateEvent = "resource update"
// TriggerUpdateEvent is the event that can be emitted
// by the updates module to trigger an update.
TriggerUpdateEvent = "trigger update"
)
var (
@@ -68,18 +65,18 @@ var (
)
const (
updateInProgress = "update-in-progress"
updateInProcessDescr = "Portmaster is currently checking and downloading updates."
updateFailed = "update-failed"
updateInProgress = "updates:in-progress"
updateFailed = "updates:failed"
updateSuccess = "updates:success"
)
func init() {
module = modules.Register(ModuleName, prep, start, stop, "base")
module.RegisterEvent(VersionUpdateEvent)
module.RegisterEvent(ResourceUpdateEvent)
module.RegisterEvent(VersionUpdateEvent, true)
module.RegisterEvent(ResourceUpdateEvent, true)
flag.StringVar(&userAgentFromFlag, "update-agent", "", "Sets the user agent for requests to the update server")
flag.BoolVar(&staging, "staging", false, "Use staging update channel (for testing only)")
flag.StringVar(&userAgentFromFlag, "update-agent", "", "set the user agent for requests to the update server")
flag.BoolVar(&staging, "staging", false, "use staging update channel; for testing only")
// initialize mandatory updates
if onWindows {
@@ -109,7 +106,7 @@ func prep() error {
return err
}
module.RegisterEvent(TriggerUpdateEvent)
return registerAPIEndpoints()
return nil
}
@@ -127,18 +124,6 @@ func start() error {
return err
}
if err := module.RegisterEventHook(
module.Name,
TriggerUpdateEvent,
"Check for and download available updates",
func(context.Context, interface{}) error {
_ = TriggerUpdate()
return nil
},
); err != nil {
return err
}
// create registry
registry = &updater.ResourceRegistry{
Name: ModuleName,
@@ -244,17 +229,21 @@ func start() error {
// TriggerUpdate queues the update task to execute ASAP.
func TriggerUpdate() error {
if !module.Online() {
if !module.OnlineSoon() {
return fmt.Errorf("module not enabled")
}
switch {
case !module.OnlineSoon():
return fmt.Errorf("updates module is disabled")
case !module.Online():
updateASAP = true
} else {
case !enableUpdates():
return fmt.Errorf("automatic updating is disabled")
default:
updateTask.StartASAP()
log.Debugf("updates: triggering update to run as soon as possible")
}
log.Debugf("updates: triggering update to run as soon as possible")
return nil
}
@@ -278,13 +267,37 @@ func checkForUpdates(ctx context.Context) (err error) {
}
defer log.Debugf("updates: finished checking for updates")
module.Hint(updateInProgress, updateInProcessDescr)
defer func() {
if err == nil {
module.Resolve(updateInProgress)
module.Resolve(updateFailed)
notifications.Notify(&notifications.Notification{
EventID: updateSuccess,
Type: notifications.Info,
Title: "Update Check Successful",
Message: "The Portmaster successfully checked for updates and downloaded any available updates. Most updates are applied automatically. You will be notified of important updates that need restarting.",
Expires: time.Now().Add(1 * time.Minute).Unix(),
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
},
})
} else {
module.Warning(updateFailed, "Failed to update: "+err.Error())
notifications.NotifyWarn(
updateFailed,
"Update Check Failed",
"The Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later.",
notifications.Action{
ID: "retry",
Text: "Try Again Now",
Type: notifications.ActionTypeWebhook,
Payload: &notifications.ActionTypeWebhookPayload{
URL: apiPathCheckForUpdates,
ResultAction: "display",
},
},
).AttachToModule(module)
}
}()

View File

@@ -109,10 +109,10 @@ func upgradeCoreNotify() error {
),
Category: "Core",
Message: fmt.Sprintf(
`:tada: Update to **Portmaster v%s** is available!
Please restart the Portmaster to apply the update.`,
`A new Portmaster version is available! Restart the Portmaster to upgrade to %s.`,
pmCoreUpdate.Version(),
),
ShowOnSystem: true,
AvailableActions: []*notifications.Action{
{
ID: "restart",
@@ -135,18 +135,10 @@ Please restart the Portmaster to apply the update.`,
func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
switch n.SelectedActionID {
case "restart":
// Cannot directly trigger due to import loop.
err := module.InjectEvent(
"user triggered restart via notification",
"core",
"restart",
nil,
)
if err != nil {
return fmt.Errorf("failed to trigger restart via notification: %s", err)
}
log.Infof("updates: user triggered restart via core update notification")
RestartNow()
case "later":
return n.Delete()
n.Delete()
}
return nil
@@ -257,19 +249,16 @@ func warnOnIncorrectParentPath() {
root := filepath.Dir(registry.StorageDir().Path)
if !strings.HasPrefix(absPath, root) {
log.Warningf("detected unexpected path %s for portmaster-start", absPath)
notifications.Notify(&notifications.Notification{
EventID: "updates:unsupported-parent",
Type: notifications.Warning,
Title: "Unsupported Launcher",
Category: "Core",
Message: fmt.Sprintf(
"The portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
notifications.NotifyWarn(
"updates:unsupported-parent",
"Unsupported Launcher",
fmt.Sprintf(
"The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
expectedFileName,
absPath,
filepath.Join(root, expectedFileName),
),
})
)
}
}