Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Trennen"
},
"tray.menu.reconnect": {
"message": "Neu verbinden"
},
"tray.menu.exitNode": {
"message": "Exit Node"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Trennen fehlgeschlagen"
},
"notify.error.reconnect": {
"message": "Neu verbinden fehlgeschlagen"
},
"notify.error.switchProfile": {
"message": "Wechsel zu {profile} fehlgeschlagen"
},
Expand Down
8 changes: 8 additions & 0 deletions client/ui/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
"message": "Disconnect",
"description": "Tray menu item that disconnects from the network. Keep short."
},
"tray.menu.reconnect": {
"message": "Reconnect",
"description": "Tray menu item that disconnects and connects again in one step. Keep short."
},
"tray.menu.exitNode": {
"message": "Exit Node",
"description": "Tray submenu title for choosing an exit node (route all traffic through another peer)."
Expand Down Expand Up @@ -175,6 +179,10 @@
"message": "Failed to disconnect",
"description": "Error notification body shown when disconnecting failed."
},
"notify.error.reconnect": {
"message": "Failed to reconnect",
"description": "Error notification body shown when reconnecting failed."
},
"notify.error.switchProfile": {
"message": "Failed to switch to {profile}",
"description": "Error notification shown when switching profiles failed. {profile} is the target profile name; keep it."
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Desconectar"
},
"tray.menu.reconnect": {
"message": "Reconectar"
},
"tray.menu.exitNode": {
"message": "Nodo de salida"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Error al desconectar"
},
"notify.error.reconnect": {
"message": "Error al reconectar"
},
"notify.error.switchProfile": {
"message": "Error al cambiar a {profile}"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Se déconnecter"
},
"tray.menu.reconnect": {
"message": "Se reconnecter"
},
"tray.menu.exitNode": {
"message": "Nœud de sortie"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Échec de la déconnexion"
},
"notify.error.reconnect": {
"message": "Échec de la reconnexion"
},
"notify.error.switchProfile": {
"message": "Échec du passage à {profile}"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/hu/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Bontás"
},
"tray.menu.reconnect": {
"message": "Újracsatlakozás"
},
"tray.menu.exitNode": {
"message": "Exit Node"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Bontás sikertelen"
},
"notify.error.reconnect": {
"message": "Újracsatlakozás sikertelen"
},
"notify.error.switchProfile": {
"message": "Átváltás sikertelen erre: {profile}"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Disconnetti"
},
"tray.menu.reconnect": {
"message": "Riconnetti"
},
"tray.menu.exitNode": {
"message": "Nodo di uscita"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Disconnessione non riuscita"
},
"notify.error.reconnect": {
"message": "Riconnessione non riuscita"
},
"notify.error.switchProfile": {
"message": "Impossibile passare a {profile}"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "切断"
},
"tray.menu.reconnect": {
"message": "再接続"
},
"tray.menu.exitNode": {
"message": "出口ノード"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "切断に失敗しました"
},
"notify.error.reconnect": {
"message": "再接続に失敗しました"
},
"notify.error.switchProfile": {
"message": "{profile} への切り替えに失敗しました"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/pt/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Desconectar"
},
"tray.menu.reconnect": {
"message": "Reconectar"
},
"tray.menu.exitNode": {
"message": "Nó de saída"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Falha ao desconectar"
},
"notify.error.reconnect": {
"message": "Falha ao reconectar"
},
"notify.error.switchProfile": {
"message": "Falha ao alternar para {profile}"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/ru/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "Отключиться"
},
"tray.menu.reconnect": {
"message": "Переподключиться"
},
"tray.menu.exitNode": {
"message": "Выходной узел"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "Не удалось отключиться"
},
"notify.error.reconnect": {
"message": "Не удалось переподключиться"
},
"notify.error.switchProfile": {
"message": "Не удалось переключиться на {profile}"
},
Expand Down
6 changes: 6 additions & 0 deletions client/ui/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"tray.menu.disconnect": {
"message": "断开连接"
},
"tray.menu.reconnect": {
"message": "重新连接"
},
"tray.menu.exitNode": {
"message": "出口节点"
},
Expand Down Expand Up @@ -131,6 +134,9 @@
"notify.error.disconnect": {
"message": "断开连接失败"
},
"notify.error.reconnect": {
"message": "重新连接失败"
},
"notify.error.switchProfile": {
"message": "切换到 {profile} 失败"
},
Expand Down
19 changes: 19 additions & 0 deletions client/ui/services/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,25 @@ func (s *Connection) WaitSSOLoginAndUp(ctx context.Context, wait WaitSSOParams,
return email, nil
}

// Reconnect tears the session down and brings it straight back up, for the
// stale state a sleep/wake or a network roam leaves behind. Down is
// synchronous, so Up goes out only once the daemon reports the teardown
// finished, and a failed Down aborts rather than stacking an Up on a session
// that is still up. A ctx cancelled between the two legs aborts as well: a
// Disconnect or a quit racing the round trip must not be undone by a late Up.
func (s *Connection) Reconnect(ctx context.Context) error {
if err := s.Down(ctx); err != nil {
return fmt.Errorf("down: %w", err)
}
if err := ctx.Err(); err != nil {
return err
}
if err := s.Up(ctx, UpParams{}); err != nil {
return fmt.Errorf("up: %w", err)
}
return nil
}

func (s *Connection) Down(ctx context.Context) error {
cli, err := s.conn.Client()
if err != nil {
Expand Down
79 changes: 79 additions & 0 deletions client/ui/services/connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//go:build !android && !ios && !freebsd && !js

package services

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/require"
"google.golang.org/grpc"

"github.com/netbirdio/netbird/client/proto"
)

// fakeDaemonClient records the Down/Up calls Reconnect makes. Embedding the
// generated interface keeps the fake to the two RPCs under test.
type fakeDaemonClient struct {
proto.DaemonServiceClient

downErr error
upErr error
calls []string
// onDown runs after Down is recorded, to simulate a Disconnect or a quit
// landing while the teardown is still in flight.
onDown func()
}

func (c *fakeDaemonClient) Down(context.Context, *proto.DownRequest, ...grpc.CallOption) (*proto.DownResponse, error) {
c.calls = append(c.calls, "down")
if c.onDown != nil {
c.onDown()
}
if c.downErr != nil {
return nil, c.downErr
}
return &proto.DownResponse{}, nil
}

func (c *fakeDaemonClient) Up(context.Context, *proto.UpRequest, ...grpc.CallOption) (*proto.UpResponse, error) {
c.calls = append(c.calls, "up")
if c.upErr != nil {
return nil, c.upErr
}
return &proto.UpResponse{}, nil
}

type fakeDaemonConn struct{ client proto.DaemonServiceClient }

func (c fakeDaemonConn) Client() (proto.DaemonServiceClient, error) { return c.client, nil }

func TestConnectionReconnectSendsDownThenUp(t *testing.T) {
client := &fakeDaemonClient{}
conn := NewConnection(fakeDaemonConn{client: client}, nil, nil)

require.NoError(t, conn.Reconnect(context.Background()))
require.Equal(t, []string{"down", "up"}, client.calls)
}

func TestConnectionReconnectFailedDownSkipsUp(t *testing.T) {
client := &fakeDaemonClient{downErr: errors.New("teardown refused")}
conn := NewConnection(fakeDaemonConn{client: client}, nil, nil)

require.Error(t, conn.Reconnect(context.Background()))
require.Equal(t, []string{"down"}, client.calls,
"Up must not stack on a session that is still up")
}

func TestConnectionReconnectCancelDuringDownSkipsUp(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// A tray Disconnect or a quit cancels the round trip mid-teardown; the Up
// must not bring the session back afterwards.
client := &fakeDaemonClient{onDown: cancel}
conn := NewConnection(fakeDaemonConn{client: client}, nil, nil)

require.ErrorIs(t, conn.Reconnect(ctx), context.Canceled)
require.Equal(t, []string{"down"}, client.calls)
}
Loading