mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 19:49:56 +00:00
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com>
149 lines
4.0 KiB
Go
149 lines
4.0 KiB
Go
//go:build !android && !ios && !freebsd && !js
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
|
|
"github.com/netbirdio/netbird/client/ui/services"
|
|
)
|
|
|
|
// exitNodeEntry is one Exit Node submenu row; ID is the network's NetID, the Select/Deselect argument.
|
|
type exitNodeEntry struct {
|
|
ID string
|
|
Selected bool
|
|
}
|
|
|
|
// fillExitNodeSubmenu uses a "✓ " prefix with plain Add, not AddCheckbox: Wails
|
|
// auto-toggles a checkbox on click before OnClick runs, so the deselect/select
|
|
// round-trip would briefly show two checked rows. Callers must hold exitNodesRebuildMu.
|
|
func (t *Tray) fillExitNodeSubmenu(nodes []exitNodeEntry) {
|
|
if t.exitNodeSubmenu == nil {
|
|
return
|
|
}
|
|
t.exitNodeSubmenu.Clear()
|
|
for _, n := range nodes {
|
|
id := n.ID
|
|
selected := n.Selected
|
|
label := id
|
|
if selected {
|
|
label = "✓ " + id
|
|
}
|
|
t.exitNodeSubmenu.Add(label).OnClick(func(*application.Context) {
|
|
t.toggleExitNode(id, selected)
|
|
})
|
|
}
|
|
}
|
|
|
|
// refreshExitNodes sources rows from Networks.List() rather than the Status stream
|
|
// because only ListNetworks carries the NetID + selected state Select/Deselect need.
|
|
// Serialized by exitNodesRebuildMu against overlapping Status pushes.
|
|
func (t *Tray) refreshExitNodes() {
|
|
t.exitNodesRebuildMu.Lock()
|
|
defer t.exitNodesRebuildMu.Unlock()
|
|
|
|
t.statusMu.Lock()
|
|
connected := t.connected
|
|
t.statusMu.Unlock()
|
|
|
|
var nodes []exitNodeEntry
|
|
if connected {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
list, err := t.svc.Networks.List(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
log.Debugf("tray list networks: %v", err)
|
|
return
|
|
}
|
|
nodes = exitNodesFromNetworks(list)
|
|
}
|
|
|
|
log.Infof("tray refreshExitNodes: %d exit node(s)", len(nodes))
|
|
for _, n := range nodes {
|
|
log.Infof("tray exit node: id=%q selected=%v", n.ID, n.Selected)
|
|
}
|
|
|
|
t.exitNodesMu.Lock()
|
|
changed := !equalExitNodes(nodes, t.exitNodes)
|
|
t.exitNodes = nodes
|
|
t.exitNodesMu.Unlock()
|
|
|
|
// relayoutMenu repaints from the cached entries, so the old exitNodeItem needs no poking here.
|
|
if changed {
|
|
t.relayoutMenu()
|
|
}
|
|
}
|
|
|
|
// toggleExitNode uses append=true: append=false would drop the whole current
|
|
// selection (default-on semantics), turning off every other routed network the
|
|
// user had enabled. Mutual exclusion of exit nodes is enforced daemon-side.
|
|
func (t *Tray) toggleExitNode(id string, selected bool) {
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
params := services.SelectNetworksParams{NetworkIDs: []string{id}, Append: true, All: false}
|
|
var err error
|
|
if selected {
|
|
err = t.svc.Networks.Deselect(ctx, params)
|
|
} else {
|
|
err = t.svc.Networks.Select(ctx, params)
|
|
}
|
|
if err != nil {
|
|
log.Errorf("tray toggle exit node %q: %v", id, err)
|
|
t.notifyError(t.loc.T("notify.error.exitNode", "name", id))
|
|
return
|
|
}
|
|
t.refreshExitNodes()
|
|
}()
|
|
}
|
|
|
|
// exitNodesFromNetworks keeps only networks whose range is a default route: those are the exit-node candidates.
|
|
func exitNodesFromNetworks(networks []services.Network) []exitNodeEntry {
|
|
out := []exitNodeEntry{}
|
|
for _, n := range networks {
|
|
if !rangeIsDefaultRoute(n.Range) {
|
|
continue
|
|
}
|
|
out = append(out, exitNodeEntry{ID: n.ID, Selected: n.Selected})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return strings.ToLower(out[i].ID) < strings.ToLower(out[j].ID)
|
|
})
|
|
return out
|
|
}
|
|
|
|
// rangeIsDefaultRoute reports whether r contains a default route. The daemon may
|
|
// comma-join a v4+v6 pair ("0.0.0.0/0, ::/0"), so each part is parsed rather than string-compared.
|
|
func rangeIsDefaultRoute(r string) bool {
|
|
for _, part := range strings.Split(r, ",") {
|
|
pref, err := netip.ParsePrefix(strings.TrimSpace(part))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if pref.Bits() == 0 && pref.Addr().IsUnspecified() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func equalExitNodes(a, b []exitNodeEntry) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|