mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-07 10:38:55 -04:00
Compare commits
6 Commits
dependabot
...
refactor/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2a4fdfe07 | ||
|
|
2ce6323602 | ||
|
|
9a05a1c698 | ||
|
|
5dd914782a | ||
|
|
8c19b7a30a | ||
|
|
1e2a7aa571 |
@@ -257,6 +257,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify fresh-install session cookie key hardening
|
||||
run: |
|
||||
grep -Fxq ' SESSION_COOKIE_ENCRYPTION_KEY=$(openssl rand -base64 32)' infrastructure_files/getting-started.sh
|
||||
grep -Fxq ' sessionCookieEncryptionKey: "$SESSION_COOKIE_ENCRYPTION_KEY"' infrastructure_files/getting-started.sh
|
||||
grep -Fxq ' install -m 600 /dev/null config.yaml' infrastructure_files/getting-started.sh
|
||||
grep -Fxq ' openssl rand -base64 32' infrastructure_files/getting-started-enterprise.sh
|
||||
grep -Fxq ' NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY=$(rand_b64_key)' infrastructure_files/getting-started-enterprise.sh
|
||||
grep -Fxq ' sessionCookieEncryptionKey: "${NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY}"' infrastructure_files/getting-started-enterprise.sh
|
||||
|
||||
- name: Verify Dex retirement notice
|
||||
run: |
|
||||
if infrastructure_files/getting-started-with-dex.sh >stdout.txt 2>stderr.txt; then
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// its wg interface into firewalld's "trusted" zone. This is required because
|
||||
// firewalld's nftables chains are created with NFT_CHAIN_OWNER on recent
|
||||
// versions, which returns EPERM to any other process that tries to insert
|
||||
// rules into them. The workaround mirrors what Tailscale does: let firewalld
|
||||
// itself add the accept rules to its own chains by trusting the interface.
|
||||
// rules into them. Trusting the interface makes firewalld itself add the
|
||||
// accept rules to its own chains instead.
|
||||
package firewalld
|
||||
|
||||
// TrustedZone is the firewalld zone name used for interfaces whose traffic
|
||||
|
||||
18
client/ui/frontend/src/components/ReadySignal.tsx
Normal file
18
client/ui/frontend/src/components/ReadySignal.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { useStatus } from "@/contexts/StatusContext.tsx";
|
||||
|
||||
const EVENT_WINDOW_PAINTED = "netbird:window-painted";
|
||||
|
||||
export const ReadySignal = () => {
|
||||
const { isReady } = useStatus();
|
||||
const sent = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || sent.current) return;
|
||||
sent.current = true;
|
||||
void Events.Emit(EVENT_WINDOW_PAINTED);
|
||||
}, [isReady]);
|
||||
|
||||
return null;
|
||||
};
|
||||
35
client/ui/frontend/src/hooks/useKeepConnectedOnQuit.ts
Normal file
35
client/ui/frontend/src/hooks/useKeepConnectedOnQuit.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Preferences } from "@bindings/services";
|
||||
|
||||
export const useKeepConnectedOnQuit = () => {
|
||||
const [keepConnected, setKeepConnected] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Preferences.Get()
|
||||
.then((prefs) => {
|
||||
if (cancelled) return;
|
||||
setKeepConnected(prefs?.keepConnectedOnQuit ?? false);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (cancelled) return;
|
||||
console.warn("[useKeepConnectedOnQuit] load preferences failed", err);
|
||||
setKeepConnected(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setKeepConnectedOnQuit = useCallback(async (keep: boolean) => {
|
||||
setKeepConnected(keep);
|
||||
try {
|
||||
await Preferences.SetKeepConnectedOnQuit(keep);
|
||||
} catch (err: unknown) {
|
||||
setKeepConnected(!keep);
|
||||
console.error("[useKeepConnectedOnQuit] SetKeepConnectedOnQuit failed", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { keepConnected, setKeepConnectedOnQuit };
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { DebugBundleProvider } from "@/contexts/DebugBundleContext.tsx";
|
||||
import { ProfileProvider } from "@/contexts/ProfileContext.tsx";
|
||||
import { DialogProvider } from "@/contexts/DialogContext.tsx";
|
||||
import { RestrictionsProvider } from "@/contexts/RestrictionsContext.tsx";
|
||||
import { ReadySignal } from "@/components/ReadySignal.tsx";
|
||||
|
||||
export const AppLayout = () => {
|
||||
return (
|
||||
@@ -16,6 +17,7 @@ export const AppLayout = () => {
|
||||
<DebugBundleProvider>
|
||||
<ClientVersionProvider>
|
||||
<Outlet />
|
||||
<ReadySignal />
|
||||
</ClientVersionProvider>
|
||||
</DebugBundleProvider>
|
||||
</RestrictionsProvider>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ManagementServerSwitch } from "@/components/ManagementServerSwitch.tsx"
|
||||
import { ManagementMode, useManagementUrl } from "@/hooks/useManagementUrl.ts";
|
||||
import { LanguagePicker } from "@/components/LanguagePicker.tsx";
|
||||
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
|
||||
import { useKeepConnectedOnQuit } from "@/hooks/useKeepConnectedOnQuit.ts";
|
||||
|
||||
export function SettingsGeneral() {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,6 +20,7 @@ export function SettingsGeneral() {
|
||||
const { mode, setMode, setUrl, displayUrl, showError, canSave, save, checking, unreachable } =
|
||||
useManagementUrl();
|
||||
const { mdm, features } = useRestrictions();
|
||||
const { keepConnected, setKeepConnectedOnQuit } = useKeepConnectedOnQuit();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const managementUrlId = useId();
|
||||
@@ -57,6 +59,15 @@ export function SettingsGeneral() {
|
||||
helpText={t("settings.general.autostart.help")}
|
||||
/>
|
||||
)}
|
||||
<FancyToggleSwitch
|
||||
value={keepConnected ?? false}
|
||||
onChange={(v) => {
|
||||
void setKeepConnectedOnQuit(v);
|
||||
}}
|
||||
loading={keepConnected === null}
|
||||
label={t("settings.general.keepConnectedOnQuit.label")}
|
||||
helpText={t("settings.general.keepConnectedOnQuit.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
{!mdm.managementURL && !features.disableUpdateSettings && (
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Ändern des Autostarts fehlgeschlagen"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Nach dem Beenden verbunden bleiben",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "Die Verbindung bleibt im Hintergrund bestehen, nachdem Sie NetBird schließen. Sie endet erst, wenn Sie sie selbst trennen.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Anzeigesprache"
|
||||
},
|
||||
|
||||
@@ -735,6 +735,14 @@
|
||||
"message": "Autostart Change Failed",
|
||||
"description": "Error-dialog title when changing the autostart setting fails."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Stay Connected After Quitting",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "The connection stays up in the background after you close NetBird. It only stops when you disconnect it yourself.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Display Language",
|
||||
"description": "Label for the display-language picker."
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Error al cambiar el inicio automático"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Permanecer conectado al salir",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "La conexión sigue activa en segundo plano después de cerrar NetBird. Solo se detiene cuando la desconectas tú.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Idioma de la interfaz"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Échec de la modification du démarrage automatique"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Rester connecté après la fermeture",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "La connexion reste active en arrière-plan après la fermeture de NetBird. Elle ne s'arrête que si vous la coupez vous-même.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Langue d’affichage"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Az automatikus indítás módosítása sikertelen"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Kapcsolat megtartása kilépéskor",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "A kapcsolat a háttérben megmarad, miután bezárod a NetBirdöt. Csak akkor szakad meg, ha te magad bontod.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Megjelenítési nyelv"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Modifica avvio automatico non riuscita"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Resta connesso dopo la chiusura",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "La connessione resta attiva in background dopo la chiusura di NetBird. Si interrompe solo quando la disconnetti tu.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Lingua dell'interfaccia"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "自動起動の変更に失敗しました"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "終了後も接続を維持",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "NetBird を閉じたあとも接続はバックグラウンドで維持されます。自分で切断したときにだけ停止します。",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "表示言語"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Falha ao alterar o início automático"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Permanecer conectado ao sair",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "A conexão continua ativa em segundo plano depois de fechar o NetBird. Ela só para quando você mesmo a desconecta.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Idioma de exibição"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Не удалось изменить автозапуск"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Оставаться подключённым после выхода",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "Соединение остаётся активным в фоне после закрытия NetBird. Оно прервётся, только когда вы отключите его сами.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Язык интерфейса"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "更改自启动设置失败"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "退出后保持连接",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "关闭 NetBird 后,连接会在后台保持。只有你自己断开时才会停止。",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "显示语言"
|
||||
},
|
||||
|
||||
@@ -139,13 +139,11 @@ func main() {
|
||||
prefStore: prefStore,
|
||||
})
|
||||
|
||||
window := newMainWindow(app, prefStore)
|
||||
|
||||
// Settings is created eagerly (hidden) so the first gear click paints
|
||||
// instantly and React keeps per-tab state across reopens. The other
|
||||
// auxiliary windows stay lazy + destroy-on-close so Wails's macOS
|
||||
// dock-reopen handler can't resurrect them.
|
||||
windowManager := services.NewWindowManager(app, window, bundle, prefStore, iconWindow)
|
||||
windowManager := services.NewWindowManager(app, nil, bundle, prefStore, iconWindow)
|
||||
windowManager.SetMainFactory(func() *application.WebviewWindow {
|
||||
return newMainWindow(app, prefStore, windowManager)
|
||||
})
|
||||
registerDockReopenHook(app, windowManager)
|
||||
// Minimal WMs (XEmbed-tray path) neither center small windows nor restore
|
||||
// position across hide -> show, dropping them top-left. Gate Go-side
|
||||
// re-centering on that environment; nil leaves placement to the WM on full
|
||||
@@ -168,7 +166,7 @@ func main() {
|
||||
// RegisterStatusNotifierItem hits a watcher we control.
|
||||
startStatusNotifierWatcher()
|
||||
|
||||
tray = NewTray(app, window, TrayServices{
|
||||
tray = NewTray(app, nil, TrayServices{
|
||||
Connection: connection,
|
||||
Settings: settings,
|
||||
Profiles: profiles,
|
||||
@@ -180,6 +178,7 @@ func main() {
|
||||
WindowManager: windowManager,
|
||||
Session: authSession,
|
||||
Localizer: localizer,
|
||||
Preferences: prefStore,
|
||||
})
|
||||
listenForShowSignal(context.Background(), tray)
|
||||
|
||||
@@ -337,9 +336,7 @@ func registerServices(app *application.App, conn *Conn, s registeredServices) {
|
||||
app.RegisterService(application.NewService(s.compat))
|
||||
}
|
||||
|
||||
// newMainWindow creates the hidden main window, sized to the user's last view
|
||||
// mode, and installs the hide-on-close and macOS dock-reopen hooks.
|
||||
func newMainWindow(app *application.App, prefStore *preferences.Store) *application.WebviewWindow {
|
||||
func newMainWindow(app *application.App, prefStore *preferences.Store, wm *services.WindowManager) *application.WebviewWindow {
|
||||
// Width matches the last view mode so Advanced-mode users don't see the
|
||||
// window pop from 380px to 900px on launch. Height is mode-agnostic.
|
||||
initialWidth := 380
|
||||
@@ -367,29 +364,25 @@ func newMainWindow(app *application.App, prefStore *preferences.Store) *applicat
|
||||
},
|
||||
})
|
||||
|
||||
// Hide instead of quit on close; "really quit" is reached via tray -> Quit.
|
||||
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
window.RegisterHook(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
if services.ShuttingDown() {
|
||||
return
|
||||
}
|
||||
e.Cancel()
|
||||
window.Hide()
|
||||
wm.ForgetMain()
|
||||
})
|
||||
|
||||
// On macOS, Wails' default applicationShouldHandleReopen handler Show()s
|
||||
// every hidden window on dock-icon click, resurrecting hide-on-close
|
||||
// surfaces like Settings. Cancel it in a hook (hooks run before listeners)
|
||||
// and show only the main window. No-op elsewhere — the event never fires.
|
||||
if runtime.GOOS == "darwin" {
|
||||
app.Event.RegisterApplicationEventHook(events.Mac.ApplicationShouldHandleReopen, func(e *application.ApplicationEvent) {
|
||||
e.Cancel()
|
||||
if e.Context().HasVisibleWindows() {
|
||||
return
|
||||
}
|
||||
window.Show()
|
||||
window.Focus()
|
||||
})
|
||||
}
|
||||
|
||||
return window
|
||||
}
|
||||
|
||||
func registerDockReopenHook(app *application.App, wm *services.WindowManager) {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return
|
||||
}
|
||||
app.Event.RegisterApplicationEventHook(events.Mac.ApplicationShouldHandleReopen, func(e *application.ApplicationEvent) {
|
||||
e.Cancel()
|
||||
if e.Context().HasVisibleWindows() {
|
||||
return
|
||||
}
|
||||
wm.ShowMain()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ type UIPreferences struct {
|
||||
// decision has run for this OS user. It only ever transitions to true
|
||||
// and is never reset, so the default-on flow runs at most once, ever.
|
||||
AutostartInitialized bool `json:"autostartInitialized"`
|
||||
// KeepConnectedOnQuit leaves the daemon connected when the GUI quits.
|
||||
// Its false zero value preserves the historical disconnect-on-quit
|
||||
// behaviour for preference files written before the field existed.
|
||||
KeepConnectedOnQuit bool `json:"keepConnectedOnQuit"`
|
||||
}
|
||||
|
||||
// LanguageValidator rejects SetLanguage inputs with no shipped bundle.
|
||||
@@ -183,6 +187,26 @@ func (s *Store) SetAutostartInitialized(done bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetKeepConnectedOnQuit persists the disconnect-on-quit opt-out. No-op if unchanged.
|
||||
func (s *Store) SetKeepConnectedOnQuit(keep bool) error {
|
||||
s.mu.Lock()
|
||||
if s.current.KeepConnectedOnQuit == keep {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
next := s.current
|
||||
next.KeepConnectedOnQuit = keep
|
||||
if err := s.persistLocked(next); err != nil {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("persist preferences: %w", err)
|
||||
}
|
||||
s.current = next
|
||||
s.mu.Unlock()
|
||||
|
||||
s.broadcast(next)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLanguage validates, persists, and broadcasts. No-op if unchanged.
|
||||
func (s *Store) SetLanguage(lang i18n.LanguageCode) error {
|
||||
if lang == "" {
|
||||
|
||||
@@ -238,6 +238,42 @@ func TestStore_SetAutostartInitializedPersistsAcrossReload(t *testing.T) {
|
||||
assert.True(t, reloaded.Get().AutostartInitialized, "marker must survive a reload from disk")
|
||||
}
|
||||
|
||||
func TestStore_SetKeepConnectedOnQuitPersistsAcrossReload(t *testing.T) {
|
||||
withTempConfigDir(t)
|
||||
emitter := &recordingEmitter{}
|
||||
s, err := NewStore(nil, emitter)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, s.Get().KeepConnectedOnQuit, "quitting must disconnect by default")
|
||||
|
||||
require.NoError(t, s.SetKeepConnectedOnQuit(true))
|
||||
assert.True(t, s.Get().KeepConnectedOnQuit, "Get should reflect the persisted opt-out")
|
||||
require.Len(t, emitter.calledWith(EventPreferencesChanged), 1, "first write should broadcast")
|
||||
|
||||
require.NoError(t, s.SetKeepConnectedOnQuit(true))
|
||||
assert.Len(t, emitter.calledWith(EventPreferencesChanged), 1, "idempotent write should not broadcast again")
|
||||
|
||||
reloaded, err := NewStore(nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, reloaded.Get().KeepConnectedOnQuit, "opt-out must survive a reload from disk")
|
||||
}
|
||||
|
||||
func TestStore_KeepConnectedOnQuitDefaultsFalseForPreExistingFile(t *testing.T) {
|
||||
withTempConfigDir(t)
|
||||
|
||||
// A preferences file written before the field existed must keep the
|
||||
// historical disconnect-on-quit behaviour rather than silently opting out.
|
||||
path, err := preferencesPath()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(`{"language":"en","viewMode":"default"}`), 0o600))
|
||||
|
||||
s, err := NewStore(nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, s.Get().KeepConnectedOnQuit, "a file predating the field must not opt out of disconnect-on-quit")
|
||||
assert.True(t, s.ExistedAtLoad(), "the pre-existing file must be seen on disk")
|
||||
}
|
||||
|
||||
func TestStore_ExistedAtLoad(t *testing.T) {
|
||||
withTempConfigDir(t)
|
||||
|
||||
|
||||
@@ -34,3 +34,7 @@ func (s *Preferences) SetViewMode(_ context.Context, mode preferences.ViewMode)
|
||||
func (s *Preferences) SetOnboardingCompleted(_ context.Context, done bool) error {
|
||||
return s.store.SetOnboardingCompleted(done)
|
||||
}
|
||||
|
||||
func (s *Preferences) SetKeepConnectedOnQuit(_ context.Context, keep bool) error {
|
||||
return s.store.SetKeepConnectedOnQuit(keep)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
@@ -29,6 +30,10 @@ const EventBrowserLoginCancel = "browser-login:cancel"
|
||||
// EventSettingsOpen tells the mounted settings window which tab to show.
|
||||
const EventSettingsOpen = "netbird:settings:open"
|
||||
|
||||
const EventWindowPainted = "netbird:window-painted"
|
||||
|
||||
const paintedFallback = 2 * time.Second
|
||||
|
||||
var WindowBackgroundColour = application.NewRGB(24, 26, 29) // bg-nb-gray-950
|
||||
|
||||
// WindowHeight is shared by the main and Settings windows.
|
||||
@@ -94,9 +99,6 @@ func DialogWindowOptions(name, title, url string, linuxIcon []byte) application.
|
||||
}
|
||||
}
|
||||
|
||||
// WindowManager owns the auxiliary windows (main is created in main.go). Settings is created
|
||||
// eagerly and hidden on close to keep React state; the rest are created on open, destroyed on
|
||||
// close, so the macOS dock-reopen handler finds no hidden window to resurrect.
|
||||
type WindowManager struct {
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
@@ -112,15 +114,29 @@ type WindowManager struct {
|
||||
// hiddenForLogin holds windows hidden while the BrowserLogin popup is open, restored on close.
|
||||
hiddenForLogin []application.Window
|
||||
mu sync.Mutex
|
||||
newMain func() *application.WebviewWindow
|
||||
ready map[uint]bool
|
||||
showPending map[uint]bool
|
||||
pendingTab map[uint]string
|
||||
fallbackTimers map[uint]*time.Timer
|
||||
// recenterOnShow is set only on the minimal-WM/XEmbed path, where the WM neither centers nor
|
||||
// restores position; nil on full desktops so re-centering can't fight a user-moved window.
|
||||
recenterOnShow func() bool
|
||||
}
|
||||
|
||||
// NewWindowManager wires the manager to the main app; translator/prefs may be nil (tests). The
|
||||
// Settings window is created here (hidden) so the first OpenSettings is instant.
|
||||
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference, linuxIcon []byte) *WindowManager {
|
||||
s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs, linuxIcon: linuxIcon}
|
||||
s := &WindowManager{
|
||||
app: app,
|
||||
mainWindow: mainWindow,
|
||||
translator: translator,
|
||||
prefs: prefs,
|
||||
linuxIcon: linuxIcon,
|
||||
ready: map[uint]bool{},
|
||||
showPending: map[uint]bool{},
|
||||
pendingTab: map[uint]string{},
|
||||
fallbackTimers: map[uint]*time.Timer{},
|
||||
}
|
||||
s.watchPainted()
|
||||
// Re-title live windows on language flip. Wired internally so the binding generator
|
||||
// doesn't try to expose the interface param.
|
||||
if sub, ok := prefs.(LanguageSubscriber); ok && sub != nil {
|
||||
@@ -136,7 +152,11 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
|
||||
}
|
||||
}()
|
||||
}
|
||||
s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *WindowManager) newSettingsWindow() *application.WebviewWindow {
|
||||
w := s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Name: "settings",
|
||||
Title: s.title("window.title.settings"),
|
||||
Width: 900,
|
||||
@@ -150,18 +170,15 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
|
||||
URL: "/#/settings",
|
||||
Mac: AppleMacOSAppearanceOptions(),
|
||||
Windows: MicrosoftWindowsAppearanceOptions(),
|
||||
Linux: LinuxAppearanceOptions(linuxIcon),
|
||||
Linux: LinuxAppearanceOptions(s.linuxIcon),
|
||||
})
|
||||
// Hide (not destroy) on close to keep React state; reset to General for a flash-free reopen.
|
||||
s.settings.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
if ShuttingDown() {
|
||||
return
|
||||
}
|
||||
e.Cancel()
|
||||
s.app.Event.Emit(EventSettingsOpen, "general")
|
||||
s.settings.Hide()
|
||||
w.RegisterHook(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
s.settings = nil
|
||||
s.forgetWindowLocked(w)
|
||||
s.mu.Unlock()
|
||||
})
|
||||
return s
|
||||
return w
|
||||
}
|
||||
|
||||
// OpenSettings shows the settings window on tab (empty → General), switching tab via
|
||||
@@ -171,11 +188,23 @@ func (s *WindowManager) OpenSettings(tab string) {
|
||||
if target == "" {
|
||||
target = "general"
|
||||
}
|
||||
s.app.Event.Emit(EventSettingsOpen, target)
|
||||
s.settings.Show()
|
||||
s.settings.Focus()
|
||||
// Re-center (minimal-WM only; see centerWhenReady).
|
||||
s.centerWhenReady(s.settings)
|
||||
|
||||
s.mu.Lock()
|
||||
fresh := s.settings == nil
|
||||
if fresh {
|
||||
s.settings = s.newSettingsWindow()
|
||||
s.armReady(s.settings)
|
||||
}
|
||||
w := s.settings
|
||||
if fresh {
|
||||
s.pendingTab[w.ID()] = target
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if !fresh {
|
||||
s.app.Event.Emit(EventSettingsOpen, target)
|
||||
}
|
||||
s.showWhenReady(w)
|
||||
}
|
||||
|
||||
// OpenBrowserLogin shows the SSO popup, creating it on first use.
|
||||
@@ -440,13 +469,150 @@ func (s *WindowManager) OpenMain() {
|
||||
// ShowMain brings the main window forward (re-centering on minimal WMs). The single entry
|
||||
// point every surface (tray, SIGUSR1, welcome) should use so centering applies uniformly.
|
||||
func (s *WindowManager) ShowMain() {
|
||||
if s.mainWindow == nil {
|
||||
s.showWhenReady(s.MainWindow())
|
||||
}
|
||||
|
||||
func (s *WindowManager) MainWindow() *application.WebviewWindow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.mainWindow == nil && s.newMain != nil {
|
||||
s.mainWindow = s.newMain()
|
||||
s.armReady(s.mainWindow)
|
||||
}
|
||||
return s.mainWindow
|
||||
}
|
||||
|
||||
func (s *WindowManager) armReady(w *application.WebviewWindow) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
s.mainWindow.Show()
|
||||
s.mainWindow.Focus()
|
||||
// Re-center (minimal-WM only; see centerWhenReady).
|
||||
s.centerWhenReady(s.mainWindow)
|
||||
w.RegisterHook(events.Common.WindowRuntimeReady, func(_ *application.WindowEvent) {
|
||||
timer := time.AfterFunc(paintedFallback, func() {
|
||||
log.Warnf("window %q never reported a first render, showing it anyway", w.Name())
|
||||
s.markReady(w)
|
||||
})
|
||||
s.mu.Lock()
|
||||
s.fallbackTimers[w.ID()] = timer
|
||||
s.mu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WindowManager) watchPainted() {
|
||||
s.app.Event.On(EventWindowPainted, func(e *application.CustomEvent) {
|
||||
if w := s.windowByName(e.Sender); w != nil {
|
||||
s.markReady(w)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WindowManager) forgetWindowLocked(w *application.WebviewWindow) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := w.ID()
|
||||
if timer := s.fallbackTimers[id]; timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
delete(s.fallbackTimers, id)
|
||||
delete(s.ready, id)
|
||||
delete(s.showPending, id)
|
||||
delete(s.pendingTab, id)
|
||||
|
||||
kept := s.hiddenForLogin[:0]
|
||||
for _, hidden := range s.hiddenForLogin {
|
||||
if hidden != application.Window(w) {
|
||||
kept = append(kept, hidden)
|
||||
}
|
||||
}
|
||||
s.hiddenForLogin = kept
|
||||
}
|
||||
|
||||
func (s *WindowManager) windowByName(name string) *application.WebviewWindow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
switch name {
|
||||
case "main":
|
||||
return s.mainWindow
|
||||
case "settings":
|
||||
return s.settings
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WindowManager) markReady(w *application.WebviewWindow) {
|
||||
id := w.ID()
|
||||
s.mu.Lock()
|
||||
already := s.ready[id]
|
||||
s.ready[id] = true
|
||||
wanted := s.showPending[id]
|
||||
tab, hasTab := s.pendingTab[id]
|
||||
if timer := s.fallbackTimers[id]; timer != nil {
|
||||
timer.Stop()
|
||||
delete(s.fallbackTimers, id)
|
||||
}
|
||||
delete(s.showPending, id)
|
||||
delete(s.pendingTab, id)
|
||||
s.mu.Unlock()
|
||||
|
||||
if already {
|
||||
return
|
||||
}
|
||||
|
||||
if hasTab {
|
||||
s.app.Event.Emit(EventSettingsOpen, tab)
|
||||
}
|
||||
|
||||
if wanted {
|
||||
s.showNow(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WindowManager) showWhenReady(w *application.WebviewWindow) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := w.ID()
|
||||
s.mu.Lock()
|
||||
ready := s.ready[id]
|
||||
if !ready {
|
||||
s.showPending[id] = true
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if ready {
|
||||
s.showNow(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WindowManager) showNow(w *application.WebviewWindow) {
|
||||
w.Show()
|
||||
w.Focus()
|
||||
s.centerWhenReady(w)
|
||||
}
|
||||
|
||||
func (s *WindowManager) ShowMainAt(url string) {
|
||||
w := s.MainWindow()
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
w.SetURL(url)
|
||||
s.showWhenReady(w)
|
||||
}
|
||||
|
||||
func (s *WindowManager) SetMainFactory(f func() *application.WebviewWindow) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.newMain = f
|
||||
}
|
||||
|
||||
func (s *WindowManager) ForgetMain() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.forgetWindowLocked(s.mainWindow)
|
||||
s.mainWindow = nil
|
||||
}
|
||||
|
||||
// SetRecenterOnShow installs the recenterOnShow predicate (see the field).
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/client/ui/authsession"
|
||||
"github.com/netbirdio/netbird/client/ui/i18n"
|
||||
"github.com/netbirdio/netbird/client/ui/preferences"
|
||||
"github.com/netbirdio/netbird/client/ui/services"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
@@ -50,8 +51,9 @@ type TrayServices struct {
|
||||
WindowManager *services.WindowManager
|
||||
// Session is bound to authsession directly because the services wrapper
|
||||
// only re-exposes the React subset.
|
||||
Session *authsession.Session
|
||||
Localizer *Localizer
|
||||
Session *authsession.Session
|
||||
Localizer *Localizer
|
||||
Preferences *preferences.Store
|
||||
}
|
||||
|
||||
type Tray struct {
|
||||
@@ -172,7 +174,7 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe
|
||||
// in the right locale — no English flash then re-paint.
|
||||
loc: svc.Localizer,
|
||||
}
|
||||
t.updater = newTrayUpdater(app, window, svc.Update, svc.Notifier, t.loc, func() { t.applyIcon() }, func() { t.relayoutMenu() })
|
||||
t.updater = newTrayUpdater(app, t.showMainAt, svc.Update, svc.Notifier, t.loc, func() { t.applyIcon() }, func() { t.relayoutMenu() })
|
||||
t.tray = app.SystemTray.New()
|
||||
// Seed panel-theme detection before the first paint so the initial icon
|
||||
// matches the panel's light/dark scheme (Linux only).
|
||||
@@ -239,9 +241,6 @@ func (t *Tray) ShowWindow() {
|
||||
w.Focus()
|
||||
return
|
||||
}
|
||||
if t.window == nil {
|
||||
return
|
||||
}
|
||||
// Route through WindowManager so the main window is centered on first
|
||||
// show — minimal WMs (fluxbox, the XEmbed tray path) otherwise drop it in
|
||||
// the top-left corner.
|
||||
@@ -249,8 +248,40 @@ func (t *Tray) ShowWindow() {
|
||||
t.svc.WindowManager.ShowMain()
|
||||
return
|
||||
}
|
||||
t.window.Show()
|
||||
t.window.Focus()
|
||||
if w := t.mainWindow(); w != nil {
|
||||
w.Show()
|
||||
w.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tray) mainWindow() *application.WebviewWindow {
|
||||
if t.svc.WindowManager == nil {
|
||||
return t.window
|
||||
}
|
||||
return t.svc.WindowManager.MainWindow()
|
||||
}
|
||||
|
||||
func (t *Tray) showMainAt(url string) {
|
||||
if t.svc.WindowManager != nil {
|
||||
t.svc.WindowManager.ShowMainAt(url)
|
||||
return
|
||||
}
|
||||
if w := t.mainWindow(); w != nil {
|
||||
w.SetURL(url)
|
||||
w.Show()
|
||||
w.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tray) showMain() {
|
||||
if t.svc.WindowManager != nil {
|
||||
t.svc.WindowManager.ShowMain()
|
||||
return
|
||||
}
|
||||
if w := t.mainWindow(); w != nil {
|
||||
w.Show()
|
||||
w.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
// applyLanguage re-renders every translated surface in the Localizer's current
|
||||
@@ -461,10 +492,12 @@ func (t *Tray) handleQuit() {
|
||||
t.profileMu.Unlock()
|
||||
t.svc.DaemonFeed.CancelProfileSwitch()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), quitDownTimeout)
|
||||
defer cancel()
|
||||
if err := t.svc.Connection.Down(ctx); err != nil {
|
||||
log.Errorf("disconnect on quit: %v", err)
|
||||
if t.svc.Preferences == nil || !t.svc.Preferences.Get().KeepConnectedOnQuit {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), quitDownTimeout)
|
||||
defer cancel()
|
||||
if err := t.svc.Connection.Down(ctx); err != nil {
|
||||
log.Errorf("disconnect on quit: %v", err)
|
||||
}
|
||||
}
|
||||
t.app.Quit()
|
||||
}
|
||||
|
||||
@@ -30,10 +30,7 @@ const (
|
||||
// handleSessionExpired notifies and brings the window forward so the user can reconnect.
|
||||
func (t *Tray) handleSessionExpired() {
|
||||
t.notify(t.loc.T("notify.sessionExpired.title"), t.loc.T("notify.sessionExpired.body"), notifyIDSessionExpired)
|
||||
if t.window != nil {
|
||||
t.window.Show()
|
||||
t.window.Focus()
|
||||
}
|
||||
t.showMain()
|
||||
}
|
||||
|
||||
// applySessionExpiry refreshes the cached SSO deadline and reports whether it changed.
|
||||
@@ -307,6 +304,7 @@ func (t *Tray) openSessionExtendFlow() {
|
||||
}
|
||||
seconds := int(time.Until(deadline).Seconds())
|
||||
if seconds <= 0 {
|
||||
t.showMain()
|
||||
t.app.Event.Emit(services.EventTriggerLogin)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// trayUpdater owns the tray UI that reacts to auto-update. Composed inside Tray.
|
||||
type trayUpdater struct {
|
||||
app *application.App
|
||||
window *application.WebviewWindow
|
||||
showMainAt func(url string)
|
||||
update *services.Update
|
||||
notifier *Notifier
|
||||
loc *Localizer
|
||||
@@ -36,10 +36,10 @@ type trayUpdater struct {
|
||||
progressWindowOpen bool
|
||||
}
|
||||
|
||||
func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *Notifier, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater {
|
||||
func newTrayUpdater(app *application.App, showMainAt func(url string), update *services.Update, notifier *Notifier, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater {
|
||||
u := &trayUpdater{
|
||||
app: app,
|
||||
window: window,
|
||||
showMainAt: showMainAt,
|
||||
update: update,
|
||||
notifier: notifier,
|
||||
loc: loc,
|
||||
@@ -185,14 +185,12 @@ func (u *trayUpdater) sendUpdateNotification(st updater.State) {
|
||||
// openProgressWindow points the main window at the /update progress page and
|
||||
// brings it forward.
|
||||
func (u *trayUpdater) openProgressWindow(version string) {
|
||||
if u.window == nil {
|
||||
if u.showMainAt == nil {
|
||||
return
|
||||
}
|
||||
url := "/#/update"
|
||||
if version != "" {
|
||||
url += "?version=" + version
|
||||
}
|
||||
u.window.SetURL(url)
|
||||
u.window.Show()
|
||||
u.window.Focus()
|
||||
u.showMainAt(url)
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -340,4 +340,4 @@ replace github.com/dexidp/dex/api/v2 => github.com/netbirdio/dex/api/v2 v2.0.0-2
|
||||
|
||||
replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0
|
||||
|
||||
replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4
|
||||
replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260807055527-fc03f984d701
|
||||
|
||||
4
go.sum
4
go.sum
@@ -490,8 +490,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax
|
||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
|
||||
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4 h1:UKztc3QjWvzU5DZk+uYaOWN0x62NSe/pkxuPvzqZIy4=
|
||||
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4/go.mod h1:BzATbK71VFikMMMCo434wAi0QcaI03P+xeaWgDvQvjw=
|
||||
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260807055527-fc03f984d701 h1:QL9nupfRom0L9jcY7N9l/Bc6QK2PtC6pHzC+ftpTqpw=
|
||||
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260807055527-fc03f984d701/go.mod h1:BzATbK71VFikMMMCo434wAi0QcaI03P+xeaWgDvQvjw=
|
||||
github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a h1:3CWK+yTvRKOcC0Q8VCTGy4l60TEb27CQVS7LkMxwjmw=
|
||||
github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
|
||||
@@ -262,6 +262,7 @@ init_environment() {
|
||||
POSTGRES_DB="netbird"
|
||||
POSTGRES_PASSWORD=$(rand_secret)
|
||||
NETBIRD_ENCRYPTION_KEY=$(rand_b64_key)
|
||||
NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY=$(rand_b64_key)
|
||||
NETBIRD_RELAY_AUTH_SECRET=$(rand_secret)
|
||||
|
||||
POSTGRES_DSN="host=postgres user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB} port=5432 sslmode=disable TimeZone=UTC"
|
||||
@@ -696,6 +697,7 @@ server:
|
||||
issuer: "https://${NETBIRD_DOMAIN}/oauth2"
|
||||
localAuthDisabled: false
|
||||
signKeyRefreshEnabled: false
|
||||
sessionCookieEncryptionKey: "${NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY}"
|
||||
dashboardRedirectURIs:
|
||||
- "https://${NETBIRD_DOMAIN}/nb-auth"
|
||||
- "https://${NETBIRD_DOMAIN}/nb-silent-auth"
|
||||
|
||||
@@ -348,6 +348,7 @@ initialize_default_values() {
|
||||
NETBIRD_RELAY_AUTH_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING")
|
||||
# Note: DataStoreEncryptionKey must keep base64 padding (=) for Go's base64.StdEncoding
|
||||
DATASTORE_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
SESSION_COOKIE_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
NETBIRD_STUN_PORT=3478
|
||||
|
||||
# Docker images
|
||||
@@ -527,7 +528,8 @@ generate_configuration_files() {
|
||||
|
||||
# Common files for all configurations
|
||||
render_dashboard_env > dashboard.env
|
||||
render_combined_yaml > config.yaml
|
||||
install -m 600 /dev/null config.yaml
|
||||
render_combined_yaml >> config.yaml
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -911,6 +913,7 @@ server:
|
||||
auth:
|
||||
issuer: "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth2"
|
||||
signKeyRefreshEnabled: true
|
||||
sessionCookieEncryptionKey: "$SESSION_COOKIE_ENCRYPTION_KEY"
|
||||
dashboardRedirectURIs:
|
||||
- "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/nb-auth"
|
||||
- "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/nb-silent-auth"
|
||||
|
||||
@@ -4,9 +4,67 @@ set -x
|
||||
|
||||
LOG_FILE=/var/log/netbird/client_pre_install.log
|
||||
AGENT=/usr/local/bin/netbird
|
||||
UI_PROCESS=netbird-ui
|
||||
|
||||
mkdir -p /var/log/netbird/
|
||||
|
||||
# wait_for_ui_exit polls for up to $1 seconds, returning 0 as soon as no UI
|
||||
# process is left and 1 if one is still running when the time is up.
|
||||
wait_for_ui_exit() {
|
||||
waited=0
|
||||
while [ "$waited" -lt "$1" ]; do
|
||||
pgrep -x "$UI_PROCESS" > /dev/null 2>&1 || return 0
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# request_ui_quit asks the UI to quit from inside the console user's session and
|
||||
# reports whether the request could be sent at all. The installer runs as root
|
||||
# outside that session, so a quit Apple event sent straight from here always
|
||||
# fails with -600.
|
||||
request_ui_quit() {
|
||||
console_user=$(stat -f%Su /dev/console 2>/dev/null)
|
||||
case "$console_user" in
|
||||
""|root|loginwindow|_mbsetupuser)
|
||||
echo "No active GUI user session (console user: '${console_user:-none}'); skipping the quit request."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
uid=$(id -u "$console_user" 2>/dev/null)
|
||||
if [ -z "$uid" ]; then
|
||||
echo "Could not resolve uid for console user '$console_user'; skipping the quit request."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Asking the NetBird UI to quit as console user $console_user (uid $uid)."
|
||||
launchctl asuser "$uid" sudo -u "$console_user" -H osascript -e 'quit app "NetBird"' || true
|
||||
}
|
||||
|
||||
# quit_ui stops a running UI so the app bundle can be replaced underneath it. A
|
||||
# UI process that survives the install keeps serving the old binary until it is
|
||||
# quit by hand, so anything still running once the quit request is out of the
|
||||
# way is signalled. Waiting for a graceful exit only makes sense when a quit
|
||||
# request was actually sent.
|
||||
quit_ui() {
|
||||
if request_ui_quit && wait_for_ui_exit 10; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
pgrep -x "$UI_PROCESS" > /dev/null 2>&1 || return 0
|
||||
|
||||
echo "NetBird UI still running; terminating it."
|
||||
pkill -x "$UI_PROCESS" || true
|
||||
if wait_for_ui_exit 3; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "NetBird UI ignored SIGTERM; killing it."
|
||||
pkill -KILL -x "$UI_PROCESS" || true
|
||||
}
|
||||
|
||||
{
|
||||
# check if it was installed with brew
|
||||
brew list --formula | grep netbird
|
||||
@@ -15,10 +73,9 @@ mkdir -p /var/log/netbird/
|
||||
echo "NetBird has been installed with Brew. Please use Brew to update the package."
|
||||
exit 1
|
||||
fi
|
||||
osascript -e 'quit app "Netbird"' || true
|
||||
quit_ui
|
||||
$AGENT service stop || true
|
||||
|
||||
echo "Preinstall complete"
|
||||
exit 0 # all good
|
||||
} &> $LOG_FILE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user