mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
Files move directly between peers over the overlay, with no server in the path. The receiver listens on the WireGuard address only, so the port is unreachable from outside the tunnel, and every offer is matched to a known peer before anything is read. Consent is the default: an offer carries metadata alone, and no payload moves until the receiver accepts. Policy is per profile and device-local — off, ask, or auto-accept, with per-sender exceptions on top. Policy and history live in the profile's preferences, so removing a profile takes its file drop state with it. Transfers interrupted by a restart are settled on load; nothing survives to finish them, and left alone they would sit in the log as permanently pending. The Android bindings pull payload bytes through a chunk-returning stream: gomobile copies a []byte argument into a fresh Java array and never copies it back, so a fill-my-buffer method would hand back the right length with no data.
143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
//go:build !android && !ios && !freebsd && !js
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/client/ui/authsession"
|
|
"github.com/netbirdio/netbird/client/ui/services"
|
|
)
|
|
|
|
// onSystemEvent fires an OS notification for daemon SystemEvents that carry a
|
|
// user-facing message. Gated by the "Notifications" toggle; critical events bypass it.
|
|
func (t *Tray) onSystemEvent(ev *application.CustomEvent) {
|
|
se, ok := ev.Data.(services.SystemEvent)
|
|
if !ok {
|
|
return
|
|
}
|
|
// config_changed carries no UserMessage, so handle it before the message gate below.
|
|
if se.Category == "system" && se.Metadata[proto.MetadataTypeKey] == proto.MetadataTypeConfigChanged {
|
|
log.Infof("config_changed event received (source=%s); refreshing tray restrictions", se.Metadata[proto.MetadataSourceKey])
|
|
go t.refreshRestrictions()
|
|
go t.loadConfig()
|
|
// MDM gets a localised toast here; the daemon's English "policy_applied"
|
|
// event is suppressed in shouldSkipSystemEvent. Other sources stay silent.
|
|
if se.Metadata[proto.MetadataSourceKey] == proto.MetadataSourceMDM {
|
|
t.profileMu.Lock()
|
|
enabled := t.notificationsEnabled
|
|
t.profileMu.Unlock()
|
|
if enabled {
|
|
t.notify(
|
|
t.loc.T("notify.mdm.policyApplied.title"),
|
|
t.loc.T("notify.mdm.policyApplied.body"),
|
|
notifyIDMDMPolicy,
|
|
)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
// Session-warning and deadline-rejected events build their body locally from
|
|
// metadata; every other event needs a UserMessage.
|
|
isSessionWarning := se.Metadata[authsession.MetaWarning] == "true"
|
|
isDeadlineRejected := se.Metadata[authsession.MetaDeadlineRejected] != ""
|
|
if !isSessionWarning && !isDeadlineRejected && se.UserMessage == "" {
|
|
return
|
|
}
|
|
if shouldSkipSystemEvent(se) {
|
|
return
|
|
}
|
|
|
|
critical := strings.EqualFold(se.Severity, services.SeverityCritical)
|
|
t.profileMu.Lock()
|
|
enabled := t.notificationsEnabled
|
|
t.profileMu.Unlock()
|
|
if !enabled && !critical {
|
|
return
|
|
}
|
|
|
|
// Session-warning events route via stable metadata flags rather than
|
|
// category/severity so a daemon-side reword still lands here. Final warning
|
|
// auto-opens the SessionExpiration dialog with no notification (the dialog is
|
|
// the last-chance reminder; doubling up would be noise).
|
|
if se.Metadata[proto.MetadataKindKey] == proto.MetadataKindFileDropOffer {
|
|
t.notifyFileDropOffer(se)
|
|
return
|
|
}
|
|
|
|
if isDeadlineRejected {
|
|
t.notify(
|
|
t.loc.T("notify.sessionDeadlineRejected.title"),
|
|
t.loc.T("notify.sessionDeadlineRejected.body"),
|
|
notifyIDSessionExpired,
|
|
)
|
|
return
|
|
}
|
|
|
|
if se.Metadata != nil && se.Metadata[authsession.MetaWarning] == "true" {
|
|
if se.Metadata[authsession.MetaFinal] == "true" {
|
|
t.openSessionExpiration()
|
|
return
|
|
}
|
|
t.notifySessionWarning(
|
|
t.loc.T("notify.sessionWarning.title"),
|
|
t.buildSessionWarningBody(se.Metadata),
|
|
)
|
|
return
|
|
}
|
|
|
|
body := se.UserMessage
|
|
if id := se.Metadata["id"]; id != "" {
|
|
body += fmt.Sprintf(" ID: %s", id)
|
|
}
|
|
t.notify(eventTitle(se), body, notifyIDEvent+se.ID)
|
|
}
|
|
|
|
// eventTitle composes a notification title, e.g. "Critical: DNS", "Warning: Authentication".
|
|
func eventTitle(e services.SystemEvent) string {
|
|
prefix := titleCase(e.Severity)
|
|
if prefix == "" {
|
|
prefix = "Info"
|
|
}
|
|
category := titleCase(e.Category)
|
|
if category == "" {
|
|
category = "System"
|
|
}
|
|
return prefix + ": " + category
|
|
}
|
|
|
|
func titleCase(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
|
}
|
|
|
|
// shouldSkipSystemEvent reports whether a daemon SystemEvent must not surface as
|
|
// a tray notification:
|
|
// - update-available announcements (trayUpdater emits its own)
|
|
// - install-progress signals (consumed by the install-progress window)
|
|
// - the ::/0 partner of an exit-node default route (0.0.0.0/0 already toasted)
|
|
func shouldSkipSystemEvent(se services.SystemEvent) bool {
|
|
// "policy_applied" carries a hardcoded English message; the localised toast
|
|
// fires on the paired config_changed (source=mdm) event instead.
|
|
if se.Metadata[proto.MetadataTypeKey] == proto.MetadataTypePolicyApplied {
|
|
return true
|
|
}
|
|
if _, isUpdate := se.Metadata["new_version_available"]; isUpdate {
|
|
return true
|
|
}
|
|
if _, isProgress := se.Metadata["progress_window"]; isProgress {
|
|
return true
|
|
}
|
|
if se.Category == "network" && se.Metadata["network"] == "::/0" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|