Add ability to upgrade systemd service files
This commit is contained in:
44
updates/assets/portmaster.service
Normal file
44
updates/assets/portmaster.service
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Portmaster by Safing
|
||||||
|
Documentation=https://safing.io
|
||||||
|
Documentation=https://docs.safing.io
|
||||||
|
Before=nss-lookup.target network.target shutdown.target
|
||||||
|
After=systemd-networkd.service
|
||||||
|
Conflicts=shutdown.target
|
||||||
|
Conflicts=firewalld.service
|
||||||
|
Wants=nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
LockPersonality=yes
|
||||||
|
MemoryDenyWriteExecute=yes
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
PIDFile=/opt/safing/portmaster/core-lock.pid
|
||||||
|
Environment=LOGLEVEL=info
|
||||||
|
Environment=PORTMASTER_ARGS=
|
||||||
|
EnvironmentFile=-/etc/default/portmaster
|
||||||
|
ProtectSystem=true
|
||||||
|
#ReadWritePaths=/var/lib/portmaster
|
||||||
|
#ReadWritePaths=/run/xtables.lock
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=yes
|
||||||
|
# In future version portmaster will require access to user home
|
||||||
|
# directories to verify application permissions.
|
||||||
|
ProtectHome=read-only
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectKernelLogs=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
|
||||||
|
CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
|
||||||
|
# SystemCallArchitectures=native
|
||||||
|
# SystemCallFilter=@system-service @module
|
||||||
|
# SystemCallErrorNumber=EPERM
|
||||||
|
ExecStart=/opt/safing/portmaster/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS
|
||||||
|
ExecStopPost=-/opt/safing/portmaster/portmaster-start recover-iptables
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
8
updates/os_integration_default.go
Normal file
8
updates/os_integration_default.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package updates
|
||||||
|
|
||||||
|
func upgradeSystemIntegration() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
204
updates/os_integration_linux.go
Normal file
204
updates/os_integration_linux.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/tevino/abool"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/dataroot"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/utils/renameio"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
portmasterCoreServiceFilePath = "portmaster.service"
|
||||||
|
portmasterNotifierServiceFilePath = "portmaster_notifier.desktop"
|
||||||
|
backupExtension = ".backup"
|
||||||
|
|
||||||
|
//go:embed assets/portmaster.service
|
||||||
|
currentPortmasterCoreServiceFile []byte
|
||||||
|
|
||||||
|
checkedSystemIntegration = abool.New()
|
||||||
|
|
||||||
|
// ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade.
|
||||||
|
ErrRequiresManualUpgrade = errors.New("requires a manual upgrade")
|
||||||
|
)
|
||||||
|
|
||||||
|
func upgradeSystemIntegration() {
|
||||||
|
// Check if we already checked the system integration.
|
||||||
|
if !checkedSystemIntegration.SetToIf(false, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade portmaster core systemd service.
|
||||||
|
err := upgradeSystemIntegrationFile(
|
||||||
|
"portmaster core systemd service",
|
||||||
|
filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath),
|
||||||
|
0o0600,
|
||||||
|
currentPortmasterCoreServiceFile,
|
||||||
|
[]string{
|
||||||
|
"bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25
|
||||||
|
"cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24
|
||||||
|
"d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade portmaster notifier systemd user service.
|
||||||
|
// Permissions only!
|
||||||
|
err = upgradeSystemIntegrationFile(
|
||||||
|
"portmaster notifier systemd user service",
|
||||||
|
filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath),
|
||||||
|
0o0644,
|
||||||
|
nil, // Do not update contents.
|
||||||
|
nil, // Do not update contents.
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeSystemIntegrationFile upgrades the file contents and permissions.
|
||||||
|
// System integration files are not necessarily present and may also be
|
||||||
|
// edited by third parties, such as the OS itself or other installers.
|
||||||
|
// The supplied hashes must be sha256 hex-encoded.
|
||||||
|
func upgradeSystemIntegrationFile(
|
||||||
|
name string,
|
||||||
|
filePath string,
|
||||||
|
fileMode fs.FileMode,
|
||||||
|
fileData []byte,
|
||||||
|
permittedUpgradeHashes []string,
|
||||||
|
) error {
|
||||||
|
// Upgrade file contents.
|
||||||
|
if len(fileData) > 0 {
|
||||||
|
if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade file permissions.
|
||||||
|
if fileMode != 0 {
|
||||||
|
if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeSystemIntegrationFileContents upgrades the file contents.
|
||||||
|
// System integration files are not necessarily present and may also be
|
||||||
|
// edited by third parties, such as the OS itself or other installers.
|
||||||
|
// The supplied hashes must be sha256 hex-encoded.
|
||||||
|
func upgradeSystemIntegrationFileContents(
|
||||||
|
name string,
|
||||||
|
filePath string,
|
||||||
|
fileData []byte,
|
||||||
|
permittedUpgradeHashes []string,
|
||||||
|
) error {
|
||||||
|
// Read existing file.
|
||||||
|
existingFileData, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file is already the current version.
|
||||||
|
existingSum := sha256.Sum256(existingFileData)
|
||||||
|
existingHexSum := hex.EncodeToString(existingSum[:])
|
||||||
|
currentSum := sha256.Sum256(fileData)
|
||||||
|
currentHexSum := hex.EncodeToString(currentSum[:])
|
||||||
|
if existingHexSum == currentHexSum {
|
||||||
|
log.Debugf("updates: %s at %s is up to date", name, filePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are allowed to upgrade from the existing file.
|
||||||
|
if !slices.Contains[string](permittedUpgradeHashes, existingHexSum) {
|
||||||
|
return fmt.Errorf("%s at %s %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, ErrRequiresManualUpgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with upgrade!
|
||||||
|
|
||||||
|
// Make backup of existing file.
|
||||||
|
err = CopyFile(filePath, filePath+backupExtension)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"failed to create backup of %s from %s to %s: %w",
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
filePath+backupExtension,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open destination file for writing.
|
||||||
|
atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err)
|
||||||
|
}
|
||||||
|
defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
|
||||||
|
|
||||||
|
// Write file.
|
||||||
|
_, err = io.Copy(atomicDstFile, bytes.NewReader(fileData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize file.
|
||||||
|
err = atomicDstFile.CloseAtomicallyReplace()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warningf("updates: %s at %s was upgraded to %s", name, filePath, currentHexSum)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeSystemIntegrationFilePermissions upgrades the file permissions.
|
||||||
|
// System integration files are not necessarily present and may also be
|
||||||
|
// edited by third parties, such as the OS itself or other installers.
|
||||||
|
func upgradeSystemIntegrationFilePermissions(
|
||||||
|
name string,
|
||||||
|
filePath string,
|
||||||
|
fileMode fs.FileMode,
|
||||||
|
) error {
|
||||||
|
// Get current file permissions.
|
||||||
|
stat, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If permissions are as expected, do nothing.
|
||||||
|
if stat.Mode().Perm() == fileMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, set correct permissions.
|
||||||
|
err = os.Chmod(filePath, fileMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -66,15 +66,21 @@ func upgrader(_ context.Context, _ interface{}) error {
|
|||||||
binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0]
|
binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0]
|
||||||
switch binBaseName {
|
switch binBaseName {
|
||||||
case "portmaster-core":
|
case "portmaster-core":
|
||||||
|
// Notify about upgrade.
|
||||||
if err := upgradeCoreNotify(); err != nil {
|
if err := upgradeCoreNotify(); err != nil {
|
||||||
log.Warningf("updates: failed to notify about core upgrade: %s", err)
|
log.Warningf("updates: failed to notify about core upgrade: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix chrome sandbox permissions.
|
||||||
if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
|
if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
|
||||||
log.Warningf("updates: failed to handle electron upgrade: %s", err)
|
log.Warningf("updates: failed to handle electron upgrade: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upgrade system integration.
|
||||||
|
upgradeSystemIntegration()
|
||||||
|
|
||||||
case "spn-hub":
|
case "spn-hub":
|
||||||
|
// Trigger upgrade procedure.
|
||||||
if err := upgradeHub(); err != nil {
|
if err := upgradeHub(); err != nil {
|
||||||
log.Warningf("updates: failed to initiate hub upgrade: %s", err)
|
log.Warningf("updates: failed to initiate hub upgrade: %s", err)
|
||||||
}
|
}
|
||||||
@@ -213,7 +219,7 @@ func upgradePortmasterStart() error {
|
|||||||
|
|
||||||
// update portmaster-start in data root
|
// update portmaster-start in data root
|
||||||
rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
|
rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
|
||||||
err := upgradeFile(rootPmStartPath, pmCtrlUpdate)
|
err := upgradeBinary(rootPmStartPath, pmCtrlUpdate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,7 +284,7 @@ func warnOnIncorrectParentPath() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
func upgradeBinary(fileToUpgrade string, file *updater.File) error {
|
||||||
fileExists := false
|
fileExists := false
|
||||||
_, err := os.Stat(fileToUpgrade)
|
_, err := os.Stat(fileToUpgrade)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -295,7 +301,7 @@ func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
|||||||
// abort if version matches
|
// abort if version matches
|
||||||
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
||||||
if currentVersion == file.Version() {
|
if currentVersion == file.Version() {
|
||||||
log.Tracef("updates: %s is already v%s", fileToUpgrade, file.Version())
|
log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version())
|
||||||
// already up to date!
|
// already up to date!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -306,7 +312,7 @@ func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
|||||||
|
|
||||||
// test currentVersion for sanity
|
// test currentVersion for sanity
|
||||||
if !rawVersionRegex.MatchString(currentVersion) {
|
if !rawVersionRegex.MatchString(currentVersion) {
|
||||||
log.Tracef("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
|
log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try removing old version
|
// try removing old version
|
||||||
|
|||||||
Reference in New Issue
Block a user