Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Liu
afbddae472 Add transparent proxy inspection engine with envoy sidecar support 2026-04-11 18:39:18 +02:00
336 changed files with 17445 additions and 29557 deletions

View File

@@ -1,62 +0,0 @@
name: Proto Version Check
on:
pull_request:
paths:
- "**/*.pb.go"
jobs:
check-proto-versions:
runs-on: ubuntu-latest
steps:
- name: Check for proto tool version changes
uses: actions/github-script@v7
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100,
});
const pbFiles = files.filter(f => f.filename.endsWith('.pb.go'));
const missingPatch = pbFiles.filter(f => !f.patch).map(f => f.filename);
if (missingPatch.length > 0) {
core.setFailed(
`Cannot inspect patch data for:\n` +
missingPatch.map(f => `- ${f}`).join('\n') +
`\nThis can happen with very large PRs. Verify proto versions manually.`
);
return;
}
const versionPattern = /^[+-]\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
const violations = [];
for (const file of pbFiles) {
const changed = file.patch
.split('\n')
.filter(line => versionPattern.test(line));
if (changed.length > 0) {
violations.push({
file: file.filename,
lines: changed,
});
}
}
if (violations.length > 0) {
const details = violations.map(v =>
`${v.file}:\n${v.lines.map(l => ' ' + l).join('\n')}`
).join('\n\n');
core.setFailed(
`Proto version strings changed in generated files.\n` +
`This usually means the wrong protoc or protoc-gen-go version was used.\n` +
`Regenerate with the matching tool versions.\n\n` +
details
);
return;
}
console.log('No proto version string changes detected');

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
SIGN_PIPE_VER: "v0.1.4"
SIGN_PIPE_VER: "v0.1.1"
GORELEASER_VER: "v2.14.3"
PRODUCT_NAME: "NetBird"
COPYRIGHT: "NetBird GmbH"

2
.gitignore vendored
View File

@@ -33,3 +33,5 @@ infrastructure_files/setup-*.env
vendor/
/netbird
client/netbird-electron/
management/server/types/testdata/comparison/
management/server/types/testdata/*.json

View File

@@ -5,7 +5,7 @@ GOLANGCI_LINT := $(shell pwd)/bin/golangci-lint
$(GOLANGCI_LINT):
@echo "Installing golangci-lint..."
@mkdir -p ./bin
@GOBIN=$(shell pwd)/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
@GOBIN=$(shell pwd)/bin go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Lint only changed files (fast, for pre-push)
lint: $(GOLANGCI_LINT)

View File

@@ -8,7 +8,6 @@ import (
"os"
"slices"
"sync"
"time"
"golang.org/x/exp/maps"
@@ -16,7 +15,6 @@ import (
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/debug"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
@@ -28,7 +26,6 @@ import (
"github.com/netbirdio/netbird/formatter"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
types "github.com/netbirdio/netbird/upload-server/types"
)
// ConnectionListener export internal Listener for mobile
@@ -71,30 +68,7 @@ type Client struct {
uiVersion string
networkChangeListener listener.NetworkChangeListener
stateMu sync.RWMutex
connectClient *internal.ConnectClient
config *profilemanager.Config
cacheDir string
}
func (c *Client) setState(cfg *profilemanager.Config, cacheDir string, cc *internal.ConnectClient) {
c.stateMu.Lock()
defer c.stateMu.Unlock()
c.config = cfg
c.cacheDir = cacheDir
c.connectClient = cc
}
func (c *Client) stateSnapshot() (*profilemanager.Config, string, *internal.ConnectClient) {
c.stateMu.RLock()
defer c.stateMu.RUnlock()
return c.config, c.cacheDir, c.connectClient
}
func (c *Client) getConnectClient() *internal.ConnectClient {
c.stateMu.RLock()
defer c.stateMu.RUnlock()
return c.connectClient
}
// NewClient instantiate a new Client
@@ -119,7 +93,6 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
cfgFile := platformFiles.ConfigurationFilePath()
stateFile := platformFiles.StateFilePath()
cacheDir := platformFiles.CacheDir()
log.Infof("Starting client with config: %s, state: %s", cfgFile, stateFile)
@@ -151,9 +124,8 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
c.setState(cfg, cacheDir, connectClient)
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile)
}
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
@@ -163,7 +135,6 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
cfgFile := platformFiles.ConfigurationFilePath()
stateFile := platformFiles.StateFilePath()
cacheDir := platformFiles.CacheDir()
log.Infof("Starting client without login with config: %s, state: %s", cfgFile, stateFile)
@@ -186,9 +157,8 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
c.setState(cfg, cacheDir, connectClient)
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile)
}
// Stop the internal client and free the resources
@@ -203,12 +173,11 @@ func (c *Client) Stop() {
}
func (c *Client) RenewTun(fd int) error {
cc := c.getConnectClient()
if cc == nil {
if c.connectClient == nil {
return fmt.Errorf("engine not running")
}
e := cc.Engine()
e := c.connectClient.Engine()
if e == nil {
return fmt.Errorf("engine not initialized")
}
@@ -216,73 +185,6 @@ func (c *Client) RenewTun(fd int) error {
return e.RenewTun(fd)
}
// DebugBundle generates a debug bundle, uploads it, and returns the upload key.
// It works both with and without a running engine.
func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool) (string, error) {
cfg, cacheDir, cc := c.stateSnapshot()
// If the engine hasn't been started, load config from disk
if cfg == nil {
var err error
cfg, err = profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
ConfigPath: platformFiles.ConfigurationFilePath(),
})
if err != nil {
return "", fmt.Errorf("load config: %w", err)
}
cacheDir = platformFiles.CacheDir()
}
deps := debug.GeneratorDependencies{
InternalConfig: cfg,
StatusRecorder: c.recorder,
TempDir: cacheDir,
}
if cc != nil {
resp, err := cc.GetLatestSyncResponse()
if err != nil {
log.Warnf("get latest sync response: %v", err)
}
deps.SyncResponse = resp
if e := cc.Engine(); e != nil {
if cm := e.GetClientMetrics(); cm != nil {
deps.ClientMetrics = cm
}
}
}
bundleGenerator := debug.NewBundleGenerator(
deps,
debug.BundleConfig{
Anonymize: anonymize,
IncludeSystemInfo: true,
},
)
path, err := bundleGenerator.Generate()
if err != nil {
return "", fmt.Errorf("generate debug bundle: %w", err)
}
defer func() {
if err := os.Remove(path); err != nil {
log.Errorf("failed to remove debug bundle file: %v", err)
}
}()
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
key, err := debug.UploadDebugBundle(uploadCtx, types.DefaultBundleURL, cfg.ManagementURL.String(), path)
if err != nil {
return "", fmt.Errorf("upload debug bundle: %w", err)
}
log.Infof("debug bundle uploaded with key %s", key)
return key, nil
}
// SetTraceLogLevel configure the logger to trace level
func (c *Client) SetTraceLogLevel() {
log.SetLevel(log.TraceLevel)
@@ -312,13 +214,12 @@ func (c *Client) PeersList() *PeerInfoArray {
}
func (c *Client) Networks() *NetworkArray {
cc := c.getConnectClient()
if cc == nil {
if c.connectClient == nil {
log.Error("not connected")
return nil
}
engine := cc.Engine()
engine := c.connectClient.Engine()
if engine == nil {
log.Error("could not get engine")
return nil
@@ -399,7 +300,7 @@ func (c *Client) toggleRoute(command routeCommand) error {
}
func (c *Client) getRouteManager() (routemanager.Manager, error) {
client := c.getConnectClient()
client := c.connectClient
if client == nil {
return nil, fmt.Errorf("not connected")
}

View File

@@ -7,5 +7,4 @@ package android
type PlatformFiles interface {
ConfigurationFilePath() string
StateFilePath() string
CacheDir() string
}

View File

@@ -75,7 +75,6 @@ var (
mtu uint16
profilesDisabled bool
updateSettingsDisabled bool
networksDisabled bool
rootCmd = &cobra.Command{
Use: "netbird",

View File

@@ -44,13 +44,10 @@ func init() {
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd)
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile. To persist this setting, use: netbird service install --disable-profiles")
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
serviceEnvDesc := `Sets extra environment variables for the service. ` +
`You can specify a comma-separated list of KEY=VALUE pairs. ` +
`New keys are merged with previously saved env vars; existing keys are overwritten. ` +
`Use --service-env "" to clear all saved env vars. ` +
`E.g. --service-env NB_LOG_LEVEL=debug,CUSTOM_VAR=value`
installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)

View File

@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
}
}
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, networksDisabled)
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled)
if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err)
}

View File

@@ -59,10 +59,6 @@ func buildServiceArguments() []string {
args = append(args, "--disable-update-settings")
}
if networksDisabled {
args = append(args, "--disable-networks")
}
return args
}

View File

@@ -28,7 +28,6 @@ type serviceParams struct {
LogFiles []string `json:"log_files,omitempty"`
DisableProfiles bool `json:"disable_profiles,omitempty"`
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
DisableNetworks bool `json:"disable_networks,omitempty"`
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
}
@@ -79,12 +78,11 @@ func currentServiceParams() *serviceParams {
LogFiles: logFiles,
DisableProfiles: profilesDisabled,
DisableUpdateSettings: updateSettingsDisabled,
DisableNetworks: networksDisabled,
}
if len(serviceEnvVars) > 0 {
parsed, err := parseServiceEnvVars(serviceEnvVars)
if err == nil {
if err == nil && len(parsed) > 0 {
params.ServiceEnvVars = parsed
}
}
@@ -144,46 +142,31 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
updateSettingsDisabled = params.DisableUpdateSettings
}
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
networksDisabled = params.DisableNetworks
}
applyServiceEnvParams(cmd, params)
}
// applyServiceEnvParams merges saved service environment variables.
// If --service-env was explicitly set with values, explicit values win on key
// conflict but saved keys not in the explicit set are carried over.
// If --service-env was explicitly set to empty, all saved env vars are cleared.
// If --service-env was explicitly set, explicit values win on key conflict
// but saved keys not in the explicit set are carried over.
// If --service-env was not set, saved env vars are used entirely.
func applyServiceEnvParams(cmd *cobra.Command, params *serviceParams) {
if !cmd.Flags().Changed("service-env") {
if len(params.ServiceEnvVars) > 0 {
// No explicit env vars: rebuild serviceEnvVars from saved params.
serviceEnvVars = envMapToSlice(params.ServiceEnvVars)
}
if len(params.ServiceEnvVars) == 0 {
return
}
// Flag was explicitly set: parse what the user provided.
if !cmd.Flags().Changed("service-env") {
// No explicit env vars: rebuild serviceEnvVars from saved params.
serviceEnvVars = envMapToSlice(params.ServiceEnvVars)
return
}
// Explicit env vars were provided: merge saved values underneath.
explicit, err := parseServiceEnvVars(serviceEnvVars)
if err != nil {
cmd.PrintErrf("Warning: parse explicit service env vars for merge: %v\n", err)
return
}
// If the user passed an empty value (e.g. --service-env ""), clear all
// saved env vars rather than merging.
if len(explicit) == 0 {
serviceEnvVars = nil
return
}
if len(params.ServiceEnvVars) == 0 {
return
}
// Merge saved values underneath explicit ones.
merged := make(map[string]string, len(params.ServiceEnvVars)+len(explicit))
maps.Copy(merged, params.ServiceEnvVars)
maps.Copy(merged, explicit) // explicit wins on conflict

View File

@@ -327,41 +327,6 @@ func TestApplyServiceEnvParams_NotChanged(t *testing.T) {
assert.Equal(t, map[string]string{"FROM_SAVED": "val"}, result)
}
func TestApplyServiceEnvParams_ExplicitEmptyClears(t *testing.T) {
origServiceEnvVars := serviceEnvVars
t.Cleanup(func() { serviceEnvVars = origServiceEnvVars })
// Simulate --service-env "" which produces [""] in the slice.
serviceEnvVars = []string{""}
cmd := &cobra.Command{}
cmd.Flags().StringSlice("service-env", nil, "")
require.NoError(t, cmd.Flags().Set("service-env", ""))
saved := &serviceParams{
ServiceEnvVars: map[string]string{"OLD_VAR": "should_be_cleared"},
}
applyServiceEnvParams(cmd, saved)
assert.Nil(t, serviceEnvVars, "explicit empty --service-env should clear all saved env vars")
}
func TestCurrentServiceParams_EmptyEnvVarsAfterParse(t *testing.T) {
origServiceEnvVars := serviceEnvVars
t.Cleanup(func() { serviceEnvVars = origServiceEnvVars })
// Simulate --service-env "" which produces [""] in the slice.
serviceEnvVars = []string{""}
params := currentServiceParams()
// After parsing, the empty string is skipped, resulting in an empty map.
// The map should still be set (not nil) so it overwrites saved values.
assert.NotNil(t, params.ServiceEnvVars, "empty env vars should produce empty map, not nil")
assert.Empty(t, params.ServiceEnvVars, "no valid env vars should be parsed from empty string")
}
// TestServiceParams_FieldsCoveredInFunctions ensures that all serviceParams fields are
// referenced in both currentServiceParams() and applyServiceParams(). If a new field is
// added to serviceParams but not wired into these functions, this test fails.
@@ -535,7 +500,6 @@ func fieldToGlobalVar(field string) string {
"LogFiles": "logFiles",
"DisableProfiles": "profilesDisabled",
"DisableUpdateSettings": "updateSettingsDisabled",
"DisableNetworks": "networksDisabled",
"ServiceEnvVars": "serviceEnvVars",
}
if v, ok := m[field]; ok {

View File

@@ -13,8 +13,6 @@ import (
"github.com/netbirdio/management-integrations/integrations"
nbcache "github.com/netbirdio/netbird/management/server/cache"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
"github.com/netbirdio/netbird/management/internals/modules/peers"
@@ -102,16 +100,9 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
jobManager := job.NewJobManager(nil, store, peersmanager)
ctx := context.Background()
iv, _ := integrations.NewIntegratedValidator(context.Background(), peersmanager, settingsManagerMock, eventStore)
cacheStore, err := nbcache.NewStore(ctx, 100*time.Millisecond, 300*time.Millisecond, 100)
if err != nil {
t.Fatal(err)
}
iv, _ := integrations.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
require.NoError(t, err)
settingsMockManager := settings.NewMockManager(ctrl)
@@ -122,11 +113,12 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
Return(&types.Settings{}, nil).
AnyTimes()
ctx := context.Background()
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersmanager), config)
accountManager, err := mgmt.BuildManager(ctx, config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
if err != nil {
t.Fatal(err)
}
@@ -160,7 +152,7 @@ func startClientDaemon(
s := grpc.NewServer()
server := client.New(ctx,
"", "", false, false, false)
"", "", false, false)
if err := server.Start(); err != nil {
t.Fatal(err)
}

View File

@@ -56,13 +56,6 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogg
return createUserspaceFirewall(iface, nil, disableServerRoutes, flowLogger, mtu)
}
// Native firewall handles packet filtering, but the userspace WireGuard bind
// needs a device filter for DNS interception hooks. Install a minimal
// hooks-only filter that passes all traffic through to the kernel firewall.
if err := iface.SetFilter(&uspfilter.HooksFilter{}); err != nil {
log.Warnf("failed to set hooks filter, DNS via memory hooks will not work: %v", err)
}
return fm, nil
}

View File

@@ -1,11 +0,0 @@
// Package firewalld integrates with the firewalld daemon so NetBird can place
// 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.
package firewalld
// TrustedZone is the firewalld zone name used for interfaces whose traffic
// should bypass firewalld filtering.
const TrustedZone = "trusted"

View File

@@ -1,260 +0,0 @@
//go:build linux
package firewalld
import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"sync"
"time"
"github.com/godbus/dbus/v5"
log "github.com/sirupsen/logrus"
)
const (
dbusDest = "org.fedoraproject.FirewallD1"
dbusPath = "/org/fedoraproject/FirewallD1"
dbusRootIface = "org.fedoraproject.FirewallD1"
dbusZoneIface = "org.fedoraproject.FirewallD1.zone"
errZoneAlreadySet = "ZONE_ALREADY_SET"
errAlreadyEnabled = "ALREADY_ENABLED"
errUnknownIface = "UNKNOWN_INTERFACE"
errNotEnabled = "NOT_ENABLED"
// callTimeout bounds each individual DBus or firewall-cmd invocation.
// A fresh context is created for each call so a slow DBus probe can't
// exhaust the deadline before the firewall-cmd fallback gets to run.
callTimeout = 3 * time.Second
)
var (
errDBusUnavailable = errors.New("firewalld dbus unavailable")
// trustLogOnce ensures the "added to trusted zone" message is logged at
// Info level only for the first successful add per process; repeat adds
// from other init paths are quieter.
trustLogOnce sync.Once
parentCtxMu sync.RWMutex
parentCtx context.Context = context.Background()
)
// SetParentContext installs a parent context whose cancellation aborts any
// in-flight TrustInterface call. It does not affect UntrustInterface, which
// always uses a fresh Background-rooted timeout so cleanup can still run
// during engine shutdown when the engine context is already cancelled.
func SetParentContext(ctx context.Context) {
parentCtxMu.Lock()
parentCtx = ctx
parentCtxMu.Unlock()
}
func getParentContext() context.Context {
parentCtxMu.RLock()
defer parentCtxMu.RUnlock()
return parentCtx
}
// TrustInterface places iface into firewalld's trusted zone if firewalld is
// running. It is idempotent and best-effort: errors are returned so callers
// can log, but a non-running firewalld is not an error. Only the first
// successful call per process logs at Info. Respects the parent context set
// via SetParentContext so startup-time cancellation unblocks it.
func TrustInterface(iface string) error {
parent := getParentContext()
if !isRunning(parent) {
return nil
}
if err := addTrusted(parent, iface); err != nil {
return fmt.Errorf("add %s to firewalld trusted zone: %w", iface, err)
}
trustLogOnce.Do(func() {
log.Infof("added %s to firewalld trusted zone", iface)
})
log.Debugf("firewalld: ensured %s is in trusted zone", iface)
return nil
}
// UntrustInterface removes iface from firewalld's trusted zone if firewalld
// is running. Idempotent. Uses a Background-rooted timeout so it still runs
// during shutdown after the engine context has been cancelled.
func UntrustInterface(iface string) error {
if !isRunning(context.Background()) {
return nil
}
if err := removeTrusted(context.Background(), iface); err != nil {
return fmt.Errorf("remove %s from firewalld trusted zone: %w", iface, err)
}
return nil
}
func newCallContext(parent context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(parent, callTimeout)
}
func isRunning(parent context.Context) bool {
ctx, cancel := newCallContext(parent)
ok, err := isRunningDBus(ctx)
cancel()
if err == nil {
return ok
}
if errors.Is(err, errDBusUnavailable) || errors.Is(err, context.DeadlineExceeded) {
ctx, cancel = newCallContext(parent)
defer cancel()
return isRunningCLI(ctx)
}
return false
}
func addTrusted(parent context.Context, iface string) error {
ctx, cancel := newCallContext(parent)
err := addDBus(ctx, iface)
cancel()
if err == nil {
return nil
}
if !errors.Is(err, errDBusUnavailable) {
log.Debugf("firewalld: dbus add failed, falling back to firewall-cmd: %v", err)
}
ctx, cancel = newCallContext(parent)
defer cancel()
return addCLI(ctx, iface)
}
func removeTrusted(parent context.Context, iface string) error {
ctx, cancel := newCallContext(parent)
err := removeDBus(ctx, iface)
cancel()
if err == nil {
return nil
}
if !errors.Is(err, errDBusUnavailable) {
log.Debugf("firewalld: dbus remove failed, falling back to firewall-cmd: %v", err)
}
ctx, cancel = newCallContext(parent)
defer cancel()
return removeCLI(ctx, iface)
}
func isRunningDBus(ctx context.Context) (bool, error) {
conn, err := dbus.SystemBus()
if err != nil {
return false, fmt.Errorf("%w: %v", errDBusUnavailable, err)
}
obj := conn.Object(dbusDest, dbusPath)
var zone string
if err := obj.CallWithContext(ctx, dbusRootIface+".getDefaultZone", 0).Store(&zone); err != nil {
return false, fmt.Errorf("firewalld getDefaultZone: %w", err)
}
return true, nil
}
func isRunningCLI(ctx context.Context) bool {
if _, err := exec.LookPath("firewall-cmd"); err != nil {
return false
}
return exec.CommandContext(ctx, "firewall-cmd", "--state").Run() == nil
}
func addDBus(ctx context.Context, iface string) error {
conn, err := dbus.SystemBus()
if err != nil {
return fmt.Errorf("%w: %v", errDBusUnavailable, err)
}
obj := conn.Object(dbusDest, dbusPath)
call := obj.CallWithContext(ctx, dbusZoneIface+".addInterface", 0, TrustedZone, iface)
if call.Err == nil {
return nil
}
if dbusErrContains(call.Err, errAlreadyEnabled) {
return nil
}
if dbusErrContains(call.Err, errZoneAlreadySet) {
move := obj.CallWithContext(ctx, dbusZoneIface+".changeZoneOfInterface", 0, TrustedZone, iface)
if move.Err != nil {
return fmt.Errorf("firewalld changeZoneOfInterface: %w", move.Err)
}
return nil
}
return fmt.Errorf("firewalld addInterface: %w", call.Err)
}
func removeDBus(ctx context.Context, iface string) error {
conn, err := dbus.SystemBus()
if err != nil {
return fmt.Errorf("%w: %v", errDBusUnavailable, err)
}
obj := conn.Object(dbusDest, dbusPath)
call := obj.CallWithContext(ctx, dbusZoneIface+".removeInterface", 0, TrustedZone, iface)
if call.Err == nil {
return nil
}
if dbusErrContains(call.Err, errUnknownIface) || dbusErrContains(call.Err, errNotEnabled) {
return nil
}
return fmt.Errorf("firewalld removeInterface: %w", call.Err)
}
func addCLI(ctx context.Context, iface string) error {
if _, err := exec.LookPath("firewall-cmd"); err != nil {
return fmt.Errorf("firewall-cmd not available: %w", err)
}
// --change-interface (no --permanent) binds the interface for the
// current runtime only; we do not want membership to persist across
// reboots because netbird re-asserts it on every startup.
out, err := exec.CommandContext(ctx,
"firewall-cmd", "--zone="+TrustedZone, "--change-interface="+iface,
).CombinedOutput()
if err != nil {
return fmt.Errorf("firewall-cmd change-interface: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
func removeCLI(ctx context.Context, iface string) error {
if _, err := exec.LookPath("firewall-cmd"); err != nil {
return fmt.Errorf("firewall-cmd not available: %w", err)
}
out, err := exec.CommandContext(ctx,
"firewall-cmd", "--zone="+TrustedZone, "--remove-interface="+iface,
).CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
if strings.Contains(msg, errUnknownIface) || strings.Contains(msg, errNotEnabled) {
return nil
}
return fmt.Errorf("firewall-cmd remove-interface: %w: %s", err, msg)
}
return nil
}
func dbusErrContains(err error, code string) bool {
if err == nil {
return false
}
var de dbus.Error
if errors.As(err, &de) {
for _, b := range de.Body {
if s, ok := b.(string); ok && strings.Contains(s, code) {
return true
}
}
}
return strings.Contains(err.Error(), code)
}

View File

@@ -1,49 +0,0 @@
//go:build linux
package firewalld
import (
"errors"
"testing"
"github.com/godbus/dbus/v5"
)
func TestDBusErrContains(t *testing.T) {
tests := []struct {
name string
err error
code string
want bool
}{
{"nil error", nil, errZoneAlreadySet, false},
{"plain error match", errors.New("ZONE_ALREADY_SET: wt0"), errZoneAlreadySet, true},
{"plain error miss", errors.New("something else"), errZoneAlreadySet, false},
{
"dbus.Error body match",
dbus.Error{Name: "org.fedoraproject.FirewallD1.Exception", Body: []any{"ZONE_ALREADY_SET: wt0"}},
errZoneAlreadySet,
true,
},
{
"dbus.Error body miss",
dbus.Error{Name: "org.fedoraproject.FirewallD1.Exception", Body: []any{"INVALID_INTERFACE"}},
errAlreadyEnabled,
false,
},
{
"dbus.Error non-string body falls back to Error()",
dbus.Error{Name: "x", Body: []any{123}},
"x",
true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := dbusErrContains(tc.err, tc.code)
if got != tc.want {
t.Fatalf("dbusErrContains(%v, %q) = %v; want %v", tc.err, tc.code, got, tc.want)
}
})
}
}

View File

@@ -1,25 +0,0 @@
//go:build !linux
package firewalld
import "context"
// SetParentContext is a no-op on non-Linux platforms because firewalld only
// runs on Linux.
func SetParentContext(context.Context) {
// intentionally empty: firewalld is a Linux-only daemon
}
// TrustInterface is a no-op on non-Linux platforms because firewalld only
// runs on Linux.
func TrustInterface(string) error {
// intentionally empty: firewalld is a Linux-only daemon
return nil
}
// UntrustInterface is a no-op on non-Linux platforms because firewalld only
// runs on Linux.
func UntrustInterface(string) error {
// intentionally empty: firewalld is a Linux-only daemon
return nil
}

View File

@@ -21,10 +21,6 @@ const (
// rules chains contains the effective ACL rules
chainNameInputRules = "NETBIRD-ACL-INPUT"
// mangleFwdKey is the entries map key for mangle FORWARD guard rules that prevent
// external DNAT from bypassing ACL rules.
mangleFwdKey = "MANGLE-FORWARD"
)
type aclEntries map[string][][]string
@@ -278,12 +274,6 @@ func (m *aclManager) cleanChains() error {
}
}
for _, rule := range m.entries[mangleFwdKey] {
if err := m.iptablesClient.DeleteIfExists(tableMangle, chainFORWARD, rule...); err != nil {
log.Errorf("failed to delete mangle FORWARD guard rule: %v, %s", rule, err)
}
}
for _, ipsetName := range m.ipsetStore.ipsetNames() {
if err := m.flushIPSet(ipsetName); err != nil {
if errors.Is(err, ipset.ErrSetNotExist) {
@@ -313,10 +303,6 @@ func (m *aclManager) createDefaultChains() error {
}
for chainName, rules := range m.entries {
// mangle FORWARD guard rules are handled separately below
if chainName == mangleFwdKey {
continue
}
for _, rule := range rules {
if err := m.iptablesClient.InsertUnique(tableName, chainName, 1, rule...); err != nil {
log.Debugf("failed to create input chain jump rule: %s", err)
@@ -336,13 +322,6 @@ func (m *aclManager) createDefaultChains() error {
}
clear(m.optionalEntries)
// Insert mangle FORWARD guard rules to prevent external DNAT bypass.
for _, rule := range m.entries[mangleFwdKey] {
if err := m.iptablesClient.AppendUnique(tableMangle, chainFORWARD, rule...); err != nil {
log.Errorf("failed to add mangle FORWARD guard rule: %v", err)
}
}
return nil
}
@@ -364,22 +343,6 @@ func (m *aclManager) seedInitialEntries() {
m.appendToEntries("FORWARD", []string{"-o", m.wgIface.Name(), "-j", chainRTFWDOUT})
m.appendToEntries("FORWARD", []string{"-i", m.wgIface.Name(), "-j", chainRTFWDIN})
// Mangle FORWARD guard: when external DNAT redirects traffic from the wg interface, it
// traverses FORWARD instead of INPUT, bypassing ACL rules. ACCEPT rules in filter FORWARD
// can be inserted above ours. Mangle runs before filter, so these guard rules enforce the
// ACL mark check where it cannot be overridden.
m.appendToEntries(mangleFwdKey, []string{
"-i", m.wgIface.Name(),
"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED",
"-j", "ACCEPT",
})
m.appendToEntries(mangleFwdKey, []string{
"-i", m.wgIface.Name(),
"-m", "conntrack", "--ctstate", "DNAT",
"-m", "mark", "!", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected),
"-j", "DROP",
})
}
func (m *aclManager) seedInitialOptionalEntries() {

View File

@@ -12,7 +12,6 @@ import (
log "github.com/sirupsen/logrus"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/firewall/firewalld"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/client/internal/statemanager"
@@ -87,12 +86,6 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
log.Warnf("raw table not available, notrack rules will be disabled: %v", err)
}
// Trust after all fatal init steps so a later failure doesn't leave the
// interface in firewalld's trusted zone without a corresponding Close.
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
// persist early to ensure cleanup of chains
go func() {
if err := stateManager.PersistState(context.Background()); err != nil {
@@ -198,12 +191,6 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
merr = multierror.Append(merr, fmt.Errorf("reset router: %w", err))
}
// Appending to merr intentionally blocks DeleteState below so ShutdownState
// stays persisted and the crash-recovery path retries firewalld cleanup.
if err := firewalld.UntrustInterface(m.wgIface.Name()); err != nil {
merr = multierror.Append(merr, err)
}
// attempt to delete state only if all other operations succeeded
if merr == nil {
if err := stateManager.DeleteState(&ShutdownState{}); err != nil {
@@ -230,11 +217,6 @@ func (m *Manager) AllowNetbird() error {
if err != nil {
return fmt.Errorf("allow netbird interface traffic: %w", err)
}
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
return nil
}
@@ -382,6 +364,28 @@ func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
return nil
}
// AddTProxyRule adds TPROXY redirect rules for the transparent proxy.
func (m *Manager) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.router.AddTProxyRule(ruleID, sources, dstPorts, redirectPort)
}
// RemoveTProxyRule removes TPROXY redirect rules by ID.
func (m *Manager) RemoveTProxyRule(ruleID string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.router.RemoveTProxyRule(ruleID)
}
// AddUDPInspectionHook is a no-op for iptables (kernel-mode firewall has no userspace packet hooks).
func (m *Manager) AddUDPInspectionHook(_ uint16, _ func([]byte) bool) string { return "" }
// RemoveUDPInspectionHook is a no-op for iptables.
func (m *Manager) RemoveUDPInspectionHook(_ string) {}
func (m *Manager) initNoTrackChain() error {
if err := m.cleanupNoTrackChain(); err != nil {
log.Debugf("cleanup notrack chain: %v", err)

View File

@@ -89,6 +89,8 @@ type router struct {
stateManager *statemanager.Manager
ipFwdState *ipfwdstate.IPForwardingState
tproxyRules []tproxyRuleEntry
}
func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint16) (*router, error) {
@@ -1109,3 +1111,92 @@ func (r *router) addPrefixToIPSet(name string, prefix netip.Prefix) error {
func (r *router) destroyIPSet(name string) error {
return ipset.Destroy(name)
}
// AddTProxyRule adds iptables nat PREROUTING REDIRECT rules for transparent proxy interception.
// Traffic from sources on dstPorts arriving on the WG interface is redirected
// to the transparent proxy listener on redirectPort.
func (r *router) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
portStr := fmt.Sprintf("%d", redirectPort)
for _, proto := range []string{"tcp", "udp"} {
srcSpecs := r.buildSourceSpecs(sources)
for _, srcSpec := range srcSpecs {
if len(dstPorts) == 0 {
rule := append(srcSpec,
"-i", r.wgIface.Name(),
"-p", proto,
"-j", "REDIRECT",
"--to-ports", portStr,
)
if err := r.iptablesClient.AppendUnique(tableNat, chainRTRDR, rule...); err != nil {
return fmt.Errorf("add redirect rule %s/%s: %w", ruleID, proto, err)
}
r.tproxyRules = append(r.tproxyRules, tproxyRuleEntry{
ruleID: ruleID,
table: tableNat,
chain: chainRTRDR,
spec: rule,
})
} else {
for _, port := range dstPorts {
rule := append(srcSpec,
"-i", r.wgIface.Name(),
"-p", proto,
"--dport", fmt.Sprintf("%d", port),
"-j", "REDIRECT",
"--to-ports", portStr,
)
if err := r.iptablesClient.AppendUnique(tableNat, chainRTRDR, rule...); err != nil {
return fmt.Errorf("add redirect rule %s/%s/%d: %w", ruleID, proto, port, err)
}
r.tproxyRules = append(r.tproxyRules, tproxyRuleEntry{
ruleID: ruleID,
table: tableNat,
chain: chainRTRDR,
spec: rule,
})
}
}
}
}
return nil
}
// RemoveTProxyRule removes all iptables REDIRECT rules for the given ruleID.
func (r *router) RemoveTProxyRule(ruleID string) error {
var remaining []tproxyRuleEntry
for _, entry := range r.tproxyRules {
if entry.ruleID != ruleID {
remaining = append(remaining, entry)
continue
}
if err := r.iptablesClient.DeleteIfExists(entry.table, entry.chain, entry.spec...); err != nil {
log.Debugf("remove tproxy rule %s: %v", ruleID, err)
}
}
r.tproxyRules = remaining
return nil
}
type tproxyRuleEntry struct {
ruleID string
table string
chain string
spec []string
}
func (r *router) buildSourceSpecs(sources []netip.Prefix) [][]string {
if len(sources) == 0 {
return [][]string{{}} // empty spec = match any source
}
specs := make([][]string, 0, len(sources))
for _, src := range sources {
specs = append(specs, []string{"-s", src.String()})
}
return specs
}

View File

@@ -180,6 +180,22 @@ type Manager interface {
// SetupEBPFProxyNoTrack creates static notrack rules for eBPF proxy loopback traffic.
// This prevents conntrack from interfering with WireGuard proxy communication.
SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error
// AddTProxyRule adds TPROXY redirect rules for specific source CIDRs and destination ports.
// Traffic from sources on dstPorts is redirected to the transparent proxy on redirectPort.
// Empty dstPorts means redirect all ports.
AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error
// RemoveTProxyRule removes TPROXY redirect rules by ID.
RemoveTProxyRule(ruleID string) error
// AddUDPInspectionHook registers a hook that inspects UDP packets before forwarding.
// The hook receives the raw packet and returns true to drop it.
// Used for QUIC SNI-based blocking. Returns a hook ID for removal.
AddUDPInspectionHook(dstPort uint16, hook func(packet []byte) bool) string
// RemoveUDPInspectionHook removes a previously registered inspection hook.
RemoveUDPInspectionHook(hookID string)
}
func GenKey(format string, pair RouterPair) string {

View File

@@ -14,7 +14,6 @@ import (
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/netbirdio/netbird/client/firewall/firewalld"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/client/internal/statemanager"
@@ -218,10 +217,6 @@ func (m *Manager) AllowNetbird() error {
return fmt.Errorf("flush allow input netbird rules: %w", err)
}
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
return nil
}
@@ -487,6 +482,28 @@ func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
return nil
}
// AddTProxyRule adds TPROXY redirect rules for the transparent proxy.
func (m *Manager) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.router.AddTProxyRule(ruleID, sources, dstPorts, redirectPort)
}
// RemoveTProxyRule removes TPROXY redirect rules by ID.
func (m *Manager) RemoveTProxyRule(ruleID string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.router.RemoveTProxyRule(ruleID)
}
// AddUDPInspectionHook is a no-op for nftables (kernel-mode firewall has no userspace packet hooks).
func (m *Manager) AddUDPInspectionHook(_ uint16, _ func([]byte) bool) string { return "" }
// RemoveUDPInspectionHook is a no-op for nftables.
func (m *Manager) RemoveUDPInspectionHook(_ string) {}
func (m *Manager) initNoTrackChains(table *nftables.Table) error {
m.notrackOutputChain = m.rConn.AddChain(&nftables.Chain{
Name: chainNameRawOutput,

View File

@@ -19,7 +19,6 @@ import (
"golang.org/x/sys/unix"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/firewall/firewalld"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
@@ -41,8 +40,6 @@ const (
chainNameForward = "FORWARD"
chainNameMangleForward = "netbird-mangle-forward"
firewalldTableName = "firewalld"
userDataAcceptForwardRuleIif = "frwacceptiif"
userDataAcceptForwardRuleOif = "frwacceptoif"
userDataAcceptInputRule = "inputaccept"
@@ -80,6 +77,7 @@ type router struct {
ipFwdState *ipfwdstate.IPForwardingState
legacyManagement bool
mtu uint16
}
func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*router, error) {
@@ -136,10 +134,6 @@ func (r *router) Reset() error {
merr = multierror.Append(merr, fmt.Errorf("remove accept filter rules: %w", err))
}
if err := firewalld.UntrustInterface(r.wgIface.Name()); err != nil {
merr = multierror.Append(merr, err)
}
if err := r.removeNatPreroutingRules(); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove filter prerouting rules: %w", err))
}
@@ -287,10 +281,6 @@ func (r *router) createContainers() error {
log.Errorf("failed to add accept rules for the forward chain: %s", err)
}
if err := firewalld.TrustInterface(r.wgIface.Name()); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
if err := r.refreshRulesMap(); err != nil {
log.Errorf("failed to refresh rules: %s", err)
}
@@ -1330,13 +1320,6 @@ func (r *router) isExternalChain(chain *nftables.Chain) bool {
return false
}
// Skip firewalld-owned chains. Firewalld creates its chains with the
// NFT_CHAIN_OWNER flag, so inserting rules into them returns EPERM.
// We delegate acceptance to firewalld by trusting the interface instead.
if chain.Table.Name == firewalldTableName {
return false
}
// Skip all iptables-managed tables in the ip family
if chain.Table.Family == nftables.TableFamilyIPv4 && isIptablesTable(chain.Table.Name) {
return false
@@ -2155,3 +2138,227 @@ func getIpSetExprs(ref refcounter.Ref[*nftables.Set], isSource bool) ([]expr.Any
},
}, nil
}
// AddTProxyRule adds nftables TPROXY redirect rules in the mangle prerouting chain.
// Traffic from sources on dstPorts arriving on the WG interface is redirected to
// the transparent proxy listener on redirectPort.
// Separate rules are created for TCP and UDP protocols.
func (r *router) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
// Use the nat redirect chain for DNAT rules.
// TPROXY doesn't work on WG kernel interfaces (socket assignment silently fails),
// so we use DNAT to 127.0.0.1:proxy_port instead. The proxy reads the original
// destination via SO_ORIGINAL_DST (conntrack).
chain := r.chains[chainNameRoutingRdr]
if chain == nil {
return fmt.Errorf("nat redirect chain not initialized")
}
for _, proto := range []uint8{unix.IPPROTO_TCP, unix.IPPROTO_UDP} {
protoName := "tcp"
if proto == unix.IPPROTO_UDP {
protoName = "udp"
}
ruleKey := fmt.Sprintf("tproxy-%s-%s", ruleID, protoName)
if existing, ok := r.rules[ruleKey]; ok && existing.Handle != 0 {
if err := r.decrementSetCounter(existing); err != nil {
log.Debugf("decrement set counter for %s: %v", ruleKey, err)
}
if err := r.conn.DelRule(existing); err != nil {
log.Debugf("remove existing tproxy rule %s: %v", ruleKey, err)
}
delete(r.rules, ruleKey)
}
exprs, err := r.buildRedirectExprs(proto, sources, dstPorts, redirectPort)
if err != nil {
return fmt.Errorf("build redirect exprs for %s: %w", protoName, err)
}
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
Table: r.workTable,
Chain: chain,
Exprs: exprs,
UserData: []byte(ruleKey),
})
}
// Accept redirected packets in the ACL input chain. After REDIRECT, the
// destination port becomes the proxy port. Without this rule, the ACL filter
// drops the packet. We match on ct state dnat so only REDIRECT'd connections
// are accepted: direct connections to the proxy port are blocked.
inputAcceptKey := fmt.Sprintf("tproxy-%s-input", ruleID)
if _, ok := r.rules[inputAcceptKey]; !ok {
inputChain := &nftables.Chain{
Name: "netbird-acl-input-rules",
Table: r.workTable,
}
r.rules[inputAcceptKey] = r.conn.InsertRule(&nftables.Rule{
Table: r.workTable,
Chain: inputChain,
Exprs: []expr.Any{
// Only accept connections that were REDIRECT'd (ct status dnat)
&expr.Ct{Register: 1, Key: expr.CtKeySTATUS},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: binaryutil.NativeEndian.PutUint32(0x20), // IPS_DST_NAT
Xor: binaryutil.NativeEndian.PutUint32(0),
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(0),
},
// Accept both TCP and UDP redirected to the proxy port.
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.BigEndian.PutUint16(redirectPort),
},
&expr.Verdict{Kind: expr.VerdictAccept},
},
UserData: []byte(inputAcceptKey),
})
}
if err := r.conn.Flush(); err != nil {
return fmt.Errorf("flush tproxy rules for %s: %w", ruleID, err)
}
return nil
}
// RemoveTProxyRule removes TPROXY redirect rules by ID (both TCP and UDP variants).
func (r *router) RemoveTProxyRule(ruleID string) error {
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
var removed int
for _, suffix := range []string{"tcp", "udp", "input"} {
ruleKey := fmt.Sprintf("tproxy-%s-%s", ruleID, suffix)
rule, ok := r.rules[ruleKey]
if !ok {
continue
}
if rule.Handle == 0 {
delete(r.rules, ruleKey)
continue
}
if err := r.decrementSetCounter(rule); err != nil {
log.Debugf("decrement set counter for %s: %v", ruleKey, err)
}
if err := r.conn.DelRule(rule); err != nil {
log.Debugf("delete tproxy rule %s: %v", ruleKey, err)
}
delete(r.rules, ruleKey)
removed++
}
if removed > 0 {
if err := r.conn.Flush(); err != nil {
return fmt.Errorf("flush tproxy rule removal for %s: %w", ruleID, err)
}
}
return nil
}
// buildRedirectExprs builds nftables expressions for a REDIRECT rule.
// Matches WG interface ingress, source CIDRs, destination ports, then REDIRECTs to the proxy port.
func (r *router) buildRedirectExprs(proto uint8, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) ([]expr.Any, error) {
var exprs []expr.Any
exprs = append(exprs,
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: ifname(r.wgIface.Name())},
)
exprs = append(exprs,
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{proto}},
)
// Source CIDRs use the named ipset shared with route rules.
if len(sources) > 0 {
srcSet := firewall.NewPrefixSet(sources)
srcExprs, err := r.getIpSet(srcSet, sources, true)
if err != nil {
return nil, fmt.Errorf("get source ipset: %w", err)
}
exprs = append(exprs, srcExprs...)
}
if len(dstPorts) == 1 {
exprs = append(exprs,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.BigEndian.PutUint16(dstPorts[0]),
},
)
} else if len(dstPorts) > 1 {
setElements := make([]nftables.SetElement, len(dstPorts))
for i, p := range dstPorts {
setElements[i] = nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(p)}
}
portSet := &nftables.Set{
Table: r.workTable,
Anonymous: true,
Constant: true,
KeyType: nftables.TypeInetService,
}
if err := r.conn.AddSet(portSet, setElements); err != nil {
return nil, fmt.Errorf("create port set: %w", err)
}
exprs = append(exprs,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
},
&expr.Lookup{
SourceRegister: 1,
SetName: portSet.Name,
SetID: portSet.ID,
},
)
}
// REDIRECT to local proxy port. Changes the destination to the interface's
// primary address + specified port. Conntrack tracks the original destination,
// readable via SO_ORIGINAL_DST.
exprs = append(exprs,
&expr.Immediate{Register: 1, Data: binaryutil.BigEndian.PutUint16(redirectPort)},
&expr.Redir{
RegisterProtoMin: 1,
},
)
return exprs, nil
}

View File

@@ -3,9 +3,6 @@
package uspfilter
import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/firewall/firewalld"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
@@ -19,9 +16,6 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
if m.nativeFirewall != nil {
return m.nativeFirewall.Close(stateManager)
}
if err := firewalld.UntrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to untrust interface in firewalld: %v", err)
}
return nil
}
@@ -30,8 +24,5 @@ func (m *Manager) AllowNetbird() error {
if m.nativeFirewall != nil {
return m.nativeFirewall.AllowNetbird()
}
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
return nil
}

View File

@@ -1,37 +0,0 @@
package common
import (
"net/netip"
"sync/atomic"
)
// PacketHook stores a registered hook for a specific IP:port.
type PacketHook struct {
IP netip.Addr
Port uint16
Fn func([]byte) bool
}
// HookMatches checks if a packet's destination matches the hook and invokes it.
func HookMatches(h *PacketHook, dstIP netip.Addr, dport uint16, packetData []byte) bool {
if h == nil {
return false
}
if h.IP == dstIP && h.Port == dport {
return h.Fn(packetData)
}
return false
}
// SetHook atomically stores a hook, handling nil removal.
func SetHook(ptr *atomic.Pointer[PacketHook], ip netip.Addr, dPort uint16, hook func([]byte) bool) {
if hook == nil {
ptr.Store(nil)
return
}
ptr.Store(&PacketHook{
IP: ip,
Port: dPort,
Fn: hook,
})
}

View File

@@ -9,7 +9,6 @@ import (
// IFaceMapper defines subset methods of interface required for manager
type IFaceMapper interface {
Name() string
SetFilter(device.PacketFilter) error
Address() wgaddr.Address
GetWGDevice() *wgdevice.Device

View File

@@ -142,8 +142,15 @@ type Manager struct {
mssClampEnabled bool
// Only one hook per protocol is supported. Outbound direction only.
udpHookOut atomic.Pointer[common.PacketHook]
tcpHookOut atomic.Pointer[common.PacketHook]
udpHookOut atomic.Pointer[packetHook]
tcpHookOut atomic.Pointer[packetHook]
}
// packetHook stores a registered hook for a specific IP:port.
type packetHook struct {
ip netip.Addr
port uint16
fn func([]byte) bool
}
// decoder for packages
@@ -634,6 +641,45 @@ func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
return m.nativeFirewall.SetupEBPFProxyNoTrack(proxyPort, wgPort)
}
// AddTProxyRule delegates to the native firewall for TPROXY rules.
// In userspace mode (no native firewall), this is a no-op since the
// forwarder intercepts traffic directly.
func (m *Manager) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
if m.nativeFirewall == nil {
return nil
}
return m.nativeFirewall.AddTProxyRule(ruleID, sources, dstPorts, redirectPort)
}
// AddUDPInspectionHook registers a hook for QUIC/UDP inspection via the packet filter.
func (m *Manager) AddUDPInspectionHook(dstPort uint16, hook func(packet []byte) bool) string {
m.SetUDPPacketHook(netip.Addr{}, dstPort, hook)
return "udp-inspection"
}
// RemoveUDPInspectionHook removes a previously registered inspection hook.
func (m *Manager) RemoveUDPInspectionHook(_ string) {
m.SetUDPPacketHook(netip.Addr{}, 0, nil)
}
// RemoveTProxyRule delegates to the native firewall for TPROXY rules.
func (m *Manager) RemoveTProxyRule(ruleID string) error {
if m.nativeFirewall == nil {
return nil
}
return m.nativeFirewall.RemoveTProxyRule(ruleID)
}
// IsLocalIP reports whether the given IP belongs to the local machine.
func (m *Manager) IsLocalIP(ip netip.Addr) bool {
return m.localipmanager.IsLocalIP(ip)
}
// GetForwarder returns the userspace packet forwarder, or nil if not initialized.
func (m *Manager) GetForwarder() *forwarder.Forwarder {
return m.forwarder.Load()
}
// UpdateSet updates the rule destinations associated with the given set
// by merging the existing prefixes with the new ones, then deduplicating.
func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
@@ -905,11 +951,21 @@ func (m *Manager) trackInbound(d *decoder, srcIP, dstIP netip.Addr, ruleID []byt
}
func (m *Manager) udpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte) bool {
return common.HookMatches(m.udpHookOut.Load(), dstIP, dport, packetData)
return hookMatches(m.udpHookOut.Load(), dstIP, dport, packetData)
}
func (m *Manager) tcpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte) bool {
return common.HookMatches(m.tcpHookOut.Load(), dstIP, dport, packetData)
return hookMatches(m.tcpHookOut.Load(), dstIP, dport, packetData)
}
func hookMatches(h *packetHook, dstIP netip.Addr, dport uint16, packetData []byte) bool {
if h == nil {
return false
}
if h.ip == dstIP && h.port == dport {
return h.fn(packetData)
}
return false
}
// filterInbound implements filtering logic for incoming packets.
@@ -1320,12 +1376,28 @@ func (m *Manager) ruleMatches(rule *RouteRule, srcAddr, dstAddr netip.Addr, prot
// SetUDPPacketHook sets the outbound UDP packet hook. Pass nil hook to remove.
func (m *Manager) SetUDPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool) {
common.SetHook(&m.udpHookOut, ip, dPort, hook)
if hook == nil {
m.udpHookOut.Store(nil)
return
}
m.udpHookOut.Store(&packetHook{
ip: ip,
port: dPort,
fn: hook,
})
}
// SetTCPPacketHook sets the outbound TCP packet hook. Pass nil hook to remove.
func (m *Manager) SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool) {
common.SetHook(&m.tcpHookOut, ip, dPort, hook)
if hook == nil {
m.tcpHookOut.Store(nil)
return
}
m.tcpHookOut.Store(&packetHook{
ip: ip,
port: dPort,
fn: hook,
})
}
// SetLogLevel sets the log level for the firewall manager

View File

@@ -31,20 +31,12 @@ var logger = log.NewFromLogrus(logrus.StandardLogger())
var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger()
type IFaceMock struct {
NameFunc func() string
SetFilterFunc func(device.PacketFilter) error
AddressFunc func() wgaddr.Address
GetWGDeviceFunc func() *wgdevice.Device
GetDeviceFunc func() *device.FilteredDevice
}
func (i *IFaceMock) Name() string {
if i.NameFunc == nil {
return "wgtest"
}
return i.NameFunc()
}
func (i *IFaceMock) GetWGDevice() *wgdevice.Device {
if i.GetWGDeviceFunc == nil {
return nil
@@ -210,9 +202,9 @@ func TestSetUDPPacketHook(t *testing.T) {
h := manager.udpHookOut.Load()
require.NotNil(t, h)
assert.Equal(t, netip.MustParseAddr("10.168.0.1"), h.IP)
assert.Equal(t, uint16(8000), h.Port)
assert.True(t, h.Fn(nil))
assert.Equal(t, netip.MustParseAddr("10.168.0.1"), h.ip)
assert.Equal(t, uint16(8000), h.port)
assert.True(t, h.fn(nil))
assert.True(t, called)
manager.SetUDPPacketHook(netip.MustParseAddr("10.168.0.1"), 8000, nil)
@@ -234,9 +226,9 @@ func TestSetTCPPacketHook(t *testing.T) {
h := manager.tcpHookOut.Load()
require.NotNil(t, h)
assert.Equal(t, netip.MustParseAddr("10.168.0.1"), h.IP)
assert.Equal(t, uint16(53), h.Port)
assert.True(t, h.Fn(nil))
assert.Equal(t, netip.MustParseAddr("10.168.0.1"), h.ip)
assert.Equal(t, uint16(53), h.port)
assert.True(t, h.fn(nil))
assert.True(t, called)
manager.SetTCPPacketHook(netip.MustParseAddr("10.168.0.1"), 53, nil)

View File

@@ -7,6 +7,7 @@ import (
"net/netip"
"runtime"
"sync"
"sync/atomic"
"time"
log "github.com/sirupsen/logrus"
@@ -21,6 +22,7 @@ import (
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
"github.com/netbirdio/netbird/client/inspect"
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
)
@@ -46,6 +48,10 @@ type Forwarder struct {
netstack bool
hasRawICMPAccess bool
pingSemaphore chan struct{}
// proxy is the optional inspection engine.
// When set, TCP connections are handed to the engine for protocol detection
// and rule evaluation. Swapped atomically for lock-free hot-path access.
proxy atomic.Pointer[inspect.Proxy]
}
func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool, mtu uint16) (*Forwarder, error) {
@@ -79,7 +85,7 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
}
if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil {
return nil, fmt.Errorf("failed to add protocol address: %s", err)
return nil, fmt.Errorf("add protocol address: %s", err)
}
defaultSubnet, err := tcpip.NewSubnet(
@@ -155,6 +161,13 @@ func (f *Forwarder) InjectIncomingPacket(payload []byte) error {
return nil
}
// SetProxy sets the inspection engine. When set, TCP connections are handed
// to it for protocol detection and rule evaluation instead of direct relay.
// Pass nil to disable inspection.
func (f *Forwarder) SetProxy(p *inspect.Proxy) {
f.proxy.Store(p)
}
// Stop gracefully shuts down the forwarder
func (f *Forwarder) Stop() {
f.cancel()
@@ -167,6 +180,25 @@ func (f *Forwarder) Stop() {
f.stack.Wait()
}
// CheckUDPPacket inspects a UDP payload against proxy rules before injection.
// This is called by the filter for QUIC SNI-based blocking.
// Returns true if the packet should be allowed, false if it should be dropped.
func (f *Forwarder) CheckUDPPacket(payload []byte, srcIP, dstIP netip.Addr, srcPort, dstPort uint16, ruleID []byte) bool {
p := f.proxy.Load()
if p == nil {
return true
}
dst := netip.AddrPortFrom(dstIP, dstPort)
src := inspect.SourceInfo{
IP: srcIP,
PolicyID: inspect.PolicyID(ruleID),
}
action := p.HandleUDPPacket(payload, dst, src)
return action != inspect.ActionBlock
}
func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
if f.netstack && f.ip.Equal(addr) {
return net.IPv4(127, 0, 0, 1)

View File

@@ -16,6 +16,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/waiter"
"github.com/netbirdio/netbird/client/inspect"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
)
@@ -23,6 +24,86 @@ import (
func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
id := r.ID()
// If the inspection engine is configured, accept the connection first and hand it off.
if p := f.proxy.Load(); p != nil {
f.handleTCPWithInspection(r, id, p)
return
}
f.handleTCPDirect(r, id)
}
// handleTCPWithInspection accepts the connection and hands it to the inspection
// engine. For allow decisions, the forwarder does its own relay (passthrough).
// For block/inspect, the engine handles everything internally.
func (f *Forwarder) handleTCPWithInspection(r *tcp.ForwarderRequest, id stack.TransportEndpointID, p *inspect.Proxy) {
flowID := uuid.New()
f.sendTCPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
wq := waiter.Queue{}
ep, epErr := r.CreateEndpoint(&wq)
if epErr != nil {
f.logger.Error1("forwarder: create TCP endpoint for inspection: %v", epErr)
r.Complete(true)
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
return
}
r.Complete(false)
inConn := gonet.NewTCPConn(&wq, ep)
srcIP := netip.AddrFrom4(id.RemoteAddress.As4())
dstIP := netip.AddrFrom4(id.LocalAddress.As4())
dst := netip.AddrPortFrom(dstIP, id.LocalPort)
var policyID []byte
if ruleID, ok := f.getRuleID(srcIP, dstIP, id.RemotePort, id.LocalPort); ok {
policyID = ruleID
}
src := inspect.SourceInfo{
IP: srcIP,
PolicyID: inspect.PolicyID(policyID),
}
f.logger.Trace1("forwarder: handing TCP %v to inspection engine", epID(id))
go func() {
result, err := p.InspectTCP(f.ctx, inConn, dst, src)
if err != nil && err != inspect.ErrBlocked {
f.logger.Debug2("forwarder: inspection error for %v: %v", epID(id), err)
}
// Passthrough: engine returned allow, forwarder does the relay.
if result.PassthroughConn != nil {
dialAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort)
outConn, dialErr := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr)
if dialErr != nil {
f.logger.Trace2("forwarder: passthrough dial error for %v: %v", epID(id), dialErr)
if closeErr := result.PassthroughConn.Close(); closeErr != nil {
f.logger.Debug1("forwarder: close passthrough conn: %v", closeErr)
}
ep.Close()
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
return
}
f.proxyTCPPassthrough(id, result.PassthroughConn, outConn, ep, flowID)
return
}
// Engine handled it (block/inspect/HTTP). Capture stats and clean up.
var rxPackets, txPackets uint64
if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
rxPackets = tcpStats.SegmentsSent.Value()
txPackets = tcpStats.SegmentsReceived.Value()
}
ep.Close()
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, rxPackets, txPackets)
}()
}
// handleTCPDirect handles TCP connections with direct relay (no proxy).
func (f *Forwarder) handleTCPDirect(r *tcp.ForwarderRequest, id stack.TransportEndpointID) {
flowID := uuid.New()
f.sendTCPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
@@ -42,7 +123,6 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
return
}
// Create wait queue for blocking syscalls
wq := waiter.Queue{}
ep, epErr := r.CreateEndpoint(&wq)
@@ -55,7 +135,6 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
return
}
// Complete the handshake
r.Complete(false)
inConn := gonet.NewTCPConn(&wq, ep)
@@ -73,7 +152,6 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
go func() {
<-ctx.Done()
// Close connections and endpoint.
if err := inConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug1("forwarder: inConn close error: %v", err)
}
@@ -132,6 +210,66 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets)
}
// proxyTCPPassthrough relays traffic between a peeked inbound connection
// (from the inspection engine passthrough) and the outbound connection.
// It accepts net.Conn for inConn since the inspection engine wraps it in a peekConn.
func (f *Forwarder) proxyTCPPassthrough(id stack.TransportEndpointID, inConn net.Conn, outConn net.Conn, ep tcpip.Endpoint, flowID uuid.UUID) {
ctx, cancel := context.WithCancel(f.ctx)
defer cancel()
go func() {
<-ctx.Done()
if err := inConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug1("forwarder: passthrough inConn close: %v", err)
}
if err := outConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug1("forwarder: passthrough outConn close: %v", err)
}
ep.Close()
}()
var wg sync.WaitGroup
wg.Add(2)
var (
bytesIn int64
bytesOut int64
errIn error
errOut error
)
go func() {
bytesIn, errIn = io.Copy(outConn, inConn)
cancel()
wg.Done()
}()
go func() {
bytesOut, errOut = io.Copy(inConn, outConn)
cancel()
wg.Done()
}()
wg.Wait()
if errIn != nil && !isClosedError(errIn) {
f.logger.Error2("proxyTCPPassthrough: copy error (in→out) for %s: %v", epID(id), errIn)
}
if errOut != nil && !isClosedError(errOut) {
f.logger.Error2("proxyTCPPassthrough: copy error (out→in) for %s: %v", epID(id), errOut)
}
var rxPackets, txPackets uint64
if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
rxPackets = tcpStats.SegmentsSent.Value()
txPackets = tcpStats.SegmentsReceived.Value()
}
f.logger.Trace5("forwarder: passthrough TCP %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesOut, txPackets, bytesIn)
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesOut), uint64(bytesIn), rxPackets, txPackets)
}
func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, rxBytes, txBytes, rxPackets, txPackets uint64) {
srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
dstIp := netip.AddrFrom4(id.LocalAddress.As4())

View File

@@ -1,90 +0,0 @@
package uspfilter
import (
"encoding/binary"
"net/netip"
"sync/atomic"
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
"github.com/netbirdio/netbird/client/iface/device"
)
const (
ipv4HeaderMinLen = 20
ipv4ProtoOffset = 9
ipv4FlagsOffset = 6
ipv4DstOffset = 16
ipProtoUDP = 17
ipProtoTCP = 6
ipv4FragOffMask = 0x1fff
// dstPortOffset is the offset of the destination port within a UDP or TCP header.
dstPortOffset = 2
)
// HooksFilter is a minimal packet filter that only handles outbound DNS hooks.
// It is installed on the WireGuard interface when the userspace bind is active
// but a full firewall filter (Manager) is not needed because a native kernel
// firewall (nftables/iptables) handles packet filtering.
type HooksFilter struct {
udpHook atomic.Pointer[common.PacketHook]
tcpHook atomic.Pointer[common.PacketHook]
}
var _ device.PacketFilter = (*HooksFilter)(nil)
// FilterOutbound checks outbound packets for DNS hook matches.
// Only IPv4 packets matching the registered hook IP:port are intercepted.
// IPv6 and non-IP packets pass through unconditionally.
func (f *HooksFilter) FilterOutbound(packetData []byte, _ int) bool {
if len(packetData) < ipv4HeaderMinLen {
return false
}
// Only process IPv4 packets, let everything else pass through.
if packetData[0]>>4 != 4 {
return false
}
ihl := int(packetData[0]&0x0f) * 4
if ihl < ipv4HeaderMinLen || len(packetData) < ihl+4 {
return false
}
// Skip non-first fragments: they don't carry L4 headers.
flagsAndOffset := binary.BigEndian.Uint16(packetData[ipv4FlagsOffset : ipv4FlagsOffset+2])
if flagsAndOffset&ipv4FragOffMask != 0 {
return false
}
dstIP, ok := netip.AddrFromSlice(packetData[ipv4DstOffset : ipv4DstOffset+4])
if !ok {
return false
}
proto := packetData[ipv4ProtoOffset]
dstPort := binary.BigEndian.Uint16(packetData[ihl+dstPortOffset : ihl+dstPortOffset+2])
switch proto {
case ipProtoUDP:
return common.HookMatches(f.udpHook.Load(), dstIP, dstPort, packetData)
case ipProtoTCP:
return common.HookMatches(f.tcpHook.Load(), dstIP, dstPort, packetData)
default:
return false
}
}
// FilterInbound allows all inbound packets (native firewall handles filtering).
func (f *HooksFilter) FilterInbound([]byte, int) bool {
return false
}
// SetUDPPacketHook registers the UDP packet hook.
func (f *HooksFilter) SetUDPPacketHook(ip netip.Addr, dPort uint16, hook func([]byte) bool) {
common.SetHook(&f.udpHook, ip, dPort, hook)
}
// SetTCPPacketHook registers the TCP packet hook.
func (f *HooksFilter) SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func([]byte) bool) {
common.SetHook(&f.tcpHook, ip, dPort, hook)
}

View File

@@ -1,6 +0,0 @@
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
build/
coverage/

View File

@@ -1,36 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "02085feb3f5d8a8156e5e28512b9d99351d510c0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
- platform: linux
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
- platform: macos
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
- platform: windows
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -1,115 +0,0 @@
# Flutter UI Migration
## Current Boundary
Keep the daemon as-is and replace only the desktop UI process. The Flutter app
should continue to talk to `DaemonService` from `client/proto/daemon.proto`.
The current UI is not a simple settings window. It owns:
- tray/menu-bar state and nested menu actions
- gRPC connection management and event subscription
- connect, disconnect, login, and session-expired flows
- profile switching, deregistration, and profile windows
- network route and exit-node selection
- advanced settings
- debug bundle creation and upload status dialogs
- enforced update notifications and progress windows
- OS sleep/wake notification to the daemon
- single-instance signaling and quick-actions windows
## Phases
1. Scaffold and generated gRPC client
- Done: generated Dart stubs from `client/proto/daemon.proto`.
- Done: app defaults to a gRPC-backed implementation and keeps
`--fake-daemon` for UI-only work.
- Remaining: replace the development user agent suffix with the release
version at build time.
2. Core connection parity
- Done: status polling and `SubscribeEvents` refresh hooks.
- Done: `connect()` runs `Login` → optional SSO browser handoff via
`openExternalUrl``WaitSSOLogin``Up`, with an `awaitingLogin` snapshot
state and a banner that exposes the verification URI and user code.
- Done: `disconnect()` calls `Down`.
- Match current daemon address defaults:
- Windows: `tcp://127.0.0.1:41731`
- Unix-like desktop: `unix:///var/run/netbird.sock`
3. Settings, profiles, and networks
- Done: `GetConfig`/`SetConfig` for the toggleable settings (auto-connect,
allow SSH, quantum resistance, lazy connections, block inbound,
notifications). Read-only fields (management URL, interface, port, MTU)
still need editable forms.
- Done: profile add/switch/remove/logout via `AddProfile`,
`SwitchProfile`, `RemoveProfile`, `Logout`.
- Done: network list with overlap filtering, per-route
`SelectNetworks`/`DeselectNetworks`, and exit-node single-selection.
4. Desktop integration
- Done: tray icon and menu via `tray_manager` (status header, profile,
Connect/Disconnect, Show window, Quit) with status-aware icons that fall
back to template variants on macOS.
- Done: window lifecycle via `window_manager` — close hides instead of
exiting; tray "Quit" actually destroys the window.
- Done: native notifications via `local_notifier`, fed by the daemon's
`SubscribeEvents` stream and gated by the `notifications` setting (with
CRITICAL severity always firing).
- Done: browser launch and clipboard via `Process.run` and
`flutter/services` Clipboard.
- Remaining: file/folder reveal for debug bundles, single-instance
signaling, quick-actions invocation, and sleep/wake forwarding through
`NotifyOSLifecycle`. Settings/Networks submenus on the tray are deferred
until the window-side flows are stable.
- Note: `local_notifier` uses macOS's deprecated `NSUserNotificationCenter`
(warns at build time). Plan to swap to `flutter_local_notifications`
before release.
5. Debug and update flows
- Done: rich debug bundle screen with anonymize, system-info, upload (URL),
and run-with-trace + duration. State machine drives `GetLogLevel`
`SetLogLevel(TRACE)``Down``SetSyncResponsePersistence``Up`
progress over duration → `StopCPUProfile``DebugBundle`, with restore
of original log level and persistence in a finally. Result dialog covers
uploaded, upload-failed, and local-only outcomes with copy/open actions.
- Done: enforced-update modal triggered by daemon `progress_window=show`
metadata. Polls `GetInstallerResult` with a 15-min timeout, blocks close
for 10 s, then surfaces success (auto-close) or failure (error message).
- Remaining: hook a "Check for updates" / "Install now" button into the
About surface that calls `TriggerUpdate` directly.
6. Release pipeline
- Update `.github/workflows/release.yml` UI build steps.
- Update `client/netbird.wxs`, `release_files/install.sh`, and
`release_files/ui-post-install.sh` where they assume the Go UI artifact.
- Update updater restart behavior in `client/internal/updater/installer`.
- Preserve public artifact names until installers and updater logic are
intentionally migrated.
## RPCs Used By The Current UI
The first production implementation should cover:
- `Status`, `Up`, `Down`
- `Login`, `WaitSSOLogin`, `Logout`
- `GetConfig`, `SetConfig`, `GetFeatures`
- `SubscribeEvents`
- `ListNetworks`, `SelectNetworks`, `DeselectNetworks`
- `ListProfiles`, `AddProfile`, `SwitchProfile`, `RemoveProfile`,
`GetActiveProfile`
- `DebugBundle`, `GetLogLevel`, `SetLogLevel`, `SetSyncResponsePersistence`,
`StartCPUProfile`, `StopCPUProfile`
- `TriggerUpdate`, `GetInstallerResult`
- `NotifyOSLifecycle`
## Risk Register
- Desktop tray support differs sharply across Windows, macOS, and Linux.
- Linux app indicators and desktop-session startup need distro-level testing.
- The updater currently restarts `netbird-ui` by process/app name on Windows and
macOS, so artifact naming changes must be coordinated.
- Dart gRPC over Unix domain sockets must be validated against the daemon's
existing `unix://` address behavior.
- Flutter desktop packaging is separate from Go builds, so release CI needs a
new toolchain and cache strategy.

View File

@@ -1,54 +0,0 @@
# NetBird Flutter UI
This is the migration workspace for a Flutter-based replacement for `client/ui`.
The existing Go/Fyne UI remains the production UI until this package reaches
feature and release-pipeline parity.
## Scope
The first target is the desktop UI only. The NetBird daemon, service lifecycle,
network engine, and daemon gRPC API stay in Go.
Initial parity target:
- tray/menu-bar entry with connection status and connect/disconnect actions
- settings and feature flags backed by `DaemonService.GetConfig` and `SetConfig`
- profile management
- network and exit-node selection
- daemon event subscription and desktop notifications
- login/session-expired flow
- debug bundle flow
- enforced-update progress window
- Windows, macOS, and Linux packaging integration
## Bootstrap
Flutter and Dart are not committed into this repository. After installing the
Flutter SDK, run:
```sh
cd client/flutter_ui
bash tool/bootstrap.sh
bash tool/generate_proto.sh
flutter run -d macos -- --daemon-addr=unix:///var/run/netbird.sock
```
Use `-d windows` or `-d linux` on those platforms. The Windows daemon address is
currently `tcp://127.0.0.1:41731`.
For UI-only development without a daemon, run:
```sh
flutter run -d macos -- --fake-daemon
```
## Layout
- `lib/main.dart`: app entry point and command-line flag parsing
- `lib/src/app_shell.dart`: first-pass desktop shell
- `lib/src/daemon_client.dart`: daemon boundary with fake and gRPC-backed clients
- `lib/src/models.dart`: UI-facing models independent from generated protobufs
- `lib/src/generated/`: generated Dart protobuf and gRPC files
- `tool/bootstrap.sh`: creates Flutter desktop platform folders once Flutter is installed
- `tool/generate_proto.sh`: generates Dart gRPC bindings from `client/proto/daemon.proto`
- `MIGRATION.md`: parity plan and release integration checklist

View File

@@ -1,10 +0,0 @@
include: package:lints/recommended.yaml
analyzer:
exclude:
- lib/src/generated/**
linter:
rules:
avoid_print: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,53 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'src/app_shell.dart';
import 'src/daemon_client.dart';
import 'src/desktop_integration.dart';
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
final daemonAddr = _readFlag(args, 'daemon-addr') ?? _defaultDaemonAddr();
final fakeDaemon = args.contains('--fake-daemon');
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(900, 640),
minimumSize: Size(720, 520),
center: true,
title: 'NetBird',
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
final client = fakeDaemon
? FakeDaemonClient(daemonAddr: daemonAddr)
: GrpcDaemonClient(daemonAddr: daemonAddr);
final integration = DesktopIntegration(client: client);
await integration.initialize();
runApp(NetBirdFlutterApp(client: client, integration: integration));
}
String? _readFlag(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
String _defaultDaemonAddr() {
if (Platform.isWindows) {
return 'tcp://127.0.0.1:41731';
}
return 'unix:///var/run/netbird.sock';
}

View File

@@ -1,889 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'daemon_client.dart';
import 'debug_screen.dart';
import 'desktop_integration.dart';
import 'models.dart';
import 'platform.dart';
import 'update_progress.dart';
class NetBirdFlutterApp extends StatelessWidget {
const NetBirdFlutterApp({required this.client, this.integration, super.key});
final DaemonClient client;
final DesktopIntegration? integration;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NetBird',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF008C95),
brightness: Brightness.light,
),
darkTheme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF008C95),
brightness: Brightness.dark,
),
home: AppShell(client: client, integration: integration),
);
}
}
class AppShell extends StatefulWidget {
const AppShell({required this.client, this.integration, super.key});
final DaemonClient client;
final DesktopIntegration? integration;
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
late ClientSnapshot _snapshot;
StreamSubscription<ClientSnapshot>? _subscription;
StreamSubscription<UpdateProgressEvent>? _updateSubscription;
StreamSubscription<int>? _tabSubscription;
int _selectedIndex = 0;
bool _busy = false;
bool _updateDialogOpen = false;
@override
void initState() {
super.initState();
_snapshot = ClientSnapshot.initial(widget.client.daemonAddr);
_subscription = widget.client.watchSnapshot().listen((snapshot) {
if (!mounted) {
return;
}
setState(() => _snapshot = snapshot);
});
_updateSubscription = widget.client.watchUpdateRequests().listen(
_showUpdateDialog,
);
_tabSubscription = widget.integration?.tabRequests.listen((index) {
if (!mounted) {
return;
}
setState(() => _selectedIndex = index);
});
}
@override
void dispose() {
_subscription?.cancel();
_updateSubscription?.cancel();
_tabSubscription?.cancel();
widget.client.dispose();
super.dispose();
}
Future<void> _showUpdateDialog(UpdateProgressEvent event) async {
if (!mounted || _updateDialogOpen) {
return;
}
_updateDialogOpen = true;
try {
await showUpdateProgressDialog(
context: context,
client: widget.client,
event: event,
);
} finally {
if (mounted) {
_updateDialogOpen = false;
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
labelType: NavigationRailLabelType.all,
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: _StatusGlyph(status: _snapshot.status),
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.hub_outlined),
selectedIcon: Icon(Icons.hub),
label: Text('Status'),
),
NavigationRailDestination(
icon: Icon(Icons.route_outlined),
selectedIcon: Icon(Icons.route),
label: Text('Networks'),
),
NavigationRailDestination(
icon: Icon(Icons.account_circle_outlined),
selectedIcon: Icon(Icons.account_circle),
label: Text('Profiles'),
),
NavigationRailDestination(
icon: Icon(Icons.tune_outlined),
selectedIcon: Icon(Icons.tune),
label: Text('Settings'),
),
NavigationRailDestination(
icon: Icon(Icons.bug_report_outlined),
selectedIcon: Icon(Icons.bug_report),
label: Text('Debug'),
),
],
),
const VerticalDivider(width: 1),
Expanded(child: SafeArea(child: _buildPage(context))),
],
),
);
}
Widget _buildPage(BuildContext context) {
return switch (_selectedIndex) {
0 => _StatusPane(
snapshot: _snapshot,
busy: _busy,
onConnect: () => _run(widget.client.connect),
onDisconnect: () => _run(widget.client.disconnect),
),
1 => _NetworksPane(snapshot: _snapshot, client: widget.client),
2 => _ProfilesPane(snapshot: _snapshot, client: widget.client),
3 => _SettingsPane(snapshot: _snapshot, client: widget.client),
_ => DebugScreen(client: widget.client),
};
}
Future<void> _run(Future<void> Function() action) async {
if (_busy) {
return;
}
setState(() => _busy = true);
try {
await action();
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
}
class _Page extends StatelessWidget {
const _Page({required this.title, required this.child, this.actions});
final String title;
final Widget child;
final List<Widget>? actions;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
),
),
if (actions != null) ...actions!,
],
),
const SizedBox(height: 20),
Expanded(child: child),
],
),
);
}
}
class _StatusPane extends StatelessWidget {
const _StatusPane({
required this.snapshot,
required this.busy,
required this.onConnect,
required this.onDisconnect,
});
final ClientSnapshot snapshot;
final bool busy;
final VoidCallback onConnect;
final VoidCallback onDisconnect;
@override
Widget build(BuildContext context) {
final connected = snapshot.status == ConnectionStatus.connected;
final connecting =
snapshot.status == ConnectionStatus.connecting ||
snapshot.status == ConnectionStatus.awaitingLogin;
return _Page(
title: 'Status',
child: ListView(
children: [
_InfoRow(label: 'Connection', value: snapshot.status.label),
_InfoRow(label: 'Daemon', value: snapshot.daemonAddr),
_InfoRow(label: 'Daemon version', value: snapshot.daemonVersion),
if (snapshot.pendingLogin != null) ...[
const SizedBox(height: 16),
_LoginBanner(pending: snapshot.pendingLogin!),
],
if (snapshot.errorMessage != null) ...[
const SizedBox(height: 16),
_ErrorBanner(message: snapshot.errorMessage!),
],
const SizedBox(height: 24),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
FilledButton.icon(
onPressed: busy || connected || connecting ? null : onConnect,
icon: const Icon(Icons.power_settings_new),
label: const Text('Connect'),
),
OutlinedButton.icon(
onPressed: busy || !connected ? null : onDisconnect,
icon: const Icon(Icons.power_off),
label: const Text('Disconnect'),
),
],
),
const SizedBox(height: 32),
_SectionLabel('Active profile'),
_ProfileTile(profile: snapshot.activeProfile),
],
),
);
}
}
class _NetworksPane extends StatefulWidget {
const _NetworksPane({required this.snapshot, required this.client});
final ClientSnapshot snapshot;
final DaemonClient client;
@override
State<_NetworksPane> createState() => _NetworksPaneState();
}
class _NetworksPaneState extends State<_NetworksPane> {
NetworkFilter _filter = NetworkFilter.all;
final Set<String> _busyRoutes = {};
@override
Widget build(BuildContext context) {
final networks = widget.snapshot.networks
.where(_filter.matches)
.toList();
return _Page(
title: 'Networks',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SegmentedButton<NetworkFilter>(
segments: const [
ButtonSegment(
value: NetworkFilter.all,
icon: Icon(Icons.all_inclusive),
label: Text('All'),
),
ButtonSegment(
value: NetworkFilter.overlapping,
icon: Icon(Icons.compare_arrows),
label: Text('Overlapping'),
),
ButtonSegment(
value: NetworkFilter.exitNode,
icon: Icon(Icons.public),
label: Text('Exit nodes'),
),
],
selected: {_filter},
onSelectionChanged: (selected) {
setState(() => _filter = selected.single);
},
),
const SizedBox(height: 16),
if (networks.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Text('No networks to show.'),
)
else
Expanded(
child: ListView.separated(
itemCount: networks.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final route = networks[index];
final exitNodeMode = _filter == NetworkFilter.exitNode;
return _NetworkTile(
route: route,
exitNodeMode: exitNodeMode,
busy: _busyRoutes.contains(route.id),
onChanged: (selected) =>
_toggle(route, selected, exitNodeMode),
);
},
),
),
],
),
);
}
Future<void> _toggle(
NetworkRoute route,
bool selected,
bool exitNodeMode,
) async {
if (_busyRoutes.contains(route.id)) {
return;
}
setState(() => _busyRoutes.add(route.id));
try {
if (exitNodeMode) {
await widget.client.setExitNode(selected ? route.id : null);
} else {
await widget.client.setNetworkSelection(route.id, selected);
}
} finally {
if (mounted) {
setState(() => _busyRoutes.remove(route.id));
}
}
}
}
class _ProfilesPane extends StatefulWidget {
const _ProfilesPane({required this.snapshot, required this.client});
final ClientSnapshot snapshot;
final DaemonClient client;
@override
State<_ProfilesPane> createState() => _ProfilesPaneState();
}
class _ProfilesPaneState extends State<_ProfilesPane> {
bool _busy = false;
@override
Widget build(BuildContext context) {
return _Page(
title: 'Profiles',
actions: [
FilledButton.tonalIcon(
onPressed: _busy ? null : _showAddDialog,
icon: const Icon(Icons.add),
label: const Text('Add profile'),
),
],
child: ListView.separated(
itemCount: widget.snapshot.profiles.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final profile = widget.snapshot.profiles[index];
return _ProfileTile(
profile: profile,
onTap: profile.active || _busy ? null : () => _confirmSwitch(profile),
trailing: _profileMenu(profile),
);
},
),
);
}
Widget _profileMenu(ProfileInfo profile) {
return PopupMenuButton<_ProfileAction>(
enabled: !_busy,
onSelected: (action) => _handleAction(action, profile),
itemBuilder: (context) => [
if (profile.active)
const PopupMenuItem(
value: _ProfileAction.logout,
child: ListTile(
leading: Icon(Icons.logout),
title: Text('Logout'),
contentPadding: EdgeInsets.zero,
),
),
PopupMenuItem(
value: _ProfileAction.remove,
enabled: !profile.active,
child: const ListTile(
leading: Icon(Icons.delete_outline),
title: Text('Remove'),
contentPadding: EdgeInsets.zero,
),
),
],
);
}
Future<void> _handleAction(
_ProfileAction action,
ProfileInfo profile,
) async {
switch (action) {
case _ProfileAction.logout:
await _confirmAndRun(
title: 'Logout from ${profile.name}?',
message:
'This disconnects the active profile and clears its session.',
run: widget.client.logoutActive,
);
case _ProfileAction.remove:
await _confirmAndRun(
title: 'Remove profile ${profile.name}?',
message: 'This deletes the profile from this device.',
run: () => widget.client.removeProfile(profile.name),
);
}
}
Future<void> _confirmSwitch(ProfileInfo profile) async {
await _confirmAndRun(
title: 'Switch to ${profile.name}?',
message: 'The connection will restart with the new profile.',
run: () => widget.client.switchProfile(profile.name),
);
}
Future<void> _showAddDialog() async {
final controller = TextEditingController();
final name = await showDialog<String>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add profile'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(labelText: 'Profile name'),
onSubmitted: (value) => Navigator.of(context).pop(value.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () =>
Navigator.of(context).pop(controller.text.trim()),
child: const Text('Add'),
),
],
);
},
);
if (name == null || name.isEmpty) {
return;
}
await _runBusy(() => widget.client.addProfile(name));
}
Future<void> _confirmAndRun({
required String title,
required String message,
required Future<void> Function() run,
}) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
),
],
);
},
);
if (confirm != true) {
return;
}
await _runBusy(run);
}
Future<void> _runBusy(Future<void> Function() action) async {
if (_busy) {
return;
}
setState(() => _busy = true);
try {
await action();
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
}
enum _ProfileAction { logout, remove }
class _SettingsPane extends StatefulWidget {
const _SettingsPane({required this.snapshot, required this.client});
final ClientSnapshot snapshot;
final DaemonClient client;
@override
State<_SettingsPane> createState() => _SettingsPaneState();
}
class _SettingsPaneState extends State<_SettingsPane> {
bool _writing = false;
@override
Widget build(BuildContext context) {
final settings = widget.snapshot.settings;
final disabled = _writing;
return _Page(
title: 'Settings',
child: ListView(
children: [
_InfoRow(label: 'Management URL', value: settings.managementUrl),
_InfoRow(label: 'Interface', value: settings.interfaceName),
_InfoRow(label: 'WireGuard port', value: '${settings.wireguardPort}'),
_InfoRow(label: 'MTU', value: '${settings.mtu}'),
const SizedBox(height: 16),
SwitchListTile(
value: settings.autoConnect,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(autoConnect: value)),
title: const Text('Connect on startup'),
),
SwitchListTile(
value: settings.allowSsh,
onChanged: disabled
? null
: (value) => _apply(settings.copyWith(allowSsh: value)),
title: const Text('Allow SSH'),
),
SwitchListTile(
value: settings.quantumResistance,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(quantumResistance: value)),
title: const Text('Quantum resistance'),
),
SwitchListTile(
value: settings.lazyConnection,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(lazyConnection: value)),
title: const Text('Lazy connections'),
),
SwitchListTile(
value: settings.blockInbound,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(blockInbound: value)),
title: const Text('Block inbound'),
),
SwitchListTile(
value: settings.notifications,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(notifications: value)),
title: const Text('Notifications'),
),
],
),
);
}
Future<void> _apply(ClientSettings updated) async {
setState(() => _writing = true);
try {
await widget.client.updateSettings(updated);
} finally {
if (mounted) {
setState(() => _writing = false);
}
}
}
}
class _StatusGlyph extends StatelessWidget {
const _StatusGlyph({required this.status});
final ConnectionStatus status;
@override
Widget build(BuildContext context) {
final color = switch (status) {
ConnectionStatus.connected => Colors.green,
ConnectionStatus.connecting => Colors.amber,
ConnectionStatus.awaitingLogin => Colors.lightBlue,
ConnectionStatus.error => Colors.red,
ConnectionStatus.disconnected => Colors.grey,
};
return Tooltip(
message: status.label,
child: Icon(Icons.circle, color: color, size: 18),
);
}
}
class _InfoRow extends StatelessWidget {
const _InfoRow({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 160,
child: Text(label, style: Theme.of(context).textTheme.labelLarge),
),
Expanded(child: Text(value)),
],
),
);
}
}
class _SectionLabel extends StatelessWidget {
const _SectionLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(text, style: Theme.of(context).textTheme.titleMedium),
);
}
}
class _ErrorBanner extends StatelessWidget {
const _ErrorBanner({required this.message});
final String message;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
color: colors.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.error_outline, color: colors.onErrorContainer),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: TextStyle(color: colors.onErrorContainer),
),
),
],
),
),
);
}
}
class _LoginBanner extends StatelessWidget {
const _LoginBanner({required this.pending});
final PendingLogin pending;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
color: colors.tertiaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sign in to continue',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colors.onTertiaryContainer,
),
),
const SizedBox(height: 8),
Text(
'A browser window opened to complete sign-in. '
'If it did not, open the URL below.',
style: TextStyle(color: colors.onTertiaryContainer),
),
const SizedBox(height: 12),
SelectableText(
pending.verificationUri,
style: TextStyle(color: colors.onTertiaryContainer),
),
const SizedBox(height: 4),
Text(
'Code: ${pending.userCode}',
style: TextStyle(color: colors.onTertiaryContainer),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
FilledButton.tonalIcon(
onPressed: () => _openUrl(pending.verificationUri),
icon: const Icon(Icons.open_in_new),
label: const Text('Open in browser'),
),
OutlinedButton.icon(
onPressed: () => _copy(context, pending.verificationUri),
icon: const Icon(Icons.copy),
label: const Text('Copy URL'),
),
],
),
],
),
),
);
}
Future<void> _openUrl(String url) async {
await openExternalUrl(url);
}
Future<void> _copy(BuildContext context, String url) async {
await Clipboard.setData(ClipboardData(text: url));
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URL copied')),
);
}
}
class _NetworkTile extends StatelessWidget {
const _NetworkTile({
required this.route,
required this.exitNodeMode,
required this.busy,
required this.onChanged,
});
final NetworkRoute route;
final bool exitNodeMode;
final bool busy;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) {
final subtitle = [
route.range,
if (route.domains.isNotEmpty) route.domains.join(', '),
].join(' ');
Widget leading;
if (busy) {
leading = const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
);
} else if (exitNodeMode) {
leading = IconButton(
icon: Icon(
route.selected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
onPressed: () => onChanged(!route.selected),
);
} else {
leading = Checkbox(
value: route.selected,
onChanged: (value) => onChanged(value ?? false),
);
}
return ListTile(
contentPadding: EdgeInsets.zero,
leading: leading,
title: Text(route.id),
subtitle: Text(subtitle),
trailing: route.isExitNode ? const Icon(Icons.public) : null,
onTap: busy ? null : () => onChanged(!route.selected),
);
}
}
class _ProfileTile extends StatelessWidget {
const _ProfileTile({required this.profile, this.onTap, this.trailing});
final ProfileInfo profile;
final VoidCallback? onTap;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
profile.active ? Icons.check_circle : Icons.circle_outlined,
),
title: Text(profile.name),
subtitle: profile.email == null ? null : Text(profile.email!),
onTap: onTap,
trailing: trailing,
);
}
}

View File

@@ -1,916 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:grpc/grpc.dart';
import 'generated/daemon.pbgrpc.dart' as daemon;
import 'models.dart';
import 'platform.dart';
const _userAgent = 'netbird-desktop-ui/development';
abstract class DaemonClient {
String get daemonAddr;
Stream<ClientSnapshot> watchSnapshot();
Stream<SystemNotification> watchEvents();
Stream<UpdateProgressEvent> watchUpdateRequests();
Future<void> connect();
Future<void> disconnect();
Future<void> bringUp();
Future<void> bringDown();
Future<DebugBundleResult> debugBundle({
required bool anonymize,
required bool systemInfo,
String? uploadUrl,
});
Future<DaemonLogLevel> getLogLevel();
Future<void> setLogLevel(DaemonLogLevel level);
Future<void> setSyncResponsePersistence(bool enabled);
Future<void> startCpuProfile();
Future<void> stopCpuProfile();
Future<TriggerUpdateResult> triggerUpdate();
Future<InstallerResult> getInstallerResult();
Future<void> updateSettings(ClientSettings updated);
Future<void> setNetworkSelection(String routeId, bool selected);
Future<void> setExitNode(String? routeId);
Future<void> switchProfile(String name);
Future<void> addProfile(String name);
Future<void> removeProfile(String name);
Future<void> logoutActive();
void dispose();
}
class GrpcDaemonClient implements DaemonClient {
GrpcDaemonClient({required this.daemonAddr}) {
_snapshot = ClientSnapshot.initial(daemonAddr);
_channel = _createChannel(daemonAddr);
_client = daemon.DaemonServiceClient(_channel);
}
@override
final String daemonAddr;
final _snapshots = StreamController<ClientSnapshot>.broadcast();
final _events = StreamController<SystemNotification>.broadcast();
final _updateRequests = StreamController<UpdateProgressEvent>.broadcast();
final _refreshInterval = const Duration(seconds: 2);
final _callTimeout = const Duration(seconds: 5);
final _ssoLoginTimeout = const Duration(minutes: 5);
final _installerPollTimeout = const Duration(minutes: 15);
late final ClientChannel _channel;
late final daemon.DaemonServiceClient _client;
late ClientSnapshot _snapshot;
Timer? _poller;
StreamSubscription<daemon.SystemEvent>? _eventSubscription;
var _started = false;
@override
Stream<ClientSnapshot> watchSnapshot() {
_start();
scheduleMicrotask(_emit);
return _snapshots.stream;
}
@override
Stream<SystemNotification> watchEvents() {
_start();
return _events.stream;
}
@override
Stream<UpdateProgressEvent> watchUpdateRequests() {
_start();
return _updateRequests.stream;
}
@override
Future<void> connect() async {
_setStatus(ConnectionStatus.connecting, clearError: true);
try {
await _runLoginFlow();
await _client.up(
daemon.UpRequest(username: _username()),
options: _options(timeout: const Duration(seconds: 30)),
);
} catch (error) {
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.error,
errorMessage: _formatError(error),
clearPendingLogin: true,
);
_emit();
return;
} finally {
await _refresh();
}
}
@override
Future<void> disconnect() async {
await _runRpc(() async {
await _client.down(daemon.DownRequest(), options: _options());
});
}
@override
Future<void> bringUp() async {
await _client.up(
daemon.UpRequest(username: _username()),
options: _options(timeout: const Duration(seconds: 30)),
);
}
@override
Future<void> bringDown() async {
await _client.down(
daemon.DownRequest(),
options: _options(timeout: const Duration(seconds: 15)),
);
}
@override
Future<DebugBundleResult> debugBundle({
required bool anonymize,
required bool systemInfo,
String? uploadUrl,
}) async {
final request = daemon.DebugBundleRequest(
anonymize: anonymize,
systemInfo: systemInfo,
uploadURL: uploadUrl ?? '',
);
final response = await _client.debugBundle(
request,
options: _options(timeout: const Duration(minutes: 2)),
);
return DebugBundleResult(
path: response.path,
uploadedKey: response.uploadedKey,
uploadFailureReason: response.uploadFailureReason,
);
}
@override
Future<DaemonLogLevel> getLogLevel() async {
final response = await _client.getLogLevel(
daemon.GetLogLevelRequest(),
options: _options(),
);
return _mapLogLevelFromProto(response.level);
}
@override
Future<void> setLogLevel(DaemonLogLevel level) async {
await _client.setLogLevel(
daemon.SetLogLevelRequest(level: _mapLogLevelToProto(level)),
options: _options(),
);
}
@override
Future<void> setSyncResponsePersistence(bool enabled) async {
await _client.setSyncResponsePersistence(
daemon.SetSyncResponsePersistenceRequest(enabled: enabled),
options: _options(),
);
}
@override
Future<void> startCpuProfile() async {
await _client.startCPUProfile(
daemon.StartCPUProfileRequest(),
options: _options(),
);
}
@override
Future<void> stopCpuProfile() async {
await _client.stopCPUProfile(
daemon.StopCPUProfileRequest(),
options: _options(),
);
}
@override
Future<TriggerUpdateResult> triggerUpdate() async {
final response = await _client.triggerUpdate(
daemon.TriggerUpdateRequest(),
options: _options(timeout: const Duration(seconds: 30)),
);
return TriggerUpdateResult(
success: response.success,
errorMessage: response.errorMsg,
);
}
@override
Future<InstallerResult> getInstallerResult() async {
final response = await _client.getInstallerResult(
daemon.InstallerResultRequest(),
options: _options(timeout: _installerPollTimeout),
);
return InstallerResult(
success: response.success,
errorMessage: response.errorMsg,
);
}
@override
Future<void> updateSettings(ClientSettings updated) async {
await _runRpc(() async {
final activeProfile = _snapshot.activeProfile.name;
await _client.setConfig(
daemon.SetConfigRequest(
username: _username(),
profileName: activeProfile,
managementUrl: updated.managementUrl,
rosenpassEnabled: updated.quantumResistance,
serverSSHAllowed: updated.allowSsh,
disableAutoConnect: !updated.autoConnect,
disableNotifications: !updated.notifications,
lazyConnectionEnabled: updated.lazyConnection,
blockInbound: updated.blockInbound,
),
options: _options(timeout: const Duration(seconds: 10)),
);
});
}
@override
Future<void> setNetworkSelection(String routeId, bool selected) async {
await _runRpc(() async {
final request = daemon.SelectNetworksRequest(networkIDs: [routeId]);
if (selected) {
await _client.selectNetworks(request, options: _options());
} else {
await _client.deselectNetworks(request, options: _options());
}
});
}
@override
Future<void> setExitNode(String? routeId) async {
await _runRpc(() async {
final exitNodeIds = _snapshot.networks
.where((route) => route.isExitNode)
.map((route) => route.id)
.toList();
if (exitNodeIds.isNotEmpty) {
await _client.deselectNetworks(
daemon.SelectNetworksRequest(networkIDs: exitNodeIds),
options: _options(),
);
}
if (routeId != null) {
await _client.selectNetworks(
daemon.SelectNetworksRequest(networkIDs: [routeId]),
options: _options(),
);
}
});
}
@override
Future<void> switchProfile(String name) async {
await _runRpc(() async {
await _client.switchProfile(
daemon.SwitchProfileRequest(profileName: name, username: _username()),
options: _options(),
);
});
}
@override
Future<void> addProfile(String name) async {
await _runRpc(() async {
await _client.addProfile(
daemon.AddProfileRequest(profileName: name, username: _username()),
options: _options(),
);
});
}
@override
Future<void> removeProfile(String name) async {
await _runRpc(() async {
await _client.removeProfile(
daemon.RemoveProfileRequest(profileName: name, username: _username()),
options: _options(),
);
});
}
@override
Future<void> logoutActive() async {
await _runRpc(() async {
final active = _snapshot.activeProfile.name;
await _client.logout(
daemon.LogoutRequest(profileName: active, username: _username()),
options: _options(timeout: const Duration(seconds: 15)),
);
});
}
@override
void dispose() {
_poller?.cancel();
unawaited(_eventSubscription?.cancel() ?? Future<void>.value());
_events.close();
_updateRequests.close();
_snapshots.close();
unawaited(_channel.shutdown());
}
void _start() {
if (_started) {
return;
}
_started = true;
unawaited(_refresh());
_poller = Timer.periodic(_refreshInterval, (_) {
unawaited(_refresh());
});
_eventSubscription = _client
.subscribeEvents(daemon.SubscribeRequest(), options: _options())
.listen(
(event) {
_checkUpdateMetadata(event);
final notification = _mapSystemEvent(event);
if (notification != null && !_events.isClosed) {
_events.add(notification);
}
unawaited(_refresh());
},
onError: (_) {},
);
}
DaemonLogLevel _mapLogLevelFromProto(daemon.LogLevel level) {
return switch (level) {
daemon.LogLevel.PANIC => DaemonLogLevel.panic,
daemon.LogLevel.FATAL => DaemonLogLevel.fatal,
daemon.LogLevel.ERROR => DaemonLogLevel.error,
daemon.LogLevel.WARN => DaemonLogLevel.warn,
daemon.LogLevel.INFO => DaemonLogLevel.info,
daemon.LogLevel.DEBUG => DaemonLogLevel.debug,
daemon.LogLevel.TRACE => DaemonLogLevel.trace,
_ => DaemonLogLevel.unknown,
};
}
daemon.LogLevel _mapLogLevelToProto(DaemonLogLevel level) {
return switch (level) {
DaemonLogLevel.panic => daemon.LogLevel.PANIC,
DaemonLogLevel.fatal => daemon.LogLevel.FATAL,
DaemonLogLevel.error => daemon.LogLevel.ERROR,
DaemonLogLevel.warn => daemon.LogLevel.WARN,
DaemonLogLevel.info => daemon.LogLevel.INFO,
DaemonLogLevel.debug => daemon.LogLevel.DEBUG,
DaemonLogLevel.trace => daemon.LogLevel.TRACE,
DaemonLogLevel.unknown => daemon.LogLevel.UNKNOWN,
};
}
void _checkUpdateMetadata(daemon.SystemEvent event) {
final action = event.metadata['progress_window'];
if (action != 'show') {
return;
}
final version = event.metadata['version'] ?? 'unknown';
if (!_updateRequests.isClosed) {
_updateRequests.add(UpdateProgressEvent(version: version));
}
}
SystemNotification? _mapSystemEvent(daemon.SystemEvent event) {
final severity = switch (event.severity) {
daemon.SystemEvent_Severity.WARNING => NotificationSeverity.warning,
daemon.SystemEvent_Severity.ERROR => NotificationSeverity.error,
daemon.SystemEvent_Severity.CRITICAL => NotificationSeverity.critical,
_ => NotificationSeverity.info,
};
final category = switch (event.category) {
daemon.SystemEvent_Category.NETWORK => NotificationCategory.network,
daemon.SystemEvent_Category.DNS => NotificationCategory.dns,
daemon.SystemEvent_Category.AUTHENTICATION =>
NotificationCategory.authentication,
daemon.SystemEvent_Category.CONNECTIVITY =>
NotificationCategory.connectivity,
daemon.SystemEvent_Category.SYSTEM => NotificationCategory.system,
_ => NotificationCategory.system,
};
return SystemNotification(
severity: severity,
category: category,
message: event.message,
userMessage: event.userMessage,
id: event.metadata['id'],
);
}
Future<void> _runLoginFlow() async {
final loginResponse = await _client.login(
daemon.LoginRequest(
isUnixDesktopClient: Platform.isLinux,
profileName: _snapshot.activeProfile.name,
username: _username(),
hint: _snapshot.activeProfile.email,
),
options: _options(timeout: const Duration(seconds: 30)),
);
if (!loginResponse.needsSSOLogin) {
return;
}
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.awaitingLogin,
pendingLogin: PendingLogin(
verificationUri: loginResponse.verificationURIComplete,
userCode: loginResponse.userCode,
),
);
_emit();
if (loginResponse.verificationURIComplete.isNotEmpty) {
await openExternalUrl(loginResponse.verificationURIComplete);
}
await _client.waitSSOLogin(
daemon.WaitSSOLoginRequest(userCode: loginResponse.userCode),
options: _options(timeout: _ssoLoginTimeout),
);
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.connecting,
clearPendingLogin: true,
);
_emit();
}
Future<void> _runRpc(Future<void> Function() action) async {
try {
_snapshot = _snapshot.copyWith(clearError: true);
_emit();
await action();
} catch (error) {
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.error,
errorMessage: _formatError(error),
);
_emit();
} finally {
await _refresh();
}
}
Future<void> _refresh() async {
try {
final status = await _client.status(
daemon.StatusRequest(),
options: _options(),
);
final activeProfile = await _loadActiveProfile();
final profiles = await _loadProfiles(activeProfile);
final networks = await _loadNetworks();
final settings = await _loadSettings(activeProfile);
final mappedStatus = _mapStatus(status.status);
final preserveAwaiting =
_snapshot.status == ConnectionStatus.awaitingLogin &&
mappedStatus != ConnectionStatus.connected;
_snapshot = ClientSnapshot(
daemonAddr: daemonAddr,
daemonVersion: status.daemonVersion.isEmpty
? 'unknown'
: status.daemonVersion,
status: preserveAwaiting ? ConnectionStatus.awaitingLogin : mappedStatus,
activeProfile: activeProfile,
profiles: profiles,
networks: networks,
settings: settings,
pendingLogin: preserveAwaiting ? _snapshot.pendingLogin : null,
);
} catch (error) {
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.error,
errorMessage: _formatError(error),
);
}
_emit();
}
Future<ProfileInfo> _loadActiveProfile() async {
try {
final active = await _client.getActiveProfile(
daemon.GetActiveProfileRequest(),
options: _options(),
);
if (active.profileName.isNotEmpty) {
return ProfileInfo(
name: active.profileName,
email: _snapshot.activeProfile.email,
active: true,
);
}
} catch (_) {
// Keep the status pane usable even when optional profile RPCs fail.
}
return _snapshot.activeProfile;
}
Future<List<ProfileInfo>> _loadProfiles(ProfileInfo activeProfile) async {
try {
final response = await _client.listProfiles(
daemon.ListProfilesRequest(username: _username()),
options: _options(),
);
final profiles = response.profiles.map((profile) {
return ProfileInfo(name: profile.name, active: profile.isActive);
}).toList();
if (profiles.isNotEmpty) {
return profiles;
}
} catch (_) {
// Profile listing is not required for core connection status.
}
return [activeProfile];
}
Future<List<NetworkRoute>> _loadNetworks() async {
try {
final response = await _client.listNetworks(
daemon.ListNetworksRequest(),
options: _options(),
);
return _mapNetworks(response.routes);
} catch (_) {
return _snapshot.networks;
}
}
Future<ClientSettings> _loadSettings(ProfileInfo activeProfile) async {
try {
final config = await _client.getConfig(
daemon.GetConfigRequest(
profileName: activeProfile.name,
username: _username(),
),
options: _options(),
);
return ClientSettings(
managementUrl: config.managementUrl.isEmpty
? 'https://api.netbird.io'
: config.managementUrl,
interfaceName: config.interfaceName.isEmpty
? 'wt0'
: config.interfaceName,
wireguardPort: config.hasWireguardPort()
? config.wireguardPort.toInt()
: 51820,
mtu: config.hasMtu() ? config.mtu.toInt() : 1280,
autoConnect: !config.disableAutoConnect,
allowSsh: config.serverSSHAllowed,
quantumResistance: config.rosenpassEnabled,
notifications: !config.disableNotifications,
lazyConnection: config.lazyConnectionEnabled,
blockInbound: config.blockInbound,
);
} catch (_) {
return _snapshot.settings;
}
}
List<NetworkRoute> _mapNetworks(Iterable<daemon.Network> routes) {
final rangeCounts = <String, int>{};
for (final route in routes) {
if (route.domains.isEmpty) {
rangeCounts.update(
route.range,
(count) => count + 1,
ifAbsent: () => 1,
);
}
}
return routes.map((route) {
final resolvedIps = route.resolvedIPs.map((domain, ipList) {
return MapEntry(domain, ipList.ips.toList());
});
return NetworkRoute(
id: route.iD,
range: route.range,
selected: route.selected,
domains: route.domains.toList(),
resolvedIps: resolvedIps,
overlapping:
route.domains.isEmpty && (rangeCounts[route.range] ?? 0) > 1,
);
}).toList()
..sort((a, b) => a.id.toLowerCase().compareTo(b.id.toLowerCase()));
}
CallOptions _options({Duration? timeout}) {
return CallOptions(timeout: timeout ?? _callTimeout);
}
void _setStatus(
ConnectionStatus status, {
bool clearError = false,
bool clearPendingLogin = false,
}) {
_snapshot = _snapshot.copyWith(
status: status,
clearError: clearError,
clearPendingLogin: clearPendingLogin,
);
_emit();
}
void _emit() {
if (!_snapshots.isClosed) {
_snapshots.add(_snapshot);
}
}
}
class FakeDaemonClient implements DaemonClient {
FakeDaemonClient({required this.daemonAddr}) {
scheduleMicrotask(_emit);
}
@override
final String daemonAddr;
final _snapshots = StreamController<ClientSnapshot>.broadcast();
late ClientSnapshot _snapshot = ClientSnapshot.initial(daemonAddr).copyWith(
daemonVersion: 'development',
profiles: const [
ProfileInfo(name: 'default', email: 'user@example.com', active: true),
ProfileInfo(name: 'staging', active: false),
],
networks: const [
NetworkRoute(id: 'office', range: '10.10.0.0/16', selected: true),
NetworkRoute(id: 'prod', range: '10.20.0.0/16'),
NetworkRoute(id: 'exit-us', range: '0.0.0.0/0'),
],
);
@override
Stream<ClientSnapshot> watchSnapshot() {
scheduleMicrotask(_emit);
return _snapshots.stream;
}
@override
Stream<SystemNotification> watchEvents() =>
const Stream<SystemNotification>.empty();
@override
Stream<UpdateProgressEvent> watchUpdateRequests() =>
const Stream<UpdateProgressEvent>.empty();
@override
Future<void> connect() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connecting);
_emit();
await Future<void>.delayed(const Duration(milliseconds: 450));
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connected);
_emit();
}
@override
Future<void> disconnect() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
_emit();
}
@override
Future<void> bringUp() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connected);
_emit();
}
@override
Future<void> bringDown() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
_emit();
}
@override
Future<DebugBundleResult> debugBundle({
required bool anonymize,
required bool systemInfo,
String? uploadUrl,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 400));
return DebugBundleResult(
path: '/tmp/netbird-debug.tar.gz',
uploadedKey: uploadUrl == null ? '' : 'fake-upload-key',
);
}
@override
Future<DaemonLogLevel> getLogLevel() async => DaemonLogLevel.info;
@override
Future<void> setLogLevel(DaemonLogLevel level) async {}
@override
Future<void> setSyncResponsePersistence(bool enabled) async {}
@override
Future<void> startCpuProfile() async {}
@override
Future<void> stopCpuProfile() async {}
@override
Future<TriggerUpdateResult> triggerUpdate() async {
return const TriggerUpdateResult(success: true);
}
@override
Future<InstallerResult> getInstallerResult() async {
await Future<void>.delayed(const Duration(seconds: 2));
return const InstallerResult(success: true);
}
@override
Future<void> updateSettings(ClientSettings updated) async {
_snapshot = _snapshot.copyWith(settings: updated);
_emit();
}
@override
Future<void> setNetworkSelection(String routeId, bool selected) async {
final next = _snapshot.networks.map((route) {
if (route.id != routeId) {
return route;
}
return NetworkRoute(
id: route.id,
range: route.range,
domains: route.domains,
resolvedIps: route.resolvedIps,
overlapping: route.overlapping,
selected: selected,
);
}).toList();
_snapshot = _snapshot.copyWith(networks: next);
_emit();
}
@override
Future<void> setExitNode(String? routeId) async {
final next = _snapshot.networks.map((route) {
if (!route.isExitNode) {
return route;
}
return NetworkRoute(
id: route.id,
range: route.range,
domains: route.domains,
resolvedIps: route.resolvedIps,
overlapping: route.overlapping,
selected: route.id == routeId,
);
}).toList();
_snapshot = _snapshot.copyWith(networks: next);
_emit();
}
@override
Future<void> switchProfile(String name) async {
final profiles = _snapshot.profiles.map((profile) {
return ProfileInfo(
name: profile.name,
email: profile.email,
active: profile.name == name,
);
}).toList();
final active = profiles.firstWhere(
(profile) => profile.active,
orElse: () => _snapshot.activeProfile,
);
_snapshot = _snapshot.copyWith(profiles: profiles, activeProfile: active);
_emit();
}
@override
Future<void> addProfile(String name) async {
final profiles = [
..._snapshot.profiles,
ProfileInfo(name: name, active: false),
];
_snapshot = _snapshot.copyWith(profiles: profiles);
_emit();
}
@override
Future<void> removeProfile(String name) async {
final profiles = _snapshot.profiles
.where((profile) => profile.name != name)
.toList();
_snapshot = _snapshot.copyWith(profiles: profiles);
_emit();
}
@override
Future<void> logoutActive() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
_emit();
}
@override
void dispose() {
_snapshots.close();
}
void _emit() {
if (!_snapshots.isClosed) {
_snapshots.add(_snapshot);
}
}
}
ClientChannel _createChannel(String daemonAddr) {
final options = ChannelOptions(
credentials: const ChannelCredentials.insecure(),
userAgent: _userAgent,
connectTimeout: const Duration(seconds: 3),
);
if (daemonAddr.startsWith('unix://')) {
final path = daemonAddr.substring('unix://'.length);
return ClientChannel(
InternetAddress(path, type: InternetAddressType.unix),
port: 0,
options: options,
);
}
final uri = daemonAddr.contains('://')
? Uri.parse(daemonAddr)
: Uri.parse('tcp://$daemonAddr');
final host = uri.host.isEmpty ? '127.0.0.1' : uri.host;
final port = uri.hasPort ? uri.port : 41731;
return ClientChannel(host, port: port, options: options);
}
ConnectionStatus _mapStatus(String status) {
return switch (status) {
'Connected' => ConnectionStatus.connected,
'Connecting' => ConnectionStatus.connecting,
'Idle' || 'SessionExpired' => ConnectionStatus.disconnected,
_ => ConnectionStatus.error,
};
}
String _username() {
if (Platform.isWindows) {
final username = Platform.environment['USERNAME'] ?? '';
final domain = Platform.environment['USERDOMAIN'] ?? '';
if (domain.isNotEmpty && username.isNotEmpty) {
return '$domain\\$username';
}
return username;
}
return Platform.environment['USER'] ?? Platform.environment['LOGNAME'] ?? '';
}
String _formatError(Object error) {
if (error is GrpcError) {
return error.message ?? error.toString();
}
return error.toString();
}

View File

@@ -1,460 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'daemon_client.dart';
import 'models.dart';
import 'platform.dart';
const _defaultUploadUrl = 'https://upload.netbird.io/';
class DebugScreen extends StatefulWidget {
const DebugScreen({required this.client, super.key});
final DaemonClient client;
@override
State<DebugScreen> createState() => _DebugScreenState();
}
class _DebugScreenState extends State<DebugScreen> {
final _uploadUrlController =
TextEditingController(text: _defaultUploadUrl);
final _durationController = TextEditingController(text: '1');
bool _anonymize = false;
bool _systemInfo = true;
bool _upload = true;
bool _runWithTrace = true;
bool _busy = false;
String _status = '';
double? _progress;
@override
void dispose() {
_uploadUrlController.dispose();
_durationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Debug', style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 16),
Text(
'Create a debug bundle to help troubleshoot issues with NetBird.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
Expanded(
child: ListView(
children: [
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _anonymize,
onChanged: _busy
? null
: (value) => setState(() => _anonymize = value ?? false),
title: const Text(
'Anonymize sensitive information (public IPs, domains, ...)',
),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _systemInfo,
onChanged: _busy
? null
: (value) => setState(() => _systemInfo = value ?? false),
title: const Text(
'Include system information (routes, interfaces, ...)',
),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _upload,
onChanged: _busy
? null
: (value) => setState(() => _upload = value ?? false),
title: const Text('Upload bundle automatically after creation'),
),
if (_upload)
Padding(
padding: const EdgeInsets.only(left: 32, bottom: 8, top: 4),
child: TextField(
controller: _uploadUrlController,
enabled: !_busy,
decoration: const InputDecoration(
labelText: 'Debug upload URL',
border: OutlineInputBorder(),
),
),
),
const Divider(height: 32),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _runWithTrace,
onChanged: _busy
? null
: (value) =>
setState(() => _runWithTrace = value ?? false),
title: const Text(
'Run with trace logs before creating bundle',
),
),
if (_runWithTrace)
Padding(
padding: const EdgeInsets.only(left: 32, top: 4),
child: Row(
children: [
const Text('Run for'),
const SizedBox(width: 12),
SizedBox(
width: 80,
child: TextField(
controller: _durationController,
enabled: !_busy,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
isDense: true,
),
),
),
const SizedBox(width: 8),
Text(_durationLabel()),
],
),
),
if (_runWithTrace)
const Padding(
padding: EdgeInsets.only(left: 32, top: 8),
child: Text(
'Note: NetBird will be brought up and down during collection.',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
const SizedBox(height: 24),
if (_status.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(_status),
),
if (_progress != null)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: LinearProgressIndicator(value: _progress),
),
Align(
alignment: Alignment.centerLeft,
child: FilledButton.icon(
onPressed: _busy ? null : _onCreate,
icon: _busy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.archive_outlined),
label: const Text('Create Debug Bundle'),
),
),
],
),
),
],
),
);
}
String _durationLabel() {
final value = int.tryParse(_durationController.text) ?? 0;
return value == 1 ? 'minute' : 'minutes';
}
Future<void> _onCreate() async {
final uploadUrl = _upload ? _uploadUrlController.text.trim() : null;
if (_upload && (uploadUrl == null || uploadUrl.isEmpty)) {
setState(() => _status = 'Upload URL is required when upload is enabled');
return;
}
Duration? traceDuration;
if (_runWithTrace) {
final minutes = int.tryParse(_durationController.text);
if (minutes == null || minutes < 1) {
setState(() => _status = 'Duration must be a number ≥ 1');
return;
}
traceDuration = Duration(minutes: minutes);
}
setState(() {
_busy = true;
_status = '';
_progress = null;
});
try {
DebugBundleResult result;
if (traceDuration != null) {
result = await _runWithTraceLogs(
duration: traceDuration,
uploadUrl: uploadUrl,
);
} else {
setState(() => _status = 'Creating debug bundle...');
result = await widget.client.debugBundle(
anonymize: _anonymize,
systemInfo: _systemInfo,
uploadUrl: uploadUrl,
);
}
if (!mounted) {
return;
}
setState(() => _status = 'Bundle created successfully');
await _showResultDialog(result);
} catch (error) {
if (!mounted) {
return;
}
setState(() {
_status = 'Error: $error';
_progress = null;
});
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<DebugBundleResult> _runWithTraceLogs({
required Duration duration,
required String? uploadUrl,
}) async {
final initialLevel = await widget.client.getLogLevel();
final wasTrace = initialLevel == DaemonLogLevel.trace;
var levelChanged = false;
var persistenceEnabled = false;
var cpuProfileStarted = false;
try {
if (!wasTrace) {
await widget.client.setLogLevel(DaemonLogLevel.trace);
levelChanged = true;
}
try {
await widget.client.bringDown();
} catch (_) {
// Already down is fine; daemon returns OK either way.
}
await Future<void>.delayed(const Duration(seconds: 1));
try {
await widget.client.setSyncResponsePersistence(true);
persistenceEnabled = true;
} catch (_) {
// Persistence is best-effort — the bundle still works without it.
}
await widget.client.bringUp();
await Future<void>.delayed(const Duration(seconds: 3));
try {
await widget.client.startCpuProfile();
cpuProfileStarted = true;
} catch (_) {
// CPU profiling is optional.
}
await _trackProgress(duration);
if (cpuProfileStarted) {
try {
await widget.client.stopCpuProfile();
} catch (_) {}
}
if (!mounted) {
return const DebugBundleResult(path: '');
}
setState(() {
_status = 'Creating debug bundle with collected logs...';
_progress = null;
});
return await widget.client.debugBundle(
anonymize: _anonymize,
systemInfo: _systemInfo,
uploadUrl: uploadUrl,
);
} finally {
if (levelChanged) {
try {
await widget.client.setLogLevel(initialLevel);
} catch (_) {}
}
if (persistenceEnabled) {
try {
await widget.client.setSyncResponsePersistence(false);
} catch (_) {}
}
}
}
Future<void> _trackProgress(Duration total) async {
final start = DateTime.now();
final end = start.add(total);
setState(() {
_progress = 0;
_status = 'Running with trace logs... ${_formatRemaining(total)} remaining';
});
while (DateTime.now().isBefore(end)) {
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!mounted) {
return;
}
final elapsed = DateTime.now().difference(start);
final fraction = (elapsed.inMilliseconds / total.inMilliseconds).clamp(
0.0,
1.0,
);
final remaining = end.difference(DateTime.now());
setState(() {
_progress = fraction;
_status =
'Running with trace logs... ${_formatRemaining(remaining < Duration.zero ? Duration.zero : remaining)} remaining';
});
}
}
String _formatRemaining(Duration d) {
final hours = d.inHours.toString().padLeft(2, '0');
final minutes = (d.inMinutes % 60).toString().padLeft(2, '0');
final seconds = (d.inSeconds % 60).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
}
Future<void> _showResultDialog(DebugBundleResult result) async {
if (!mounted) {
return;
}
await showDialog<void>(
context: context,
builder: (context) => _DebugResultDialog(result: result),
);
}
}
class _DebugResultDialog extends StatelessWidget {
const _DebugResultDialog({required this.result});
final DebugBundleResult result;
@override
Widget build(BuildContext context) {
final folder = _parentFolder(result.path);
String title;
Widget body;
if (result.uploadFailed) {
title = 'Upload Failed';
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Bundle upload failed:\n${result.uploadFailureReason}'),
const SizedBox(height: 12),
SelectableText('Local copy: ${result.path}'),
],
);
} else if (result.uploaded) {
title = 'Upload Successful';
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Bundle uploaded successfully.'),
const SizedBox(height: 12),
const Text('Upload key:'),
SelectableText(result.uploadedKey),
const SizedBox(height: 12),
SelectableText('Local copy: ${result.path}'),
],
);
} else {
title = 'Debug Bundle Created';
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Bundle created locally at:\n${result.path}'),
const SizedBox(height: 8),
const Text(
'Administrator privileges may be required to access the file.',
style: TextStyle(fontStyle: FontStyle.italic),
),
],
);
}
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(child: body),
actions: [
if (result.uploaded)
TextButton.icon(
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: result.uploadedKey),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Upload key copied')),
);
}
},
icon: const Icon(Icons.copy),
label: const Text('Copy key'),
),
TextButton.icon(
onPressed: result.path.isEmpty
? null
: () => openExternalUrl(result.path),
icon: const Icon(Icons.description_outlined),
label: const Text('Open file'),
),
TextButton.icon(
onPressed: folder.isEmpty ? null : () => openExternalUrl(folder),
icon: const Icon(Icons.folder_open),
label: const Text('Open folder'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
String _parentFolder(String path) {
if (path.isEmpty) {
return '';
}
final lastSlash = path.lastIndexOf(RegExp(r'[/\\]'));
return lastSlash <= 0 ? '' : path.substring(0, lastSlash);
}
}

View File

@@ -1,434 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:local_notifier/local_notifier.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
import 'daemon_client.dart';
import 'models.dart';
import 'platform.dart';
const uiVersion = '0.1.0';
const _githubUrl = 'https://github.com/netbirdio/netbird';
const _downloadUrl = 'https://netbird.io/download/';
class TabIndex {
static const status = 0;
static const networks = 1;
static const profiles = 2;
static const settings = 3;
static const debug = 4;
}
/// Owns native desktop integration: window lifecycle (hide on close), system
/// tray icon and menu, and OS-level notifications driven by daemon events.
class DesktopIntegration with TrayListener, WindowListener {
DesktopIntegration({required this.client});
final DaemonClient client;
final _tabRequests = StreamController<int>.broadcast();
StreamSubscription<ClientSnapshot>? _snapshotSub;
StreamSubscription<SystemNotification>? _eventSub;
ClientSnapshot? _lastSnapshot;
String? _lastMenuKey;
bool _disposed = false;
bool _settingsBusy = false;
Stream<int> get tabRequests => _tabRequests.stream;
static const _trayMenuConnect = 'connect';
static const _trayMenuDisconnect = 'disconnect';
static const _trayMenuShow = 'show';
static const _trayMenuQuit = 'quit';
static const _trayMenuAllowSSH = 'settings.allow_ssh';
static const _trayMenuAutoConnect = 'settings.auto_connect';
static const _trayMenuQuantum = 'settings.quantum';
static const _trayMenuLazy = 'settings.lazy';
static const _trayMenuBlockInbound = 'settings.block_inbound';
static const _trayMenuNotifications = 'settings.notifications';
static const _trayMenuAdvancedSettings = 'open.settings';
static const _trayMenuDebugBundle = 'open.debug';
static const _trayMenuNetworks = 'open.networks';
static const _trayMenuManageProfiles = 'open.profiles';
static const _trayMenuLogout = 'profile.logout';
static const _trayMenuGithub = 'about.github';
static const _trayMenuDownload = 'about.download';
static const _profileSwitchPrefix = 'profile.switch:';
Future<void> initialize() async {
await localNotifier.setup(appName: 'NetBird');
await windowManager.setPreventClose(true);
windowManager.addListener(this);
trayManager.addListener(this);
await _applyTrayIcon(ConnectionStatus.disconnected);
await trayManager.setToolTip('NetBird');
await _refreshTrayMenu(null);
_snapshotSub = client.watchSnapshot().listen(_onSnapshot);
_eventSub = client.watchEvents().listen(_onEvent);
}
Future<void> dispose() async {
if (_disposed) {
return;
}
_disposed = true;
await _snapshotSub?.cancel();
await _eventSub?.cancel();
await _tabRequests.close();
windowManager.removeListener(this);
trayManager.removeListener(this);
await trayManager.destroy();
}
@override
void onWindowClose() {
unawaited(_handleWindowClose());
}
Future<void> _handleWindowClose() async {
final prevent = await windowManager.isPreventClose();
if (prevent) {
await windowManager.hide();
}
}
@override
void onTrayIconMouseDown() {
if (Platform.isMacOS) {
unawaited(trayManager.popUpContextMenu());
} else {
unawaited(_showWindow());
}
}
@override
void onTrayIconRightMouseDown() {
unawaited(trayManager.popUpContextMenu());
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
final key = menuItem.key;
if (key == null) {
return;
}
if (key.startsWith(_profileSwitchPrefix)) {
final name = key.substring(_profileSwitchPrefix.length);
unawaited(_switchProfile(name));
return;
}
switch (key) {
case _trayMenuConnect:
unawaited(client.connect());
case _trayMenuDisconnect:
unawaited(client.disconnect());
case _trayMenuShow:
unawaited(_showWindow());
case _trayMenuQuit:
unawaited(_quit());
case _trayMenuAllowSSH:
unawaited(_toggleSetting((s) => s.copyWith(allowSsh: !s.allowSsh)));
case _trayMenuAutoConnect:
unawaited(
_toggleSetting((s) => s.copyWith(autoConnect: !s.autoConnect)),
);
case _trayMenuQuantum:
unawaited(
_toggleSetting(
(s) => s.copyWith(quantumResistance: !s.quantumResistance),
),
);
case _trayMenuLazy:
unawaited(
_toggleSetting(
(s) => s.copyWith(lazyConnection: !s.lazyConnection),
),
);
case _trayMenuBlockInbound:
unawaited(
_toggleSetting(
(s) => s.copyWith(blockInbound: !s.blockInbound),
),
);
case _trayMenuNotifications:
unawaited(
_toggleSetting(
(s) => s.copyWith(notifications: !s.notifications),
),
);
case _trayMenuAdvancedSettings:
unawaited(_openTab(TabIndex.settings));
case _trayMenuDebugBundle:
unawaited(_openTab(TabIndex.debug));
case _trayMenuNetworks:
unawaited(_openTab(TabIndex.networks));
case _trayMenuManageProfiles:
unawaited(_openTab(TabIndex.profiles));
case _trayMenuLogout:
unawaited(client.logoutActive());
case _trayMenuGithub:
unawaited(openExternalUrl(_githubUrl));
case _trayMenuDownload:
unawaited(openExternalUrl(_downloadUrl));
}
}
Future<void> _openTab(int index) async {
if (!_tabRequests.isClosed) {
_tabRequests.add(index);
}
await _showWindow();
}
Future<void> _toggleSetting(
ClientSettings Function(ClientSettings) mutate,
) async {
if (_settingsBusy) {
return;
}
final snapshot = _lastSnapshot;
if (snapshot == null) {
return;
}
_settingsBusy = true;
try {
await client.updateSettings(mutate(snapshot.settings));
} finally {
_settingsBusy = false;
}
}
Future<void> _switchProfile(String name) async {
final snapshot = _lastSnapshot;
if (snapshot == null || snapshot.activeProfile.name == name) {
return;
}
await client.switchProfile(name);
}
Future<void> _showWindow() async {
await windowManager.show();
await windowManager.focus();
}
Future<void> _quit() async {
await dispose();
await windowManager.setPreventClose(false);
await windowManager.destroy();
}
void _onSnapshot(ClientSnapshot snapshot) {
final previous = _lastSnapshot;
_lastSnapshot = snapshot;
if (previous == null || previous.status != snapshot.status) {
unawaited(_applyTrayIcon(snapshot.status));
unawaited(trayManager.setToolTip('NetBird — ${snapshot.status.label}'));
}
unawaited(_refreshTrayMenu(snapshot));
}
void _onEvent(SystemNotification event) {
if (event.userMessage.isEmpty) {
return;
}
final notificationsEnabled =
_lastSnapshot?.settings.notifications ?? true;
final critical = event.severity == NotificationSeverity.critical;
if (!notificationsEnabled && !critical) {
return;
}
final title = '${_severityPrefix(event.severity)} [${event.category.label}]';
final body = event.id == null
? event.userMessage
: '${event.userMessage} (id: ${event.id})';
unawaited(
LocalNotification(title: title, body: body).show(),
);
}
Future<void> _applyTrayIcon(ConnectionStatus status) async {
final basename = switch (status) {
ConnectionStatus.connected => 'connected',
ConnectionStatus.connecting ||
ConnectionStatus.awaitingLogin => 'connecting',
ConnectionStatus.error => 'error',
ConnectionStatus.disconnected => 'disconnected',
};
final asset = Platform.isMacOS
? 'assets/tray/$basename-macos.png'
: 'assets/tray/$basename.png';
await trayManager.setIcon(asset, isTemplate: Platform.isMacOS);
}
Future<void> _refreshTrayMenu(ClientSnapshot? snapshot) async {
final key = _menuKey(snapshot);
if (key == _lastMenuKey) {
return;
}
_lastMenuKey = key;
final connected = snapshot?.status == ConnectionStatus.connected;
final connecting = snapshot?.status == ConnectionStatus.connecting ||
snapshot?.status == ConnectionStatus.awaitingLogin;
final statusLabel =
snapshot?.status.label ?? ConnectionStatus.disconnected.label;
final settings = snapshot?.settings ?? const ClientSettings();
final activeName = snapshot?.activeProfile.name ?? 'unknown';
final email = snapshot?.activeProfile.email;
final daemonVersion = snapshot?.daemonVersion ?? 'unknown';
final profileItems = <MenuItem>[];
final profiles = snapshot?.profiles ?? const <ProfileInfo>[];
for (final profile in profiles) {
profileItems.add(
MenuItem.checkbox(
key: '$_profileSwitchPrefix${profile.name}',
label: profile.name,
checked: profile.active,
),
);
}
if (profileItems.isNotEmpty) {
profileItems.add(MenuItem.separator());
}
profileItems
..add(MenuItem(key: _trayMenuManageProfiles, label: 'Manage Profiles'))
..add(
MenuItem(
key: _trayMenuLogout,
label: 'Deregister',
disabled: !connected,
),
);
await trayManager.setContextMenu(
Menu(
items: [
MenuItem(label: statusLabel, disabled: true),
MenuItem.submenu(
label: 'Profile: $activeName',
submenu: Menu(items: profileItems),
),
if (email != null && email.isNotEmpty)
MenuItem(label: '($email)', disabled: true),
MenuItem.separator(),
MenuItem(
key: _trayMenuConnect,
label: 'Connect',
disabled: connected || connecting,
),
MenuItem(
key: _trayMenuDisconnect,
label: 'Disconnect',
disabled: !connected,
),
MenuItem.separator(),
MenuItem.submenu(
label: 'Settings',
submenu: Menu(
items: [
MenuItem.checkbox(
key: _trayMenuAllowSSH,
label: 'Allow SSH',
checked: settings.allowSsh,
),
MenuItem.checkbox(
key: _trayMenuAutoConnect,
label: 'Connect on Startup',
checked: settings.autoConnect,
),
MenuItem.checkbox(
key: _trayMenuQuantum,
label: 'Enable Quantum-Resistance',
checked: settings.quantumResistance,
),
MenuItem.checkbox(
key: _trayMenuLazy,
label: 'Enable Lazy Connections',
checked: settings.lazyConnection,
),
MenuItem.checkbox(
key: _trayMenuBlockInbound,
label: 'Block Inbound Connections',
checked: settings.blockInbound,
),
MenuItem.checkbox(
key: _trayMenuNotifications,
label: 'Notifications',
checked: settings.notifications,
),
MenuItem.separator(),
MenuItem(
key: _trayMenuAdvancedSettings,
label: 'Advanced Settings',
),
MenuItem(
key: _trayMenuDebugBundle,
label: 'Create Debug Bundle',
),
],
),
),
MenuItem(key: _trayMenuNetworks, label: 'Networks'),
MenuItem.separator(),
MenuItem.submenu(
label: 'About',
submenu: Menu(
items: [
MenuItem(key: _trayMenuGithub, label: 'GitHub'),
MenuItem(label: 'GUI: $uiVersion', disabled: true),
MenuItem(label: 'Daemon: $daemonVersion', disabled: true),
MenuItem(
key: _trayMenuDownload,
label: 'Download latest version',
),
],
),
),
MenuItem.separator(),
MenuItem(key: _trayMenuShow, label: 'Show window'),
MenuItem(key: _trayMenuQuit, label: 'Quit'),
],
),
);
}
String _menuKey(ClientSnapshot? snapshot) {
if (snapshot == null) {
return 'null';
}
final s = snapshot.settings;
final profiles = snapshot.profiles
.map((p) => '${p.name}:${p.active}:${p.email ?? ''}')
.join(',');
return [
snapshot.status.name,
snapshot.activeProfile.name,
snapshot.activeProfile.email ?? '',
snapshot.daemonVersion,
profiles,
s.allowSsh,
s.autoConnect,
s.quantumResistance,
s.lazyConnection,
s.blockInbound,
s.notifications,
].join('|');
}
String _severityPrefix(NotificationSeverity severity) {
return switch (severity) {
NotificationSeverity.critical => 'Critical',
NotificationSeverity.error => 'Error',
NotificationSeverity.warning => 'Warning',
NotificationSeverity.info => 'Info',
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +0,0 @@
// This is a generated file - do not edit.
//
// Generated from daemon.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class LogLevel extends $pb.ProtobufEnum {
static const LogLevel UNKNOWN =
LogLevel._(0, _omitEnumNames ? '' : 'UNKNOWN');
static const LogLevel PANIC = LogLevel._(1, _omitEnumNames ? '' : 'PANIC');
static const LogLevel FATAL = LogLevel._(2, _omitEnumNames ? '' : 'FATAL');
static const LogLevel ERROR = LogLevel._(3, _omitEnumNames ? '' : 'ERROR');
static const LogLevel WARN = LogLevel._(4, _omitEnumNames ? '' : 'WARN');
static const LogLevel INFO = LogLevel._(5, _omitEnumNames ? '' : 'INFO');
static const LogLevel DEBUG = LogLevel._(6, _omitEnumNames ? '' : 'DEBUG');
static const LogLevel TRACE = LogLevel._(7, _omitEnumNames ? '' : 'TRACE');
static const $core.List<LogLevel> values = <LogLevel>[
UNKNOWN,
PANIC,
FATAL,
ERROR,
WARN,
INFO,
DEBUG,
TRACE,
];
static final $core.List<LogLevel?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 7);
static LogLevel? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const LogLevel._(super.value, super.name);
}
class ExposeProtocol extends $pb.ProtobufEnum {
static const ExposeProtocol EXPOSE_HTTP =
ExposeProtocol._(0, _omitEnumNames ? '' : 'EXPOSE_HTTP');
static const ExposeProtocol EXPOSE_HTTPS =
ExposeProtocol._(1, _omitEnumNames ? '' : 'EXPOSE_HTTPS');
static const ExposeProtocol EXPOSE_TCP =
ExposeProtocol._(2, _omitEnumNames ? '' : 'EXPOSE_TCP');
static const ExposeProtocol EXPOSE_UDP =
ExposeProtocol._(3, _omitEnumNames ? '' : 'EXPOSE_UDP');
static const ExposeProtocol EXPOSE_TLS =
ExposeProtocol._(4, _omitEnumNames ? '' : 'EXPOSE_TLS');
static const $core.List<ExposeProtocol> values = <ExposeProtocol>[
EXPOSE_HTTP,
EXPOSE_HTTPS,
EXPOSE_TCP,
EXPOSE_UDP,
EXPOSE_TLS,
];
static final $core.List<ExposeProtocol?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 4);
static ExposeProtocol? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const ExposeProtocol._(super.value, super.name);
}
/// avoid collision with loglevel enum
class OSLifecycleRequest_CycleType extends $pb.ProtobufEnum {
static const OSLifecycleRequest_CycleType UNKNOWN =
OSLifecycleRequest_CycleType._(0, _omitEnumNames ? '' : 'UNKNOWN');
static const OSLifecycleRequest_CycleType SLEEP =
OSLifecycleRequest_CycleType._(1, _omitEnumNames ? '' : 'SLEEP');
static const OSLifecycleRequest_CycleType WAKEUP =
OSLifecycleRequest_CycleType._(2, _omitEnumNames ? '' : 'WAKEUP');
static const $core.List<OSLifecycleRequest_CycleType> values =
<OSLifecycleRequest_CycleType>[
UNKNOWN,
SLEEP,
WAKEUP,
];
static final $core.List<OSLifecycleRequest_CycleType?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 2);
static OSLifecycleRequest_CycleType? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const OSLifecycleRequest_CycleType._(super.value, super.name);
}
class SystemEvent_Severity extends $pb.ProtobufEnum {
static const SystemEvent_Severity INFO =
SystemEvent_Severity._(0, _omitEnumNames ? '' : 'INFO');
static const SystemEvent_Severity WARNING =
SystemEvent_Severity._(1, _omitEnumNames ? '' : 'WARNING');
static const SystemEvent_Severity ERROR =
SystemEvent_Severity._(2, _omitEnumNames ? '' : 'ERROR');
static const SystemEvent_Severity CRITICAL =
SystemEvent_Severity._(3, _omitEnumNames ? '' : 'CRITICAL');
static const $core.List<SystemEvent_Severity> values = <SystemEvent_Severity>[
INFO,
WARNING,
ERROR,
CRITICAL,
];
static final $core.List<SystemEvent_Severity?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3);
static SystemEvent_Severity? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const SystemEvent_Severity._(super.value, super.name);
}
class SystemEvent_Category extends $pb.ProtobufEnum {
static const SystemEvent_Category NETWORK =
SystemEvent_Category._(0, _omitEnumNames ? '' : 'NETWORK');
static const SystemEvent_Category DNS =
SystemEvent_Category._(1, _omitEnumNames ? '' : 'DNS');
static const SystemEvent_Category AUTHENTICATION =
SystemEvent_Category._(2, _omitEnumNames ? '' : 'AUTHENTICATION');
static const SystemEvent_Category CONNECTIVITY =
SystemEvent_Category._(3, _omitEnumNames ? '' : 'CONNECTIVITY');
static const SystemEvent_Category SYSTEM =
SystemEvent_Category._(4, _omitEnumNames ? '' : 'SYSTEM');
static const $core.List<SystemEvent_Category> values = <SystemEvent_Category>[
NETWORK,
DNS,
AUTHENTICATION,
CONNECTIVITY,
SYSTEM,
];
static final $core.List<SystemEvent_Category?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 4);
static SystemEvent_Category? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const SystemEvent_Category._(super.value, super.name);
}
const $core.bool _omitEnumNames =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +0,0 @@
enum ConnectionStatus {
disconnected,
connecting,
awaitingLogin,
connected,
error;
String get label {
return switch (this) {
ConnectionStatus.disconnected => 'Disconnected',
ConnectionStatus.connecting => 'Connecting',
ConnectionStatus.awaitingLogin => 'Awaiting login',
ConnectionStatus.connected => 'Connected',
ConnectionStatus.error => 'Error',
};
}
}
enum NetworkFilter {
all,
overlapping,
exitNode;
bool matches(NetworkRoute route) {
return switch (this) {
NetworkFilter.all => true,
NetworkFilter.overlapping => route.overlapping,
NetworkFilter.exitNode => route.isExitNode,
};
}
}
class ClientSnapshot {
const ClientSnapshot({
required this.daemonAddr,
required this.daemonVersion,
required this.status,
required this.activeProfile,
required this.profiles,
required this.networks,
required this.settings,
this.errorMessage,
this.pendingLogin,
});
factory ClientSnapshot.initial(String daemonAddr) {
return ClientSnapshot(
daemonAddr: daemonAddr,
daemonVersion: 'unknown',
status: ConnectionStatus.disconnected,
activeProfile: const ProfileInfo(name: 'default', active: true),
profiles: const [ProfileInfo(name: 'default', active: true)],
networks: const [],
settings: const ClientSettings(),
);
}
final String daemonAddr;
final String daemonVersion;
final ConnectionStatus status;
final ProfileInfo activeProfile;
final List<ProfileInfo> profiles;
final List<NetworkRoute> networks;
final ClientSettings settings;
final String? errorMessage;
final PendingLogin? pendingLogin;
ClientSnapshot copyWith({
String? daemonAddr,
String? daemonVersion,
ConnectionStatus? status,
ProfileInfo? activeProfile,
List<ProfileInfo>? profiles,
List<NetworkRoute>? networks,
ClientSettings? settings,
String? errorMessage,
PendingLogin? pendingLogin,
bool clearError = false,
bool clearPendingLogin = false,
}) {
return ClientSnapshot(
daemonAddr: daemonAddr ?? this.daemonAddr,
daemonVersion: daemonVersion ?? this.daemonVersion,
status: status ?? this.status,
activeProfile: activeProfile ?? this.activeProfile,
profiles: profiles ?? this.profiles,
networks: networks ?? this.networks,
settings: settings ?? this.settings,
errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
pendingLogin: clearPendingLogin
? null
: pendingLogin ?? this.pendingLogin,
);
}
}
class PendingLogin {
const PendingLogin({
required this.verificationUri,
required this.userCode,
});
final String verificationUri;
final String userCode;
}
class ProfileInfo {
const ProfileInfo({required this.name, required this.active, this.email});
final String name;
final String? email;
final bool active;
}
class NetworkRoute {
const NetworkRoute({
required this.id,
required this.range,
this.domains = const [],
this.resolvedIps = const {},
this.selected = false,
this.overlapping = false,
});
final String id;
final String range;
final List<String> domains;
final Map<String, List<String>> resolvedIps;
final bool selected;
final bool overlapping;
bool get isExitNode => range == '0.0.0.0/0';
}
enum DaemonLogLevel { unknown, panic, fatal, error, warn, info, debug, trace }
class DebugBundleResult {
const DebugBundleResult({
required this.path,
this.uploadedKey = '',
this.uploadFailureReason = '',
});
final String path;
final String uploadedKey;
final String uploadFailureReason;
bool get uploaded => uploadedKey.isNotEmpty && uploadFailureReason.isEmpty;
bool get uploadFailed => uploadFailureReason.isNotEmpty;
}
class TriggerUpdateResult {
const TriggerUpdateResult({required this.success, this.errorMessage = ''});
final bool success;
final String errorMessage;
}
class InstallerResult {
const InstallerResult({required this.success, this.errorMessage = ''});
final bool success;
final String errorMessage;
}
class UpdateProgressEvent {
const UpdateProgressEvent({required this.version});
final String version;
}
enum NotificationSeverity { info, warning, error, critical }
enum NotificationCategory {
network,
dns,
authentication,
connectivity,
system;
String get label {
return switch (this) {
NotificationCategory.network => 'Network',
NotificationCategory.dns => 'DNS',
NotificationCategory.authentication => 'Authentication',
NotificationCategory.connectivity => 'Connectivity',
NotificationCategory.system => 'System',
};
}
}
class SystemNotification {
const SystemNotification({
required this.severity,
required this.category,
required this.message,
required this.userMessage,
this.id,
});
final NotificationSeverity severity;
final NotificationCategory category;
final String message;
final String userMessage;
final String? id;
}
class ClientSettings {
const ClientSettings({
this.managementUrl = 'https://api.netbird.io',
this.interfaceName = 'wt0',
this.wireguardPort = 51820,
this.mtu = 1280,
this.autoConnect = true,
this.allowSsh = false,
this.quantumResistance = false,
this.notifications = true,
this.lazyConnection = false,
this.blockInbound = false,
});
final String managementUrl;
final String interfaceName;
final int wireguardPort;
final int mtu;
final bool autoConnect;
final bool allowSsh;
final bool quantumResistance;
final bool notifications;
final bool lazyConnection;
final bool blockInbound;
ClientSettings copyWith({
String? managementUrl,
String? interfaceName,
int? wireguardPort,
int? mtu,
bool? autoConnect,
bool? allowSsh,
bool? quantumResistance,
bool? notifications,
bool? lazyConnection,
bool? blockInbound,
}) {
return ClientSettings(
managementUrl: managementUrl ?? this.managementUrl,
interfaceName: interfaceName ?? this.interfaceName,
wireguardPort: wireguardPort ?? this.wireguardPort,
mtu: mtu ?? this.mtu,
autoConnect: autoConnect ?? this.autoConnect,
allowSsh: allowSsh ?? this.allowSsh,
quantumResistance: quantumResistance ?? this.quantumResistance,
notifications: notifications ?? this.notifications,
lazyConnection: lazyConnection ?? this.lazyConnection,
blockInbound: blockInbound ?? this.blockInbound,
);
}
}

View File

@@ -1,22 +0,0 @@
import 'dart:io';
/// Opens a URL in the user's default browser. Returns false if the platform
/// helper exits non-zero or is missing. Mirrors the Go UI's `openURL` logic.
Future<bool> openExternalUrl(String url) async {
try {
final ProcessResult result;
if (Platform.isMacOS) {
result = await Process.run('open', [url]);
} else if (Platform.isWindows) {
result = await Process.run('rundll32', [
'url.dll,FileProtocolHandler',
url,
]);
} else {
result = await Process.run('xdg-open', [url]);
}
return result.exitCode == 0;
} catch (_) {
return false;
}
}

View File

@@ -1,140 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'daemon_client.dart';
import 'models.dart';
const _allowCloseAfter = Duration(seconds: 10);
const _dotInterval = Duration(seconds: 1);
/// Shows a modal dialog while the daemon installs an update. Polls
/// `GetInstallerResult` and resolves when the daemon finishes or fails.
Future<void> showUpdateProgressDialog({
required BuildContext context,
required DaemonClient client,
required UpdateProgressEvent event,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => _UpdateProgressDialog(client: client, event: event),
);
}
class _UpdateProgressDialog extends StatefulWidget {
const _UpdateProgressDialog({required this.client, required this.event});
final DaemonClient client;
final UpdateProgressEvent event;
@override
State<_UpdateProgressDialog> createState() => _UpdateProgressDialogState();
}
class _UpdateProgressDialogState extends State<_UpdateProgressDialog> {
Timer? _dotTimer;
Timer? _allowCloseTimer;
int _dots = 0;
bool _canClose = false;
String _status = 'Updating';
String? _error;
bool _resolved = false;
@override
void initState() {
super.initState();
_dotTimer = Timer.periodic(_dotInterval, (_) => _tick());
_allowCloseTimer = Timer(_allowCloseAfter, () {
if (mounted) {
setState(() => _canClose = true);
}
});
unawaited(_pollInstaller());
}
@override
void dispose() {
_dotTimer?.cancel();
_allowCloseTimer?.cancel();
super.dispose();
}
void _tick() {
if (!mounted) {
return;
}
setState(() {
_dots = (_dots + 1) % 4;
_status = 'Updating${'.' * _dots}';
});
}
Future<void> _pollInstaller() async {
try {
final result = await widget.client.getInstallerResult();
if (!mounted) {
return;
}
if (result.success) {
Navigator.of(context).pop();
return;
}
setState(() {
_resolved = true;
_canClose = true;
_status = 'Update failed';
_error = result.errorMessage.isEmpty
? 'Unknown error'
: result.errorMessage;
});
} catch (error) {
if (!mounted) {
return;
}
setState(() {
_resolved = true;
_canClose = true;
_status = 'Update timed out';
_error = error.toString();
});
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: _canClose,
child: AlertDialog(
title: const Text('Updating client'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your client version is older than the auto-update version set in '
'Management.\nUpdating client to ${widget.event.version}.',
),
const SizedBox(height: 16),
if (!_resolved) const LinearProgressIndicator(),
const SizedBox(height: 12),
Text(_status),
if (_error != null) ...[
const SizedBox(height: 12),
Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
),
actions: [
TextButton(
onPressed: _canClose ? () => Navigator.of(context).pop() : null,
child: const Text('Close'),
),
],
),
);
}
}

View File

@@ -1 +0,0 @@
flutter/ephemeral

View File

@@ -1,128 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "netbird_flutter_ui")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "io.netbird.netbird_flutter_ui")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -1,88 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -1,27 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

View File

@@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -1,27 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
local_notifier
screen_retriever_linux
tray_manager
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -1,26 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View File

@@ -1,6 +0,0 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -1,148 +0,0 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "netbird_flutter_ui");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "netbird_flutter_ui");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View File

@@ -1,21 +0,0 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View File

@@ -1,7 +0,0 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -1,2 +0,0 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1,2 +0,0 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1,18 +0,0 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import local_notifier
import screen_retriever_macos
import tray_manager
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@@ -1,42 +0,0 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

View File

@@ -1,40 +0,0 @@
PODS:
- FlutterMacOS (1.0.0)
- local_notifier (0.1.0):
- FlutterMacOS
- screen_retriever_macos (0.0.1):
- FlutterMacOS
- tray_manager (0.0.1):
- FlutterMacOS
- window_manager (0.5.0):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
local_notifier:
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
window_manager: b729e31d38fb04905235df9ea896128991cad99e
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
COCOAPODS: 1.16.2

View File

@@ -1,801 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
5F10F38F17483368E6B26C16 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */; };
6E2193E107D1C306C0B38295 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA24562430C7E3798566E220 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = netbird_flutter_ui.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
5344037698CB477EF6AE75A3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
AA24562430C7E3798566E220 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E69F59E3113C82C71F7A2757 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
EB350F2E61DA77DD3D20E0EB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5F10F38F17483368E6B26C16 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6E2193E107D1C306C0B38295 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
16123F31EB7196617B509F9C /* Pods */ = {
isa = PBXGroup;
children = (
5344037698CB477EF6AE75A3 /* Pods-Runner.debug.xcconfig */,
E69F59E3113C82C71F7A2757 /* Pods-Runner.release.xcconfig */,
EB350F2E61DA77DD3D20E0EB /* Pods-Runner.profile.xcconfig */,
97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */,
3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */,
14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
16123F31EB7196617B509F9C /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
AA24562430C7E3798566E220 /* Pods_Runner.framework */,
2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
13F875F4B0174355038870C8 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
6D776BDDAB33DFA32528CFE2 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
DF9F03510A6543FA652C823E /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
13F875F4B0174355038870C8 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
6D776BDDAB33DFA32528CFE2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DF9F03510A6543FA652C823E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View File

@@ -1,68 +0,0 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,343 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

View File

@@ -1,14 +0,0 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = netbird_flutter_ui
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 io.netbird. All rights reserved.

View File

@@ -1,2 +0,0 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -1,2 +0,0 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -1,13 +0,0 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -1,15 +0,0 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -1,12 +0,0 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -1,413 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
fixnum:
dependency: "direct main"
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
google_cloud:
dependency: transitive
description:
name: google_cloud
sha256: fbcde933b2d8600c3cdb2328f8f4c47628ec29a39e9cef85dee535c7868993c4
url: "https://pub.dev"
source: hosted
version: "0.4.1"
google_identity_services_web:
dependency: transitive
description:
name: google_identity_services_web
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
url: "https://pub.dev"
source: hosted
version: "0.3.3+1"
googleapis_auth:
dependency: transitive
description:
name: googleapis_auth
sha256: "661738b763d3e524de69df53bf4e03943e4e01e98265cebcc6684871b06a5379"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
grpc:
dependency: "direct main"
description:
name: grpc
sha256: "86be3a7d39ad865b214a7370021ac80e68939238b507730de6d97fc662cb2723"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http2:
dependency: transitive
description:
name: http2
sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
url: "https://pub.dev"
source: hosted
version: "4.11.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
local_notifier:
dependency: "direct main"
description:
name: local_notifier
sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025
url: "https://pub.dev"
source: hosted
version: "0.1.6"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
menu_base:
dependency: transitive
description:
name: menu_base
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
protobuf:
dependency: "direct main"
description:
name: protobuf
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shortid:
dependency: transitive
description:
name: shortid
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
url: "https://pub.dev"
source: hosted
version: "0.1.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
tray_manager:
dependency: "direct main"
description:
name: tray_manager
sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b
url: "https://pub.dev"
source: hosted
version: "0.5.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.dev"
source: hosted
version: "15.1.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View File

@@ -1,28 +0,0 @@
name: netbird_flutter_ui
description: Experimental Flutter desktop UI for NetBird.
publish_to: none
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
fixnum: ^1.1.1
grpc: ^5.1.0
protobuf: ^6.0.0
tray_manager: ^0.5.0
window_manager: ^0.5.1
local_notifier: ^0.1.6
dev_dependencies:
flutter_test:
sdk: flutter
lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/tray/

View File

@@ -1,19 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:netbird_flutter_ui/src/app_shell.dart';
import 'package:netbird_flutter_ui/src/daemon_client.dart';
void main() {
testWidgets('renders the status shell', (tester) async {
await tester.pumpWidget(
NetBirdFlutterApp(
client: FakeDaemonClient(daemonAddr: 'tcp://127.0.0.1:41731'),
),
);
await tester.pump();
expect(find.text('Status'), findsWidgets);
expect(find.text('Connect'), findsOneWidget);
expect(find.text('Disconnect'), findsOneWidget);
});
}

Some files were not shown because too many files have changed in this diff Show More