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>
This commit is contained in:
Daniel Hååvi
2024-08-09 17:15:48 +02:00
committed by GitHub
parent 10a77498f4
commit 80664d1a27
647 changed files with 37690 additions and 3366 deletions

View File

@@ -0,0 +1,51 @@
package osdetail
import (
"sync"
"golang.org/x/sys/windows"
)
var (
colorSupport bool
colorSupportChecked bool
checkingColorSupport sync.Mutex
)
// EnableColorSupport tries to enable color support for cmd on windows and returns whether it is enabled.
func EnableColorSupport() bool {
checkingColorSupport.Lock()
defer checkingColorSupport.Unlock()
if !colorSupportChecked {
colorSupport = enableColorSupport()
colorSupportChecked = true
}
return colorSupport
}
func enableColorSupport() bool {
if IsAtLeastWindowsNTVersionWithDefault("10", false) {
// check if windows.Stdout is file
if windows.GetFileInformationByHandle(windows.Stdout, &windows.ByHandleFileInformation{}) == nil {
return false
}
var mode uint32
err := windows.GetConsoleMode(windows.Stdout, &mode)
if err == nil {
if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 {
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
err = windows.SetConsoleMode(windows.Stdout, mode)
if err != nil {
return false
}
}
return true
}
}
return false
}

View File

@@ -0,0 +1,51 @@
package osdetail
import (
"bytes"
"errors"
"os/exec"
"strings"
)
// RunCmd runs the given command and run error checks on the output.
func RunCmd(command ...string) (output []byte, err error) {
// Create command to execute.
var cmd *exec.Cmd
switch len(command) {
case 0:
return nil, errors.New("no command supplied")
case 1:
cmd = exec.Command(command[0])
default:
cmd = exec.Command(command[0], command[1:]...)
}
// Create and assign output buffers.
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
// Run command and collect output.
err = cmd.Run()
stdout, stderr := stdoutBuf.Bytes(), stderrBuf.Bytes()
if err != nil {
return nil, err
}
// Command might not return an error, but just write to stdout instead.
if len(stderr) > 0 {
return nil, errors.New(strings.SplitN(string(stderr), "\n", 2)[0])
}
// Debugging output:
// fmt.Printf("command stdout: %s\n", stdout)
// fmt.Printf("command stderr: %s\n", stderr)
// Finalize stdout.
cleanedOutput := bytes.TrimSpace(stdout)
if len(cleanedOutput) == 0 {
return nil, ErrEmptyOutput
}
return cleanedOutput, nil
}

View File

@@ -0,0 +1,17 @@
package osdetail
import (
"os/exec"
)
// EnableDNSCache enables the Windows Service "DNS Client" by setting the registry value "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Dnscache" to 2 (Automatic).
// A reboot is required for this setting to take effect.
func EnableDNSCache() error {
return exec.Command("reg", "add", "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\services\\Dnscache", "/v", "Start", "/t", "REG_DWORD", "/d", "2", "/f").Run()
}
// DisableDNSCache disables the Windows Service "DNS Client" by setting the registry value "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Dnscache" to 4 (Disabled).
// A reboot is required for this setting to take effect.
func DisableDNSCache() error {
return exec.Command("reg", "add", "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\services\\Dnscache", "/v", "Start", "/t", "REG_DWORD", "/d", "4", "/f").Run()
}

View File

@@ -0,0 +1,12 @@
package osdetail
import "errors"
var (
// ErrNotSupported is returned when an operation is not supported on the current platform.
ErrNotSupported = errors.New("not supported")
// ErrNotFound is returned when the desired data is not found.
ErrNotFound = errors.New("not found")
// ErrEmptyOutput is a special error that is returned when an operation has no error, but also returns to data.
ErrEmptyOutput = errors.New("command succeeded with empty output")
)

View File

@@ -0,0 +1,112 @@
package osdetail
import (
"errors"
"fmt"
"os/exec"
"strings"
"time"
)
// Service Status
const (
StatusUnknown uint8 = iota
StatusRunningStoppable
StatusRunningNotStoppable
StatusStartPending
StatusStopPending
StatusStopped
)
// Exported errors
var (
ErrServiceNotStoppable = errors.New("the service is not stoppable")
)
// GetServiceStatus returns the current status of a Windows Service (limited implementation).
func GetServiceStatus(name string) (status uint8, err error) {
output, err := exec.Command("sc", "query", name).Output()
if err != nil {
return StatusUnknown, fmt.Errorf("failed to query service: %s", err)
}
outputString := string(output)
switch {
case strings.Contains(outputString, "RUNNING"):
if strings.Contains(outputString, "NOT_STOPPABLE") {
return StatusRunningNotStoppable, nil
}
return StatusRunningStoppable, nil
case strings.Contains(outputString, "STOP_PENDING"):
return StatusStopPending, nil
case strings.Contains(outputString, "STOPPED"):
return StatusStopped, nil
case strings.Contains(outputString, "START_PENDING"):
return StatusStopPending, nil
}
return StatusUnknown, errors.New("unknown service status")
}
// StopService stops a Windows Service.
func StopService(name string) (err error) {
pendingCnt := 0
for {
// get status
status, err := GetServiceStatus(name)
if err != nil {
return err
}
switch status {
case StatusRunningStoppable:
err := exec.Command("sc", "stop", name).Run()
if err != nil {
return fmt.Errorf("failed to stop service: %s", err)
}
case StatusRunningNotStoppable:
return ErrServiceNotStoppable
case StatusStartPending, StatusStopPending:
pendingCnt++
if pendingCnt > 50 {
return errors.New("service stuck in pending status (5s)")
}
case StatusStopped:
return nil
}
time.Sleep(100 * time.Millisecond)
}
}
// SartService starts a Windows Service.
func SartService(name string) (err error) {
pendingCnt := 0
for {
// get status
status, err := GetServiceStatus(name)
if err != nil {
return err
}
switch status {
case StatusRunningStoppable, StatusRunningNotStoppable:
return nil
case StatusStartPending, StatusStopPending:
pendingCnt++
if pendingCnt > 50 {
return errors.New("service stuck in pending status (5s)")
}
case StatusStopped:
err := exec.Command("sc", "start", name).Run()
if err != nil {
return fmt.Errorf("failed to stop service: %s", err)
}
}
time.Sleep(100 * time.Millisecond)
}
}

View File

@@ -0,0 +1,49 @@
package osdetail
import (
"bytes"
"errors"
)
// RunPowershellCmd runs a powershell command and returns its output.
func RunPowershellCmd(script string) (output []byte, err error) {
// Create command to execute.
return RunCmd(
"powershell.exe",
"-ExecutionPolicy", "Bypass",
"-NoProfile",
"-NonInteractive",
"[System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n"+script,
)
}
const outputSeparator = "pwzzhtuvpwdgozhzbnjj"
// RunTerminalCmd runs a Windows cmd command and returns its output.
// It sets the output of the cmd to UTF-8 in order to avoid encoding errors.
func RunTerminalCmd(command ...string) (output []byte, err error) {
output, err = RunCmd(append([]string{
"cmd.exe",
"/c",
"chcp", // Set output encoding...
"65001", // ...to UTF-8.
"&",
"echo",
outputSeparator,
"&",
},
command...,
)...)
if err != nil {
return nil, err
}
// Find correct start of output and shift start.
index := bytes.IndexAny(output, outputSeparator+"\r\n")
if index < 0 {
return nil, errors.New("failed to post-process output: could not find output separator")
}
output = output[index+len(outputSeparator)+2:]
return output, nil
}

View File

@@ -0,0 +1,120 @@
package osdetail
import (
"bufio"
"bytes"
"errors"
"fmt"
"os/exec"
"strconv"
"strings"
"sync"
)
var (
serviceNames map[int32][]string
serviceNamesLock sync.Mutex
)
// Errors
var (
ErrServiceNotFound = errors.New("no service with the given PID was found")
)
// GetServiceNames returns all service names assosicated with a svchost.exe process on Windows.
func GetServiceNames(pid int32) ([]string, error) {
serviceNamesLock.Lock()
defer serviceNamesLock.Unlock()
if serviceNames != nil {
names, ok := serviceNames[pid]
if ok {
return names, nil
}
}
serviceNames, err := GetAllServiceNames()
if err != nil {
return nil, err
}
names, ok := serviceNames[pid]
if ok {
return names, nil
}
return nil, ErrServiceNotFound
}
// GetAllServiceNames returns a list of service names assosicated with svchost.exe processes on Windows.
func GetAllServiceNames() (map[int32][]string, error) {
output, err := exec.Command("tasklist", "/svc", "/fi", "imagename eq svchost.exe").Output()
if err != nil {
return nil, fmt.Errorf("failed to get svchost tasklist: %s", err)
}
// file scanner
scanner := bufio.NewScanner(bytes.NewReader(output))
scanner.Split(bufio.ScanLines)
// skip output header
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "=") {
break
}
}
var (
pid int32
services []string
collection = make(map[int32][]string)
)
for scanner.Scan() {
// get fields of line
fields := strings.Fields(scanner.Text())
// check fields length
if len(fields) == 0 {
continue
}
// new entry
if fields[0] == "svchost.exe" {
// save old entry
if pid != 0 {
collection[pid] = services
}
// reset PID
pid = 0
services = make([]string, 0, len(fields))
// check fields length
if len(fields) < 3 {
continue
}
// get pid
i, err := strconv.ParseInt(fields[1], 10, 32)
if err != nil {
continue
}
pid = int32(i)
// skip used fields
fields = fields[2:]
}
// add service names
for _, field := range fields {
services = append(services, strings.Trim(strings.TrimSpace(field), ","))
}
}
if pid != 0 {
// save last entry
collection[pid] = services
}
return collection, nil
}

View File

@@ -0,0 +1,99 @@
package osdetail
import (
"fmt"
"strings"
"sync"
"github.com/hashicorp/go-version"
"github.com/shirou/gopsutil/host"
)
var (
// versionRe = regexp.MustCompile(`[0-9\.]+`)
windowsNTVersion string
windowsNTVersionForCmp *version.Version
fetching sync.Mutex
fetched bool
)
// WindowsNTVersion returns the current Windows version.
func WindowsNTVersion() (string, error) {
var err error
fetching.Lock()
defer fetching.Unlock()
if !fetched {
_, _, windowsNTVersion, err = host.PlatformInformation()
windowsNTVersion = strings.SplitN(windowsNTVersion, " ", 2)[0]
if err != nil {
return "", fmt.Errorf("failed to obtain Windows-Version: %s", err)
}
windowsNTVersionForCmp, err = version.NewVersion(windowsNTVersion)
if err != nil {
return "", fmt.Errorf("failed to parse Windows-Version %s: %s", windowsNTVersion, err)
}
fetched = true
}
return windowsNTVersion, err
}
// IsAtLeastWindowsNTVersion returns whether the current WindowsNT version is at least the given version or newer.
func IsAtLeastWindowsNTVersion(v string) (bool, error) {
_, err := WindowsNTVersion()
if err != nil {
return false, err
}
versionForCmp, err := version.NewVersion(v)
if err != nil {
return false, err
}
return windowsNTVersionForCmp.GreaterThanOrEqual(versionForCmp), nil
}
// IsAtLeastWindowsNTVersionWithDefault is like IsAtLeastWindowsNTVersion(), but keeps the Error and returns the default Value in Errorcase
func IsAtLeastWindowsNTVersionWithDefault(v string, defaultValue bool) bool {
val, err := IsAtLeastWindowsNTVersion(v)
if err != nil {
return defaultValue
}
return val
}
// IsAtLeastWindowsVersion returns whether the current Windows version is at least the given version or newer.
func IsAtLeastWindowsVersion(v string) (bool, error) {
var NTVersion string
switch v {
case "7":
NTVersion = "6.1"
case "8":
NTVersion = "6.2"
case "8.1":
NTVersion = "6.3"
case "10":
NTVersion = "10"
default:
return false, fmt.Errorf("failed to compare Windows-Version: Windows %s is unknown", v)
}
return IsAtLeastWindowsNTVersion(NTVersion)
}
// IsAtLeastWindowsVersionWithDefault is like IsAtLeastWindowsVersion(), but keeps the Error and returns the default Value in Errorcase
func IsAtLeastWindowsVersionWithDefault(v string, defaultValue bool) bool {
val, err := IsAtLeastWindowsVersion(v)
if err != nil {
return defaultValue
}
return val
}

View File

@@ -0,0 +1,29 @@
package osdetail
import "testing"
func TestWindowsNTVersion(t *testing.T) {
if str, err := WindowsNTVersion(); str == "" || err != nil {
t.Fatalf("failed to obtain windows version: %s", err)
}
}
func TestIsAtLeastWindowsNTVersion(t *testing.T) {
ret, err := IsAtLeastWindowsNTVersion("6")
if err != nil {
t.Fatalf("failed to compare windows versions: %s", err)
}
if !ret {
t.Fatalf("WindowsNTVersion is less than 6 (Vista)")
}
}
func TestIsAtLeastWindowsVersion(t *testing.T) {
ret, err := IsAtLeastWindowsVersion("7")
if err != nil {
t.Fatalf("failed to compare windows versions: %s", err)
}
if !ret {
t.Fatalf("WindowsVersion is less than 7")
}
}