safing-portmaster/service/compat/wfpstate.go
Daniel Hååvi 80664d1a27
Restructure modules (#1572)
* Move portbase into monorepo

* Add new simple module mgr

* [WIP] Switch to new simple module mgr

* Add StateMgr and more worker variants

* [WIP] Switch more modules

* [WIP] Switch more modules

* [WIP] swtich more modules

* [WIP] switch all SPN modules

* [WIP] switch all service modules

* [WIP] Convert all workers to the new module system

* [WIP] add new task system to module manager

* [WIP] Add second take for scheduling workers

* [WIP] Add FIXME for bugs in new scheduler

* [WIP] Add minor improvements to scheduler

* [WIP] Add new worker scheduler

* [WIP] Fix more bug related to new module system

* [WIP] Fix start handing of the new module system

* [WIP] Improve startup process

* [WIP] Fix minor issues

* [WIP] Fix missing subsystem in settings

* [WIP] Initialize managers in constructor

* [WIP] Move module event initialization to constrictors

* [WIP] Fix setting for enabling and disabling the SPN module

* [WIP] Move API registeration into module construction

* [WIP] Update states mgr for all modules

* [WIP] Add CmdLine operation support

* Add state helper methods to module group and instance

* Add notification and module status handling to status package

* Fix starting issues

* Remove pilot widget and update security lock to new status data

* Remove debug logs

* Improve http server shutdown

* Add workaround for cleanly shutting down firewall+netquery

* Improve logging

* Add syncing states with notifications for new module system

* Improve starting, stopping, shutdown; resolve FIXMEs/TODOs

* [WIP] Fix most unit tests

* Review new module system and fix minor issues

* Push shutdown and restart events again via API

* Set sleep mode via interface

* Update example/template module

* [WIP] Fix spn/cabin unit test

* Remove deprecated UI elements

* Make log output more similar for the logging transition phase

* Switch spn hub and observer cmds to new module system

* Fix log sources

* Make worker mgr less error prone

* Fix tests and minor issues

* Fix observation hub

* Improve shutdown and restart handling

* Split up big connection.go source file

* Move varint and dsd packages to structures repo

* Improve expansion test

* Fix linter warnings

* Fix interception module on windows

* Fix linter errors

---------

Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
2024-08-09 18:15:48 +03:00

527 lines
16 KiB
Go

package compat
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"text/tabwriter"
"github.com/safing/portmaster/base/utils/osdetail"
)
// GetWFPState queries the system for the WFP state and returns a simplified
// and cleaned version.
func GetWFPState() (*SimplifiedWFPState, error) {
// Use a file to get the wfp state, as the terminal isn't able to return the
// data encoded in UTF-8.
tmpDir, err := os.MkdirTemp("", "portmaster-debug-data-wfpstate")
if err != nil {
return nil, fmt.Errorf("failed to create tmp dir for wfpstate: %w", err)
}
defer func() {
_ = os.RemoveAll(tmpDir)
}()
tmpFile := filepath.Join(tmpDir, "wfpstate.xml")
// Get wfp state and write it to the tmp file.
_, err = osdetail.RunCmd(
"netsh.exe",
"wfp",
"show",
"state",
tmpFile,
)
if err != nil {
return nil, fmt.Errorf("failed to write wfp state to tmp file: %w", err)
}
// Get tmp file contents.
output, err := os.ReadFile(tmpFile)
if err != nil {
return nil, fmt.Errorf("failed to read wfp state to tmp file: %w", err)
}
if len(output) == 0 {
return nil, errors.New("wfp state tmp file was empty")
}
// Parse wfp state.
parsedState, err := parseWFPState(output)
if err != nil {
return nil, fmt.Errorf("failed to parse wfpstate: %w", err)
}
// Return simplified and cleaned state.
return parsedState.simplified(), nil
}
/*
Interesting data is found at:
providers->item[]
->displayData->name
->displayData->description
->providerKey
subLayers->item[]
->displayData->name
->displayData->description
->subLayerKey
layers->item[]->callouts->item[]
->displayData->name
->displayData->description
->calloutKey
->providerKey
->applicableLayer
layers->item[]->filters->item[]
->displayData->name
->displayData->description
->filterKey
->providerKey
->layerKey
->subLayerKey
*/
// SimplifiedWFPState is a simplified version of the full WFP state.
type SimplifiedWFPState struct {
Providers []*WFPProvider
SubLayers []*WFPSubLayer
Callouts []*WFPCallout
Filters []*WFPFilter
}
// WFPProvider represents a WFP Provider.
type WFPProvider struct {
Name string
Description string
ProviderKey string
}
// WFPSubLayer represents a WFP SubLayer.
type WFPSubLayer struct {
Name string
Description string
SubLayerKey string
}
// WFPCallout represents a WFP Callout.
type WFPCallout struct {
Name string
Description string
CalloutKey string
ProviderKey string
ApplicableLayer string
}
// WFPFilter represents a WFP Filter.
type WFPFilter struct {
Name string
Description string
FilterKey string
ProviderKey string
LayerKey string
SubLayerKey string
}
// Keys returns all keys found in the WFP state.
func (sw *SimplifiedWFPState) Keys() map[string]struct{} {
lookupMap := make(map[string]struct{}, len(sw.Providers)+len(sw.SubLayers)+len(sw.Callouts)+len(sw.Filters))
// Collect keys.
for _, provider := range sw.Providers {
lookupMap[provider.ProviderKey] = struct{}{}
}
for _, subLayer := range sw.SubLayers {
lookupMap[subLayer.SubLayerKey] = struct{}{}
}
for _, callout := range sw.Callouts {
lookupMap[callout.CalloutKey] = struct{}{}
}
for _, filter := range sw.Filters {
lookupMap[filter.FilterKey] = struct{}{}
}
return lookupMap
}
// AsTable formats the simplified WFP state as a table.
func (sw *SimplifiedWFPState) AsTable() string {
rows := make([]string, 0, len(sw.Providers)+len(sw.SubLayers)+len(sw.Callouts)+len(sw.Filters))
// Collect data and put it into rows.
for _, provider := range sw.Providers {
rows = append(rows, strings.Join([]string{
provider.Name,
"Provider",
provider.Description,
provider.ProviderKey,
}, "\t"))
}
for _, subLayer := range sw.SubLayers {
rows = append(rows, strings.Join([]string{
subLayer.Name,
"SubLayer",
subLayer.Description,
subLayer.SubLayerKey,
}, "\t"))
}
for _, callout := range sw.Callouts {
rows = append(rows, strings.Join([]string{
callout.Name,
"Callout",
callout.Description,
callout.CalloutKey,
callout.ProviderKey,
callout.ApplicableLayer,
}, "\t"))
}
for _, filter := range sw.Filters {
rows = append(rows, strings.Join([]string{
filter.Name,
"Filter",
filter.Description,
filter.FilterKey,
filter.ProviderKey,
filter.LayerKey,
filter.SubLayerKey,
}, "\t"))
}
// Sort and build table.
sort.Strings(rows)
buf := bytes.NewBuffer(nil)
tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
for _, row := range rows {
fmt.Fprint(tabWriter, row)
fmt.Fprint(tabWriter, "\n")
}
_ = tabWriter.Flush()
return buf.String()
}
// wfpState is the WFP state as returned by `netsh.exe wfp show state -`.
type wfpState struct {
XMLName xml.Name `xml:"wfpstate"`
Text string `xml:",chardata"`
TimeStamp string `xml:"timeStamp"`
Providers struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
ProviderKey string `xml:"providerKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item string `xml:"item"`
} `xml:"flags"`
ProviderData string `xml:"providerData"`
ServiceName string `xml:"serviceName"`
} `xml:"item"`
} `xml:"providers"`
SubLayers struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
SubLayerKey string `xml:"subLayerKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item string `xml:"item"`
} `xml:"flags"`
ProviderKey string `xml:"providerKey"`
ProviderData string `xml:"providerData"`
Weight string `xml:"weight"`
} `xml:"item"`
} `xml:"subLayers"`
Layers struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
Layer struct {
Text string `xml:",chardata"`
LayerKey string `xml:"layerKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []string `xml:"item"`
} `xml:"flags"`
Field struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
FieldKey string `xml:"fieldKey"`
Type string `xml:"type"`
DataType string `xml:"dataType"`
} `xml:"item"`
} `xml:"field"`
DefaultSubLayerKey string `xml:"defaultSubLayerKey"`
LayerID string `xml:"layerId"`
} `xml:"layer"`
Callouts struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
CalloutKey string `xml:"calloutKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []string `xml:"item"`
} `xml:"flags"`
ProviderKey string `xml:"providerKey"`
ProviderData string `xml:"providerData"`
ApplicableLayer string `xml:"applicableLayer"`
CalloutID string `xml:"calloutId"`
} `xml:"item"`
} `xml:"callouts"`
Filters struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
FilterKey string `xml:"filterKey"`
DisplayData struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Description string `xml:"description"`
} `xml:"displayData"`
Flags struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []string `xml:"item"`
} `xml:"flags"`
ProviderKey string `xml:"providerKey"`
ProviderData struct {
Text string `xml:",chardata"`
Data string `xml:"data"`
AsString string `xml:"asString"`
} `xml:"providerData"`
LayerKey string `xml:"layerKey"`
SubLayerKey string `xml:"subLayerKey"`
Weight struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint8 string `xml:"uint8"`
Uint64 string `xml:"uint64"`
} `xml:"weight"`
FilterCondition struct {
Text string `xml:",chardata"`
NumItems string `xml:"numItems,attr"`
Item []struct {
Text string `xml:",chardata"`
FieldKey string `xml:"fieldKey"`
MatchType string `xml:"matchType"`
ConditionValue struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint32 string `xml:"uint32"`
Uint16 string `xml:"uint16"`
RangeValue struct {
Text string `xml:",chardata"`
ValueLow struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint16 string `xml:"uint16"`
Uint32 string `xml:"uint32"`
ByteArray16 string `xml:"byteArray16"`
} `xml:"valueLow"`
ValueHigh struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint16 string `xml:"uint16"`
Uint32 string `xml:"uint32"`
ByteArray16 string `xml:"byteArray16"`
} `xml:"valueHigh"`
} `xml:"rangeValue"`
Uint8 string `xml:"uint8"`
ByteBlob struct {
Text string `xml:",chardata"`
Data string `xml:"data"`
AsString string `xml:"asString"`
} `xml:"byteBlob"`
Sd string `xml:"sd"`
Sid string `xml:"sid"`
Uint64 string `xml:"uint64"`
} `xml:"conditionValue"`
} `xml:"item"`
} `xml:"filterCondition"`
Action struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
FilterType string `xml:"filterType"`
} `xml:"action"`
RawContext string `xml:"rawContext"`
Reserved string `xml:"reserved"`
FilterID string `xml:"filterId"`
EffectiveWeight struct {
Text string `xml:",chardata"`
Type string `xml:"type"`
Uint64 string `xml:"uint64"`
} `xml:"effectiveWeight"`
ProviderContextKey string `xml:"providerContextKey"`
} `xml:"item"`
} `xml:"filters"`
} `xml:"item"`
} `xml:"layers"`
}
func parseWFPState(data []byte) (*wfpState, error) {
w := &wfpState{}
err := xml.Unmarshal(data, w)
if err != nil {
return nil, err
}
return w, nil
}
func (w *wfpState) simplified() *SimplifiedWFPState {
sw := &SimplifiedWFPState{
Providers: make([]*WFPProvider, 0, len(w.Providers.Item)),
SubLayers: make([]*WFPSubLayer, 0, len(w.SubLayers.Item)),
Callouts: make([]*WFPCallout, 0, len(w.Layers.Item)),
Filters: make([]*WFPFilter, 0, len(w.Layers.Item)),
}
// Collect data.
for _, provider := range w.Providers.Item {
if isIgnoredProvider(provider.DisplayData.Name, provider.ProviderKey) {
continue
}
sw.Providers = append(sw.Providers, &WFPProvider{
Name: defaultTo(provider.DisplayData.Name, "[no name]"),
Description: defaultTo(provider.DisplayData.Description, "[no description]"),
ProviderKey: defaultTo(provider.ProviderKey, "[no provider key]"),
})
}
for _, subLayer := range w.SubLayers.Item {
if isIgnoredProvider(subLayer.DisplayData.Name, "") {
continue
}
sw.SubLayers = append(sw.SubLayers, &WFPSubLayer{
Name: defaultTo(subLayer.DisplayData.Name, "[no name]"),
Description: defaultTo(subLayer.DisplayData.Description, "[no description]"),
SubLayerKey: defaultTo(subLayer.SubLayerKey, "[no sublayer key]"),
})
}
for _, layer := range w.Layers.Item {
for _, callout := range layer.Callouts.Item {
if isIgnoredProvider(callout.DisplayData.Name, callout.ProviderKey) {
continue
}
sw.Callouts = append(sw.Callouts, &WFPCallout{
Name: defaultTo(callout.DisplayData.Name, "[no name]"),
Description: defaultTo(callout.DisplayData.Description, "[no description]"),
CalloutKey: defaultTo(callout.CalloutKey, "[no callout key]"),
ProviderKey: defaultTo(callout.ProviderKey, "[no provider key]"),
ApplicableLayer: defaultTo(callout.ApplicableLayer, "[no applicable layer]"),
})
}
for _, filter := range layer.Filters.Item {
if isIgnoredProvider(filter.DisplayData.Name, filter.ProviderKey) {
continue
}
sw.Filters = append(sw.Filters, &WFPFilter{
Name: defaultTo(filter.DisplayData.Name, "[no name]"),
Description: defaultTo(filter.DisplayData.Description, "[no description]"),
FilterKey: defaultTo(filter.FilterKey, "[no filter key]"),
ProviderKey: defaultTo(filter.ProviderKey, "[no provider key]"),
LayerKey: defaultTo(filter.LayerKey, "[no layer key]"),
SubLayerKey: defaultTo(filter.SubLayerKey, "[no sublayer key]"),
})
}
}
return sw
}
func isIgnoredProvider(name, key string) bool {
// Check provider key.
if key != "" {
matched := true
switch key {
case "{1bebc969-61a5-4732-a177-847a0817862a}": // Microsoft Windows Defender Firewall IPsec Provider.
case "{4b153735-1049-4480-aab4-d1b9bdc03710}": // Microsoft Windows Defender Firewall Provider.
case "{893a4f22-9bba-49b7-8c66-3d40929c8fd5}": // Microsoft Windows Teredo firewall provider.
case "{8e44982a-f477-11df-85ce-78e7d1810190}": // Windows Network Data Usage (NDU) Provider.
case "{9c2532b4-0314-434f-8274-0cbaebdbda56}": // Microsoft Windows edge traversal socket option authorization provider.
case "{aa6a7d87-7f8f-4d2a-be53-fda555cd5fe3}": // Microsoft Windows Defender Firewall IPsec Provider.
case "{c698301d-9129-450c-937c-f4b834bfb374}": // Microsoft Windows edge traversal socket option authorization provider.
case "{decc16ca-3f33-4346-be1e-8fb4ae0f3d62}": // Microsoft Windows Defender Firewall Provider.
case "FWPM_PROVIDER_IKEEXT": // Microsoft Windows WFP Built-in IKEEXT provider used to identify filters added by IKE/AuthIP.
case "FWPM_PROVIDER_IPSEC_DOSP_CONFIG": // Microsoft Windows WFP Built-in IPsec DoS Protection configuration provider used to identify filters added by IPsec Denial of Service Protection.
case "FWPM_PROVIDER_MPSSVC_APP_ISOLATION": // Microsoft Windows WFP Built-in MPSSVC App Isolation provider.
case "FWPM_PROVIDER_MPSSVC_EDP": // Microsoft Windows WFP Built-in MPSSVC Enterprise Data Protection provider.
case "FWPM_PROVIDER_MPSSVC_TENANT_RESTRICTIONS": // Microsoft Windows WFP Built-in MPSSVC Tenant Restrictions provider.
case "FWPM_PROVIDER_MPSSVC_WF": // Microsoft Windows WFP Built-in MPSSVC Windows Firewall provider.
case "FWPM_PROVIDER_MPSSVC_WSH": // Microsoft Windows WFP Built-in MPSSVC Windows Service Hardening and Quarantine provider.
case "FWPM_PROVIDER_TCP_CHIMNEY_OFFLOAD": // Microsoft Windows WFP Built-in TCP Chimney Offload provider used to identify filters added by TCP Chimney Offload.
case "FWPM_PROVIDER_TCP_TEMPLATES": // Microsoft Windows WFP Built-in TCP Templates provider used to identify filters added by TCP Template based configuration.
default:
matched = false
}
if matched {
return true
}
}
// Some entries don't have a provider key (set).
// These are pretty generic, but the output strings are localized.
if name != "" {
switch {
case strings.Contains(name, "Microsoft Corporation"):
return true
case strings.Contains(name, "windefend"):
return true
case strings.Contains(name, "WFP"):
return true
case strings.Contains(name, "RPC"):
return true
case strings.Contains(name, "NDU"):
return true
}
}
return false
}
func defaultTo(a, b string) string {
if a != "" {
return a
}
return b
}