Compare commits

..

1 Commits

Author SHA1 Message Date
mlsmaycon
555f5233cc add flutter example app test 2026-04-28 03:41:36 +02:00
284 changed files with 20431 additions and 11646 deletions

View File

@@ -1,130 +0,0 @@
body:
- type: markdown
attributes:
value: |
## Ideas & Feature Requests
Use this category for feature requests, enhancements, integrations, and product ideas.
NetBird uses community traction in discussions — upvotes, replies, affected users, and use-case detail — as an input when deciding what should become a maintainer-curated issue or roadmap item. A clear problem statement is more useful than a solution-only request.
Please search first and add your use case to an existing discussion when one already exists.
- type: checkboxes
id: preflight
attributes:
label: Before posting
options:
- label: I searched existing discussions and issues for similar requests.
required: true
- label: I checked the documentation to confirm this is not already supported.
required: true
- label: This is a product idea or enhancement request, not a support question.
required: true
- label: I removed or anonymized sensitive details from examples and screenshots.
required: true
- type: dropdown
id: area
attributes:
label: Product area
description: Select every area this request touches.
multiple: true
options:
- Client / Agent
- CLI
- Desktop UI
- Mobile app
- Dashboard / Admin UI
- Management service / API
- Signal service
- Relay
- DNS
- Routes / Exit nodes
- NetBird SSH
- Access control policies
- Posture checks
- Identity provider / SSO
- Self-hosting / Deployment
- Kubernetes / Operator
- Terraform / Automation
- Documentation
- Other / not sure
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem or use case
description: What are you trying to accomplish, and what is difficult or impossible today?
placeholder: |
As a ...
I want to ...
Because ...
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe the behavior, workflow, API, UI, or integration you would like to see.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives or workarounds considered
description: What have you tried today? Why is the current workaround not enough?
- type: textarea
id: impact
attributes:
label: Community impact and priority
description: Help us understand who benefits and how urgent this is.
placeholder: |
- Number of users/teams/peers affected:
- Deployment type: Cloud / self-hosted / both
- Frequency: daily / weekly / occasional
- Blocking production adoption? yes/no
- Related comments, discussions, or customer requests:
validations:
required: true
- type: textarea
id: examples
attributes:
label: Examples from other tools or products
description: If another tool solves this well, link or describe the behavior.
- type: textarea
id: security
attributes:
label: Security, privacy, and compatibility considerations
description: Note any access-control, audit, data retention, network, platform, or backward-compatibility concerns.
- type: textarea
id: implementation
attributes:
label: Implementation ideas
description: Optional. If you are familiar with the codebase or API, share possible implementation notes.
- type: dropdown
id: contribution
attributes:
label: Are you willing to help?
options:
- Yes, I can submit a PR if the approach is accepted.
- Yes, I can test or validate a proposed implementation.
- Yes, I can provide more use-case details.
- Not at this time.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add screenshots, diagrams, links, or anything else that helps explain the request.

View File

@@ -1,237 +0,0 @@
body:
- type: markdown
attributes:
value: |
## Issue Triage
Use this category for reproducible bugs and regressions in NetBird.
The more context you include, the faster we can validate and act on your report. If you're not sure whether something is a bug, **Q&A / Support** is a good starting point — we can always move the conversation here once we've confirmed it's a product issue.
Intermittent issues are useful too. Include the trigger, frequency, timing, and any logs or debug evidence you have, and we'll work from there.
Please don't include secrets, tokens, private keys, internal hostnames, or public IPs. Security vulnerabilities should be reported through the repository security policy rather than a public discussion.
- type: checkboxes
id: preflight
attributes:
label: Before posting
options:
- label: I searched existing discussions and issues, including closed ones, and checked the relevant docs.
required: true
- label: I believe this is a product bug rather than a configuration or setup question.
required: true
- label: I can reproduce this issue, or for intermittent issues I've included trigger, frequency, and timing details below.
required: true
- label: I removed or anonymized sensitive data from logs, screenshots, and configuration.
required: true
- type: dropdown
id: area
attributes:
label: Affected area
description: Select every area this report touches.
multiple: true
options:
- Client / Agent
- Reverse Proxy
- CLI
- Desktop UI
- Mobile app
- Peer connectivity
- DNS
- Routes / Exit nodes
- NetBird SSH
- Relay / Signal / NAT traversal
- Login / Authentication / IdP
- Dashboard / Admin UI
- Management service / API
- Access control policies / Posture checks
- Self-hosting / Deployment
- Kubernetes / Operator
- Documentation
- Other / not sure
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: Deployment type
options:
- NetBird Cloud
- Self-hosted - quickstart script
- Self-hosted - advanced/custom deployment
- Local development build
- Not sure / environment I do not fully control
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Operating system or environment
description: Select every environment involved in the reproduction.
multiple: true
options:
- Linux
- macOS
- Windows
- Android
- iOS
- FreeBSD
- OpenWRT
- Docker
- Kubernetes
- Synology
- Browser
- Other / not sure
validations:
required: true
- type: textarea
id: version
attributes:
label: NetBird version and upgrade status
description: Run `netbird version` where applicable. For self-hosted deployments, include management, signal, relay, and dashboard versions if available. If you cannot test on a current/supported version, explain why.
placeholder: |
Example:
- Client: 0.30.2
- Management: 0.30.2
- Signal: 0.30.2
- Relay: 0.30.2
- Dashboard: 0.30.2
- Upgrade status: reproduced on current version / cannot upgrade because ...
validations:
required: true
- type: dropdown
id: regression
attributes:
label: Did this work before?
options:
- Yes, this worked before
- No, this never worked
- Not sure
validations:
required: true
- type: textarea
id: regression-details
attributes:
label: Regression details
description: If this worked before, include the last known working version, first known broken version, and any recent upgrade, configuration, network, or IdP changes.
placeholder: |
- Last known working version:
- First known broken version:
- Recent changes:
- type: textarea
id: summary
attributes:
label: Summary
description: Briefly describe the reproducible bug.
placeholder: What is broken?
validations:
required: true
- type: textarea
id: current-behavior
attributes:
label: Current behavior
description: What happens now? Include exact errors, timeouts, UI messages, or failed commands when possible.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: What did you expect to happen instead?
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Provide the smallest set of steps that reliably reproduces the bug. If the issue is intermittent, include the trigger, frequency, timing, and relevant timestamps.
placeholder: |
1. Configure ...
2. Run ...
3. Observe ...
For intermittent issues:
- Trigger:
- Frequency:
- Timing/timestamps:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment and topology
description: Include the relevant topology and software involved in the reproduction. For UI/docs-only reports, write `N/A` if this does not apply. Use `None`, `Unknown`, or `N/A` where appropriate.
placeholder: |
- Peer A:
- Peer B:
- Same LAN or different networks:
- NAT/CGNAT/corporate firewall/mobile network:
- Other VPN software:
- Firewall, DNS, or endpoint security software:
- Routes, DNS, policies, posture checks, or SSH rules involved:
- IdP, reverse proxy, or browser involved:
validations:
required: true
- type: textarea
id: self-hosted-details
attributes:
label: Self-hosted details, if available
description: Optional. If you use self-hosting and have access to these details, include them. If you do not administer the environment, provide what you know and say what you cannot access.
placeholder: |
- Deployment method: quickstart / Docker Compose / Helm / operator / custom
- Management/signal/relay/dashboard versions:
- Reverse proxy:
- IdP/provider:
- STUN/TURN/coturn/relay details:
- Relevant component logs:
- type: textarea
id: logs
attributes:
label: Logs, status output, or debug evidence
description: |
For client, connectivity, DNS, route, relay/signal, or self-hosted reports, logs are essential — please include anonymized output from `netbird status -dA`, or a debug bundle via `netbird debug for 1m -AS -U`. Debug bundles are automatically deleted after 30 days.
For UI, dashboard, or documentation reports, leave the pre-filled `N/A`.
value: "N/A"
render: shell
validations:
required: true
- type: textarea
id: related-reports
attributes:
label: Related issues or discussions
description: Optional. Link similar reports you found while searching, if any.
placeholder: |
- Related issue/discussion:
- Why this may be the same or different:
- type: textarea
id: impact
attributes:
label: Impact
description: Optional. Help us understand priority. How many users, peers, environments, or workflows are affected? Is there a workaround?
placeholder: |
- Affected users/peers:
- Business or production impact:
- Workaround available:
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add links to related discussions, issues, docs, screenshots, recordings, or anything else that may help validation.

View File

@@ -1,146 +0,0 @@
body:
- type: markdown
attributes:
value: |
## Q&A / Support
Use this category for questions about configuration, setup, self-hosted deployments, troubleshooting, and general NetBird usage.
This is community support and does not provide an SLA. For NetBird Cloud support, use the official support channel linked from the issue creation page. Please do not post secrets, tokens, private keys, internal hostnames, or public IPs unless you intentionally want them public.
If your question turns into a reproducible product defect, DevRel or a maintainer may ask you to open or move the conversation to Issue Triage.
- type: checkboxes
id: preflight
attributes:
label: Before posting
options:
- label: I searched existing discussions and issues for similar questions.
required: true
- label: I reviewed the relevant NetBird documentation or troubleshooting guide.
required: true
- label: I removed or anonymized sensitive data from logs, screenshots, and configuration.
required: true
- type: dropdown
id: topic
attributes:
label: Topic
multiple: true
options:
- Getting started
- Self-hosting
- Client / Agent
- CLI
- Desktop UI
- Mobile app
- Dashboard / Admin UI
- DNS
- Routes / Exit nodes
- NetBird SSH
- Relay
- Access control policies
- Posture checks
- Identity provider / SSO
- API
- Kubernetes / Operator
- Terraform / Automation
- Documentation
- Other / not sure
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: Deployment type
options:
- NetBird Cloud
- Self-hosted - quickstart script
- Self-hosted - advanced/custom deployment
- Local development build
- Not sure
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Operating system or environment
multiple: true
options:
- Linux
- macOS
- Windows
- Android
- iOS
- FreeBSD
- OpenWRT
- Docker
- Kubernetes
- Synology
- Browser
- Other / not sure
validations:
required: true
- type: input
id: version
attributes:
label: NetBird version
description: Run `netbird version` where applicable. For self-hosted deployments, include component versions if relevant.
placeholder: "Example: client 0.30.2, management 0.30.2"
- type: textarea
id: question
attributes:
label: Question
description: What are you trying to understand or accomplish?
placeholder: Describe your question clearly.
validations:
required: true
- type: textarea
id: goal
attributes:
label: Desired outcome
description: What would a successful answer help you do?
placeholder: |
I want to configure ...
I expected ...
I need help deciding ...
- type: textarea
id: attempted
attributes:
label: What have you tried?
description: Include commands, documentation links, configuration attempts, or troubleshooting steps already tried.
placeholder: |
- Read ...
- Ran ...
- Changed ...
- Observed ...
- type: textarea
id: environment
attributes:
label: Relevant environment details
description: Include redacted topology, IdP/provider, reverse proxy, firewall, DNS, route, policy, or self-hosted setup details that may affect the answer.
placeholder: |
- Deployment:
- Components involved:
- Network/topology:
- Related config:
- type: textarea
id: logs
attributes:
label: Logs or output
description: Optional. Include anonymized logs, command output, screenshots, or `netbird status -dA` if relevant.
render: shell
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add links, diagrams, screenshots, or other details that may help the community answer.

View File

@@ -0,0 +1,71 @@
---
name: Bug/Issue report
about: Create a report to help us improve
title: ''
labels: ['triage-needed']
assignees: ''
---
**Describe the problem**
A clear and concise description of what the problem is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Are you using NetBird Cloud?**
Please specify whether you use NetBird Cloud or self-host NetBird's control plane.
**NetBird version**
`netbird version`
**Is any other VPN software installed?**
If yes, which one?
**Debug output**
To help us resolve the problem, please attach the following anonymized status output
netbird status -dA
Create and upload a debug bundle, and share the returned file key:
netbird debug for 1m -AS -U
*Uploaded files are automatically deleted after 30 days.*
Alternatively, create the file only and attach it here manually:
netbird debug for 1m -AS
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
**Have you tried these troubleshooting steps?**
- [ ] Reviewed [client troubleshooting](https://docs.netbird.io/how-to/troubleshooting-client) (if applicable)
- [ ] Checked for newer NetBird versions
- [ ] Searched for similar issues on GitHub (including closed ones)
- [ ] Restarted the NetBird client
- [ ] Disabled other VPN software
- [ ] Checked firewall settings

View File

@@ -1,26 +1,14 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Start an Issue Triage discussion
url: https://github.com/netbirdio/netbird/discussions/new?category=issue-triage
about: Report a bug, regression, or unexpected behavior so DevRel can validate it before it becomes an issue.
- name: Propose an idea or feature request
url: https://github.com/netbirdio/netbird/discussions/new?category=ideas-feature-requests
about: Share feature requests, enhancements, and integration ideas for community feedback and prioritization.
- name: Ask a Q&A / Support question
url: https://github.com/netbirdio/netbird/discussions/new?category=q-a-support
about: Get help with setup, configuration, self-hosting, troubleshooting, and general usage.
- name: Security vulnerability disclosure
url: https://github.com/netbirdio/netbird/security/policy
about: Please do not report security vulnerabilities in public issues or discussions.
- name: Community Support Forum
- name: Community Support
url: https://forum.netbird.io/
about: Community support forum.
about: Community support forum
- name: Cloud Support
url: https://docs.netbird.io/help/report-bug-issues
about: Contact NetBird for Cloud support.
- name: Client / Connection Troubleshooting
about: Contact us for support
- name: Client/Connection Troubleshooting
url: https://docs.netbird.io/help/troubleshooting-client
about: See the client troubleshooting guide for common connectivity issues.
about: See our client troubleshooting guide for help addressing common issues
- name: Self-host Troubleshooting
url: https://docs.netbird.io/selfhosted/troubleshooting
about: See the self-host troubleshooting guide for common deployment issues.
about: See our self-host troubleshooting guide for help addressing common issues

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ['feature-request']
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,128 +0,0 @@
name: Validated issue
description: Maintainer/DevRel only. Create an issue after a discussion has been validated or for internally validated work.
title: "[Validated]: "
body:
- type: markdown
attributes:
value: |
## Discussion-first issue policy
Issues are maintainer-curated work items. Community reports and feature requests should start in [Discussions](https://github.com/netbirdio/netbird/discussions) so DevRel can validate, reproduce, and route them before engineering time is committed.
Use this form when:
- A discussion has been validated and should become actionable work.
- A maintainer is opening internally validated work that can bypass the discussion-first flow.
Issues opened without a relevant validated discussion or maintainer context may be closed and redirected to Discussions.
- type: checkboxes
id: validation-checks
attributes:
label: Validation checklist
options:
- label: This issue is linked to a validated discussion, or it is being opened directly by a maintainer.
required: true
- label: The report has enough context for engineering to act on it without re-triaging from scratch.
required: true
- label: Sensitive data, secrets, private keys, internal hostnames, and public IPs have been removed or intentionally disclosed.
required: true
- type: dropdown
id: issue-type
attributes:
label: Issue type
options:
- Bug / Regression
- Feature / Enhancement
- Documentation
- Maintenance / Refactor
- Cross-repository coordination
- Other
validations:
required: true
- type: input
id: source-discussion
attributes:
label: Source discussion
description: Link the GitHub Discussion that was validated. Maintainers bypassing the flow can write "Maintainer-created" and explain why below.
placeholder: https://github.com/netbirdio/netbird/discussions/1234
validations:
required: true
- type: input
id: validation-owner
attributes:
label: Validation owner
description: GitHub handle of the DevRel team member or maintainer who validated this work.
placeholder: "@username"
validations:
required: true
- type: dropdown
id: target-repository
attributes:
label: Target repository
description: Where should the implementation work happen?
options:
- netbirdio/netbird
- netbirdio/dashboard
- netbirdio/kubernetes-operator
- netbirdio/docs
- Multiple repositories
- Unknown / needs routing
validations:
required: true
- type: textarea
id: summary
attributes:
label: Summary
description: Concise description of the validated work.
placeholder: What needs to be fixed, changed, documented, or built?
validations:
required: true
- type: textarea
id: evidence
attributes:
label: Validation evidence
description: For bugs, include reproduction status, affected versions, logs, and environment. For features, include community traction, affected users, and alignment notes.
placeholder: |
- Reproduced by:
- Affected versions / platforms:
- Community signal:
- Related logs or screenshots:
validations:
required: true
- type: textarea
id: scope
attributes:
label: Proposed scope
description: Describe what is in scope and, if helpful, what is explicitly out of scope.
placeholder: |
In scope:
- ...
Out of scope:
- ...
validations:
required: true
- type: textarea
id: acceptance-criteria
attributes:
label: Acceptance criteria
description: What must be true for this issue to be closed?
placeholder: |
- [ ] ...
- [ ] ...
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Links to related PRs, docs, issues in other repositories, roadmap items, or implementation notes.

View File

@@ -114,13 +114,7 @@ jobs:
retention-days: 30
release:
runs-on: ubuntu-24.04-8-core
outputs:
release_artifact_url: ${{ steps.upload_release.outputs.artifact-url }}
linux_packages_artifact_url: ${{ steps.upload_linux_packages.outputs.artifact-url }}
windows_packages_artifact_url: ${{ steps.upload_windows_packages.outputs.artifact-url }}
macos_packages_artifact_url: ${{ steps.upload_macos_packages.outputs.artifact-url }}
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
runs-on: ubuntu-latest-m
env:
flags: ""
steps:
@@ -219,13 +213,10 @@ jobs:
if: always()
run: rm -f /tmp/gpg-rpm-signing-key.asc
- name: Tag and push images (amd64 only)
id: tag_and_push_images
if: |
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
run: |
set -euo pipefail
resolve_tags() {
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "pr-${{ github.event.pull_request.number }}"
@@ -234,17 +225,6 @@ jobs:
fi
}
ghcr_package_url() {
local image="$1" package encoded_package
package="${image#ghcr.io/}"
package="${package#*/}"
package="${package%%:*}"
encoded_package="${package//\//%2F}"
echo "https://github.com/orgs/netbirdio/packages/container/package/${encoded_package}"
}
image_refs=()
tag_and_push() {
local src="$1" img_name tag dst
img_name="${src%%:*}"
@@ -253,56 +233,35 @@ jobs:
echo "Tagging ${src} -> ${dst}"
docker tag "$src" "$dst"
docker push "$dst"
image_refs+=("$dst")
done
}
cat > /tmp/goreleaser-artifacts.json <<'JSON'
${{ steps.goreleaser.outputs.artifacts }}
JSON
export -f tag_and_push resolve_tags
mapfile -t src_images < <(
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name | select(startswith("ghcr.io/"))' /tmp/goreleaser-artifacts.json
)
for src in "${src_images[@]}"; do
tag_and_push "$src"
done
{
echo "images_markdown<<EOF"
if [[ ${#image_refs[@]} -eq 0 ]]; then
echo "_No GHCR images were pushed._"
else
printf '%s\n' "${image_refs[@]}" | sort -u | while read -r image; do
printf -- '- [`%s`](%s)\n' "$image" "$(ghcr_package_url "$image")"
done
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo '${{ steps.goreleaser.outputs.artifacts }}' | \
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name' | \
grep '^ghcr.io/' | while read -r SRC; do
tag_and_push "$SRC"
done
- name: upload non tags for debug purposes
id: upload_release
uses: actions/upload-artifact@v4
with:
name: release
path: dist/
retention-days: 7
- name: upload linux packages
id: upload_linux_packages
uses: actions/upload-artifact@v4
with:
name: linux-packages
path: dist/netbird_linux**
retention-days: 7
- name: upload windows packages
id: upload_windows_packages
uses: actions/upload-artifact@v4
with:
name: windows-packages
path: dist/netbird_windows**
retention-days: 7
- name: upload macos packages
id: upload_macos_packages
uses: actions/upload-artifact@v4
with:
name: macos-packages
@@ -311,8 +270,6 @@ jobs:
release_ui:
runs-on: ubuntu-latest
outputs:
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
steps:
- name: Parse semver string
id: semver_parser
@@ -403,7 +360,6 @@ jobs:
if: always()
run: rm -f /tmp/gpg-rpm-signing-key.asc
- name: upload non tags for debug purposes
id: upload_release_ui
uses: actions/upload-artifact@v4
with:
name: release-ui
@@ -412,8 +368,6 @@ jobs:
release_ui_darwin:
runs-on: macos-latest
outputs:
release_ui_darwin_artifact_url: ${{ steps.upload_release_ui_darwin.outputs.artifact-url }}
steps:
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
run: echo "flags=--snapshot" >> $GITHUB_ENV
@@ -448,258 +402,15 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: upload non tags for debug purposes
id: upload_release_ui_darwin
uses: actions/upload-artifact@v4
with:
name: release-ui-darwin
path: dist/
retention-days: 3
test_windows_installer:
name: "Windows Installer / Build Test"
runs-on: windows-2022
needs: [release, release_ui]
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
wintun_arch: amd64
- arch: arm64
wintun_arch: arm64
defaults:
run:
shell: powershell
env:
PackageWorkdir: netbird_windows_${{ matrix.arch }}
downloadPath: '${{ github.workspace }}\temp'
steps:
- name: Parse semver string
id: semver_parser
uses: booxmedialtd/ws-action-parse-semver@v1
with:
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
version_extractor_regex: '\/v(.*)$'
- name: Checkout
uses: actions/checkout@v4
- name: Add 7-Zip to PATH
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
name: release
path: release
- name: Download UI release artifacts
uses: actions/download-artifact@v4
with:
name: release-ui
path: release-ui
- name: Stage binaries into dist
run: |
$workdir = "dist\${{ env.PackageWorkdir }}"
New-Item -ItemType Directory -Force -Path $workdir | Out-Null
$client = Get-ChildItem -Recurse -Path release -Filter "netbird_*_windows_${{ matrix.arch }}.tar.gz" | Select-Object -First 1
$ui = Get-ChildItem -Recurse -Path release-ui -Filter "netbird-ui-windows_*_windows_${{ matrix.arch }}.tar.gz" | Select-Object -First 1
if (-not $client) { Write-Host "::error::client tarball not found for ${{ matrix.arch }}"; exit 1 }
if (-not $ui) { Write-Host "::error::ui tarball not found for ${{ matrix.arch }}"; exit 1 }
Write-Host "Client: $($client.FullName)"
Write-Host "UI: $($ui.FullName)"
tar -zvxf $client.FullName -C $workdir
tar -zvxf $ui.FullName -C $workdir
Get-ChildItem $workdir
- name: Download wintun
uses: carlosperate/download-file-action@v2
id: download-wintun
with:
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
file-name: wintun.zip
location: ${{ env.downloadPath }}
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
- name: Decompress wintun files
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
- name: Move wintun.dll into dist
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
- name: Download Mesa3D (amd64 only)
uses: carlosperate/download-file-action@v2
id: download-mesa3d
if: matrix.arch == 'amd64'
with:
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
file-name: mesa3d.7z
location: ${{ env.downloadPath }}
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
- name: Extract Mesa3D driver (amd64 only)
if: matrix.arch == 'amd64'
run: 7z x -o"${{ env.downloadPath }}" "${{ env.downloadPath }}/mesa3d.7z"
- name: Move opengl32.dll into dist (amd64 only)
if: matrix.arch == 'amd64'
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
- name: Download EnVar plugin for NSIS
uses: carlosperate/download-file-action@v2
with:
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
file-name: envar_plugin.zip
location: ${{ github.workspace }}
- name: Extract EnVar plugin
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
uses: carlosperate/download-file-action@v2
if: matrix.arch == 'amd64'
with:
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
file-name: ShellExecAsUser_amd64-Unicode.7z
location: ${{ github.workspace }}
- name: Extract ShellExecAsUser plugin (amd64 only)
if: matrix.arch == 'amd64'
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
- name: Build NSIS installer
uses: joncloud/makensis-action@v3.3
with:
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
script-file: client/installer.nsis
arguments: "/V4 /DARCH=${{ matrix.arch }}"
env:
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
- name: Rename NSIS installer
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
- name: Install WiX
run: |
dotnet tool install --global wix --version 6.0.2
wix extension add WixToolset.Util.wixext/6.0.2
- name: Build MSI installer
env:
NETBIRD_VERSION: "${{ steps.semver_parser.outputs.fullversion }}"
run: wix build -arch ${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }} -ext WixToolset.Util.wixext -o netbird_installer_test_windows_${{ matrix.arch }}.msi .\client\netbird.wxs -d ProcessorArchitecture=${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }} -d ArchSuffix=${{ matrix.arch }}
- name: Upload installer artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: windows-installer-test-${{ matrix.arch }}
path: |
netbird_installer_test_windows_${{ matrix.arch }}.exe
netbird_installer_test_windows_${{ matrix.arch }}.msi
retention-days: 3
comment_release_artifacts:
name: Comment release artifacts
runs-on: ubuntu-latest
needs: [release, release_ui, release_ui_darwin]
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Create or update PR comment
uses: actions/github-script@v7
env:
RELEASE_RESULT: ${{ needs.release.result }}
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
RELEASE_UI_DARWIN_RESULT: ${{ needs.release_ui_darwin.result }}
RELEASE_ARTIFACT_URL: ${{ needs.release.outputs.release_artifact_url }}
LINUX_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.linux_packages_artifact_url }}
WINDOWS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.windows_packages_artifact_url }}
MACOS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.macos_packages_artifact_url }}
RELEASE_UI_ARTIFACT_URL: ${{ needs.release_ui.outputs.release_ui_artifact_url }}
RELEASE_UI_DARWIN_ARTIFACT_URL: ${{ needs.release_ui_darwin.outputs.release_ui_darwin_artifact_url }}
GHCR_IMAGES_MARKDOWN: ${{ needs.release.outputs.ghcr_images }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const marker = '<!-- netbird-release-artifacts -->';
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const shortSha = context.payload.pull_request.head.sha.slice(0, 7);
const artifactCell = (url, result) => {
if (url) return `[Download](${url})`;
return result && result !== 'success' ? `_Not available (${result})_` : '_Not available_';
};
const artifacts = [
['All release artifacts', process.env.RELEASE_ARTIFACT_URL, process.env.RELEASE_RESULT],
['Linux packages', process.env.LINUX_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
['Windows packages', process.env.WINDOWS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
['macOS packages', process.env.MACOS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
['UI artifacts', process.env.RELEASE_UI_ARTIFACT_URL, process.env.RELEASE_UI_RESULT],
['UI macOS artifacts', process.env.RELEASE_UI_DARWIN_ARTIFACT_URL, process.env.RELEASE_UI_DARWIN_RESULT],
];
const artifactRows = artifacts
.map(([name, url, result]) => `| ${name} | ${artifactCell(url, result)} |`)
.join('\n');
const ghcrImages = (process.env.GHCR_IMAGES_MARKDOWN || '').trim() || '_No GHCR images were pushed._';
const body = [
marker,
'## Release artifacts',
'',
`Built for PR head \`${shortSha}\` in [workflow run #${process.env.GITHUB_RUN_NUMBER}](${runUrl}).`,
'',
'| Artifact | Link |',
'| --- | --- |',
artifactRows,
'',
'### GHCR images (amd64)',
ghcrImages,
'',
'_This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy._',
].join('\n');
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const previous = comments.find(comment =>
comment.user?.type === 'Bot' && comment.body?.includes(marker)
);
if (previous) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: previous.id,
body,
});
core.info(`Updated release artifacts comment ${previous.id}`);
} else {
const { data } = await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
core.info(`Created release artifacts comment ${data.id}`);
}
trigger_signer:
runs-on: ubuntu-latest
needs: [release, release_ui, release_ui_darwin, test_windows_installer]
needs: [release, release_ui, release_ui_darwin]
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger binaries sign pipelines

View File

@@ -9,8 +9,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
# Receiving workflows (cloud sync-tag, mobile bump-netbird) expect the short
# tag form (e.g. v0.30.0), not refs/tags/v0.30.0 — github.ref_name, not github.ref.
jobs:
trigger_sync_tag:
runs-on: ubuntu-latest
@@ -22,30 +20,4 @@ jobs:
ref: main
repo: ${{ secrets.UPSTREAM_REPO }}
token: ${{ secrets.NC_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref_name }}" }'
trigger_android_bump:
runs-on: ubuntu-latest
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
steps:
- name: Trigger android-client submodule bump
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
with:
workflow: bump-netbird.yml
ref: main
repo: netbirdio/android-client
token: ${{ secrets.NC_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref_name }}" }'
trigger_ios_bump:
runs-on: ubuntu-latest
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
steps:
- name: Trigger ios-client submodule bump
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
with:
workflow: bump-netbird.yml
ref: main
repo: netbirdio/ios-client
token: ${{ secrets.NC_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref_name }}" }'

View File

@@ -58,11 +58,6 @@ linters:
govet:
enable:
- nilness
disable:
# The inline analyzer flags x/exp/maps Clone/Clear with //go:fix inline
# directives but cannot perform the rewrite due to generic type
# parameter inference limitations in the Go inliner.
- inline
enable-all: false
revive:
rules:
@@ -92,9 +87,6 @@ linters:
- linters:
- unused
path: client/firewall/iptables/rule\.go
- linters:
- unused
path: client/internal/dns/dnsfw/(types|syscall|zsyscall)_windows.*\.go
- linters:
- gosec
- mirror

View File

@@ -17,7 +17,6 @@ ENV \
NETBIRD_BIN="/usr/local/bin/netbird" \
NB_LOG_FILE="console,/var/log/netbird/client.log" \
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
NB_ENABLE_CAPTURE="false" \
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]

View File

@@ -23,7 +23,6 @@ ENV \
NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
NB_LOG_FILE="console,/var/lib/netbird/client.log" \
NB_DISABLE_DNS="true" \
NB_ENABLE_CAPTURE="false" \
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]

View File

@@ -1,196 +0,0 @@
package cmd
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util/capture"
)
var captureCmd = &cobra.Command{
Use: "capture",
Short: "Capture packets on the WireGuard interface",
Long: `Captures decrypted packets flowing through the WireGuard interface.
Default output is human-readable text. Use --pcap or --output for pcap binary.
Requires --enable-capture to be set at service install or reconfigure time.
Examples:
netbird debug capture
netbird debug capture host 100.64.0.1 and port 443
netbird debug capture tcp
netbird debug capture icmp
netbird debug capture src host 10.0.0.1 and dst port 80
netbird debug capture -o capture.pcap
netbird debug capture --pcap | tshark -r -
netbird debug capture --pcap | tcpdump -r - -n`,
Args: cobra.ArbitraryArgs,
RunE: runCapture,
}
func init() {
debugCmd.AddCommand(captureCmd)
captureCmd.Flags().Bool("pcap", false, "Force pcap binary output (default when --output is set)")
captureCmd.Flags().BoolP("verbose", "v", false, "Show seq/ack, TTL, window, total length")
captureCmd.Flags().Bool("ascii", false, "Print payload as ASCII after each packet (useful for HTTP)")
captureCmd.Flags().Uint32("snap-len", 0, "Max bytes per packet (0 = full)")
captureCmd.Flags().DurationP("duration", "d", 0, "Capture duration (0 = until interrupted)")
captureCmd.Flags().StringP("output", "o", "", "Write pcap to file instead of stdout")
}
func runCapture(cmd *cobra.Command, args []string) error {
conn, err := getClient(cmd)
if err != nil {
return err
}
defer func() {
if err := conn.Close(); err != nil {
cmd.PrintErrf(errCloseConnection, err)
}
}()
client := proto.NewDaemonServiceClient(conn)
req, err := buildCaptureRequest(cmd, args)
if err != nil {
return err
}
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
stream, err := client.StartCapture(ctx, req)
if err != nil {
return handleCaptureError(err)
}
// First Recv is the empty acceptance message from the server. If the
// device is unavailable (kernel WG, not connected, capture disabled),
// the server returns an error instead.
if _, err := stream.Recv(); err != nil {
return handleCaptureError(err)
}
out, cleanup, err := captureOutput(cmd)
if err != nil {
return err
}
if req.TextOutput {
cmd.PrintErrf("Capturing packets... Press Ctrl+C to stop.\n")
} else {
cmd.PrintErrf("Capturing packets (pcap)... Press Ctrl+C to stop.\n")
}
streamErr := streamCapture(ctx, cmd, stream, out)
cleanupErr := cleanup()
if streamErr != nil {
return streamErr
}
return cleanupErr
}
func buildCaptureRequest(cmd *cobra.Command, args []string) (*proto.StartCaptureRequest, error) {
req := &proto.StartCaptureRequest{}
if len(args) > 0 {
expr := strings.Join(args, " ")
if _, err := capture.ParseFilter(expr); err != nil {
return nil, fmt.Errorf("invalid filter: %w", err)
}
req.FilterExpr = expr
}
if snap, _ := cmd.Flags().GetUint32("snap-len"); snap > 0 {
req.SnapLen = snap
}
if d, _ := cmd.Flags().GetDuration("duration"); d != 0 {
if d < 0 {
return nil, fmt.Errorf("duration must not be negative")
}
req.Duration = durationpb.New(d)
}
req.Verbose, _ = cmd.Flags().GetBool("verbose")
req.Ascii, _ = cmd.Flags().GetBool("ascii")
outPath, _ := cmd.Flags().GetString("output")
forcePcap, _ := cmd.Flags().GetBool("pcap")
req.TextOutput = !forcePcap && outPath == ""
return req, nil
}
func streamCapture(ctx context.Context, cmd *cobra.Command, stream proto.DaemonService_StartCaptureClient, out io.Writer) error {
for {
pkt, err := stream.Recv()
if err != nil {
if ctx.Err() != nil {
cmd.PrintErrf("\nCapture stopped.\n")
return nil //nolint:nilerr // user interrupted
}
if err == io.EOF {
cmd.PrintErrf("\nCapture finished.\n")
return nil
}
return handleCaptureError(err)
}
if _, err := out.Write(pkt.GetData()); err != nil {
return fmt.Errorf("write output: %w", err)
}
}
}
// captureOutput returns the writer for capture data and a cleanup function
// that finalizes the file. Errors from the cleanup must be propagated.
func captureOutput(cmd *cobra.Command) (io.Writer, func() error, error) {
outPath, _ := cmd.Flags().GetString("output")
if outPath == "" {
return os.Stdout, func() error { return nil }, nil
}
f, err := os.CreateTemp(filepath.Dir(outPath), filepath.Base(outPath)+".*.tmp")
if err != nil {
return nil, nil, fmt.Errorf("create output file: %w", err)
}
tmpPath := f.Name()
return f, func() error {
var merr *multierror.Error
if err := f.Close(); err != nil {
merr = multierror.Append(merr, fmt.Errorf("close output file: %w", err))
}
fi, statErr := os.Stat(tmpPath)
if statErr != nil || fi.Size() == 0 {
if rmErr := os.Remove(tmpPath); rmErr != nil && !os.IsNotExist(rmErr) {
merr = multierror.Append(merr, fmt.Errorf("remove empty output file: %w", rmErr))
}
return nberrors.FormatErrorOrNil(merr)
}
if err := os.Rename(tmpPath, outPath); err != nil {
merr = multierror.Append(merr, fmt.Errorf("rename output file: %w", err))
return nberrors.FormatErrorOrNil(merr)
}
cmd.PrintErrf("Wrote %s\n", outPath)
return nberrors.FormatErrorOrNil(merr)
}, nil
}
func handleCaptureError(err error) error {
if s, ok := status.FromError(err); ok {
return fmt.Errorf("%s", s.Message())
}
return err
}

View File

@@ -9,7 +9,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/debug"
@@ -240,50 +239,11 @@ func runForDuration(cmd *cobra.Command, args []string) error {
}()
}
captureStarted := false
if wantCapture, _ := cmd.Flags().GetBool("capture"); wantCapture {
captureTimeout := duration + 30*time.Second
const maxBundleCapture = 10 * time.Minute
if captureTimeout > maxBundleCapture {
captureTimeout = maxBundleCapture
}
_, err := client.StartBundleCapture(cmd.Context(), &proto.StartBundleCaptureRequest{
Timeout: durationpb.New(captureTimeout),
})
if err != nil {
cmd.PrintErrf("Failed to start packet capture: %v\n", status.Convert(err).Message())
} else {
captureStarted = true
cmd.Println("Packet capture started.")
// Safety: always stop on exit, even if the normal stop below runs too.
defer func() {
if captureStarted {
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if _, err := client.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
cmd.PrintErrf("Failed to stop packet capture: %v\n", err)
}
}
}()
}
}
if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil {
return waitErr
}
cmd.Println("\nDuration completed")
if captureStarted {
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if _, err := client.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
cmd.PrintErrf("Failed to stop packet capture: %v\n", err)
} else {
captureStarted = false
cmd.Println("Packet capture stopped.")
}
}
if cpuProfilingStarted {
if _, err := client.StopCPUProfile(cmd.Context(), &proto.StopCPUProfileRequest{}); err != nil {
cmd.PrintErrf("Failed to stop CPU profiling: %v\n", err)
@@ -456,5 +416,4 @@ func init() {
forCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
forCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server")
forCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
forCmd.Flags().Bool("capture", false, "Capture packets during the debug duration and include in bundle")
}

View File

@@ -10,7 +10,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
@@ -24,7 +23,6 @@ import (
func init() {
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
loginCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
}
@@ -258,7 +256,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
}
func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.LoginResponse, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager) error {
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser, showQR)
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
if err != nil {
@@ -326,7 +324,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
}
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser, showQR)
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser)
tokenInfo, err := oAuthFlow.WaitToken(context.TODO(), flowInfo)
if err != nil {
@@ -336,7 +334,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
return &tokenInfo, nil
}
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser, showQR bool) {
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser bool) {
var codeMsg string
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
@@ -350,12 +348,6 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
verificationURIComplete + " " + codeMsg)
}
if showQR {
if f, ok := cmd.OutOrStdout().(*os.File); ok && term.IsTerminal(int(f.Fd())) {
printQRCode(f, verificationURIComplete)
}
}
cmd.Println("")
if !noBrowser {

View File

@@ -1,25 +0,0 @@
package cmd
import (
"io"
"github.com/mdp/qrterminal/v3"
)
// printQRCode prints a QR code for the given URL to the writer.
// Called only when the user explicitly requests QR output via --qr.
func printQRCode(w io.Writer, url string) {
if url == "" {
return
}
qrterminal.GenerateWithConfig(url, qrterminal.Config{
Level: qrterminal.M,
Writer: w,
HalfBlocks: true,
BlackChar: qrterminal.BLACK_BLACK,
WhiteChar: qrterminal.WHITE_WHITE,
BlackWhiteChar: qrterminal.BLACK_WHITE,
WhiteBlackChar: qrterminal.WHITE_BLACK,
QuietZone: qrterminal.QUIET_ZONE,
})
}

View File

@@ -1,26 +0,0 @@
package cmd
import (
"bytes"
"testing"
)
func TestPrintQRCode_EmptyURL(t *testing.T) {
var buf bytes.Buffer
printQRCode(&buf, "")
if buf.Len() != 0 {
t.Error("expected no output for empty URL")
}
}
func TestPrintQRCode_WritesOutput(t *testing.T) {
var buf bytes.Buffer
printQRCode(&buf, "https://example.com/auth")
if buf.Len() == 0 {
t.Error("expected QR code output for non-empty URL")
}
}

View File

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

View File

@@ -44,7 +44,6 @@ 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(&captureEnabled, "enable-capture", false, "Enables packet capture via 'netbird debug capture'. To persist, use: netbird service install --enable-capture")
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")

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, captureEnabled, networksDisabled)
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, networksDisabled)
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 captureEnabled {
args = append(args, "--enable-capture")
}
if networksDisabled {
args = append(args, "--disable-networks")
}

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"`
EnableCapture bool `json:"enable_capture,omitempty"`
DisableNetworks bool `json:"disable_networks,omitempty"`
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
}
@@ -80,7 +79,6 @@ func currentServiceParams() *serviceParams {
LogFiles: logFiles,
DisableProfiles: profilesDisabled,
DisableUpdateSettings: updateSettingsDisabled,
EnableCapture: captureEnabled,
DisableNetworks: networksDisabled,
}
@@ -146,10 +144,6 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
updateSettingsDisabled = params.DisableUpdateSettings
}
if !serviceCmd.PersistentFlags().Changed("enable-capture") {
captureEnabled = params.EnableCapture
}
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
networksDisabled = params.DisableNetworks
}

View File

@@ -535,7 +535,6 @@ func fieldToGlobalVar(field string) string {
"LogFiles": "logFiles",
"DisableProfiles": "profilesDisabled",
"DisableUpdateSettings": "updateSettingsDisabled",
"EnableCapture": "captureEnabled",
"DisableNetworks": "networksDisabled",
"ServiceEnvVars": "serviceEnvVars",
}

View File

@@ -135,7 +135,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
if err != nil {
t.Fatal(err)
}
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil)
if err != nil {
t.Fatal(err)
}
@@ -160,7 +160,7 @@ func startClientDaemon(
s := grpc.NewServer()
server := client.New(ctx,
"", "", false, false, false, false)
"", "", false, false, false)
if err := server.Start(); err != nil {
t.Fatal(err)
}

View File

@@ -39,9 +39,6 @@ const (
noBrowserFlag = "no-browser"
noBrowserDesc = "do not open the browser for SSO login"
showQRFlag = "qr"
showQRDesc = "show QR code for the SSO login URL (useful for headless machines without browser access)"
profileNameFlag = "profile"
profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used."
)
@@ -51,7 +48,6 @@ var (
dnsLabels []string
dnsLabelsValidated domain.List
noBrowser bool
showQR bool
profileName string
configPath string
@@ -84,7 +80,6 @@ func init() {
)
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
upCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ")

View File

@@ -1,65 +0,0 @@
package embed
import (
"io"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/util/capture"
)
// CaptureOptions configures a packet capture session.
type CaptureOptions struct {
// Output receives pcap-formatted data. Nil disables pcap output.
Output io.Writer
// TextOutput receives human-readable packet summaries. Nil disables text output.
TextOutput io.Writer
// Filter is a BPF-like filter expression (e.g. "host 10.0.0.1 and tcp port 443").
// Empty captures all packets.
Filter string
// Verbose adds seq/ack, TTL, window, and total length to text output.
Verbose bool
// ASCII dumps transport payload as printable ASCII after each packet line.
ASCII bool
}
// CaptureStats reports capture session counters.
type CaptureStats struct {
Packets int64
Bytes int64
Dropped int64
}
// CaptureSession represents an active packet capture. Call Stop to end the
// capture and flush buffered packets.
type CaptureSession struct {
sess *capture.Session
engine *internal.Engine
}
// Stop ends the capture, flushes remaining packets, and detaches from the device.
// Safe to call multiple times.
func (cs *CaptureSession) Stop() {
if cs.engine != nil {
_ = cs.engine.SetCapture(nil)
cs.engine = nil
}
if cs.sess != nil {
cs.sess.Stop()
}
}
// Stats returns current capture counters.
func (cs *CaptureSession) Stats() CaptureStats {
s := cs.sess.Stats()
return CaptureStats{
Packets: s.Packets,
Bytes: s.Bytes,
Dropped: s.Dropped,
}
}
// Done returns a channel that is closed when the capture's writer goroutine
// has fully exited and all buffered packets have been flushed.
func (cs *CaptureSession) Done() <-chan struct{} {
return cs.sess.Done()
}

View File

@@ -24,7 +24,6 @@ import (
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/domain"
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/util/capture"
)
var (
@@ -66,7 +65,7 @@ type Options struct {
PrivateKey string
// ManagementURL overrides the default management server URL
ManagementURL string
// PreSharedKey is the pre-shared key for the tunnel interface
// PreSharedKey is the pre-shared key for the WireGuard interface
PreSharedKey string
// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
LogOutput io.Writer
@@ -82,9 +81,9 @@ type Options struct {
DisableClientRoutes bool
// BlockInbound blocks all inbound connections from peers
BlockInbound bool
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
WireguardPort *int
// MTU is the MTU for the tunnel interface.
// MTU is the MTU for the WireGuard interface.
// Valid values are in the range 576..8192 bytes.
// If non-nil, this value overrides any value stored in the config file.
// If nil, the existing config MTU (if non-zero) is preserved; otherwise it defaults to 1280.
@@ -470,52 +469,6 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
}
// StartCapture begins capturing packets on this client's tunnel device.
// Only one capture can be active at a time; starting a new one stops the previous.
// Call StopCapture (or CaptureSession.Stop) to end it.
func (c *Client) StartCapture(opts CaptureOptions) (*CaptureSession, error) {
engine, err := c.getEngine()
if err != nil {
return nil, err
}
var matcher capture.Matcher
if opts.Filter != "" {
m, err := capture.ParseFilter(opts.Filter)
if err != nil {
return nil, fmt.Errorf("parse filter: %w", err)
}
matcher = m
}
sess, err := capture.NewSession(capture.Options{
Output: opts.Output,
TextOutput: opts.TextOutput,
Matcher: matcher,
Verbose: opts.Verbose,
ASCII: opts.ASCII,
})
if err != nil {
return nil, fmt.Errorf("create capture session: %w", err)
}
if err := engine.SetCapture(sess); err != nil {
sess.Stop()
return nil, fmt.Errorf("set capture: %w", err)
}
return &CaptureSession{sess: sess, engine: engine}, nil
}
// StopCapture stops the active capture session if one is running.
func (c *Client) StopCapture() error {
engine, err := c.getEngine()
if err != nil {
return err
}
return engine.SetCapture(nil)
}
// getEngine safely retrieves the engine from the client with proper locking.
// Returns ErrClientNotStarted if the client is not started.
// Returns ErrEngineNotStarted if the engine is not available.

View File

@@ -115,13 +115,12 @@ type Manager struct {
localipmanager *localIPManager
udpTracker *conntrack.UDPTracker
icmpTracker *conntrack.ICMPTracker
tcpTracker *conntrack.TCPTracker
forwarder atomic.Pointer[forwarder.Forwarder]
pendingCapture atomic.Pointer[forwarder.PacketCapture]
logger *nblog.Logger
flowLogger nftypes.FlowLogger
udpTracker *conntrack.UDPTracker
icmpTracker *conntrack.ICMPTracker
tcpTracker *conntrack.TCPTracker
forwarder atomic.Pointer[forwarder.Forwarder]
logger *nblog.Logger
flowLogger nftypes.FlowLogger
blockRule firewall.Rule
@@ -352,19 +351,6 @@ func (m *Manager) determineRouting() error {
return nil
}
// SetPacketCapture sets or clears packet capture on the forwarder endpoint.
// This captures outbound response packets that bypass the FilteredDevice in netstack mode.
func (m *Manager) SetPacketCapture(pc forwarder.PacketCapture) {
if pc == nil {
m.pendingCapture.Store(nil)
} else {
m.pendingCapture.Store(&pc)
}
if fwder := m.forwarder.Load(); fwder != nil {
fwder.SetCapture(pc)
}
}
// initForwarder initializes the forwarder, it disables routing on errors
func (m *Manager) initForwarder() error {
if m.forwarder.Load() != nil {
@@ -386,11 +372,6 @@ func (m *Manager) initForwarder() error {
m.forwarder.Store(forwarder)
// Re-load after store: a concurrent SetPacketCapture may have seen forwarder as nil and only updated pendingCapture.
if pc := m.pendingCapture.Load(); pc != nil {
forwarder.SetCapture(*pc)
}
log.Debug("forwarder initialized")
return nil
@@ -633,7 +614,6 @@ func (m *Manager) resetState() {
}
if fwder := m.forwarder.Load(); fwder != nil {
fwder.SetCapture(nil)
fwder.Stop()
}

View File

@@ -12,19 +12,12 @@ import (
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
)
// PacketCapture captures raw packets for debugging. Implementations must be
// safe for concurrent use and must not block.
type PacketCapture interface {
Offer(data []byte, outbound bool)
}
// endpoint implements stack.LinkEndpoint and handles integration with the wireguard device
type endpoint struct {
logger *nblog.Logger
dispatcher stack.NetworkDispatcher
device *wgdevice.Device
mtu atomic.Uint32
capture atomic.Pointer[PacketCapture]
}
func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) {
@@ -61,17 +54,13 @@ func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error)
continue
}
pktBytes := data.AsSlice()
// Send the packet through WireGuard
address := netHeader.DestinationAddress()
if err := e.device.CreateOutboundPacket(pktBytes, address.AsSlice()); err != nil {
err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice())
if err != nil {
e.logger.Error1("CreateOutboundPacket: %v", err)
continue
}
if pc := e.capture.Load(); pc != nil {
(*pc).Offer(pktBytes, true)
}
written++
}

View File

@@ -139,16 +139,6 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
return f, nil
}
// SetCapture sets or clears the packet capture on the forwarder endpoint.
// This captures outbound packets that bypass the FilteredDevice (netstack forwarding).
func (f *Forwarder) SetCapture(pc PacketCapture) {
if pc == nil {
f.endpoint.capture.Store(nil)
return
}
f.endpoint.capture.Store(&pc)
}
func (f *Forwarder) InjectIncomingPacket(payload []byte) error {
if len(payload) < header.IPv4MinimumSize {
return fmt.Errorf("packet too small: %d bytes", len(payload))

View File

@@ -270,9 +270,5 @@ func (f *Forwarder) injectICMPReply(id stack.TransportEndpointID, icmpPayload []
return 0
}
if pc := f.endpoint.capture.Load(); pc != nil {
(*pc).Offer(fullPacket, true)
}
return len(fullPacket)
}

6
client/flutter_ui/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,36 @@
# 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

@@ -0,0 +1,115 @@
# 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

@@ -0,0 +1,54 @@
# 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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,53 @@
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

@@ -0,0 +1,889 @@
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

@@ -0,0 +1,916 @@
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

@@ -0,0 +1,460 @@
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

@@ -0,0 +1,434 @@
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

@@ -0,0 +1,153 @@
// 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

@@ -0,0 +1,257 @@
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

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,140 @@
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'),
),
],
),
);
}
}

1
client/flutter_ui/linux/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,128 @@
# 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

@@ -0,0 +1,88 @@
# 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

@@ -0,0 +1,27 @@
//
// 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

@@ -0,0 +1,15 @@
//
// 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

@@ -0,0 +1,27 @@
#
# 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

@@ -0,0 +1,26 @@
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

@@ -0,0 +1,6 @@
#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

@@ -0,0 +1,148 @@
#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

@@ -0,0 +1,21 @@
#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_

7
client/flutter_ui/macos/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
//
// 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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,40 @@
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

@@ -0,0 +1,801 @@
// !$*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

@@ -0,0 +1,8 @@
<?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

@@ -0,0 +1,99 @@
<?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

@@ -0,0 +1,10 @@
<?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

@@ -0,0 +1,8 @@
<?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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,68 @@
{
"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.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,343 @@
<?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

@@ -0,0 +1,14 @@
// 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

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

View File

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

View File

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,12 @@
<?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

@@ -0,0 +1,32 @@
<?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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,10 @@
<?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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,413 @@
# 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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,19 @@
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);
});
}

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT
command -v flutter >/dev/null 2>&1 || {
echo "flutter is not installed"
exit 1
}
cp "$project_dir/pubspec.yaml" "$tmp_dir/pubspec.yaml"
cp "$project_dir/analysis_options.yaml" "$tmp_dir/analysis_options.yaml"
cp -R "$project_dir/lib" "$tmp_dir/lib"
cp -R "$project_dir/test" "$tmp_dir/test"
flutter create \
--platforms=windows,macos,linux \
--project-name=netbird_flutter_ui \
--org=io.netbird \
"$project_dir"
cp "$tmp_dir/pubspec.yaml" "$project_dir/pubspec.yaml"
cp "$tmp_dir/analysis_options.yaml" "$project_dir/analysis_options.yaml"
rm -rf "$project_dir/lib"
cp -R "$tmp_dir/lib" "$project_dir/lib"
rm -rf "$project_dir/test"
cp -R "$tmp_dir/test" "$project_dir/test"
cd "$project_dir"
flutter pub get

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