wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
3
cmds/hub/.gitignore
vendored
Normal file
3
cmds/hub/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Compiled binaries
|
||||
hub
|
||||
hub.exe
|
||||
60
cmds/hub/build
Executable file
60
cmds/hub/build
Executable 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
66
cmds/hub/main.go
Normal 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
123
cmds/hub/pack
Executable 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
|
||||
@@ -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
3
cmds/observation-hub/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Compiled binaries
|
||||
observation-hub
|
||||
observation-hub.exe
|
||||
38
cmds/observation-hub/Dockerfile
Normal file
38
cmds/observation-hub/Dockerfile
Normal 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"]
|
||||
257
cmds/observation-hub/apprise.go
Normal file
257
cmds/observation-hub/apprise.go
Normal 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)
|
||||
// }
|
||||
// }
|
||||
84
cmds/observation-hub/apprise_test.go
Normal file
84
cmds/observation-hub/apprise_test.go
Normal 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
60
cmds/observation-hub/build
Executable 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}" $*
|
||||
44
cmds/observation-hub/main.go
Normal file
44
cmds/observation-hub/main.go
Normal 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())
|
||||
}
|
||||
75
cmds/observation-hub/notifications.tmpl
Normal file
75
cmds/observation-hub/notifications.tmpl
Normal 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 }}
|
||||
407
cmds/observation-hub/observe.go
Normal file
407
cmds/observation-hub/observe.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safing/portmaster/updates/helper"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
3
cmds/testsuite/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Compiled binaries
|
||||
testsuite
|
||||
testsuite.exe
|
||||
33
cmds/testsuite/db.go
Normal file
33
cmds/testsuite/db.go
Normal 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
125
cmds/testsuite/login.go
Normal 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
69
cmds/testsuite/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
51
cmds/testsuite/report_healthcheck.go
Normal file
51
cmds/testsuite/report_healthcheck.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user