wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

3
cmds/hub/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Compiled binaries
hub
hub.exe

60
cmds/hub/build Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# get build data
if [[ "$BUILD_COMMIT" == "" ]]; then
BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null)
fi
if [[ "$BUILD_USER" == "" ]]; then
BUILD_USER=$(id -un)
fi
if [[ "$BUILD_HOST" == "" ]]; then
BUILD_HOST=$(hostname -f)
fi
if [[ "$BUILD_DATE" == "" ]]; then
BUILD_DATE=$(date +%d.%m.%Y)
fi
if [[ "$BUILD_SOURCE" == "" ]]; then
BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1)
fi
if [[ "$BUILD_SOURCE" == "" ]]; then
BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1)
fi
BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g")
# check
if [[ "$BUILD_COMMIT" == "" ]]; then
echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_USER" == "" ]]; then
echo "could not automatically determine BUILD_USER, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_HOST" == "" ]]; then
echo "could not automatically determine BUILD_HOST, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_DATE" == "" ]]; then
echo "could not automatically determine BUILD_DATE, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_SOURCE" == "" ]]; then
echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable."
exit 1
fi
# set build options
export CGO_ENABLED=0
if [[ $1 == "dev" ]]; then
shift
export CGO_ENABLED=1
DEV="-race"
fi
echo "Please notice, that this build script includes metadata into the build."
echo "This information is useful for debugging and license compliance."
echo "Run the compiled binary with the -version flag to see the information included."
# build
BUILD_PATH="github.com/safing/portbase/info"
go build $DEV -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*

66
cmds/hub/main.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"flag"
"fmt"
"os"
"runtime"
"github.com/safing/portbase/info"
"github.com/safing/portbase/metrics"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/run"
_ "github.com/safing/portmaster/service/core/base"
_ "github.com/safing/portmaster/service/ui"
"github.com/safing/portmaster/service/updates"
"github.com/safing/portmaster/service/updates/helper"
_ "github.com/safing/portmaster/spn/captain"
"github.com/safing/portmaster/spn/conf"
)
func init() {
flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade")
}
func main() {
info.Set("SPN Hub", "0.7.6", "AGPLv3", true)
// Configure metrics.
_ = metrics.SetNamespace("hub")
// Configure updating.
updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
helper.IntelOnly()
// Configure SPN mode.
conf.EnablePublicHub(true)
conf.EnableClient(false)
// Disable module management, as we want to start all modules.
modules.DisableModuleManagement()
// Configure microtask threshold.
// Scale with CPU/GOMAXPROCS count, but keep a baseline and minimum:
// CPUs -> MicroTasks
// 0 -> 8 (increased to minimum)
// 1 -> 8 (increased to minimum)
// 2 -> 8
// 3 -> 10
// 4 -> 12
// 8 -> 20
// 16 -> 36
//
// Start with number of GOMAXPROCS.
microTasksThreshold := runtime.GOMAXPROCS(0) * 2
// Use at least 4 microtasks based on GOMAXPROCS.
if microTasksThreshold < 4 {
microTasksThreshold = 4
}
// Add a 4 microtask baseline.
microTasksThreshold += 4
// Set threshold.
modules.SetMaxConcurrentMicroTasks(microTasksThreshold)
// Start.
os.Exit(run.Run())
}

123
cmds/hub/pack Executable file
View File

@@ -0,0 +1,123 @@
#!/bin/bash
baseDir="$( cd "$(dirname "$0")" && pwd )"
cd "$baseDir"
COL_OFF="\033[0m"
COL_BOLD="\033[01;01m"
COL_RED="\033[31m"
COL_GREEN="\033[32m"
COL_YELLOW="\033[33m"
destDirPart1="../../dist"
destDirPart2="hub"
function prep {
# output
output="main"
# get version
version=$(grep "info.Set" main.go | cut -d'"' -f4)
# build versioned file name
filename="spn-hub_v${version//./-}"
# platform
platform="${GOOS}_${GOARCH}"
if [[ $GOOS == "windows" ]]; then
filename="${filename}.exe"
output="${output}.exe"
fi
# build destination path
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
}
function check {
prep
# check if file exists
if [[ -f $destPath ]]; then
echo "[hub] $platform v$version already built"
else
echo -e "${COL_BOLD}[hub] $platform v$version${COL_OFF}"
fi
}
function build {
prep
# check if file exists
if [[ -f $destPath ]]; then
echo "[hub] $platform already built in v$version, skipping..."
return
fi
# build
./build main.go
if [[ $? -ne 0 ]]; then
echo -e "\n${COL_BOLD}[hub] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}"
exit 1
fi
mkdir -p $(dirname $destPath)
cp $output $destPath
echo -e "\n${COL_BOLD}[hub] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}"
}
function reset {
prep
# delete if file exists
if [[ -f $destPath ]]; then
rm $destPath
echo "[hub] $platform v$version deleted."
fi
}
function check_all {
GOOS=linux GOARCH=amd64 check
GOOS=windows GOARCH=amd64 check
GOOS=darwin GOARCH=amd64 check
GOOS=linux GOARCH=arm64 check
GOOS=windows GOARCH=arm64 check
GOOS=darwin GOARCH=arm64 check
}
function build_all {
GOOS=linux GOARCH=amd64 build
GOOS=windows GOARCH=amd64 build
GOOS=darwin GOARCH=amd64 build
GOOS=linux GOARCH=arm64 build
GOOS=windows GOARCH=arm64 build
GOOS=darwin GOARCH=arm64 build
}
function reset_all {
GOOS=linux GOARCH=amd64 reset
GOOS=windows GOARCH=amd64 reset
GOOS=darwin GOARCH=amd64 reset
GOOS=linux GOARCH=arm64 reset
GOOS=windows GOARCH=arm64 reset
GOOS=darwin GOARCH=arm64 reset
}
case $1 in
"check" )
check_all
;;
"build" )
build_all
;;
"reset" )
reset_all
;;
* )
echo ""
echo "build list:"
echo ""
check_all
echo ""
read -p "press [Enter] to start building" x
echo ""
build_all
echo ""
echo "finished building."
echo ""
;;
esac

View File

@@ -7,9 +7,9 @@ import (
processInfo "github.com/shirou/gopsutil/process"
"github.com/spf13/cobra"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/network/socket"
"github.com/safing/portmaster/network/state"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/socket"
"github.com/safing/portmaster/service/network/state"
)
func init() {

3
cmds/observation-hub/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Compiled binaries
observation-hub
observation-hub.exe

View File

@@ -0,0 +1,38 @@
# Docker Image for Observation Hub
# Important:
# You need to build this from the repo root!
# Run: docker build -f cmds/observation-hub/Dockerfile -t safing/observation-hub:latest .
# Check With: docker run -ti --rm safing/observation-hub:latest --help
# golang 1.21 linux/amd64 on debian bookworm
# https://github.com/docker-library/golang/blob/master/1.21/bookworm/Dockerfile
FROM golang:1.21-bookworm as builder
# Ensure ca-certficates are up to date
RUN update-ca-certificates
# Install dependencies
WORKDIR $GOPATH/src/github.com/safing/portmaster/spn
COPY go.mod .
COPY go.sum .
ENV GO111MODULE=on
RUN go mod download
RUN go mod verify
# Copy source code
COPY . .
# Build the static binary
RUN cd cmds/observation-hub && \
CGO_ENABLED=0 ./build -o /go/bin/observation-hub
# Use static image
# https://github.com/GoogleContainerTools/distroless
FROM gcr.io/distroless/static-debian12
# Copy our static executable
COPY --from=builder --chmod=0755 /go/bin/observation-hub /go/bin/observation-hub
# Run the observation-hub binary.
ENTRYPOINT ["/go/bin/observation-hub"]

View File

@@ -0,0 +1,257 @@
package main
import (
"bytes"
"crypto/tls"
_ "embed"
"errors"
"flag"
"fmt"
"net/http"
"strings"
"text/template"
"time"
"github.com/safing/portbase/apprise"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/service/intel/geoip"
)
var (
appriseModule *modules.Module
appriseNotifier *apprise.Notifier
appriseURL string
appriseTag string
appriseClientCert string
appriseClientKey string
appriseGreet bool
)
func init() {
appriseModule = modules.Register("apprise", nil, startApprise, nil)
flag.StringVar(&appriseURL, "apprise-url", "", "set the apprise URL to enable notifications via apprise")
flag.StringVar(&appriseTag, "apprise-tag", "", "set the apprise tag(s) according to their docs")
flag.StringVar(&appriseClientCert, "apprise-client-cert", "", "set the apprise client certificate")
flag.StringVar(&appriseClientKey, "apprise-client-key", "", "set the apprise client key")
flag.BoolVar(&appriseGreet, "apprise-greet", false, "send a greeting message to apprise on start")
}
func startApprise() error {
// Check if apprise should be configured.
if appriseURL == "" {
return nil
}
// Check if there is a tag.
if appriseTag == "" {
return errors.New("an apprise tag is required")
}
// Create notifier.
appriseNotifier = &apprise.Notifier{
URL: appriseURL,
DefaultType: apprise.TypeInfo,
DefaultTag: appriseTag,
DefaultFormat: apprise.FormatMarkdown,
AllowUntagged: false,
}
if appriseClientCert != "" || appriseClientKey != "" {
// Load client cert from disk.
cert, err := tls.LoadX509KeyPair(appriseClientCert, appriseClientKey)
if err != nil {
return fmt.Errorf("failed to load client cert/key: %w", err)
}
// Set client cert in http client.
appriseNotifier.SetClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
},
},
Timeout: 10 * time.Second,
})
}
if appriseGreet {
err := appriseNotifier.Send(appriseModule.Ctx, &apprise.Message{
Title: "👋 Observation Hub Reporting In",
Body: "I am the Observation Hub. I am connected to the SPN and watch out for it. I will report notable changes to the network here.",
})
if err != nil {
log.Warningf("apprise: failed to send test message: %s", err)
} else {
log.Info("apprise: sent greeting message")
}
}
return nil
}
func reportToApprise(change *observedChange) (errs error) {
// Check if configured.
if appriseNotifier == nil {
return nil
}
handleTag:
for _, tag := range strings.Split(appriseNotifier.DefaultTag, ",") {
// Check if we are shutting down.
if appriseModule.IsStopping() {
return nil
}
// Render notification based on tag / destination.
buf := &bytes.Buffer{}
switch {
case strings.HasPrefix(tag, "matrix-"):
if err := templates.ExecuteTemplate(buf, "matrix-notification", change); err != nil {
return fmt.Errorf("failed to render notification: %w", err)
}
case strings.HasPrefix(tag, "discord-"):
if err := templates.ExecuteTemplate(buf, "discord-notification", change); err != nil {
return fmt.Errorf("failed to render notification: %w", err)
}
default:
// Use matrix notification template as default for now.
if err := templates.ExecuteTemplate(buf, "matrix-notification", change); err != nil {
return fmt.Errorf("failed to render notification: %w", err)
}
}
// Send notification to apprise.
var err error
for i := 0; i < 3; i++ {
// Try three times.
err = appriseNotifier.Send(appriseModule.Ctx, &apprise.Message{
Body: buf.String(),
Tag: tag,
})
if err == nil {
continue handleTag
}
// Wait for 5 seconds, then try again.
time.Sleep(5 * time.Second)
}
// Add error to errors.
if err != nil {
errs = errors.Join(errs, fmt.Errorf("| failed to send: %w", err))
}
}
return errs
}
// var (
// entityTemplate = template.Must(template.New("entity").Parse(
// `Entity: {{ . }}
// {{ .IP }} [{{ .ASN }} - {{ .ASOrg }}]
// `,
// ))
// // {{ with .GetCountryInfo -}}
// // {{ .Name }} ({{ .Code }})
// // {{- end }}
// matrixTemplate = template.Must(template.New("matrix observer notification").Parse(
// `{{ .Title }}
// {{ if .Summary }}
// Details:
// {{ .Summary }}
// Note: Changes were registered at {{ .UpdateTime }} and were possibly merged.
// {{ end }}
// {{ template "entity" .UpdatedPin.EntityV4 }}
// Hub Info:
// Test: {{ .UpdatedPin.EntityV4 }}
// {{ template "entity" .UpdatedPin.EntityV4 }}
// {{ template "entity" .UpdatedPin.EntityV6 }}
// `,
// ))
// discordTemplate = template.Must(template.New("discord observer notification").Parse(
// ``,
// ))
// defaultTemplate = template.Must(template.New("default observer notification").Parse(
// ``,
// ))
// )
var (
//go:embed notifications.tmpl
templateFile string
templates = template.Must(template.New("notifications").Funcs(
template.FuncMap{
"joinStrings": joinStrings,
"textBlock": textBlock,
"getCountryInfo": getCountryInfo,
},
).Parse(templateFile))
)
func joinStrings(slice []string, sep string) string {
return strings.Join(slice, sep)
}
func textBlock(block, addPrefix, addSuffix string) string {
// Trim whitespaces.
block = strings.TrimSpace(block)
// Prepend and append string for every line.
lines := strings.Split(block, "\n")
for i, line := range lines {
lines[i] = addPrefix + line + addSuffix
}
// Return as block.
return strings.Join(lines, "\n")
}
func getCountryInfo(code string) geoip.CountryInfo {
// Get the country info directly instead of via the entity location,
// so it also works in test without the geoip module.
return geoip.GetCountryInfo(code)
}
// func init() {
// templates = template.Must(template.New(templateFile).Parse(templateFile))
// nt, err := templates.New("entity").Parse(
// `Entity: {{ . }}
// {{ .IP }} [{{ .ASN }} - {{ .ASOrg }}]
// `,
// )
// if err != nil {
// panic(err)
// }
// templates.AddParseTree(nt.Tree)
// if _, err := templates.New("matrix-notification").Parse(
// `{{ .Title }}
// {{ if .Summary }}
// Details:
// {{ .Summary }}
// Note: Changes were registered at {{ .UpdateTime }} and were possibly merged.
// {{ end }}
// {{ template "entity" .UpdatedPin.EntityV4 }}
// Hub Info:
// Test: {{ .UpdatedPin.EntityV4 }}
// {{ template "entity" .UpdatedPin.EntityV4 }}
// {{ template "entity" .UpdatedPin.EntityV6 }}
// `,
// ); err != nil {
// panic(err)
// }
// }

View File

@@ -0,0 +1,84 @@
package main
import (
"bytes"
"fmt"
"net"
"testing"
"time"
"github.com/safing/portmaster/service/intel"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/spn/hub"
"github.com/safing/portmaster/spn/navigator"
)
var observedTestChange = &observedChange{
Title: "Hub Changed: fogos (8uLe-zUkC)",
Summary: `ConnectedTo.ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5.HubID removed ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5
ConnectedTo.ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5.Capacity removed 3403661
ConnectedTo.ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5.Latency removed 252.350006ms`,
UpdatedPin: &navigator.PinExport{
ID: "Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC",
Name: "fogos",
Map: "main",
FirstSeen: time.Now(),
EntityV4: &intel.Entity{
IP: net.IPv4(138, 201, 140, 70),
IPScope: netutils.Global,
Country: "DE",
ASN: 24940,
ASOrg: "Hetzner Online GmbH",
},
States: []string{"HasRequiredInfo", "Reachable", "Active", "Trusted"},
VerifiedOwner: "Safing",
HopDistance: 3,
SessionActive: false,
Info: &hub.Announcement{
ID: "Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC",
Timestamp: 1677682008,
Name: "fogos",
Group: "Safing",
ContactAddress: "abuse@safing.io",
ContactService: "email",
Hosters: []string{"Hetzner"},
Datacenter: "DE-Hetzner-FSN",
IPv4: net.IPv4(138, 201, 140, 70),
IPv6: net.ParseIP("2a01:4f8:172:3753::2"),
Transports: []string{"tcp:17", "tcp:17017"},
Entry: []string{},
Exit: []string{"- * TCP/25"},
},
Status: &hub.Status{
Timestamp: 1694180778,
Version: "0.6.19 ",
},
},
UpdateTime: time.Now(),
}
func TestNotificationTemplate(t *testing.T) {
t.Parallel()
fmt.Println("==========\nFound templates:")
for _, tpl := range templates.Templates() {
fmt.Println(tpl.Name())
}
fmt.Println("")
fmt.Println("\n\n==========\nMatrix template:")
matrixOutput := &bytes.Buffer{}
err := templates.ExecuteTemplate(matrixOutput, "matrix-notification", observedTestChange)
if err != nil {
t.Errorf("failed to render matrix template: %s", err)
}
fmt.Println(matrixOutput.String())
fmt.Println("\n\n==========\nDiscord template:")
discordOutput := &bytes.Buffer{}
err = templates.ExecuteTemplate(discordOutput, "discord-notification", observedTestChange)
if err != nil {
t.Errorf("failed to render discord template: %s", err)
}
fmt.Println(discordOutput.String())
}

60
cmds/observation-hub/build Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# get build data
if [[ "$BUILD_COMMIT" == "" ]]; then
BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null)
fi
if [[ "$BUILD_USER" == "" ]]; then
BUILD_USER=$(id -un)
fi
if [[ "$BUILD_HOST" == "" ]]; then
BUILD_HOST=$(hostname -f)
fi
if [[ "$BUILD_DATE" == "" ]]; then
BUILD_DATE=$(date +%d.%m.%Y)
fi
if [[ "$BUILD_SOURCE" == "" ]]; then
BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1)
fi
if [[ "$BUILD_SOURCE" == "" ]]; then
BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1)
fi
BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g")
# check
if [[ "$BUILD_COMMIT" == "" ]]; then
echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_USER" == "" ]]; then
echo "could not automatically determine BUILD_USER, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_HOST" == "" ]]; then
echo "could not automatically determine BUILD_HOST, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_DATE" == "" ]]; then
echo "could not automatically determine BUILD_DATE, please supply manually as environment variable."
exit 1
fi
if [[ "$BUILD_SOURCE" == "" ]]; then
echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable."
exit 1
fi
# set build options
export CGO_ENABLED=0
if [[ $1 == "dev" ]]; then
shift
export CGO_ENABLED=1
DEV="-race"
fi
echo "Please notice, that this build script includes metadata into the build."
echo "This information is useful for debugging and license compliance."
echo "Run the compiled binary with the -version flag to see the information included."
# build
BUILD_PATH="github.com/safing/portbase/info"
go build $DEV -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*

View File

@@ -0,0 +1,44 @@
package main
import (
"fmt"
"os"
"runtime"
"github.com/safing/portbase/api"
"github.com/safing/portbase/info"
"github.com/safing/portbase/metrics"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/run"
"github.com/safing/portmaster/service/updates"
"github.com/safing/portmaster/service/updates/helper"
"github.com/safing/portmaster/spn/captain"
"github.com/safing/portmaster/spn/conf"
"github.com/safing/portmaster/spn/sluice"
)
func main() {
info.Set("SPN Observation Hub", "0.7.1", "AGPLv3", true)
// Configure metrics.
_ = metrics.SetNamespace("observer")
// Configure user agent.
updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
helper.IntelOnly()
// Configure SPN mode.
conf.EnableClient(true)
conf.EnablePublicHub(false)
captain.DisableAccount = true
// Disable unneeded listeners.
sluice.EnableListener = false
api.EnableServer = false
// Disable module management, as we want to start all modules.
modules.DisableModuleManagement()
// Start.
os.Exit(run.Run())
}

View File

@@ -0,0 +1,75 @@
{{ define "entity" -}}
{{ .IP }} [AS{{ .ASN }} - {{ .ASOrg }}] in {{ if .Country }}
{{- with getCountryInfo .Country -}}
{{ .Name }} ({{ .Code }}; Region {{ .Continent.Region }})
{{- end }}
{{- end }}
{{- end }}
{{ define "matrix-notification" -}}
### 🌍 {{ .Title }}{{ if .Summary }}
{{ textBlock .Summary "" " " }}
{{ end }}
> Note: Changes were registered at {{ .UpdateTime.UTC.Format "15:04:05 02.01.2006 MST" }} and were possibly merged.
##### Hub Info
> Name: {{ .UpdatedPin.Name }}
> ID: {{ .UpdatedPin.ID }}
> IPv4: {{ if .UpdatedPin.EntityV4 }}{{ template "entity" .UpdatedPin.EntityV4 }}{{ end }}
> IPv6: {{ if .UpdatedPin.EntityV6 }}{{ template "entity" .UpdatedPin.EntityV6 }}{{ end }}
> Version: {{ .UpdatedPin.Status.Version }}
> States: {{ joinStrings .UpdatedPin.States ", " }}
> Status: {{ len .UpdatedPin.Status.Lanes }} Lanes, {{ len .UpdatedPin.Status.Keys }} Keys, {{ .UpdatedPin.Status.Load }} Load
> Verified Owner: {{ .UpdatedPin.VerifiedOwner }}
> Transports: {{ joinStrings .UpdatedPin.Info.Transports ", " }}
> Entry: {{ joinStrings .UpdatedPin.Info.Entry ", " }}
> Exit: {{ joinStrings .UpdatedPin.Info.Exit ", " }}
> Relations: {{ if .UpdatedPin.Info.Group -}}
Group={{ .UpdatedPin.Info.Group }} {{ end }}
{{- if .UpdatedPin.Info.Datacenter -}}
Datacenter={{ .UpdatedPin.Info.Datacenter }} {{ end }}
{{- if .UpdatedPin.Info.Hosters -}}
Hosters={{ joinStrings .UpdatedPin.Info.Hosters ";" }} {{ end }}
{{- if .UpdatedPin.Info.ContactAddress -}}
Contact= {{ .UpdatedPin.Info.ContactAddress }}{{ if .UpdatedPin.Info.ContactService }} via {{ .UpdatedPin.Info.ContactService }}{{ end }}{{ end }}
{{- end }}
{{ define "discord-notification" -}}
# 🌍 {{ .Title }}{{ if .Summary }}
{{ .Summary }}
{{- end }}
##### Note: Changes were registered at {{ .UpdateTime.UTC.Format "15:04:05 02.01.2006 MST" }} and were possibly merged. - Hub Info:
Name: {{ .UpdatedPin.Name }}
ID: {{ .UpdatedPin.ID }}
IPv4: {{ if .UpdatedPin.EntityV4 }}{{ template "entity" .UpdatedPin.EntityV4 }}{{ end }}
IPv6: {{ if .UpdatedPin.EntityV6 }}{{ template "entity" .UpdatedPin.EntityV6 }}{{ end }}
Version: {{ .UpdatedPin.Status.Version }}
States: {{ joinStrings .UpdatedPin.States ", " }}
Status: {{ len .UpdatedPin.Status.Lanes }} Lanes, {{ len .UpdatedPin.Status.Keys }} Keys, {{ .UpdatedPin.Status.Load }} Load
Verified Owner: {{ .UpdatedPin.VerifiedOwner }}
Transports: {{ joinStrings .UpdatedPin.Info.Transports ", " }}
Entry: {{ joinStrings .UpdatedPin.Info.Entry ", " }}
Exit: {{ joinStrings .UpdatedPin.Info.Exit ", " }}
Relations: {{ if .UpdatedPin.Info.Group -}}
Group={{ .UpdatedPin.Info.Group }} {{ end }}
{{- if .UpdatedPin.Info.Datacenter -}}
Datacenter={{ .UpdatedPin.Info.Datacenter }} {{ end }}
{{- if .UpdatedPin.Info.Hosters -}}
Hosters={{ joinStrings .UpdatedPin.Info.Hosters ";" }} {{ end }}
{{- if .UpdatedPin.Info.ContactAddress -}}
Contact= {{ .UpdatedPin.Info.ContactAddress }}{{ if .UpdatedPin.Info.ContactService }} via {{ .UpdatedPin.Info.ContactService }}{{ end }}{{ end }}
{{- end }}

View File

@@ -0,0 +1,407 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"path"
"strings"
"time"
diff "github.com/r3labs/diff/v3"
"golang.org/x/exp/slices"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/spn/captain"
"github.com/safing/portmaster/spn/navigator"
)
var (
observerModule *modules.Module
db = database.NewInterface(&database.Options{
Local: true,
Internal: true,
})
reportAllChanges bool
errNoChanges = errors.New("no changes")
reportingDelayFlag string
reportingDelay = 10 * time.Minute
)
func init() {
observerModule = modules.Register("observer", prepObserver, startObserver, nil, "captain", "apprise")
flag.BoolVar(&reportAllChanges, "report-all-changes", false, "report all changes, no just interesting ones")
flag.StringVar(&reportingDelayFlag, "reporting-delay", "10m", "delay reports to summarize changes")
}
func prepObserver() error {
if reportingDelayFlag != "" {
duration, err := time.ParseDuration(reportingDelayFlag)
if err != nil {
return fmt.Errorf("failed to parse reporting-delay: %w", err)
}
reportingDelay = duration
}
return nil
}
func startObserver() error {
observerModule.StartServiceWorker("observer", 0, observerWorker)
return nil
}
type observedPin struct {
previous *navigator.PinExport
latest *navigator.PinExport
lastUpdate time.Time
lastUpdateReported bool
}
type observedChange struct {
Title string
Summary string
UpdatedPin *navigator.PinExport
UpdateTime time.Time
SPNStatus *captain.SPNStatus
}
func observerWorker(ctx context.Context) error {
log.Info("observer: starting")
defer log.Info("observer: stopped")
// Subscribe to SPN status.
statusSub, err := db.Subscribe(query.New("runtime:spn/status"))
if err != nil {
return fmt.Errorf("failed to subscribe to spn status: %w", err)
}
defer statusSub.Cancel() //nolint:errcheck
// Get latest status.
latestStatus := captain.GetSPNStatus()
// Step 1: Wait for SPN to connect, if needed.
if latestStatus.Status != captain.StatusConnected {
log.Info("observer: waiting for SPN to connect")
waitForConnect:
for {
select {
case r := <-statusSub.Feed:
if r == nil {
return errors.New("status feed ended")
}
statusUpdate, ok := r.(*captain.SPNStatus)
switch {
case !ok:
log.Warningf("observer: received invalid SPN status: %s", r)
case statusUpdate.Status == captain.StatusFailed:
log.Warningf("observer: SPN failed to connect")
case statusUpdate.Status == captain.StatusConnected:
break waitForConnect
}
case <-ctx.Done():
return nil
}
}
}
// Wait for one second for the navigator to settle things.
log.Info("observer: connected to network, waiting for navigator")
time.Sleep(1 * time.Second)
// Step 2: Get current state.
mapQuery := query.New("map:main/")
q, err := db.Query(mapQuery)
if err != nil {
return fmt.Errorf("failed to start map query: %w", err)
}
defer q.Cancel()
// Put all current pins in a map.
observedPins := make(map[string]*observedPin)
query:
for {
select {
case r := <-q.Next:
// Check if we are done.
if r == nil {
break query
}
// Add all pins to seen pins.
if pin, ok := r.(*navigator.PinExport); ok {
observedPins[pin.ID] = &observedPin{
previous: pin,
latest: pin,
}
} else {
log.Warningf("observer: received invalid pin export: %s", r)
}
case <-ctx.Done():
return nil
}
}
if q.Err() != nil {
return fmt.Errorf("failed to finish map query: %w", q.Err())
}
// Step 3: Monitor for changes.
sub, err := db.Subscribe(mapQuery)
if err != nil {
return fmt.Errorf("failed to start map sub: %w", err)
}
defer sub.Cancel() //nolint:errcheck
// Start ticker for checking for changes.
reportChangesTicker := time.NewTicker(10 * time.Second)
defer reportChangesTicker.Stop()
log.Info("observer: listening for hub changes")
for {
select {
case <-ctx.Done():
return nil
case r := <-statusSub.Feed:
// Keep SPN connection status up to date.
if r == nil {
return errors.New("status feed ended")
}
if statusUpdate, ok := r.(*captain.SPNStatus); ok {
latestStatus = statusUpdate
log.Infof("observer: SPN status is now %s", statusUpdate.Status)
} else {
log.Warningf("observer: received invalid pin export: %s", r)
}
case r := <-sub.Feed:
// Save all observed pins.
switch {
case r == nil:
return errors.New("pin feed ended")
case r.Meta().IsDeleted():
delete(observedPins, path.Base(r.DatabaseKey()))
default:
if pin, ok := r.(*navigator.PinExport); ok {
existingObservedPin, ok := observedPins[pin.ID]
if ok {
// Update previously observed Hub.
existingObservedPin.latest = pin
existingObservedPin.lastUpdate = time.Now()
existingObservedPin.lastUpdateReported = false
} else {
// Add new Hub.
observedPins[pin.ID] = &observedPin{
latest: pin,
lastUpdate: time.Now(),
}
}
} else {
log.Warningf("observer: received invalid pin export: %s", r)
}
}
case <-reportChangesTicker.C:
// Report changed pins.
for _, observedPin := range observedPins {
// Check if context was canceled.
select {
case <-ctx.Done():
return nil
default:
}
switch {
case observedPin.lastUpdateReported:
// Change already reported.
case time.Since(observedPin.lastUpdate) < reportingDelay:
// Only report changes if older than the configured delay.
default:
// Format and report.
title, changes, err := formatPinChanges(observedPin.previous, observedPin.latest)
if err != nil {
if !errors.Is(err, errNoChanges) {
log.Warningf("observer: failed to format pin changes: %s", err)
}
} else {
// Report changes.
reportChanges(&observedChange{
Title: title,
Summary: changes,
UpdatedPin: observedPin.latest,
UpdateTime: observedPin.lastUpdate,
SPNStatus: latestStatus,
})
}
// Update observed pin.
observedPin.previous = observedPin.latest
observedPin.lastUpdateReported = true
}
}
}
}
}
func reportChanges(change *observedChange) {
// Log changes.
log.Infof("observer:\n%s\n%s", change.Title, change.Summary)
// Report via Apprise.
err := reportToApprise(change)
if err != nil {
log.Warningf("observer: failed to report changes to apprise: %s", err)
}
}
var (
ignoreChangesIn = []string{
"ConnectedTo",
"HopDistance",
"Info.entryPolicy", // Alternatively, ignore "Info.Entry"
"Info.exitPolicy", // Alternatively, ignore "Info.Exit"
"Info.parsedTransports",
"Info.Timestamp",
"SessionActive",
"Status.Keys",
"Status.Lanes",
"Status.Load",
"Status.Timestamp",
}
ignoreStates = []string{
"IsHomeHub",
"Failing",
}
)
func ignoreChange(path string) bool {
if reportAllChanges {
return false
}
for _, pathPrefix := range ignoreChangesIn {
if strings.HasPrefix(path, pathPrefix) {
return true
}
}
return false
}
func formatPinChanges(from, to *navigator.PinExport) (title, changes string, err error) {
// Return immediately if pin is new.
if from == nil {
return fmt.Sprintf("New Hub: %s", makeHubName(to.Name, to.ID)), "", nil
}
// Find notable changes.
changelog, err := diff.Diff(from, to)
if err != nil {
return "", "", fmt.Errorf("failed to diff: %w", err)
}
if len(changelog) > 0 {
// Build changelog message.
changes := make([]string, 0, len(changelog))
for _, change := range changelog {
// Create path to changed field.
fullPath := strings.Join(change.Path, ".")
// Check if this path should be ignored.
if ignoreChange(fullPath) {
continue
}
// Add to reportable changes.
changeMsg := formatChange(change, fullPath)
if changeMsg != "" {
changes = append(changes, changeMsg)
}
}
// Log the changes, if there are any left.
if len(changes) > 0 {
return fmt.Sprintf("Hub Changed: %s", makeHubName(to.Name, to.ID)),
strings.Join(changes, "\n"),
nil
}
}
return "", "", errNoChanges
}
func formatChange(change diff.Change, fullPath string) string {
switch {
case strings.HasPrefix(fullPath, "States"):
switch change.Type {
case diff.CREATE:
return formatState(fmt.Sprintf("%v", change.To), true)
case diff.UPDATE:
a := formatState(fmt.Sprintf("%v", change.To), true)
b := formatState(fmt.Sprintf("%v", change.From), false)
switch {
case a != "" && b != "":
return a + "\n" + b
case a != "":
return a
case b != "":
return b
}
case diff.DELETE:
return formatState(fmt.Sprintf("%v", change.From), false)
}
default:
switch change.Type {
case diff.CREATE:
return fmt.Sprintf("%s added %v", fullPath, change.To)
case diff.UPDATE:
return fmt.Sprintf("%s changed from %v to %v", fullPath, change.From, change.To)
case diff.DELETE:
return fmt.Sprintf("%s removed %v", fullPath, change.From)
}
}
return ""
}
func formatState(name string, isSet bool) string {
// Check if state should be ignored.
if !reportAllChanges && slices.Contains[[]string, string](ignoreStates, name) {
return ""
}
if isSet {
return fmt.Sprintf("State is %v", name)
}
return fmt.Sprintf("State is NOT %v", name)
}
func makeHubName(name, id string) string {
shortenedID := id[len(id)-8:len(id)-4] +
"-" +
id[len(id)-4:]
// Be more careful, as the Hub name is user input.
switch {
case name == "":
return shortenedID
case len(name) > 16:
return fmt.Sprintf("%s (%s)", name[:16], shortenedID)
default:
return fmt.Sprintf("%s (%s)", name, shortenedID)
}
}

View File

@@ -10,16 +10,16 @@ import (
"github.com/safing/portbase/log"
"github.com/safing/portbase/metrics"
"github.com/safing/portbase/run"
"github.com/safing/portmaster/updates"
"github.com/safing/spn/conf"
"github.com/safing/portmaster/service/updates"
"github.com/safing/portmaster/spn/conf"
// Include packages here.
_ "github.com/safing/portbase/modules/subsystems"
_ "github.com/safing/portmaster/core"
_ "github.com/safing/portmaster/firewall"
_ "github.com/safing/portmaster/nameserver"
_ "github.com/safing/portmaster/ui"
_ "github.com/safing/spn/captain"
_ "github.com/safing/portmaster/service/core"
_ "github.com/safing/portmaster/service/firewall"
_ "github.com/safing/portmaster/service/nameserver"
_ "github.com/safing/portmaster/service/ui"
_ "github.com/safing/portmaster/spn/captain"
)
func main() {

View File

@@ -20,7 +20,7 @@ import (
portlog "github.com/safing/portbase/log"
"github.com/safing/portbase/updater"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/updates/helper"
"github.com/safing/portmaster/service/updates/helper"
)
var (

View File

@@ -9,7 +9,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"github.com/safing/portmaster/firewall/interception"
"github.com/safing/portmaster/service/firewall/interception"
)
var recoverIPTablesCmd = &cobra.Command{

View File

@@ -16,7 +16,7 @@ import (
"github.com/spf13/cobra"
"github.com/tevino/abool"
"github.com/safing/portmaster/updates/helper"
"github.com/safing/portmaster/service/updates/helper"
)
const (

View File

@@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/safing/portmaster/updates/helper"
"github.com/safing/portmaster/service/updates/helper"
)
func init() {

View File

@@ -10,7 +10,7 @@ import (
portlog "github.com/safing/portbase/log"
"github.com/safing/portbase/updater"
"github.com/safing/portmaster/updates/helper"
"github.com/safing/portmaster/service/updates/helper"
)
var (

View File

@@ -15,7 +15,7 @@ import (
"github.com/safing/jess/filesig"
portlog "github.com/safing/portbase/log"
"github.com/safing/portbase/updater"
"github.com/safing/portmaster/updates/helper"
"github.com/safing/portmaster/service/updates/helper"
)
var (

3
cmds/testsuite/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Compiled binaries
testsuite
testsuite.exe

33
cmds/testsuite/db.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"github.com/safing/portbase/database"
_ "github.com/safing/portbase/database/storage/hashmap"
)
func setupDatabases(path string) error {
err := database.InitializeWithPath(path)
if err != nil {
return err
}
_, err = database.Register(&database.Database{
Name: "core",
Description: "Holds core data, such as settings and profiles",
StorageType: "hashmap",
})
if err != nil {
return err
}
_, err = database.Register(&database.Database{
Name: "cache",
Description: "Cached data, such as Intelligence and DNS Records",
StorageType: "hashmap",
})
if err != nil {
return err
}
return nil
}

125
cmds/testsuite/login.go Normal file
View File

@@ -0,0 +1,125 @@
package main
import (
"errors"
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/access/account"
)
var (
loginCmd = &cobra.Command{
Use: "login",
Short: "Test login and token issuing",
RunE: runTestCommand(testLogin),
}
loginUsername string
loginPassword string
loginDeviceID string
)
func init() {
rootCmd.AddCommand(loginCmd)
// Add flags for login options.
flags := loginCmd.Flags()
flags.StringVar(&loginUsername, "username", "", "set username to use for the login test")
flags.StringVar(&loginPassword, "password", "", "set password to use for the login test")
flags.StringVar(&loginDeviceID, "device-id", "", "set device ID to use for the login test")
// Mark all as required.
_ = loginCmd.MarkFlagRequired("username")
_ = loginCmd.MarkFlagRequired("password")
_ = loginCmd.MarkFlagRequired("device-id")
}
func testLogin(cmd *cobra.Command, args []string) (err error) {
// Init token zones.
err = access.InitializeZones()
if err != nil {
return fmt.Errorf("failed to initialize token zones: %w", err)
}
// Set initial user object in order to set the device ID that should be used for login.
initialUser := &access.UserRecord{
User: &account.User{
Username: loginUsername,
Device: &account.Device{
ID: loginDeviceID,
},
},
}
err = initialUser.Save()
if err != nil {
return fmt.Errorf("failed to save initial user with device ID: %w", err)
}
// Login.
_, _, err = access.Login(loginUsername, loginPassword)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
// Check user.
user, err := access.GetUser()
if err != nil {
return fmt.Errorf("failed to get user after login: %w", err)
}
if verbose {
log.Printf("user (from login): %+v", user.User)
log.Printf("device (from login): %+v", user.User.Device)
}
// Check if the device ID is unchanged.
if user.Device.ID != loginDeviceID {
return errors.New("device ID changed")
}
// Check Auth Token.
authToken, err := access.GetAuthToken()
if err != nil {
return fmt.Errorf("failed to get auth token after login: %w", err)
}
if verbose {
log.Printf("auth token (from login): %+v", authToken.Token)
}
firstAuthToken := authToken.Token.Token
// Update User.
_, _, err = access.UpdateUser()
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
// Check if we received a new Auth Token.
authToken, err = access.GetAuthToken()
if err != nil {
return fmt.Errorf("failed to get auth token after user update: %w", err)
}
if verbose {
log.Printf("auth token (from update): %+v", authToken.Token)
}
if authToken.Token.Token == firstAuthToken {
return errors.New("auth token did not change after update")
}
// Get Tokens.
err = access.UpdateTokens()
if err != nil {
return fmt.Errorf("failed to update tokens: %w", err)
}
regular, fallback := access.GetTokenAmount(access.ExpandAndConnectZones)
if verbose {
log.Printf("received tokens: %d regular, %d fallback", regular, fallback)
}
if regular == 0 || fallback == 0 {
return fmt.Errorf("not enough tokens after fetching: %d regular, %d fallback", regular, fallback)
}
return nil
}

69
cmds/testsuite/main.go Normal file
View File

@@ -0,0 +1,69 @@
package main
import (
"fmt"
"log"
"os"
"github.com/spf13/cobra"
)
var (
rootCmd = &cobra.Command{
Use: "testsuite",
Short: "An integration and end-to-end test tool for the SPN",
}
verbose bool
)
func runTestCommand(cmdFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
// Setup
dbDir, err := os.MkdirTemp("", "spn-testsuite-")
if err != nil {
makeReports(cmd, fmt.Errorf("internal test error: failed to setup datbases: %w", err))
return err
}
if err = setupDatabases(dbDir); err != nil {
makeReports(cmd, fmt.Errorf("internal test error: failed to setup datbases: %w", err))
return err
}
// Run Test
err = cmdFunc(cmd, args)
if err != nil {
log.Printf("test failed: %s", err)
}
// Report
makeReports(cmd, err)
// Cleanup and return more important error.
cleanUpErr := os.RemoveAll(dbDir)
if cleanUpErr != nil {
// Only log if the test failed, so we can return the more important error
if err == nil {
return cleanUpErr
}
log.Printf("cleanup failed: %s", err)
}
return err
}
}
func makeReports(cmd *cobra.Command, err error) {
reportToHealthCheckIfEnabled(cmd, err)
}
func init() {
flags := rootCmd.PersistentFlags()
flags.BoolVarP(&verbose, "verbose", "v", false, "enable verbose logging")
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@@ -0,0 +1,51 @@
package main
import (
"log"
"net/http"
"strings"
"github.com/spf13/cobra"
)
var healthCheckReportURL string
func init() {
flags := rootCmd.PersistentFlags()
flags.StringVar(&healthCheckReportURL, "report-to-healthcheck", "", "report to the given healthchecks URL")
}
func reportToHealthCheckIfEnabled(_ *cobra.Command, failureErr error) {
if healthCheckReportURL == "" {
return
}
if failureErr != nil {
// Report failure.
resp, err := http.Post(
healthCheckReportURL+"/fail",
"text/plain; utf-8",
strings.NewReader(failureErr.Error()),
)
if err != nil {
log.Printf("failed to report failure to healthcheck at %q: %s", healthCheckReportURL, err)
return
}
_ = resp.Body.Close()
// Always log that we've report the error.
log.Printf("reported failure to healthcheck at %q", healthCheckReportURL)
} else {
// Report success.
resp, err := http.Get(healthCheckReportURL) //nolint:gosec
if err != nil {
log.Printf("failed to report success to healthcheck at %q: %s", healthCheckReportURL, err)
return
}
_ = resp.Body.Close()
if verbose {
log.Printf("reported success to healthcheck at %q", healthCheckReportURL)
}
}
}

View File

@@ -1,3 +1,4 @@
//go:build windows
// +build windows
package main
@@ -11,8 +12,8 @@ import (
"syscall"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/firewall/interception/windowskext"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/service/firewall/interception/windowskext"
"github.com/safing/portmaster/service/network/packet"
)
var (