diff --git a/base/log/logging.go b/base/log/logging.go index 93be59e7..fb9fc1b4 100644 --- a/base/log/logging.go +++ b/base/log/logging.go @@ -145,10 +145,18 @@ func GetLogLevel() Severity { // SetLogLevel sets a new log level. Only effective after Start(). func SetLogLevel(level Severity) { + previous := GetLogLevel() + atomic.StoreUint32(logLevel, uint32(level)) // Setup slog here for the transition period. setupSLog(level) + + // Write directly to GlobalWriter (bypassing the level filter) so the + // message is always visible regardless of the current or new level. + if previous != level { + writeLogLevelChange(previous.Name(), level.Name()) + } } // Name returns the name of the log level. diff --git a/base/log/output.go b/base/log/output.go index 91acd525..9049f803 100644 --- a/base/log/output.go +++ b/base/log/output.go @@ -83,6 +83,28 @@ func writeVersion() { } } +func writeLogLevelChange(from, to string) { + if GlobalWriter == nil { + return + } + if GlobalWriter.isStdout { + fmt.Fprintf(GlobalWriter, "%s%s%s log level changed from %s%s%s to %s%s%s\n", + dimColor(), + time.Now().Format(timeFormat), + endDimColor(), + + blueColor(), + from, + endColor(), + + blueColor(), + to, + endColor()) + } else { + fmt.Fprintf(GlobalWriter, "%s log level changed from %s to %s\n", time.Now().Format(timeFormat), from, to) + } +} + func writerManager() { defer shutdownWaitGroup.Done() diff --git a/base/log/slog.go b/base/log/slog.go index 65dec5ab..960a3a41 100644 --- a/base/log/slog.go +++ b/base/log/slog.go @@ -5,51 +5,62 @@ import ( "log/slog" "os" "runtime" + "sync" "github.com/lmittmann/tint" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" ) +// slogLevel is the shared log level variable for the default slog logger. +// All loggers derived from slog.Default() (e.g. via .With()) share the same +// underlying handler and therefore respect changes to this variable immediately. +var ( + slogLevel = new(slog.LevelVar) + slogSetupOnce sync.Once +) + func setupSLog(level Severity) { - // TODO: Changes in the log level are not yet reflected onto the slog handlers in the modules. + // Update the shared level variable. All existing handlers and derived + // loggers read from this pointer, so they pick up the change instantly. + slogLevel.Set(level.toSLogLevel()) - // Set highest possible level, so it can be changed in runtime. - handlerLogLevel := level.toSLogLevel() + // Create the handler and set slog.Default() exactly once, so that + // managers created after startup always hold a logger whose underlying + // handler is controlled by slogLevel. + slogSetupOnce.Do(func() { + var logHandler slog.Handler + switch runtime.GOOS { + case "windows": + logHandler = tint.NewHandler( + windowsColoring(GlobalWriter), // Enable coloring on Windows. + &tint.Options{ + AddSource: true, + Level: slogLevel, + TimeFormat: timeFormat, + NoColor: !( /* Color: */ GlobalWriter.IsStdout() && isatty.IsTerminal(GlobalWriter.file.Fd())), + }, + ) - // Create handler depending on OS. - var logHandler slog.Handler - switch runtime.GOOS { - case "windows": - logHandler = tint.NewHandler( - windowsColoring(GlobalWriter), // Enable coloring on Windows. - &tint.Options{ + case "linux": + logHandler = tint.NewHandler(GlobalWriter, &tint.Options{ AddSource: true, - Level: handlerLogLevel, + Level: slogLevel, TimeFormat: timeFormat, NoColor: !( /* Color: */ GlobalWriter.IsStdout() && isatty.IsTerminal(GlobalWriter.file.Fd())), - }, - ) + }) - case "linux": - logHandler = tint.NewHandler(GlobalWriter, &tint.Options{ - AddSource: true, - Level: handlerLogLevel, - TimeFormat: timeFormat, - NoColor: !( /* Color: */ GlobalWriter.IsStdout() && isatty.IsTerminal(GlobalWriter.file.Fd())), - }) + default: + logHandler = tint.NewHandler(os.Stdout, &tint.Options{ + AddSource: true, + Level: slogLevel, + TimeFormat: timeFormat, + NoColor: true, + }) + } - default: - logHandler = tint.NewHandler(os.Stdout, &tint.Options{ - AddSource: true, - Level: handlerLogLevel, - TimeFormat: timeFormat, - NoColor: true, - }) - } - - // Set as default logger. - slog.SetDefault(slog.New(logHandler)) + slog.SetDefault(slog.New(logHandler)) + }) } func windowsColoring(lw *LogWriter) io.Writer { diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts index c0b1ec88..8824ff0d 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts @@ -4,7 +4,7 @@ import { Observable, forkJoin, of } from "rxjs"; import { catchError, map, mergeMap } from "rxjs/operators"; import { AppProfileService } from "./app-profile.service"; import { AppProfile } from "./app-profile.types"; -import { DNSContext, IPScope, Reason, TLSContext, TunnelContext, Verdict } from "./network.types"; +import { DNSContext, IPScope, Reason, SplitTunContext, TLSContext, TunnelContext, Verdict } from "./network.types"; import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from "./portapi.service"; import { Container } from "postcss"; @@ -162,6 +162,7 @@ export interface NetqueryConnection { blockedEntities?: string[]; reason?: Reason; tunnel?: TunnelContext; + split_tun?: SplitTunContext; dns?: DNSContext; tls?: TLSContext; }; @@ -459,6 +460,7 @@ export class Netquery { case Verdict.Accept: case Verdict.RerouteToNs: case Verdict.RerouteToTunnel: + case Verdict.RerouteToSplitTun: case Verdict.Undeterminable: stats.size += res.totalCount stats.countAllowed += res.totalCount; diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts index 6cdef998..bdc56dcf 100644 --- a/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts @@ -8,7 +8,8 @@ export enum Verdict { Drop = 4, RerouteToNs = 5, RerouteToTunnel = 6, - Failed = 7 + Failed = 7, + RerouteToSplitTun = 8 } export enum IPProtocol { @@ -209,6 +210,13 @@ export interface TunnelContext { RoutingAlg: 'default'; } +export interface SplitTunContext { + // Interface is the name of the network interface the connection is bound to. + Interface: string; + // IP is the IP address used to bind the connection to the interface. + IP: string; +} + export interface GeoIPInfo { IP: string; Country: string; diff --git a/desktop/angular/src/app/app.module.ts b/desktop/angular/src/app/app.module.ts index b083642c..257f33fa 100644 --- a/desktop/angular/src/app/app.module.ts +++ b/desktop/angular/src/app/app.module.ts @@ -29,6 +29,7 @@ import { AppOverviewComponent, AppViewComponent, QuickSettingInternetButtonCompo import { QsHistoryComponent } from './pages/app-view/qs-history/qs-history.component'; import { QuickSettingSelectExitButtonComponent } from './pages/app-view/qs-select-exit/qs-select-exit'; import { QuickSettingUseSPNButtonComponent } from './pages/app-view/qs-use-spn/qs-use-spn'; +import { QuickSettingUseSplitTunButtonComponent } from './pages/app-view/qs-use-splittun/qs-use-splittun'; import { DashboardPageComponent } from './pages/dashboard/dashboard.component'; import { FeatureCardComponent } from './pages/dashboard/feature-card/feature-card.component'; import { MonitorPageComponent } from './pages/monitor'; @@ -138,6 +139,7 @@ const localeConfig = { QuickSettingInternetButtonComponent, QuickSettingUseSPNButtonComponent, QuickSettingSelectExitButtonComponent, + QuickSettingUseSplitTunButtonComponent, AppOverviewComponent, PlaceholderComponent, LoadingComponent, diff --git a/desktop/angular/src/app/pages/app-view/app-view.html b/desktop/angular/src/app/pages/app-view/app-view.html index 72a7ad81..96235c8c 100644 --- a/desktop/angular/src/app/pages/app-view/app-view.html +++ b/desktop/angular/src/app/pages/app-view/app-view.html @@ -76,6 +76,9 @@ + + +