mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-30 14:16:38 +00:00
Compare commits
7 Commits
feat/byod-
...
v0.70.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5474e199f | ||
|
|
db44848e2d | ||
|
|
9417ce3b3a | ||
|
|
8fc4265995 | ||
|
|
9c50819f20 | ||
|
|
6f0eff3ba0 | ||
|
|
f8745723fc |
@@ -200,6 +200,7 @@ Pop $0
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
|
SetRegView 64
|
||||||
StrCpy $INSTDIR "${INSTALL_DIR}"
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
${If} $R0 != ""
|
${If} $R0 != ""
|
||||||
@@ -214,6 +215,10 @@ ${If} $R0 != ""
|
|||||||
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.onInit
|
||||||
|
SetRegView 64
|
||||||
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
@@ -228,6 +233,7 @@ Section -MainProgram
|
|||||||
!else
|
!else
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
!endif
|
!endif
|
||||||
|
File "..\\client\\ui\\assets\\netbird.png"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@@ -247,9 +253,11 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
|||||||
; Create autostart registry entry based on checkbox
|
; Create autostart registry entry based on checkbox
|
||||||
DetailPrint "Autostart enabled: $AutostartEnabled"
|
DetailPrint "Autostart enabled: $AutostartEnabled"
|
||||||
${If} $AutostartEnabled == "1"
|
${If} $AutostartEnabled == "1"
|
||||||
WriteRegStr HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}" "$INSTDIR\${UI_APP_EXE}.exe"
|
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
||||||
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
||||||
${Else}
|
${Else}
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
DetailPrint "Autostart not enabled by user"
|
DetailPrint "Autostart not enabled by user"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
@@ -283,6 +291,8 @@ ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
|||||||
|
|
||||||
; Remove autostart registry entry
|
; Remove autostart registry entry
|
||||||
DetailPrint "Removing autostart registry entry if exists..."
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
|
||||||
; Handle data deletion based on checkbox
|
; Handle data deletion based on checkbox
|
||||||
@@ -321,6 +331,7 @@ DetailPrint "Removing registry keys..."
|
|||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
||||||
|
DeleteRegKey HKCU "Software\Classes\AppUserModelId\${APP_NAME}"
|
||||||
|
|
||||||
DetailPrint "Removing application directory from PATH..."
|
DetailPrint "Removing application directory from PATH..."
|
||||||
EnVar::SetHKLM
|
EnVar::SetHKLM
|
||||||
|
|||||||
@@ -333,6 +333,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.statusRecorder.MarkSignalConnected()
|
c.statusRecorder.MarkSignalConnected()
|
||||||
|
|
||||||
relayURLs, token := parseRelayInfo(loginResp)
|
relayURLs, token := parseRelayInfo(loginResp)
|
||||||
|
if override, ok := peer.OverrideRelayURLs(); ok {
|
||||||
|
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
|
||||||
|
relayURLs = override
|
||||||
|
}
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)
|
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)
|
||||||
|
|||||||
@@ -944,7 +944,12 @@ func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
|
|||||||
return fmt.Errorf("update relay token: %w", err)
|
return fmt.Errorf("update relay token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.relayManager.UpdateServerURLs(update.Urls)
|
urls := update.Urls
|
||||||
|
if override, ok := peer.OverrideRelayURLs(); ok {
|
||||||
|
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
|
||||||
|
urls = override
|
||||||
|
}
|
||||||
|
e.relayManager.UpdateServerURLs(urls)
|
||||||
|
|
||||||
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
||||||
// We can ignore all errors because the guard will manage the reconnection retries.
|
// We can ignore all errors because the guard will manage the reconnection retries.
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
||||||
|
EnvKeyNBHomeRelayServers = "NB_HOME_RELAY_SERVERS"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsForceRelayed() bool {
|
func IsForceRelayed() bool {
|
||||||
@@ -16,3 +17,28 @@ func IsForceRelayed() bool {
|
|||||||
}
|
}
|
||||||
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OverrideRelayURLs returns the relay server URL list set in
|
||||||
|
// NB_HOME_RELAY_SERVERS (comma-separated) and a boolean indicating whether
|
||||||
|
// the override is active. When the env var is unset, the boolean is false
|
||||||
|
// and the caller should keep the list received from the management server.
|
||||||
|
// Intended for lab/debug scenarios where a peer must pin to a specific home
|
||||||
|
// relay regardless of what management offers.
|
||||||
|
func OverrideRelayURLs() ([]string, bool) {
|
||||||
|
raw := os.Getenv(EnvKeyNBHomeRelayServers)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
urls := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p != "" {
|
||||||
|
urls = append(urls, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return urls, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,10 +18,17 @@
|
|||||||
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
||||||
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
|
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
</File>
|
</File>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
||||||
|
<File Id="NetbirdToastIcon" Name="netbird.png" Source=".\client\ui\assets\netbird.png" />
|
||||||
<?if $(var.ArchSuffix) = "amd64" ?>
|
<?if $(var.ArchSuffix) = "amd64" ?>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
||||||
<?endif ?>
|
<?endif ?>
|
||||||
@@ -46,8 +53,19 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<!-- Per-user component: HKCU keypath (auto GUID via "*"), separate from
|
||||||
|
the per-machine NetbirdFiles component to satisfy ICE57. -->
|
||||||
|
<StandardDirectory Id="ProgramMenuFolder">
|
||||||
|
<Component Id="NetbirdAumidRegistry" Guid="*">
|
||||||
|
<RegistryKey Root="HKCU" Key="Software\Classes\AppUserModelId\NetBird" ForceDeleteOnUninstall="yes">
|
||||||
|
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
</RegistryKey>
|
||||||
|
</Component>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
<ComponentGroup Id="NetbirdFilesComponent">
|
<ComponentGroup Id="NetbirdFilesComponent">
|
||||||
<ComponentRef Id="NetbirdFiles" />
|
<ComponentRef Id="NetbirdFiles" />
|
||||||
|
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
|
|
||||||
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||||
"github.com/netbirdio/netbird/client/ui/event"
|
"github.com/netbirdio/netbird/client/ui/event"
|
||||||
|
"github.com/netbirdio/netbird/client/ui/notifier"
|
||||||
"github.com/netbirdio/netbird/client/ui/process"
|
"github.com/netbirdio/netbird/client/ui/process"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
@@ -260,6 +261,7 @@ type serviceClient struct {
|
|||||||
|
|
||||||
// application with main windows.
|
// application with main windows.
|
||||||
app fyne.App
|
app fyne.App
|
||||||
|
notifier notifier.Notifier
|
||||||
wSettings fyne.Window
|
wSettings fyne.Window
|
||||||
showAdvancedSettings bool
|
showAdvancedSettings bool
|
||||||
sendNotification bool
|
sendNotification bool
|
||||||
@@ -364,6 +366,7 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
addr: args.addr,
|
addr: args.addr,
|
||||||
app: args.app,
|
app: args.app,
|
||||||
|
notifier: notifier.New(args.app),
|
||||||
logFile: args.logFile,
|
logFile: args.logFile,
|
||||||
sendNotification: false,
|
sendNotification: false,
|
||||||
|
|
||||||
@@ -892,7 +895,7 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("get service status: %v", err)
|
log.Errorf("get service status: %v", err)
|
||||||
if s.connected {
|
if s.connected {
|
||||||
s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost"))
|
s.notifier.Send("Error", "Connection to service lost")
|
||||||
}
|
}
|
||||||
s.setDisconnectedStatus()
|
s.setDisconnectedStatus()
|
||||||
return err
|
return err
|
||||||
@@ -1109,7 +1112,7 @@ func (s *serviceClient) onTrayReady() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.eventManager = event.NewManager(s.app, s.addr)
|
s.eventManager = event.NewManager(s.notifier, s.addr)
|
||||||
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
||||||
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
||||||
if event.Category == proto.SystemEvent_SYSTEM {
|
if event.Category == proto.SystemEvent_SYSTEM {
|
||||||
@@ -1548,7 +1551,7 @@ func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) {
|
|||||||
|
|
||||||
if enforced && s.lastNotifiedVersion != newVersion {
|
if enforced && s.lastNotifiedVersion != newVersion {
|
||||||
s.lastNotifiedVersion = newVersion
|
s.lastNotifiedVersion = newVersion
|
||||||
s.app.SendNotification(fyne.NewNotification("Update available", "A new version "+newVersion+" is ready to install"))
|
s.notifier.Send("Update available", "A new version "+newVersion+" is ready to install")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -18,11 +17,17 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Notifier sends desktop notifications. Defined here so the event package
|
||||||
|
// does not depend on fyne or the platform-specific notifier implementation.
|
||||||
|
type Notifier interface {
|
||||||
|
Send(title, body string)
|
||||||
|
}
|
||||||
|
|
||||||
type Handler func(*proto.SystemEvent)
|
type Handler func(*proto.SystemEvent)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
app fyne.App
|
notifier Notifier
|
||||||
addr string
|
addr string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -31,10 +36,10 @@ type Manager struct {
|
|||||||
handlers []Handler
|
handlers []Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(app fyne.App, addr string) *Manager {
|
func NewManager(notifier Notifier, addr string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
app: app,
|
notifier: notifier,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) {
|
|||||||
if id != "" {
|
if id != "" {
|
||||||
body += fmt.Sprintf(" ID: %s", id)
|
body += fmt.Sprintf(" ID: %s", id)
|
||||||
}
|
}
|
||||||
e.app.SendNotification(fyne.NewNotification(title, body))
|
e.notifier.Send(title, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, handler := range handlers {
|
for _, handler := range handlers {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/systray"
|
"fyne.io/systray"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -87,7 +86,7 @@ func (h *eventHandler) handleConnectClick() {
|
|||||||
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
||||||
log.Debugf("connect operation cancelled by user")
|
log.Debugf("connect operation cancelled by user")
|
||||||
} else {
|
} else {
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to connect"))
|
h.client.notifier.Send("Error", "Failed to connect")
|
||||||
log.Errorf("connect failed: %v", err)
|
log.Errorf("connect failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +111,7 @@ func (h *eventHandler) handleDisconnectClick() {
|
|||||||
if err := h.client.menuDownClick(); err != nil {
|
if err := h.client.menuDownClick(); err != nil {
|
||||||
st, ok := status.FromError(err)
|
st, ok := status.FromError(err)
|
||||||
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to disconnect"))
|
h.client.notifier.Send("Error", "Failed to disconnect")
|
||||||
log.Errorf("disconnect failed: %v", err)
|
log.Errorf("disconnect failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("disconnect cancelled or already disconnecting")
|
log.Debugf("disconnect cancelled or already disconnecting")
|
||||||
@@ -130,7 +129,7 @@ func (h *eventHandler) handleAllowSSHClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update SSH settings"))
|
h.client.notifier.Send("Error", "Failed to update SSH settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -140,7 +139,7 @@ func (h *eventHandler) handleAutoConnectClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update auto-connect settings"))
|
h.client.notifier.Send("Error", "Failed to update auto-connect settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +148,7 @@ func (h *eventHandler) handleRosenpassClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update Rosenpass settings"))
|
h.client.notifier.Send("Error", "Failed to update Rosenpass settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ func (h *eventHandler) handleLazyConnectionClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update lazy connection settings"))
|
h.client.notifier.Send("Error", "Failed to update lazy connection settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +166,7 @@ func (h *eventHandler) handleBlockInboundClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update block inbound settings"))
|
h.client.notifier.Send("Error", "Failed to update block inbound settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ func (h *eventHandler) handleNotificationsClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update notifications settings"))
|
h.client.notifier.Send("Error", "Failed to update notifications settings")
|
||||||
} else if h.client.eventManager != nil {
|
} else if h.client.eventManager != nil {
|
||||||
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
||||||
}
|
}
|
||||||
|
|||||||
27
client/ui/notifier/notifier.go
Normal file
27
client/ui/notifier/notifier.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Package notifier sends desktop notifications. On Windows it uses the WinRT
|
||||||
|
// COM API directly via go-toast/v2 to avoid the PowerShell window flash that
|
||||||
|
// fyne's default implementation produces. On other platforms it delegates to
|
||||||
|
// fyne.
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
|
||||||
|
// Notifier sends desktop notifications.
|
||||||
|
type Notifier interface {
|
||||||
|
Send(title, body string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a platform-specific Notifier. The fyne app is used as the
|
||||||
|
// fallback notifier on platforms where no native implementation is wired up,
|
||||||
|
// and on Windows when the COM path fails to initialize.
|
||||||
|
func New(app fyne.App) Notifier {
|
||||||
|
return newNotifier(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fyneNotifier struct {
|
||||||
|
app fyne.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fyneNotifier) Send(title, body string) {
|
||||||
|
f.app.SendNotification(fyne.NewNotification(title, body))
|
||||||
|
}
|
||||||
9
client/ui/notifier/notifier_other.go
Normal file
9
client/ui/notifier/notifier_other.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
|
||||||
|
func newNotifier(app fyne.App) Notifier {
|
||||||
|
return &fyneNotifier{app: app}
|
||||||
|
}
|
||||||
88
client/ui/notifier/notifier_windows.go
Normal file
88
client/ui/notifier/notifier_windows.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||||
|
"git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// appID is the AppUserModelID shown in the Windows Action Center. It
|
||||||
|
// must match the System.AppUserModel.ID property set on the Start Menu
|
||||||
|
// shortcut by the MSI (see client/netbird.wxs); otherwise Windows
|
||||||
|
// groups toasts under a separate, unbranded entry.
|
||||||
|
appID = "NetBird"
|
||||||
|
|
||||||
|
// appGUID identifies the COM activation callback class. Generated once
|
||||||
|
// for NetBird; do not change without coordinating an installer bump,
|
||||||
|
// since old registry entries pointing at the previous GUID would orphan.
|
||||||
|
appGUID = "{0E1B4DE7-E148-432B-9814-544F941826EC}"
|
||||||
|
)
|
||||||
|
|
||||||
|
type comNotifier struct {
|
||||||
|
fallback *fyneNotifier
|
||||||
|
ready bool
|
||||||
|
iconPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
initOnce sync.Once
|
||||||
|
initErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func newNotifier(app fyne.App) Notifier {
|
||||||
|
n := &comNotifier{
|
||||||
|
fallback: &fyneNotifier{app: app},
|
||||||
|
iconPath: resolveIcon(),
|
||||||
|
}
|
||||||
|
initOnce.Do(func() {
|
||||||
|
initErr = wintoast.SetAppData(wintoast.AppData{
|
||||||
|
AppID: appID,
|
||||||
|
GUID: appGUID,
|
||||||
|
IconPath: n.iconPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if initErr != nil {
|
||||||
|
log.Warnf("toast: register app data failed, falling back to fyne notifications: %v", initErr)
|
||||||
|
return n.fallback
|
||||||
|
}
|
||||||
|
n.ready = true
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *comNotifier) Send(title, body string) {
|
||||||
|
if !n.ready {
|
||||||
|
n.fallback.Send(title, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification := toast.Notification{
|
||||||
|
AppID: appID,
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
Icon: n.iconPath,
|
||||||
|
}
|
||||||
|
if err := notification.Push(); err != nil {
|
||||||
|
log.Warnf("toast: push failed, using fyne fallback: %v", err)
|
||||||
|
n.fallback.Send(title, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveIcon returns an absolute path to the toast icon, or an empty string
|
||||||
|
// when no icon can be located. Windows requires a PNG/JPG for the
|
||||||
|
// AppUserModelId IconUri registry value; .ico is silently ignored.
|
||||||
|
func resolveIcon() string {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
candidate := filepath.Join(filepath.Dir(exe), "netbird.png")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -548,7 +548,7 @@ func (p *profileMenu) refresh() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to switch profile: %v", err)
|
log.Errorf("failed to switch profile: %v", err)
|
||||||
// show notification dialog
|
// show notification dialog
|
||||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile"))
|
p.serviceClient.notifier.Send("Error", "Failed to switch profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,9 +628,9 @@ func (p *profileMenu) refresh() {
|
|||||||
}
|
}
|
||||||
if err := p.eventHandler.logout(p.ctx); err != nil {
|
if err := p.eventHandler.logout(p.ctx); err != nil {
|
||||||
log.Errorf("logout failed: %v", err)
|
log.Errorf("logout failed: %v", err)
|
||||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to deregister"))
|
p.serviceClient.notifier.Send("Error", "Failed to deregister")
|
||||||
} else {
|
} else {
|
||||||
p.app.SendNotification(fyne.NewNotification("Success", "Deregistered successfully"))
|
p.serviceClient.notifier.Send("Success", "Deregistered successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -30,6 +30,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.7.0
|
fyne.io/fyne/v2 v2.7.0
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
|
||||||
github.com/awnumar/memguard v0.23.0
|
github.com/awnumar/memguard v0.23.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.38.3
|
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -15,6 +15,8 @@ fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
|||||||
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func (c *Connector) ToStorageConnector() (storage.Connector, error) {
|
|||||||
// are stored with types that Dex can open.
|
// are stored with types that Dex can open.
|
||||||
func mapConnectorToDex(connType string, config map[string]interface{}) (string, map[string]interface{}) {
|
func mapConnectorToDex(connType string, config map[string]interface{}) (string, map[string]interface{}) {
|
||||||
switch connType {
|
switch connType {
|
||||||
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
|
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak", "adfs":
|
||||||
return "oidc", applyOIDCDefaults(connType, config)
|
return "oidc", applyOIDCDefaults(connType, config)
|
||||||
default:
|
default:
|
||||||
return connType, config
|
return connType, config
|
||||||
@@ -218,6 +218,8 @@ func applyOIDCDefaults(connType string, config map[string]interface{}) map[strin
|
|||||||
setDefault(augmented, "claimMapping", map[string]string{"email": "preferred_username"})
|
setDefault(augmented, "claimMapping", map[string]string{"email": "preferred_username"})
|
||||||
case "okta", "pocketid":
|
case "okta", "pocketid":
|
||||||
augmented["scopes"] = []string{"openid", "profile", "email", "groups"}
|
augmented["scopes"] = []string{"openid", "profile", "email", "groups"}
|
||||||
|
case "adfs":
|
||||||
|
augmented["scopes"] = []string{"openid", "profile", "email", "allatclaims"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return augmented
|
return augmented
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func (p *Provider) buildStorageConnector(cfg *ConnectorConfig) (storage.Connecto
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch cfg.Type {
|
switch cfg.Type {
|
||||||
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
|
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak", "adfs":
|
||||||
dexType = "oidc"
|
dexType = "oidc"
|
||||||
configData, err = buildOIDCConnectorConfig(cfg, redirectURI)
|
configData, err = buildOIDCConnectorConfig(cfg, redirectURI)
|
||||||
case "google":
|
case "google":
|
||||||
@@ -220,6 +220,8 @@ func buildOIDCConnectorConfig(cfg *ConnectorConfig, redirectURI string) ([]byte,
|
|||||||
oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
|
oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
|
||||||
case "pocketid":
|
case "pocketid":
|
||||||
oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
|
oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
|
||||||
|
case "adfs":
|
||||||
|
oidcConfig["scopes"] = []string{"openid", "profile", "email", "allatclaims"}
|
||||||
}
|
}
|
||||||
return encodeConnectorConfig(oidcConfig)
|
return encodeConnectorConfig(oidcConfig)
|
||||||
}
|
}
|
||||||
@@ -283,7 +285,7 @@ func inferIdentityProviderType(dexType, connectorID string, _ map[string]interfa
|
|||||||
// inferOIDCProviderType infers the specific OIDC provider from connector ID
|
// inferOIDCProviderType infers the specific OIDC provider from connector ID
|
||||||
func inferOIDCProviderType(connectorID string) string {
|
func inferOIDCProviderType(connectorID string) string {
|
||||||
connectorIDLower := strings.ToLower(connectorID)
|
connectorIDLower := strings.ToLower(connectorID)
|
||||||
for _, provider := range []string{"pocketid", "zitadel", "entra", "okta", "authentik", "keycloak"} {
|
for _, provider := range []string{"pocketid", "zitadel", "entra", "okta", "authentik", "keycloak", "adfs"} {
|
||||||
if strings.Contains(connectorIDLower, provider) {
|
if strings.Contains(connectorIDLower, provider) {
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,7 +231,20 @@ get_upstream_host() {
|
|||||||
|
|
||||||
wait_management_proxy() {
|
wait_management_proxy() {
|
||||||
local proxy_container="${1:-traefik}"
|
local proxy_container="${1:-traefik}"
|
||||||
|
local use_docker_logs=false
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
|
if [[ "$proxy_container" == "detect-traefik" ]]; then
|
||||||
|
proxy_container=$(docker ps --format "{{.ID}}\t{{.Image}}\t{{.Ports}}" \
|
||||||
|
| awk -F'\t' '$2 ~ /traefik/ && $3 ~ /:(80|443)->/ {print $1; exit}')
|
||||||
|
|
||||||
|
if [[ -z "$proxy_container" ]]; then
|
||||||
|
echo "Warning: could not auto-detect Traefik container, log output will be skipped on timeout." > /dev/stderr
|
||||||
|
else
|
||||||
|
use_docker_logs=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo -n "Waiting for NetBird server to become ready"
|
echo -n "Waiting for NetBird server to become ready"
|
||||||
counter=1
|
counter=1
|
||||||
while true; do
|
while true; do
|
||||||
@@ -242,7 +255,13 @@ wait_management_proxy() {
|
|||||||
if [[ $counter -eq 60 ]]; then
|
if [[ $counter -eq 60 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "Taking too long. Checking logs..."
|
echo "Taking too long. Checking logs..."
|
||||||
$DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container"
|
if [[ -n "$proxy_container" ]]; then
|
||||||
|
if [[ "$use_docker_logs" == "true" ]]; then
|
||||||
|
docker logs --tail=20 "$proxy_container"
|
||||||
|
else
|
||||||
|
$DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
$DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server
|
$DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server
|
||||||
fi
|
fi
|
||||||
echo -n " ."
|
echo -n " ."
|
||||||
@@ -518,7 +537,7 @@ start_services_and_show_instructions() {
|
|||||||
$DOCKER_COMPOSE_COMMAND up -d
|
$DOCKER_COMPOSE_COMMAND up -d
|
||||||
|
|
||||||
sleep 3
|
sleep 3
|
||||||
wait_management_direct
|
wait_management_proxy detect-traefik
|
||||||
|
|
||||||
echo -e "$MSG_DONE"
|
echo -e "$MSG_DONE"
|
||||||
print_post_setup_instructions
|
print_post_setup_instructions
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ func identityProviderToConnectorConfig(idpConfig *types.IdentityProvider) *dex.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateIdentityProviderID generates a unique ID for an identity provider.
|
// generateIdentityProviderID generates a unique ID for an identity provider.
|
||||||
// For specific provider types (okta, zitadel, entra, google, pocketid, microsoft),
|
// For specific provider types (okta, zitadel, entra, google, pocketid, microsoft, adfs),
|
||||||
// the ID is prefixed with the type name. Generic OIDC providers get no prefix.
|
// the ID is prefixed with the type name. Generic OIDC providers get no prefix.
|
||||||
func generateIdentityProviderID(idpType types.IdentityProviderType) string {
|
func generateIdentityProviderID(idpType types.IdentityProviderType) string {
|
||||||
id := xid.New().String()
|
id := xid.New().String()
|
||||||
@@ -296,6 +296,8 @@ func generateIdentityProviderID(idpType types.IdentityProviderType) string {
|
|||||||
return "authentik-" + id
|
return "authentik-" + id
|
||||||
case types.IdentityProviderTypeKeycloak:
|
case types.IdentityProviderTypeKeycloak:
|
||||||
return "keycloak-" + id
|
return "keycloak-" + id
|
||||||
|
case types.IdentityProviderTypeADFS:
|
||||||
|
return "adfs-" + id
|
||||||
default:
|
default:
|
||||||
// Generic OIDC - no prefix
|
// Generic OIDC - no prefix
|
||||||
return id
|
return id
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ import (
|
|||||||
|
|
||||||
const remoteJobsMinVer = "0.64.0"
|
const remoteJobsMinVer = "0.64.0"
|
||||||
|
|
||||||
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
|
// GetPeers returns peers visible to the user within an account.
|
||||||
// the current user is not an admin.
|
// Users with "peers:read" see all peers. Otherwise, users see only their own peers, or none if restricted by account settings.
|
||||||
func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
||||||
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -46,14 +46,8 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
|
|||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accountPeers, err := am.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, nameFilter, ipFilter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// @note if the user has permission to read peers it shows all account peers
|
|
||||||
if allowed {
|
if allowed {
|
||||||
return accountPeers, nil
|
return am.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, nameFilter, ipFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
||||||
@@ -65,41 +59,7 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
|
|||||||
return []*nbpeer.Peer{}, nil
|
return []*nbpeer.Peer{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// @note if it does not have permission read peers then only display it's own peers
|
return am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
||||||
peers := make([]*nbpeer.Peer, 0)
|
|
||||||
peersMap := make(map[string]*nbpeer.Peer)
|
|
||||||
|
|
||||||
for _, peer := range accountPeers {
|
|
||||||
if user.Id != peer.UserID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
peers = append(peers, peer)
|
|
||||||
peersMap[peer.ID] = peer
|
|
||||||
}
|
|
||||||
|
|
||||||
return am.getUserAccessiblePeers(ctx, accountID, peersMap, peers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, accountID string, peersMap map[string]*nbpeer.Peer, peers []*nbpeer.Peer) ([]*nbpeer.Peer, error) {
|
|
||||||
account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch all the peers that have access to the user's peers
|
|
||||||
for _, peer := range peers {
|
|
||||||
aclPeers, _, _, _ := account.GetPeerConnectionResources(ctx, peer, approvedPeersMap, account.GetActiveGroupUsers())
|
|
||||||
for _, p := range aclPeers {
|
|
||||||
peersMap[p.ID] = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maps.Values(peersMap), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
|
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
|
||||||
@@ -1230,7 +1190,8 @@ func peerLoginExpired(ctx context.Context, peer *nbpeer.Peer, settings *types.Se
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeer for a given accountID, peerID and userID error if not found.
|
// GetPeer returns a peer visible to the user within an account.
|
||||||
|
// Users with "peers:read" permission can access any peer. Otherwise, users can access only their own peer.
|
||||||
func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) {
|
func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) {
|
||||||
peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1255,36 +1216,6 @@ func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID,
|
|||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return am.checkIfUserOwnsPeer(ctx, accountID, userID, peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) {
|
|
||||||
account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// it is also possible that user doesn't own the peer but some of his peers have access to it,
|
|
||||||
// this is a valid case, show the peer as well.
|
|
||||||
userPeers, err := am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range userPeers {
|
|
||||||
aclPeers, _, _, _ := account.GetPeerConnectionResources(ctx, p, approvedPeersMap, account.GetActiveGroupUsers())
|
|
||||||
for _, aclPeer := range aclPeers {
|
|
||||||
if aclPeer.ID == peer.ID {
|
|
||||||
return peer, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, status.Errorf(status.Internal, "user %s has no access to peer %s under account %s", userID, peer.ID, accountID)
|
return nil, status.Errorf(status.Internal, "user %s has no access to peer %s under account %s", userID, peer.ID, accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -559,25 +559,9 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.NotNil(t, peer)
|
assert.NotNil(t, peer)
|
||||||
|
|
||||||
// the user can see peer2 because peer1 of the user has access to peer2 due to the All group and the default rule 0 all-to-all access
|
// the user can NOT see peer2 because it is not owned by them.
|
||||||
peer, err = manager.GetPeer(context.Background(), accountID, peer2.ID, someUser)
|
// Regular users only see peers they directly own.
|
||||||
if err != nil {
|
_, err = manager.GetPeer(context.Background(), accountID, peer2.ID, someUser)
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.NotNil(t, peer)
|
|
||||||
|
|
||||||
// delete the all-to-all policy so that user's peer1 has no access to peer2
|
|
||||||
for _, policy := range account.Policies {
|
|
||||||
err = manager.DeletePolicy(context.Background(), accountID, policy.ID, adminUser)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// at this point the user can't see the details of peer2
|
|
||||||
peer, err = manager.GetPeer(context.Background(), accountID, peer2.ID, someUser) //nolint
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// admin users can always access all the peers
|
// admin users can always access all the peers
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const (
|
|||||||
IdentityProviderTypeAuthentik IdentityProviderType = "authentik"
|
IdentityProviderTypeAuthentik IdentityProviderType = "authentik"
|
||||||
// IdentityProviderTypeKeycloak is the Keycloak identity provider
|
// IdentityProviderTypeKeycloak is the Keycloak identity provider
|
||||||
IdentityProviderTypeKeycloak IdentityProviderType = "keycloak"
|
IdentityProviderTypeKeycloak IdentityProviderType = "keycloak"
|
||||||
|
// IdentityProviderTypeADFS is the Microsoft AD FS identity provider
|
||||||
|
IdentityProviderTypeADFS IdentityProviderType = "adfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IdentityProvider represents an identity provider configuration
|
// IdentityProvider represents an identity provider configuration
|
||||||
@@ -112,7 +114,8 @@ func (t IdentityProviderType) IsValid() bool {
|
|||||||
switch t {
|
switch t {
|
||||||
case IdentityProviderTypeOIDC, IdentityProviderTypeZitadel, IdentityProviderTypeEntra,
|
case IdentityProviderTypeOIDC, IdentityProviderTypeZitadel, IdentityProviderTypeEntra,
|
||||||
IdentityProviderTypeGoogle, IdentityProviderTypeOkta, IdentityProviderTypePocketID,
|
IdentityProviderTypeGoogle, IdentityProviderTypeOkta, IdentityProviderTypePocketID,
|
||||||
IdentityProviderTypeMicrosoft, IdentityProviderTypeAuthentik, IdentityProviderTypeKeycloak:
|
IdentityProviderTypeMicrosoft, IdentityProviderTypeAuthentik, IdentityProviderTypeKeycloak,
|
||||||
|
IdentityProviderTypeADFS:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -146,7 +146,11 @@ func (c *ClaimsExtractor) ToGroups(token *jwt.Token, claimName string) []string
|
|||||||
userJWTGroups := make([]string, 0)
|
userJWTGroups := make([]string, 0)
|
||||||
|
|
||||||
if claim, ok := claims[claimName]; ok {
|
if claim, ok := claims[claimName]; ok {
|
||||||
if claimGroups, ok := claim.([]interface{}); ok {
|
switch claimGroups := claim.(type) {
|
||||||
|
case string:
|
||||||
|
// Some IdPs emit a single group claim as a string instead of an array.
|
||||||
|
userJWTGroups = append(userJWTGroups, claimGroups)
|
||||||
|
case []any:
|
||||||
for _, g := range claimGroups {
|
for _, g := range claimGroups {
|
||||||
if group, ok := g.(string); ok {
|
if group, ok := g.(string); ok {
|
||||||
userJWTGroups = append(userJWTGroups, group)
|
userJWTGroups = append(userJWTGroups, group)
|
||||||
@@ -154,9 +158,11 @@ func (c *ClaimsExtractor) ToGroups(token *jwt.Token, claimName string) []string
|
|||||||
log.Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
|
log.Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
log.Debugf("JWT claim %q is not a string or string array (type: %T): %v", claimName, claim, claim)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("JWT claim %q is not a string array", claimName)
|
log.Debugf("JWT claim %q is missing", claimName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return userJWTGroups
|
return userJWTGroups
|
||||||
|
|||||||
@@ -249,6 +249,15 @@ func TestClaimsExtractor_ToGroups(t *testing.T) {
|
|||||||
groupClaimName: "groups",
|
groupClaimName: "groups",
|
||||||
expectedGroups: []string{},
|
expectedGroups: []string{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "extracts single group string from claim",
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"sub": "user-123",
|
||||||
|
"groups": "admin",
|
||||||
|
},
|
||||||
|
groupClaimName: "groups",
|
||||||
|
expectedGroups: []string{"admin"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "handles custom claim name",
|
name: "handles custom claim name",
|
||||||
claims: jwt.MapClaims{
|
claims: jwt.MapClaims{
|
||||||
|
|||||||
@@ -252,21 +252,19 @@ func (c *GrpcClient) handleJobStream(
|
|||||||
c.notifyDisconnected(err)
|
c.notifyDisconnected(err)
|
||||||
return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer
|
return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer
|
||||||
case codes.Canceled:
|
case codes.Canceled:
|
||||||
log.Debugf("management connection context has been canceled, this usually indicates shutdown")
|
log.Debugf("job stream context has been canceled, this usually indicates shutdown")
|
||||||
return err
|
return err
|
||||||
case codes.Unimplemented:
|
case codes.Unimplemented:
|
||||||
log.Warn("Job feature is not supported by the current management server version. " +
|
log.Warn("Job feature is not supported by the current management server version. " +
|
||||||
"Please update the management service to use this feature.")
|
"Please update the management service to use this feature.")
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
c.notifyDisconnected(err)
|
log.Warnf("job stream disconnected, will retry silently. Reason: %v", err)
|
||||||
log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// non-gRPC error
|
// non-gRPC error
|
||||||
c.notifyDisconnected(err)
|
log.Warnf("job stream disconnected, will retry silently. Reason: %v", err)
|
||||||
log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2917,6 +2917,7 @@ components:
|
|||||||
- okta
|
- okta
|
||||||
- pocketid
|
- pocketid
|
||||||
- microsoft
|
- microsoft
|
||||||
|
- adfs
|
||||||
example: oidc
|
example: oidc
|
||||||
IdentityProvider:
|
IdentityProvider:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ const (
|
|||||||
IdentityProviderTypeOkta IdentityProviderType = "okta"
|
IdentityProviderTypeOkta IdentityProviderType = "okta"
|
||||||
IdentityProviderTypePocketid IdentityProviderType = "pocketid"
|
IdentityProviderTypePocketid IdentityProviderType = "pocketid"
|
||||||
IdentityProviderTypeZitadel IdentityProviderType = "zitadel"
|
IdentityProviderTypeZitadel IdentityProviderType = "zitadel"
|
||||||
|
IdentityProviderTypeAdfs IdentityProviderType = "adfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Valid indicates whether the value is a known member of the IdentityProviderType enum.
|
// Valid indicates whether the value is a known member of the IdentityProviderType enum.
|
||||||
@@ -537,6 +538,8 @@ func (e IdentityProviderType) Valid() bool {
|
|||||||
return true
|
return true
|
||||||
case IdentityProviderTypeZitadel:
|
case IdentityProviderTypeZitadel:
|
||||||
return true
|
return true
|
||||||
|
case IdentityProviderTypeAdfs:
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const defaultMaxBackoffInterval = 60 * time.Second
|
||||||
// TODO: make it configurable, the manager should validate all configurable parameters
|
|
||||||
reconnectingTimeout = 60 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Guard manage the reconnection tries to the Relay server in case of disconnection event.
|
// Guard manage the reconnection tries to the Relay server in case of disconnection event.
|
||||||
type Guard struct {
|
type Guard struct {
|
||||||
@@ -19,14 +16,23 @@ type Guard struct {
|
|||||||
OnNewRelayClient chan *Client
|
OnNewRelayClient chan *Client
|
||||||
OnReconnected chan struct{}
|
OnReconnected chan struct{}
|
||||||
serverPicker *ServerPicker
|
serverPicker *ServerPicker
|
||||||
|
|
||||||
|
// maxBackoffInterval caps the exponential backoff between reconnect
|
||||||
|
// attempts.
|
||||||
|
maxBackoffInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGuard creates a new guard for the relay client.
|
// NewGuard creates a new guard for the relay client. A non-positive
|
||||||
func NewGuard(sp *ServerPicker) *Guard {
|
// maxBackoffInterval falls back to defaultMaxBackoffInterval.
|
||||||
|
func NewGuard(sp *ServerPicker, maxBackoffInterval time.Duration) *Guard {
|
||||||
|
if maxBackoffInterval <= 0 {
|
||||||
|
maxBackoffInterval = defaultMaxBackoffInterval
|
||||||
|
}
|
||||||
g := &Guard{
|
g := &Guard{
|
||||||
OnNewRelayClient: make(chan *Client, 1),
|
OnNewRelayClient: make(chan *Client, 1),
|
||||||
OnReconnected: make(chan struct{}, 1),
|
OnReconnected: make(chan struct{}, 1),
|
||||||
serverPicker: sp,
|
serverPicker: sp,
|
||||||
|
maxBackoffInterval: maxBackoffInterval,
|
||||||
}
|
}
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
@@ -49,7 +55,7 @@ func (g *Guard) StartReconnectTrys(ctx context.Context, relayClient *Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start a ticker to pick a new server
|
// start a ticker to pick a new server
|
||||||
ticker := exponentTicker(ctx)
|
ticker := g.exponentTicker(ctx)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -125,11 +131,11 @@ func (g *Guard) notifyReconnected() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func exponentTicker(ctx context.Context) *backoff.Ticker {
|
func (g *Guard) exponentTicker(ctx context.Context) *backoff.Ticker {
|
||||||
bo := backoff.WithContext(&backoff.ExponentialBackOff{
|
bo := backoff.WithContext(&backoff.ExponentialBackOff{
|
||||||
InitialInterval: 2 * time.Second,
|
InitialInterval: 2 * time.Second,
|
||||||
Multiplier: 2,
|
Multiplier: 2,
|
||||||
MaxInterval: reconnectingTimeout,
|
MaxInterval: g.maxBackoffInterval,
|
||||||
Clock: backoff.SystemClock,
|
Clock: backoff.SystemClock,
|
||||||
}, ctx)
|
}, ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ func NewRelayTrack() *RelayTrack {
|
|||||||
|
|
||||||
type OnServerCloseListener func()
|
type OnServerCloseListener func()
|
||||||
|
|
||||||
|
// ManagerOption configures a Manager at construction time.
|
||||||
|
type ManagerOption func(*Manager)
|
||||||
|
|
||||||
|
// WithMaxBackoffInterval caps the exponential backoff between reconnect
|
||||||
|
// attempts to the home relay. A non-positive value keeps the default.
|
||||||
|
func WithMaxBackoffInterval(d time.Duration) ManagerOption {
|
||||||
|
return func(m *Manager) { m.maxBackoffInterval = d }
|
||||||
|
}
|
||||||
|
|
||||||
// Manager is a manager for the relay client instances. It establishes one persistent connection to the given relay URL
|
// Manager is a manager for the relay client instances. It establishes one persistent connection to the given relay URL
|
||||||
// and automatically reconnect to them in case disconnection.
|
// and automatically reconnect to them in case disconnection.
|
||||||
// The manager also manage temporary relay connection. If a client wants to communicate with a client on a
|
// The manager also manage temporary relay connection. If a client wants to communicate with a client on a
|
||||||
@@ -64,12 +73,13 @@ type Manager struct {
|
|||||||
onReconnectedListenerFn func()
|
onReconnectedListenerFn func()
|
||||||
listenerLock sync.Mutex
|
listenerLock sync.Mutex
|
||||||
|
|
||||||
mtu uint16
|
mtu uint16
|
||||||
|
maxBackoffInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new manager instance.
|
// NewManager creates a new manager instance.
|
||||||
// The serverURL address can be empty. In this case, the manager will not serve.
|
// The serverURL address can be empty. In this case, the manager will not serve.
|
||||||
func NewManager(ctx context.Context, serverURLs []string, peerID string, mtu uint16) *Manager {
|
func NewManager(ctx context.Context, serverURLs []string, peerID string, mtu uint16, opts ...ManagerOption) *Manager {
|
||||||
tokenStore := &relayAuth.TokenStore{}
|
tokenStore := &relayAuth.TokenStore{}
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
@@ -86,8 +96,11 @@ func NewManager(ctx context.Context, serverURLs []string, peerID string, mtu uin
|
|||||||
relayClients: make(map[string]*RelayTrack),
|
relayClients: make(map[string]*RelayTrack),
|
||||||
onDisconnectedListeners: make(map[string]*list.List),
|
onDisconnectedListeners: make(map[string]*list.List),
|
||||||
}
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(m)
|
||||||
|
}
|
||||||
m.serverPicker.ServerURLs.Store(serverURLs)
|
m.serverPicker.ServerURLs.Store(serverURLs)
|
||||||
m.reconnectGuard = NewGuard(m.serverPicker)
|
m.reconnectGuard = NewGuard(m.serverPicker, m.maxBackoffInterval)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,19 +303,36 @@ func (m *Manager) onServerConnected() {
|
|||||||
go m.onReconnectedListenerFn()
|
go m.onReconnectedListenerFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
// onServerDisconnected start to reconnection for home server only
|
// onServerDisconnected handles relay disconnect events. For the home server it
|
||||||
|
// starts the reconnect guard. For foreign servers it evicts the now-dead client
|
||||||
|
// from the cache so the next OpenConn builds a fresh one instead of reusing a
|
||||||
|
// closed client.
|
||||||
func (m *Manager) onServerDisconnected(serverAddress string) {
|
func (m *Manager) onServerDisconnected(serverAddress string) {
|
||||||
m.relayClientMu.Lock()
|
m.relayClientMu.Lock()
|
||||||
if serverAddress == m.relayClient.connectionURL {
|
isHome := m.relayClient != nil && serverAddress == m.relayClient.connectionURL
|
||||||
|
if isHome {
|
||||||
go func(client *Client) {
|
go func(client *Client) {
|
||||||
m.reconnectGuard.StartReconnectTrys(m.ctx, client)
|
m.reconnectGuard.StartReconnectTrys(m.ctx, client)
|
||||||
}(m.relayClient)
|
}(m.relayClient)
|
||||||
}
|
}
|
||||||
m.relayClientMu.Unlock()
|
m.relayClientMu.Unlock()
|
||||||
|
|
||||||
|
if !isHome {
|
||||||
|
m.evictForeignRelay(serverAddress)
|
||||||
|
}
|
||||||
|
|
||||||
m.notifyOnDisconnectListeners(serverAddress)
|
m.notifyOnDisconnectListeners(serverAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) evictForeignRelay(serverAddress string) {
|
||||||
|
m.relayClientsMutex.Lock()
|
||||||
|
defer m.relayClientsMutex.Unlock()
|
||||||
|
if _, ok := m.relayClients[serverAddress]; ok {
|
||||||
|
delete(m.relayClients, serverAddress)
|
||||||
|
log.Debugf("evicted disconnected foreign relay client: %s", serverAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) listenGuardEvent(ctx context.Context) {
|
func (m *Manager) listenGuardEvent(ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -360,7 +361,8 @@ func TestAutoReconnect(t *testing.T) {
|
|||||||
t.Fatalf("failed to serve manager: %s", err)
|
t.Fatalf("failed to serve manager: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientAlice := NewManager(mCtx, toURL(srvCfg), "alice", iface.DefaultMTU)
|
clientAlice := NewManager(mCtx, toURL(srvCfg), "alice", iface.DefaultMTU,
|
||||||
|
WithMaxBackoffInterval(2*time.Second))
|
||||||
err = clientAlice.Serve()
|
err = clientAlice.Serve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to serve manager: %s", err)
|
t.Fatalf("failed to serve manager: %s", err)
|
||||||
@@ -384,7 +386,9 @@ func TestAutoReconnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("waiting for reconnection")
|
log.Infof("waiting for reconnection")
|
||||||
time.Sleep(reconnectingTimeout + 1*time.Second)
|
if err := waitForReady(ctx, clientAlice, 15*time.Second); err != nil {
|
||||||
|
t.Fatalf("manager did not reconnect: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("reopent the connection")
|
log.Infof("reopent the connection")
|
||||||
_, err = clientAlice.OpenConn(ctx, ra, "bob")
|
_, err = clientAlice.OpenConn(ctx, ra, "bob")
|
||||||
@@ -393,6 +397,21 @@ func TestAutoReconnect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForReady(ctx context.Context, m *Manager, timeout time.Duration) error {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if m.Ready() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("manager not ready within %s", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotifierDoubleAdd(t *testing.T) {
|
func TestNotifierDoubleAdd(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user