Merge pull request #399 from safing/feature/patch-set-2

Updates and Improvements (related to SPN v0.3)
This commit is contained in:
Daniel 2021-09-27 14:12:30 +02:00 committed by GitHub
commit 5c0bf25f95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 255 additions and 120 deletions

View file

@ -3,6 +3,7 @@ package firewall
import (
"github.com/safing/portbase/config"
"github.com/safing/portbase/modules/subsystems"
"github.com/safing/spn/captain"
"github.com/safing/portbase/modules"
@ -13,6 +14,7 @@ import (
var (
filterModule *modules.Module
filterEnabled config.BoolOption
tunnelEnabled config.BoolOption
)
func init() {
@ -28,8 +30,8 @@ func init() {
Key: CfgOptionEnableFilterKey,
Description: "Start the Privacy Filter module. If turned off, all privacy filter protections are fully disabled on this device.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelBeta,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: true,
Annotations: config.Annotations{
config.CategoryAnnotation: "General",
@ -44,6 +46,7 @@ func filterPrep() (err error) {
return err
}
filterEnabled = config.GetAsBool(CfgOptionEnableFilterKey, true)
filterEnabled = config.Concurrent.GetAsBool(CfgOptionEnableFilterKey, true)
tunnelEnabled = config.Concurrent.GetAsBool(captain.CfgOptionEnableSPNKey, false)
return nil
}

View file

@ -9,6 +9,8 @@ import (
"sync/atomic"
"time"
"github.com/safing/spn/captain"
"github.com/google/gopacket/layers"
"github.com/safing/portmaster/netenv"
"golang.org/x/sync/singleflight"
@ -22,7 +24,7 @@ import (
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/spn/captain"
"github.com/safing/spn/crew"
"github.com/safing/spn/sluice"
// module dependencies
@ -346,32 +348,41 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
return
}
// check if filtering is enabled
if !filterEnabled() {
conn.Inspecting = false
// TODO: enable inspecting again
conn.Inspecting = false
// Filter, if enabled.
if filterEnabled() {
log.Tracer(pkt.Ctx()).Trace("filter: starting decision process")
DecideOnConnection(pkt.Ctx(), conn, pkt)
} else {
conn.Accept("privacy filter disabled", noReasonOptionKey)
conn.StopFirewallHandler()
issueVerdict(conn, pkt, 0, true)
return
}
log.Tracer(pkt.Ctx()).Trace("filter: starting decision process")
DecideOnConnection(pkt.Ctx(), conn, pkt)
conn.Inspecting = false // TODO: enable inspecting again
// Tunnel, if enabled.
if pkt.IsOutbound() && conn.Entity.IPScope.IsGlobal() &&
tunnelEnabled() && conn.Verdict == network.VerdictAccept &&
conn.Process().Profile() != nil &&
conn.Process().Profile().UseSPN() {
// tunneling
// TODO: add implementation for forced tunneling
if pkt.IsOutbound() &&
captain.ClientReady() &&
conn.Entity.IPScope.IsGlobal() &&
conn.Verdict == network.VerdictAccept {
// try to tunnel
err := sluice.AwaitRequest(pkt.Info(), conn.Entity.Domain)
if err != nil {
log.Tracer(pkt.Ctx()).Tracef("filter: not tunneling: %s", err)
} else {
log.Tracer(pkt.Ctx()).Trace("filter: tunneling request")
conn.Verdict = network.VerdictRerouteToTunnel
// Exclude requests of the SPN itself.
if !captain.IsExcepted(conn.Entity.IP) {
// Check if client is ready.
if captain.ClientReady() {
// Queue request in sluice.
err := sluice.AwaitRequest(conn, crew.HandleSluiceRequest)
if err != nil {
log.Tracer(pkt.Ctx()).Warningf("failed to rqeuest tunneling: %s", err)
conn.Failed("failed to request tunneling", "")
} else {
log.Tracer(pkt.Ctx()).Trace("filter: tunneling requested")
conn.Verdict = network.VerdictRerouteToTunnel
}
} else {
// Block connection as SPN is not ready yet.
log.Tracer(pkt.Ctx()).Trace("SPN not ready for tunneling")
conn.Failed("SPN not ready for tunneling", "")
}
}
}

View file

@ -30,7 +30,7 @@ func lookupBlockLists(entity, value string) ([]string, error) {
return nil, nil
}
log.Debugf("intel/filterlists: searching for entries with %s", key)
// log.Debugf("intel/filterlists: searching for entries with %s", key)
entry, err := getEntityRecordByKey(key)
if err != nil {
if err == database.ErrNotFound {

View file

@ -2,7 +2,9 @@ package netenv
import (
"errors"
"fmt"
"net"
"sort"
"sync"
"syscall"
"time"
@ -22,9 +24,7 @@ var (
locationTestingIPv4 = "1.1.1.1"
locationTestingIPv4Addr *net.IPAddr
locations = &DeviceLocations{
All: make(map[string]*DeviceLocation),
}
locations = &DeviceLocations{}
locationsLock sync.Mutex
gettingLocationsLock sync.Mutex
locationNetworkChangedFlag = GetNetworkChangedFlag()
@ -36,8 +36,32 @@ func prepLocation() (err error) {
}
type DeviceLocations struct {
Best *DeviceLocation
All map[string]*DeviceLocation
All []*DeviceLocation
}
func (dl *DeviceLocations) Best() *DeviceLocation {
if len(dl.All) > 0 {
return dl.All[0]
}
return nil
}
func (dl *DeviceLocations) BestV4() *DeviceLocation {
for _, loc := range dl.All {
if loc.IPVersion == packet.IPv4 {
return loc
}
}
return nil
}
func (dl *DeviceLocations) BestV6() *DeviceLocation {
for _, loc := range dl.All {
if loc.IPVersion == packet.IPv6 {
return loc
}
}
return nil
}
func copyDeviceLocations() *DeviceLocations {
@ -45,23 +69,20 @@ func copyDeviceLocations() *DeviceLocations {
defer locationsLock.Unlock()
// Create a copy of the locations, but not the entries.
cp := *locations
cp.All = make(map[string]*DeviceLocation, len(locations.All))
for k, v := range locations.All {
cp.All[k] = v
cp := &DeviceLocations{
All: make([]*DeviceLocation, len(locations.All)),
}
copy(cp.All, locations.All)
return &cp
return cp
}
// DeviceLocation represents a single IP and metadata. It must not be changed
// once created.
type DeviceLocation struct {
IP net.IP
Continent string
Country string
ASN uint
ASOrg string
IPVersion packet.IPVersion
Location *geoip.Location
Source DeviceLocationSource
SourceAccuracy int
}
@ -71,19 +92,43 @@ type DeviceLocation struct {
func (dl *DeviceLocation) IsMoreAccurateThan(other *DeviceLocation) bool {
switch {
case dl.SourceAccuracy > other.SourceAccuracy:
// Higher accuracy is better.
// Higher source accuracy is better.
return true
case dl.ASN != 0 && other.ASN == 0:
case dl.Location.AutonomousSystemNumber != 0 && other.Location.AutonomousSystemNumber == 0:
// Having an ASN is better than having none.
return true
case dl.Country == "" && other.Country != "":
case dl.Location.Continent.Code != "" && other.Location.Continent.Code == "":
// Having a Continent is better than having none.
return true
case dl.Location.Country.ISOCode != "" && other.Location.Country.ISOCode == "":
// Having a Country is better than having none.
return true
case dl.Location.Coordinates.AccuracyRadius < other.Location.Coordinates.AccuracyRadius:
// Higher geo accuracy is better.
return true
}
return false
}
func (dl *DeviceLocation) LocationOrNil() *geoip.Location {
if dl == nil {
return nil
}
return dl.Location
}
func (dl *DeviceLocation) String() string {
switch {
case dl == nil:
return "<none>"
case dl.Location == nil:
return dl.IP.String()
default:
return fmt.Sprintf("%s (AS%d in %s)", dl.IP, dl.Location.AutonomousSystemNumber, dl.Location.Country.ISOCode)
}
}
type DeviceLocationSource string
const (
@ -111,6 +156,12 @@ func (dls DeviceLocationSource) Accuracy() int {
}
}
type sortLocationsByAccuracy []*DeviceLocation
func (a sortLocationsByAccuracy) Len() int { return len(a) }
func (a sortLocationsByAccuracy) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortLocationsByAccuracy) Less(i, j int) bool { return a[j].IsMoreAccurateThan(a[i]) }
func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
// Check if IP is global.
if netutils.GetIPScope(ip) != netutils.Global {
@ -123,39 +174,41 @@ func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
Source: source,
SourceAccuracy: source.Accuracy(),
}
if v4 := ip.To4(); v4 != nil {
loc.IPVersion = packet.IPv4
} else {
loc.IPVersion = packet.IPv6
}
// Get geoip information, but continue if it fails.
geoLoc, err := geoip.GetLocation(ip)
if err != nil {
log.Warningf("netenv: failed to get geolocation data of %s (from %s): %s", ip, source, err)
} else {
loc.Continent = geoLoc.Continent.Code
loc.Country = geoLoc.Country.ISOCode
loc.ASN = geoLoc.AutonomousSystemNumber
loc.ASOrg = geoLoc.AutonomousSystemOrganization
loc.Location = geoLoc
}
locationsLock.Lock()
defer locationsLock.Unlock()
// Add to locations, if better.
key := loc.IP.String()
existing, ok := locations.All[key]
if ok && existing.IsMoreAccurateThan(loc) {
// Existing entry is more accurate, abort adding.
// Return true, because the IP address is already part of the locations.
return true
}
locations.All[key] = loc
// Find best location.
best := loc
for _, dl := range locations.All {
if dl.IsMoreAccurateThan(best) {
best = dl
var exists bool
for i, existing := range locations.All {
if ip.Equal(existing.IP) {
exists = true
if loc.IsMoreAccurateThan(existing) {
// Replace
locations.All[i] = loc
break
}
}
}
locations.Best = best
if !exists {
locations.All = append(locations.All, loc)
}
// Sort locations.
sort.Sort(sortLocationsByAccuracy(locations.All))
return true
}
@ -163,10 +216,10 @@ func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
// DEPRECATED: Please use GetInternetLocation instead.
func GetApproximateInternetLocation() (net.IP, error) {
loc, ok := GetInternetLocation()
if !ok {
if !ok || loc.Best() == nil {
return nil, errors.New("no location data available")
}
return loc.Best.IP, nil
return loc.Best().IP, nil
}
func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
@ -179,38 +232,54 @@ func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
}
locationNetworkChangedFlag.Refresh()
// Check different sources, return on first success.
switch {
case getLocationFromInterfaces():
case getLocationFromTraceroute():
default:
// Get all assigned addresses.
v4s, v6s, err := GetAssignedAddresses()
if err != nil {
log.Warningf("netenv: failed to get assigned addresses: %s", err)
return nil, false
}
// Check interfaces for global addresses.
v4ok, v6ok := getLocationFromInterfaces()
// Try other methods for missing locations.
if len(v4s) > 0 && !v4ok {
v4ok = getLocationFromTraceroute()
}
if len(v6s) > 0 && !v6ok {
// TODO
log.Warningf("netenv: could not get IPv6 location")
}
// Check if we have any locations.
if !v4ok && !v6ok {
return nil, false
}
// Return gathered locations.
cp := copyDeviceLocations()
return cp, cp.Best != nil
return cp, true
}
func getLocationFromInterfaces() (ok bool) {
func getLocationFromInterfaces() (v4ok, v6ok bool) {
globalIPv4, globalIPv6, err := GetAssignedGlobalAddresses()
if err != nil {
log.Warningf("netenv: location: failed to get assigned global addresses: %s", err)
return false
return false, false
}
for _, ip := range globalIPv4 {
if SetInternetLocation(ip, SourceInterface) {
ok = true
v4ok = true
}
}
for _, ip := range globalIPv6 {
if SetInternetLocation(ip, SourceInterface) {
ok = true
v6ok = true
}
}
return ok
return
}
// TODO: Check feasibility of getting the external IP via UPnP.
@ -223,7 +292,7 @@ func getLocationFromUPnP() (ok bool) {
}
*/
func getLocationFromTraceroute() (ok bool) {
func getLocationFromTraceroute() (v4ok bool) {
// Create connection.
conn, err := net.ListenPacket("ip4:icmp", "")
if err != nil {

View file

@ -502,10 +502,14 @@ func (conn *Connection) SetVerdict(newVerdict Verdict, reason, reasonOptionKey s
conn.Verdict = newVerdict
conn.Reason.Msg = reason
conn.Reason.Context = reasonCtx
conn.Reason.Profile = ""
conn.Reason.OptionKey = ""
if reasonOptionKey != "" && conn.Process() != nil {
conn.Reason.OptionKey = reasonOptionKey
conn.Reason.Profile = conn.Process().Profile().GetProfileSource(conn.Reason.OptionKey)
conn.Reason.OptionKey = reasonOptionKey
}
return true
}
return false

View file

@ -206,7 +206,7 @@ func (ipHelper *IPHelper) getTable(ipVersion, protocol uint8) (connections []*so
case winErrInsufficientBuffer:
if i >= maxTries {
return nil, nil, fmt.Errorf(
"insufficient buffer error (tried %d times): %s bytes required - [NT 0x%X] %s",
"insufficient buffer error (tried %d times): %d bytes required - [NT 0x%X] %s",
i, bufSize, r1, err,
)
}

View file

@ -109,9 +109,9 @@ func registerConfiguration() error {
// ask - ask mode: if not verdict is found, the user is consulted
// block - allowlist mode: everything is blocked unless explicitly allowed
err := config.Register(&config.Option{
Name: "Default Action",
Name: "Default Network Action",
Key: CfgOptionDefaultActionKey,
Description: `The default action when nothing else allows or blocks an outgoing connection. Incoming connections are always blocked by default.`,
Description: `The default network action is applied when nothing else allows or blocks an outgoing connection. Incoming connections are always blocked by default.`,
OptType: config.OptTypeString,
DefaultValue: "permit",
Annotations: config.Annotations{
@ -169,6 +169,7 @@ func registerConfiguration() error {
- By address: "192.168.0.1"
- By network: "192.168.0.1/24"
- By network scope: "Localhost", "LAN" or "Internet"
- By domain:
- Matching a distinct domain: "example.com"
- Matching a domain with subdomains: ".example.com"
@ -176,6 +177,7 @@ func registerConfiguration() error {
- Matching with a wildcard suffix: "example.*"
- Matching domains containing text: "*example*"
- By country (based on IP): "US"
- By AS number: "AS123456"
- By filter list - use the filterlist ID prefixed with "L:": "L:MAL"
- Match anything: "*"
@ -333,9 +335,9 @@ The lists are automatically updated every hour using incremental updates.
// Block Scope Local
err = config.Register(&config.Option{
Name: "Block Device-Local Connections",
Name: "Force Block Device-Local Connections",
Key: CfgOptionBlockScopeLocalKey,
Description: "Block all internal connections on your own device, ie. localhost. Is stronger than Rules (see below).",
Description: "Force Block all internal connections on your own device, ie. localhost. Is stronger than Rules (see below).",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
DefaultValue: status.SecurityLevelOff,
@ -354,9 +356,9 @@ The lists are automatically updated every hour using incremental updates.
// Block Scope LAN
err = config.Register(&config.Option{
Name: "Block LAN",
Name: "Force Block LAN",
Key: CfgOptionBlockScopeLANKey,
Description: "Block all connections from and to the Local Area Network. Is stronger than Rules (see below).",
Description: "Force Block all connections from and to the Local Area Network. Is stronger than Rules (see below).",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelsHighAndExtreme,
PossibleValues: status.AllSecurityLevelValues,
@ -374,9 +376,9 @@ The lists are automatically updated every hour using incremental updates.
// Block Scope Internet
err = config.Register(&config.Option{
Name: "Block Internet Access",
Name: "Force Block Internet Access",
Key: CfgOptionBlockScopeInternetKey,
Description: "Block connections from and to the Internet. Is stronger than Rules (see below).",
Description: "Force Block connections from and to the Internet. Is stronger than Rules (see below).",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelOff,
PossibleValues: status.AllSecurityLevelValues,
@ -394,7 +396,7 @@ The lists are automatically updated every hour using incremental updates.
// Block Peer to Peer Connections
err = config.Register(&config.Option{
Name: "Block P2P/Direct Connections",
Name: "Force Block P2P/Direct Connections",
Key: CfgOptionBlockP2PKey,
Description: "These are connections that are established directly to an IP address or peer on the Internet without resolving a domain name via DNS first. Is stronger than Rules (see below).",
OptType: config.OptTypeInt,
@ -414,7 +416,7 @@ The lists are automatically updated every hour using incremental updates.
// Block Inbound Connections
err = config.Register(&config.Option{
Name: "Block Incoming Connections",
Name: "Force Block Incoming Connections",
Key: CfgOptionBlockInboundKey,
Description: "Connections initiated towards your device from the LAN or Internet. This will usually only be the case if you are running a network service or are using peer to peer software. Is stronger than Rules (see below).",
OptType: config.OptTypeInt,

View file

@ -32,6 +32,8 @@ var (
ErrFailure = errors.New("query failed")
// ErrContinue is returned when the resolver has no answer, and the next resolver should be asked
ErrContinue = errors.New("resolver has no answer")
// ErrShuttingDown is returned when the resolver is shutting down.
ErrShuttingDown = errors.New("resolver is shutting down")
// detailed errors
@ -275,6 +277,8 @@ retry:
case <-time.After(maxRequestTimeout):
// something went wrong with the query, retry
goto retry
case <-ctx.Done():
return nil
}
} else {
// but that someone is taking too long
@ -331,7 +335,7 @@ resolveLoop:
for i = 0; i < 2; i++ {
for _, resolver := range resolvers {
if module.IsStopping() {
return nil, errors.New("shutting down")
return nil, ErrShuttingDown
}
// check if resolver failed recently (on first run)
@ -364,6 +368,12 @@ resolveLoop:
resolver.Conn.ReportFailure()
log.Tracer(ctx).Debugf("resolver: query to %s timed out", resolver.Info.ID())
continue
case errors.Is(err, context.Canceled):
return nil, err
case errors.Is(err, context.DeadlineExceeded):
return nil, err
case errors.Is(err, ErrShuttingDown):
return nil, err
default:
resolver.Conn.ReportFailure()
log.Tracer(ctx).Debugf("resolver: query to %s failed: %s", resolver.Info.ID(), err)

View file

@ -418,6 +418,8 @@ func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
if err != nil {
return rrCache, nil
}
case <-ctx.Done():
return nil, ctx.Err()
}
// Respond with NXDomain.

View file

@ -105,7 +105,7 @@ func (tr *TCPResolver) UseTLS() *TCPResolver {
return tr
}
func (tr *TCPResolver) getOrCreateResolverConn() (*tcpResolverConn, error) {
func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolverConn, error) {
tr.Lock()
defer tr.Unlock()
@ -117,6 +117,10 @@ func (tr *TCPResolver) getOrCreateResolverConn() (*tcpResolverConn, error) {
return tr.resolverConn, nil
case <-time.After(heartbeatTimeout):
log.Warningf("resolver: heartbeat for dns client %s failed", tr.resolver.Info.DescriptiveName())
case <-ctx.Done():
return nil, ctx.Err()
case <-module.Stopping():
return nil, ErrShuttingDown
}
}
@ -130,7 +134,6 @@ func (tr *TCPResolver) getOrCreateResolverConn() (*tcpResolverConn, error) {
}
// Connect to server.
var err error
conn, err := tr.dnsClient.Dial(tr.resolver.ServerAddress)
if err != nil {
log.Debugf("resolver: failed to connect to %s", tr.resolver.Info.DescriptiveName())
@ -171,7 +174,7 @@ func (tr *TCPResolver) getOrCreateResolverConn() (*tcpResolverConn, error) {
// Query executes the given query against the resolver.
func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
// Get resolver connection.
resolverConn, err := tr.getOrCreateResolverConn()
resolverConn, err := tr.getOrCreateResolverConn(ctx)
if err != nil {
return nil, err
}
@ -185,6 +188,10 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
// Submit query request to live connection.
select {
case resolverConn.queries <- tq:
case <-ctx.Done():
return nil, ctx.Err()
case <-module.Stopping():
return nil, ErrShuttingDown
case <-time.After(defaultRequestTimeout):
return nil, ErrTimeout
}
@ -193,6 +200,10 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
var reply *dns.Msg
select {
case reply = <-tq.Response:
case <-ctx.Done():
return nil, ctx.Err()
case <-module.Stopping():
return nil, ErrShuttingDown
case <-time.After(defaultRequestTimeout):
return nil, ErrTimeout
}

View file

@ -3,11 +3,18 @@ package helper
import (
"fmt"
"runtime"
"github.com/tevino/abool"
)
const (
onWindows = runtime.GOOS == "windows"
)
const onWindows = runtime.GOOS == "windows"
var intelOnly = abool.New()
// IntelOnly specifies that only intel data is mandatory.
func IntelOnly() {
intelOnly.Set()
}
// PlatformIdentifier converts identifier for the current platform.
func PlatformIdentifier(identifier string) string {
@ -20,33 +27,10 @@ func PlatformIdentifier(identifier string) string {
// MandatoryUpdates returns mandatory updates that should be loaded on install
// or reset.
func MandatoryUpdates() (identifiers []string) {
// Binaries
if onWindows {
identifiers = []string{
PlatformIdentifier("core/portmaster-core.exe"),
PlatformIdentifier("kext/portmaster-kext.dll"),
PlatformIdentifier("kext/portmaster-kext.sys"),
PlatformIdentifier("start/portmaster-start.exe"),
PlatformIdentifier("notifier/portmaster-notifier.exe"),
PlatformIdentifier("notifier/portmaster-snoretoast.exe"),
}
} else {
identifiers = []string{
PlatformIdentifier("core/portmaster-core"),
PlatformIdentifier("start/portmaster-start"),
PlatformIdentifier("notifier/portmaster-notifier"),
}
}
// Components, Assets and Data
// Intel
identifiers = append(
identifiers,
// User interface components
PlatformIdentifier("app/portmaster-app.zip"),
"all/ui/modules/portmaster.zip",
"all/ui/modules/assets.zip",
// Filter lists data
"all/intel/lists/index.dsd",
"all/intel/lists/base.dsdl",
@ -58,11 +42,50 @@ func MandatoryUpdates() (identifiers []string) {
"all/intel/geoip/geoipv6.mmdb.gz",
)
// Stop here if we only want intel data.
if intelOnly.IsSet() {
return
}
// Binaries
if onWindows {
identifiers = append(
identifiers,
PlatformIdentifier("core/portmaster-core.exe"),
PlatformIdentifier("kext/portmaster-kext.dll"),
PlatformIdentifier("kext/portmaster-kext.sys"),
PlatformIdentifier("start/portmaster-start.exe"),
PlatformIdentifier("notifier/portmaster-notifier.exe"),
PlatformIdentifier("notifier/portmaster-snoretoast.exe"),
)
} else {
identifiers = append(
identifiers,
PlatformIdentifier("core/portmaster-core"),
PlatformIdentifier("start/portmaster-start"),
PlatformIdentifier("notifier/portmaster-notifier"),
)
}
// Components, Assets and Data
identifiers = append(
identifiers,
// User interface components
PlatformIdentifier("app/portmaster-app.zip"),
"all/ui/modules/portmaster.zip",
"all/ui/modules/assets.zip",
)
return identifiers
}
// AutoUnpackUpdates returns assets that need unpacking.
func AutoUnpackUpdates() []string {
if intelOnly.IsSet() {
return []string{}
}
return []string{
PlatformIdentifier("app/portmaster-app.zip"),
}