mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-06 00:56:39 +00:00
Compare commits
41 Commits
revert-dns
...
windows-dn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42b8aed90 | ||
|
|
0415137acd | ||
|
|
7fd16666e3 | ||
|
|
0571eeaba0 | ||
|
|
6a201d12b5 | ||
|
|
4810e79a00 | ||
|
|
1795bc801d | ||
|
|
31395f8bd2 | ||
|
|
cd8e71002f | ||
|
|
97db824929 | ||
|
|
77a0992dc2 | ||
|
|
104990dfdd | ||
|
|
bde632c3b2 | ||
|
|
4268a5cfb7 | ||
|
|
a547fc74ed | ||
|
|
a21f6ecb0a | ||
|
|
6262b0d841 | ||
|
|
50b58a6828 | ||
|
|
057d651d2e | ||
|
|
c4b2da4c92 | ||
|
|
dcd1db42ef | ||
|
|
f29f5a0978 | ||
|
|
3fc5a8d4a1 | ||
|
|
57945fc328 | ||
|
|
ed828b7af4 | ||
|
|
11ac2af2f5 | ||
|
|
df197d5001 | ||
|
|
ad93dcf980 | ||
|
|
7eba5dafd8 | ||
|
|
28fe26637b | ||
|
|
407e9d304b | ||
|
|
e5474e199f | ||
|
|
db44848e2d | ||
|
|
9417ce3b3a | ||
|
|
8fc4265995 | ||
|
|
9c50819f20 | ||
|
|
6f0eff3ba0 | ||
|
|
f8745723fc | ||
|
|
154b81645a | ||
|
|
34167c8a16 | ||
|
|
d6f08e4840 |
130
.github/DISCUSSION_TEMPLATE/ideas-feature-requests.yml
vendored
Normal file
130
.github/DISCUSSION_TEMPLATE/ideas-feature-requests.yml
vendored
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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.
|
||||||
237
.github/DISCUSSION_TEMPLATE/issue-triage.yml
vendored
Normal file
237
.github/DISCUSSION_TEMPLATE/issue-triage.yml
vendored
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
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.
|
||||||
146
.github/DISCUSSION_TEMPLATE/q-a-support.yml
vendored
Normal file
146
.github/DISCUSSION_TEMPLATE/q-a-support.yml
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
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.
|
||||||
71
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
71
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
|
|
||||||
26
.github/ISSUE_TEMPLATE/config.yml
vendored
26
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,14 +1,26 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Community Support
|
- 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
|
||||||
url: https://forum.netbird.io/
|
url: https://forum.netbird.io/
|
||||||
about: Community support forum
|
about: Community support forum.
|
||||||
- name: Cloud Support
|
- name: Cloud Support
|
||||||
url: https://docs.netbird.io/help/report-bug-issues
|
url: https://docs.netbird.io/help/report-bug-issues
|
||||||
about: Contact us for support
|
about: Contact NetBird for Cloud support.
|
||||||
- name: Client/Connection Troubleshooting
|
- name: Client / Connection Troubleshooting
|
||||||
url: https://docs.netbird.io/help/troubleshooting-client
|
url: https://docs.netbird.io/help/troubleshooting-client
|
||||||
about: See our client troubleshooting guide for help addressing common issues
|
about: See the client troubleshooting guide for common connectivity issues.
|
||||||
- name: Self-host Troubleshooting
|
- name: Self-host Troubleshooting
|
||||||
url: https://docs.netbird.io/selfhosted/troubleshooting
|
url: https://docs.netbird.io/selfhosted/troubleshooting
|
||||||
about: See our self-host troubleshooting guide for help addressing common issues
|
about: See the self-host troubleshooting guide for common deployment issues.
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
128
.github/ISSUE_TEMPLATE/validated_issue.yml
vendored
Normal file
128
.github/ISSUE_TEMPLATE/validated_issue.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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.
|
||||||
307
.github/workflows/release.yml
vendored
307
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.2"
|
SIGN_PIPE_VER: "v0.1.4"
|
||||||
GORELEASER_VER: "v2.14.3"
|
GORELEASER_VER: "v2.14.3"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
@@ -114,7 +114,13 @@ jobs:
|
|||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest-m
|
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 }}
|
||||||
env:
|
env:
|
||||||
flags: ""
|
flags: ""
|
||||||
steps:
|
steps:
|
||||||
@@ -213,10 +219,13 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||||
- name: Tag and push images (amd64 only)
|
- name: Tag and push images (amd64 only)
|
||||||
|
id: tag_and_push_images
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
resolve_tags() {
|
resolve_tags() {
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
echo "pr-${{ github.event.pull_request.number }}"
|
echo "pr-${{ github.event.pull_request.number }}"
|
||||||
@@ -225,6 +234,17 @@ jobs:
|
|||||||
fi
|
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() {
|
tag_and_push() {
|
||||||
local src="$1" img_name tag dst
|
local src="$1" img_name tag dst
|
||||||
img_name="${src%%:*}"
|
img_name="${src%%:*}"
|
||||||
@@ -233,35 +253,56 @@ jobs:
|
|||||||
echo "Tagging ${src} -> ${dst}"
|
echo "Tagging ${src} -> ${dst}"
|
||||||
docker tag "$src" "$dst"
|
docker tag "$src" "$dst"
|
||||||
docker push "$dst"
|
docker push "$dst"
|
||||||
|
image_refs+=("$dst")
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
export -f tag_and_push resolve_tags
|
cat > /tmp/goreleaser-artifacts.json <<'JSON'
|
||||||
|
${{ steps.goreleaser.outputs.artifacts }}
|
||||||
|
JSON
|
||||||
|
|
||||||
echo '${{ steps.goreleaser.outputs.artifacts }}' | \
|
mapfile -t src_images < <(
|
||||||
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name' | \
|
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name | select(startswith("ghcr.io/"))' /tmp/goreleaser-artifacts.json
|
||||||
grep '^ghcr.io/' | while read -r SRC; do
|
)
|
||||||
tag_and_push "$SRC"
|
|
||||||
done
|
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"
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
|
id: upload_release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload linux packages
|
- name: upload linux packages
|
||||||
|
id: upload_linux_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-packages
|
name: linux-packages
|
||||||
path: dist/netbird_linux**
|
path: dist/netbird_linux**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload windows packages
|
- name: upload windows packages
|
||||||
|
id: upload_windows_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-packages
|
name: windows-packages
|
||||||
path: dist/netbird_windows**
|
path: dist/netbird_windows**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload macos packages
|
- name: upload macos packages
|
||||||
|
id: upload_macos_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-packages
|
name: macos-packages
|
||||||
@@ -270,6 +311,8 @@ jobs:
|
|||||||
|
|
||||||
release_ui:
|
release_ui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
@@ -360,6 +403,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
|
id: upload_release_ui
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
@@ -368,6 +412,8 @@ jobs:
|
|||||||
|
|
||||||
release_ui_darwin:
|
release_ui_darwin:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
outputs:
|
||||||
|
release_ui_darwin_artifact_url: ${{ steps.upload_release_ui_darwin.outputs.artifact-url }}
|
||||||
steps:
|
steps:
|
||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
@@ -402,15 +448,258 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
|
id: upload_release_ui_darwin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
trigger_signer:
|
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
|
runs-on: ubuntu-latest
|
||||||
needs: [release, release_ui, release_ui_darwin]
|
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]
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger binaries sign pipelines
|
- name: Trigger binaries sign pipelines
|
||||||
|
|||||||
28
.github/workflows/sync-tag.yml
vendored
28
.github/workflows/sync-tag.yml
vendored
@@ -9,6 +9,8 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
cancel-in-progress: true
|
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:
|
jobs:
|
||||||
trigger_sync_tag:
|
trigger_sync_tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -20,4 +22,30 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
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 }}" }'
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
@@ -58,6 +58,11 @@ linters:
|
|||||||
govet:
|
govet:
|
||||||
enable:
|
enable:
|
||||||
- nilness
|
- 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
|
enable-all: false
|
||||||
revive:
|
revive:
|
||||||
rules:
|
rules:
|
||||||
@@ -87,6 +92,9 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- unused
|
- unused
|
||||||
path: client/firewall/iptables/rule\.go
|
path: client/firewall/iptables/rule\.go
|
||||||
|
- linters:
|
||||||
|
- unused
|
||||||
|
path: client/internal/dns/dnsfw/(types|syscall|zsyscall)_windows.*\.go
|
||||||
- linters:
|
- linters:
|
||||||
- gosec
|
- gosec
|
||||||
- mirror
|
- mirror
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ ENV \
|
|||||||
NETBIRD_BIN="/usr/local/bin/netbird" \
|
NETBIRD_BIN="/usr/local/bin/netbird" \
|
||||||
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
||||||
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
||||||
|
NB_ENABLE_CAPTURE="false" \
|
||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ ENV \
|
|||||||
NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
|
NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
|
||||||
NB_LOG_FILE="console,/var/lib/netbird/client.log" \
|
NB_LOG_FILE="console,/var/lib/netbird/client.log" \
|
||||||
NB_DISABLE_DNS="true" \
|
NB_DISABLE_DNS="true" \
|
||||||
|
NB_ENABLE_CAPTURE="false" \
|
||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
|||||||
196
client/cmd/capture.go
Normal file
196
client/cmd/capture.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/debug"
|
"github.com/netbirdio/netbird/client/internal/debug"
|
||||||
@@ -239,11 +240,50 @@ 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 {
|
if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil {
|
||||||
return waitErr
|
return waitErr
|
||||||
}
|
}
|
||||||
cmd.Println("\nDuration completed")
|
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 cpuProfilingStarted {
|
||||||
if _, err := client.StopCPUProfile(cmd.Context(), &proto.StopCPUProfileRequest{}); err != nil {
|
if _, err := client.StopCPUProfile(cmd.Context(), &proto.StopCPUProfileRequest{}); err != nil {
|
||||||
cmd.PrintErrf("Failed to stop CPU profiling: %v\n", err)
|
cmd.PrintErrf("Failed to stop CPU profiling: %v\n", err)
|
||||||
@@ -416,4 +456,5 @@ func init() {
|
|||||||
forCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
|
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().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().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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
||||||
|
loginCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
|
||||||
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
||||||
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
|
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
|
||||||
}
|
}
|
||||||
@@ -256,7 +258,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 {
|
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)
|
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser, showQR)
|
||||||
|
|
||||||
resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -324,7 +326,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
|
|||||||
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser)
|
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser, showQR)
|
||||||
|
|
||||||
tokenInfo, err := oAuthFlow.WaitToken(context.TODO(), flowInfo)
|
tokenInfo, err := oAuthFlow.WaitToken(context.TODO(), flowInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -334,7 +336,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
|
|||||||
return &tokenInfo, nil
|
return &tokenInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser bool) {
|
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser, showQR bool) {
|
||||||
var codeMsg string
|
var codeMsg string
|
||||||
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
|
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
|
||||||
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
||||||
@@ -348,6 +350,12 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
|
|||||||
verificationURIComplete + " " + codeMsg)
|
verificationURIComplete + " " + codeMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showQR {
|
||||||
|
if f, ok := cmd.OutOrStdout().(*os.File); ok && term.IsTerminal(int(f.Fd())) {
|
||||||
|
printQRCode(f, verificationURIComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Println("")
|
cmd.Println("")
|
||||||
|
|
||||||
if !noBrowser {
|
if !noBrowser {
|
||||||
|
|||||||
25
client/cmd/qr.go
Normal file
25
client/cmd/qr.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
26
client/cmd/qr_test.go
Normal file
26
client/cmd/qr_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,7 @@ var (
|
|||||||
mtu uint16
|
mtu uint16
|
||||||
profilesDisabled bool
|
profilesDisabled bool
|
||||||
updateSettingsDisabled bool
|
updateSettingsDisabled bool
|
||||||
|
captureEnabled bool
|
||||||
networksDisabled bool
|
networksDisabled bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func init() {
|
|||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd)
|
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(&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(&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")
|
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")
|
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, networksDisabled)
|
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled)
|
||||||
if err := serverInstance.Start(); err != nil {
|
if err := serverInstance.Start(); err != nil {
|
||||||
log.Fatalf("failed to start daemon: %v", err)
|
log.Fatalf("failed to start daemon: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ func buildServiceArguments() []string {
|
|||||||
args = append(args, "--disable-update-settings")
|
args = append(args, "--disable-update-settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if captureEnabled {
|
||||||
|
args = append(args, "--enable-capture")
|
||||||
|
}
|
||||||
|
|
||||||
if networksDisabled {
|
if networksDisabled {
|
||||||
args = append(args, "--disable-networks")
|
args = append(args, "--disable-networks")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type serviceParams struct {
|
|||||||
LogFiles []string `json:"log_files,omitempty"`
|
LogFiles []string `json:"log_files,omitempty"`
|
||||||
DisableProfiles bool `json:"disable_profiles,omitempty"`
|
DisableProfiles bool `json:"disable_profiles,omitempty"`
|
||||||
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
||||||
|
EnableCapture bool `json:"enable_capture,omitempty"`
|
||||||
DisableNetworks bool `json:"disable_networks,omitempty"`
|
DisableNetworks bool `json:"disable_networks,omitempty"`
|
||||||
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -79,6 +80,7 @@ func currentServiceParams() *serviceParams {
|
|||||||
LogFiles: logFiles,
|
LogFiles: logFiles,
|
||||||
DisableProfiles: profilesDisabled,
|
DisableProfiles: profilesDisabled,
|
||||||
DisableUpdateSettings: updateSettingsDisabled,
|
DisableUpdateSettings: updateSettingsDisabled,
|
||||||
|
EnableCapture: captureEnabled,
|
||||||
DisableNetworks: networksDisabled,
|
DisableNetworks: networksDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +146,10 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
|
|||||||
updateSettingsDisabled = params.DisableUpdateSettings
|
updateSettingsDisabled = params.DisableUpdateSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !serviceCmd.PersistentFlags().Changed("enable-capture") {
|
||||||
|
captureEnabled = params.EnableCapture
|
||||||
|
}
|
||||||
|
|
||||||
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
|
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
|
||||||
networksDisabled = params.DisableNetworks
|
networksDisabled = params.DisableNetworks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -535,6 +535,7 @@ func fieldToGlobalVar(field string) string {
|
|||||||
"LogFiles": "logFiles",
|
"LogFiles": "logFiles",
|
||||||
"DisableProfiles": "profilesDisabled",
|
"DisableProfiles": "profilesDisabled",
|
||||||
"DisableUpdateSettings": "updateSettingsDisabled",
|
"DisableUpdateSettings": "updateSettingsDisabled",
|
||||||
|
"EnableCapture": "captureEnabled",
|
||||||
"DisableNetworks": "networksDisabled",
|
"DisableNetworks": "networksDisabled",
|
||||||
"ServiceEnvVars": "serviceEnvVars",
|
"ServiceEnvVars": "serviceEnvVars",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ func startClientDaemon(
|
|||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
|
|
||||||
server := client.New(ctx,
|
server := client.New(ctx,
|
||||||
"", "", false, false, false)
|
"", "", false, false, false, false)
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ const (
|
|||||||
noBrowserFlag = "no-browser"
|
noBrowserFlag = "no-browser"
|
||||||
noBrowserDesc = "do not open the browser for SSO login"
|
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"
|
profileNameFlag = "profile"
|
||||||
profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used."
|
profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used."
|
||||||
)
|
)
|
||||||
@@ -48,6 +51,7 @@ var (
|
|||||||
dnsLabels []string
|
dnsLabels []string
|
||||||
dnsLabelsValidated domain.List
|
dnsLabelsValidated domain.List
|
||||||
noBrowser bool
|
noBrowser bool
|
||||||
|
showQR bool
|
||||||
profileName string
|
profileName string
|
||||||
configPath string
|
configPath string
|
||||||
|
|
||||||
@@ -80,6 +84,7 @@ func init() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
||||||
|
upCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
|
||||||
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
||||||
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ")
|
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ")
|
||||||
|
|
||||||
|
|||||||
65
client/embed/capture.go
Normal file
65
client/embed/capture.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/util/capture"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -65,7 +66,7 @@ type Options struct {
|
|||||||
PrivateKey string
|
PrivateKey string
|
||||||
// ManagementURL overrides the default management server URL
|
// ManagementURL overrides the default management server URL
|
||||||
ManagementURL string
|
ManagementURL string
|
||||||
// PreSharedKey is the pre-shared key for the WireGuard interface
|
// PreSharedKey is the pre-shared key for the tunnel interface
|
||||||
PreSharedKey string
|
PreSharedKey string
|
||||||
// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
|
// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
|
||||||
LogOutput io.Writer
|
LogOutput io.Writer
|
||||||
@@ -81,9 +82,9 @@ type Options struct {
|
|||||||
DisableClientRoutes bool
|
DisableClientRoutes bool
|
||||||
// BlockInbound blocks all inbound connections from peers
|
// BlockInbound blocks all inbound connections from peers
|
||||||
BlockInbound bool
|
BlockInbound bool
|
||||||
// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
|
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
||||||
WireguardPort *int
|
WireguardPort *int
|
||||||
// MTU is the MTU for the WireGuard interface.
|
// MTU is the MTU for the tunnel interface.
|
||||||
// Valid values are in the range 576..8192 bytes.
|
// Valid values are in the range 576..8192 bytes.
|
||||||
// If non-nil, this value overrides any value stored in the config file.
|
// 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.
|
// If nil, the existing config MTU (if non-zero) is preserved; otherwise it defaults to 1280.
|
||||||
@@ -469,6 +470,52 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
|||||||
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
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.
|
// getEngine safely retrieves the engine from the client with proper locking.
|
||||||
// Returns ErrClientNotStarted if the client is not started.
|
// Returns ErrClientNotStarted if the client is not started.
|
||||||
// Returns ErrEngineNotStarted if the engine is not available.
|
// Returns ErrEngineNotStarted if the engine is not available.
|
||||||
|
|||||||
@@ -115,12 +115,13 @@ type Manager struct {
|
|||||||
|
|
||||||
localipmanager *localIPManager
|
localipmanager *localIPManager
|
||||||
|
|
||||||
udpTracker *conntrack.UDPTracker
|
udpTracker *conntrack.UDPTracker
|
||||||
icmpTracker *conntrack.ICMPTracker
|
icmpTracker *conntrack.ICMPTracker
|
||||||
tcpTracker *conntrack.TCPTracker
|
tcpTracker *conntrack.TCPTracker
|
||||||
forwarder atomic.Pointer[forwarder.Forwarder]
|
forwarder atomic.Pointer[forwarder.Forwarder]
|
||||||
logger *nblog.Logger
|
pendingCapture atomic.Pointer[forwarder.PacketCapture]
|
||||||
flowLogger nftypes.FlowLogger
|
logger *nblog.Logger
|
||||||
|
flowLogger nftypes.FlowLogger
|
||||||
|
|
||||||
blockRule firewall.Rule
|
blockRule firewall.Rule
|
||||||
|
|
||||||
@@ -351,6 +352,19 @@ func (m *Manager) determineRouting() error {
|
|||||||
return nil
|
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
|
// initForwarder initializes the forwarder, it disables routing on errors
|
||||||
func (m *Manager) initForwarder() error {
|
func (m *Manager) initForwarder() error {
|
||||||
if m.forwarder.Load() != nil {
|
if m.forwarder.Load() != nil {
|
||||||
@@ -372,6 +386,11 @@ func (m *Manager) initForwarder() error {
|
|||||||
|
|
||||||
m.forwarder.Store(forwarder)
|
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")
|
log.Debug("forwarder initialized")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -614,6 +633,7 @@ func (m *Manager) resetState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fwder := m.forwarder.Load(); fwder != nil {
|
if fwder := m.forwarder.Load(); fwder != nil {
|
||||||
|
fwder.SetCapture(nil)
|
||||||
fwder.Stop()
|
fwder.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,19 @@ import (
|
|||||||
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
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
|
// endpoint implements stack.LinkEndpoint and handles integration with the wireguard device
|
||||||
type endpoint struct {
|
type endpoint struct {
|
||||||
logger *nblog.Logger
|
logger *nblog.Logger
|
||||||
dispatcher stack.NetworkDispatcher
|
dispatcher stack.NetworkDispatcher
|
||||||
device *wgdevice.Device
|
device *wgdevice.Device
|
||||||
mtu atomic.Uint32
|
mtu atomic.Uint32
|
||||||
|
capture atomic.Pointer[PacketCapture]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||||
@@ -54,13 +61,17 @@ func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the packet through WireGuard
|
pktBytes := data.AsSlice()
|
||||||
|
|
||||||
address := netHeader.DestinationAddress()
|
address := netHeader.DestinationAddress()
|
||||||
err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice())
|
if err := e.device.CreateOutboundPacket(pktBytes, address.AsSlice()); err != nil {
|
||||||
if err != nil {
|
|
||||||
e.logger.Error1("CreateOutboundPacket: %v", err)
|
e.logger.Error1("CreateOutboundPacket: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pc := e.capture.Load(); pc != nil {
|
||||||
|
(*pc).Offer(pktBytes, true)
|
||||||
|
}
|
||||||
written++
|
written++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,16 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
|||||||
return f, nil
|
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 {
|
func (f *Forwarder) InjectIncomingPacket(payload []byte) error {
|
||||||
if len(payload) < header.IPv4MinimumSize {
|
if len(payload) < header.IPv4MinimumSize {
|
||||||
return fmt.Errorf("packet too small: %d bytes", len(payload))
|
return fmt.Errorf("packet too small: %d bytes", len(payload))
|
||||||
|
|||||||
@@ -270,5 +270,9 @@ func (f *Forwarder) injectICMPReply(id stack.TransportEndpointID, icmpPayload []
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pc := f.endpoint.capture.Load(); pc != nil {
|
||||||
|
(*pc).Offer(fullPacket, true)
|
||||||
|
}
|
||||||
|
|
||||||
return len(fullPacket)
|
return len(fullPacket)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package device
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun"
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
)
|
)
|
||||||
@@ -28,11 +29,20 @@ type PacketFilter interface {
|
|||||||
SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool)
|
SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PacketCapture captures raw packets for debugging. Implementations must be
|
||||||
|
// safe for concurrent use and must not block.
|
||||||
|
type PacketCapture interface {
|
||||||
|
// Offer submits a packet for capture. outbound is true for packets
|
||||||
|
// leaving the host (Read path), false for packets arriving (Write path).
|
||||||
|
Offer(data []byte, outbound bool)
|
||||||
|
}
|
||||||
|
|
||||||
// FilteredDevice to override Read or Write of packets
|
// FilteredDevice to override Read or Write of packets
|
||||||
type FilteredDevice struct {
|
type FilteredDevice struct {
|
||||||
tun.Device
|
tun.Device
|
||||||
|
|
||||||
filter PacketFilter
|
filter PacketFilter
|
||||||
|
capture atomic.Pointer[PacketCapture]
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
@@ -63,20 +73,25 @@ func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, er
|
|||||||
if n, err = d.Device.Read(bufs, sizes, offset); err != nil {
|
if n, err = d.Device.Read(bufs, sizes, offset); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mutex.RLock()
|
d.mutex.RLock()
|
||||||
filter := d.filter
|
filter := d.filter
|
||||||
d.mutex.RUnlock()
|
d.mutex.RUnlock()
|
||||||
|
|
||||||
if filter == nil {
|
if filter != nil {
|
||||||
return
|
for i := 0; i < n; i++ {
|
||||||
|
if filter.FilterOutbound(bufs[i][offset:offset+sizes[i]], sizes[i]) {
|
||||||
|
bufs = append(bufs[:i], bufs[i+1:]...)
|
||||||
|
sizes = append(sizes[:i], sizes[i+1:]...)
|
||||||
|
n--
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
if pc := d.capture.Load(); pc != nil {
|
||||||
if filter.FilterOutbound(bufs[i][offset:offset+sizes[i]], sizes[i]) {
|
for i := 0; i < n; i++ {
|
||||||
bufs = append(bufs[:i], bufs[i+1:]...)
|
(*pc).Offer(bufs[i][offset:offset+sizes[i]], true)
|
||||||
sizes = append(sizes[:i], sizes[i+1:]...)
|
|
||||||
n--
|
|
||||||
i--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +100,13 @@ func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, er
|
|||||||
|
|
||||||
// Write wraps write method with filtering feature
|
// Write wraps write method with filtering feature
|
||||||
func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
||||||
|
// Capture before filtering so dropped packets are still visible in captures.
|
||||||
|
if pc := d.capture.Load(); pc != nil {
|
||||||
|
for _, buf := range bufs {
|
||||||
|
(*pc).Offer(buf[offset:], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.mutex.RLock()
|
d.mutex.RLock()
|
||||||
filter := d.filter
|
filter := d.filter
|
||||||
d.mutex.RUnlock()
|
d.mutex.RUnlock()
|
||||||
@@ -96,9 +118,10 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
|||||||
filteredBufs := make([][]byte, 0, len(bufs))
|
filteredBufs := make([][]byte, 0, len(bufs))
|
||||||
dropped := 0
|
dropped := 0
|
||||||
for _, buf := range bufs {
|
for _, buf := range bufs {
|
||||||
if !filter.FilterInbound(buf[offset:], len(buf)) {
|
if filter.FilterInbound(buf[offset:], len(buf)) {
|
||||||
filteredBufs = append(filteredBufs, buf)
|
|
||||||
dropped++
|
dropped++
|
||||||
|
} else {
|
||||||
|
filteredBufs = append(filteredBufs, buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,3 +136,14 @@ func (d *FilteredDevice) SetFilter(filter PacketFilter) {
|
|||||||
d.filter = filter
|
d.filter = filter
|
||||||
d.mutex.Unlock()
|
d.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCapture sets or clears the packet capture sink. Pass nil to disable.
|
||||||
|
// Uses atomic store so the hot path (Read/Write) is a single pointer load
|
||||||
|
// with no locking overhead when capture is off.
|
||||||
|
func (d *FilteredDevice) SetCapture(pc PacketCapture) {
|
||||||
|
if pc == nil {
|
||||||
|
d.capture.Store(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.capture.Store(&pc)
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func TestDeviceWrapperRead(t *testing.T) {
|
|||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if n != 0 {
|
if n != 1 {
|
||||||
t.Errorf("expected n=1, got %d", n)
|
t.Errorf("expected n=1, got %d", n)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,18 @@ Pop $0
|
|||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
StrCpy $INSTDIR "${INSTALL_DIR}"
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
|
; Default autostart to enabled so silent installs (/S) match the interactive default
|
||||||
|
StrCpy $AutostartEnabled "1"
|
||||||
|
|
||||||
|
; Pre-0.70.1 installers ran without SetRegView, so their uninstall keys live
|
||||||
|
; in the 32-bit view. Fall back to it so upgrades still find them.
|
||||||
|
SetRegView 64
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
|
${If} $R0 == ""
|
||||||
|
SetRegView 32
|
||||||
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
|
SetRegView 64
|
||||||
|
${EndIf}
|
||||||
${If} $R0 != ""
|
${If} $R0 != ""
|
||||||
# if silent install jump to uninstall step
|
# if silent install jump to uninstall step
|
||||||
IfSilent uninstall
|
IfSilent uninstall
|
||||||
@@ -214,6 +225,10 @@ ${If} $R0 != ""
|
|||||||
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.onInit
|
||||||
|
SetRegView 64
|
||||||
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
@@ -228,6 +243,7 @@ Section -MainProgram
|
|||||||
!else
|
!else
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
!endif
|
!endif
|
||||||
|
File "..\\client\\ui\\assets\\netbird.png"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@@ -247,9 +263,11 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
|||||||
; Create autostart registry entry based on checkbox
|
; Create autostart registry entry based on checkbox
|
||||||
DetailPrint "Autostart enabled: $AutostartEnabled"
|
DetailPrint "Autostart enabled: $AutostartEnabled"
|
||||||
${If} $AutostartEnabled == "1"
|
${If} $AutostartEnabled == "1"
|
||||||
WriteRegStr HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}" "$INSTDIR\${UI_APP_EXE}.exe"
|
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
||||||
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
||||||
${Else}
|
${Else}
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
DetailPrint "Autostart not enabled by user"
|
DetailPrint "Autostart not enabled by user"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
@@ -283,6 +301,8 @@ ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
|||||||
|
|
||||||
; Remove autostart registry entry
|
; Remove autostart registry entry
|
||||||
DetailPrint "Removing autostart registry entry if exists..."
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
|
||||||
; Handle data deletion based on checkbox
|
; Handle data deletion based on checkbox
|
||||||
@@ -321,6 +341,7 @@ DetailPrint "Removing registry keys..."
|
|||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
||||||
|
DeleteRegKey HKCU "Software\Classes\AppUserModelId\${APP_NAME}"
|
||||||
|
|
||||||
DetailPrint "Removing application directory from PATH..."
|
DetailPrint "Removing application directory from PATH..."
|
||||||
EnVar::SetHKLM
|
EnVar::SetHKLM
|
||||||
|
|||||||
@@ -333,6 +333,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.statusRecorder.MarkSignalConnected()
|
c.statusRecorder.MarkSignalConnected()
|
||||||
|
|
||||||
relayURLs, token := parseRelayInfo(loginResp)
|
relayURLs, token := parseRelayInfo(loginResp)
|
||||||
|
if override, ok := peer.OverrideRelayURLs(); ok {
|
||||||
|
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
|
||||||
|
relayURLs = override
|
||||||
|
}
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)
|
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ allocs.prof: Allocations profiling information.
|
|||||||
threadcreate.prof: Thread creation profiling information.
|
threadcreate.prof: Thread creation profiling information.
|
||||||
cpu.prof: CPU profiling information.
|
cpu.prof: CPU profiling information.
|
||||||
stack_trace.txt: Complete stack traces of all goroutines at the time of bundle creation.
|
stack_trace.txt: Complete stack traces of all goroutines at the time of bundle creation.
|
||||||
|
capture.pcap: Packet capture in pcap format. Only present when capture was running during bundle collection. Omitted from anonymized bundles because it contains raw decrypted packet data.
|
||||||
|
|
||||||
|
|
||||||
Anonymization Process
|
Anonymization Process
|
||||||
@@ -234,6 +235,7 @@ type BundleGenerator struct {
|
|||||||
logPath string
|
logPath string
|
||||||
tempDir string
|
tempDir string
|
||||||
cpuProfile []byte
|
cpuProfile []byte
|
||||||
|
capturePath string
|
||||||
refreshStatus func() // Optional callback to refresh status before bundle generation
|
refreshStatus func() // Optional callback to refresh status before bundle generation
|
||||||
clientMetrics MetricsExporter
|
clientMetrics MetricsExporter
|
||||||
|
|
||||||
@@ -257,7 +259,8 @@ type GeneratorDependencies struct {
|
|||||||
LogPath string
|
LogPath string
|
||||||
TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used.
|
TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used.
|
||||||
CPUProfile []byte
|
CPUProfile []byte
|
||||||
RefreshStatus func() // Optional callback to refresh status before bundle generation
|
CapturePath string
|
||||||
|
RefreshStatus func()
|
||||||
ClientMetrics MetricsExporter
|
ClientMetrics MetricsExporter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +280,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
|
|||||||
logPath: deps.LogPath,
|
logPath: deps.LogPath,
|
||||||
tempDir: deps.TempDir,
|
tempDir: deps.TempDir,
|
||||||
cpuProfile: deps.CPUProfile,
|
cpuProfile: deps.CPUProfile,
|
||||||
|
capturePath: deps.CapturePath,
|
||||||
refreshStatus: deps.RefreshStatus,
|
refreshStatus: deps.RefreshStatus,
|
||||||
clientMetrics: deps.ClientMetrics,
|
clientMetrics: deps.ClientMetrics,
|
||||||
|
|
||||||
@@ -346,6 +350,10 @@ func (g *BundleGenerator) createArchive() error {
|
|||||||
log.Errorf("failed to add CPU profile to debug bundle: %v", err)
|
log.Errorf("failed to add CPU profile to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := g.addCaptureFile(); err != nil {
|
||||||
|
log.Errorf("failed to add capture file to debug bundle: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := g.addStackTrace(); err != nil {
|
if err := g.addStackTrace(); err != nil {
|
||||||
log.Errorf("failed to add stack trace to debug bundle: %v", err)
|
log.Errorf("failed to add stack trace to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
@@ -669,6 +677,29 @@ func (g *BundleGenerator) addCPUProfile() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *BundleGenerator) addCaptureFile() error {
|
||||||
|
if g.capturePath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.anonymize {
|
||||||
|
log.Info("skipping capture file in anonymized bundle (contains raw packet data)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(g.capturePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open capture file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := g.addFileToZip(f, "capture.pcap"); err != nil {
|
||||||
|
return fmt.Errorf("add capture file to zip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) addStackTrace() error {
|
func (g *BundleGenerator) addStackTrace() error {
|
||||||
buf := make([]byte, 5242880) // 5 MB buffer
|
buf := make([]byte, 5242880) // 5 MB buffer
|
||||||
n := runtime.Stack(buf, true)
|
n := runtime.Stack(buf, true)
|
||||||
|
|||||||
63
client/internal/dns/dnsfw/config.go
Normal file
63
client/internal/dns/dnsfw/config.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EnvDisable disables the DNS firewall entirely when set to a truthy value.
|
||||||
|
EnvDisable = "NB_DISABLE_DNS_FIREWALL"
|
||||||
|
// EnvPorts overrides the comma-separated list of remote ports to block.
|
||||||
|
// Empty disables the firewall.
|
||||||
|
EnvPorts = "NB_DNS_FIREWALL_PORTS"
|
||||||
|
// EnvStrict enables strict mode: permit DNS only to the virtual DNS IP
|
||||||
|
// and the netbird daemon. Default mode also permits anything on the
|
||||||
|
// netbird tunnel interface, which is safer if NRPT is silently ignored
|
||||||
|
// by Windows but lets apps reach custom DNS servers via the tunnel.
|
||||||
|
EnvStrict = "NB_DNS_FIREWALL_STRICT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultBlockedPorts are the well-known DNS ports we block for non-netbird
|
||||||
|
// processes: 53 (plain DNS) and 853 (DNS-over-TLS).
|
||||||
|
var defaultBlockedPorts = []uint16{53, 853}
|
||||||
|
|
||||||
|
// blockedPorts returns the effective port list, honoring env overrides.
|
||||||
|
// A nil return means the firewall should not be installed.
|
||||||
|
func blockedPorts() []uint16 {
|
||||||
|
if disabled, _ := strconv.ParseBool(os.Getenv(EnvDisable)); disabled {
|
||||||
|
log.Infof("dns firewall disabled via %s", EnvDisable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override, ok := os.LookupEnv(EnvPorts)
|
||||||
|
if !ok {
|
||||||
|
return defaultBlockedPorts
|
||||||
|
}
|
||||||
|
|
||||||
|
var ports []uint16
|
||||||
|
for _, raw := range strings.Split(override, ",") {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseUint(raw, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("dns firewall: ignoring invalid port %q in %s: %v", raw, EnvPorts, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
log.Warnf("dns firewall: ignoring port 0 in %s", EnvPorts)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ports = append(ports, uint16(port))
|
||||||
|
}
|
||||||
|
if len(ports) == 0 {
|
||||||
|
log.Infof("dns firewall disabled: %s yielded no valid ports", EnvPorts)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}
|
||||||
39
client/internal/dns/dnsfw/config_test.go
Normal file
39
client/internal/dns/dnsfw/config_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlockedPorts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
disable string
|
||||||
|
ports string
|
||||||
|
setPorts bool
|
||||||
|
want []uint16
|
||||||
|
}{
|
||||||
|
{name: "default", want: defaultBlockedPorts},
|
||||||
|
{name: "disabled", disable: "true", want: nil},
|
||||||
|
{name: "disabled false keeps default", disable: "false", want: defaultBlockedPorts},
|
||||||
|
{name: "override single port", ports: "53", setPorts: true, want: []uint16{53}},
|
||||||
|
{name: "override multi", ports: "53, 853 ,5353", setPorts: true, want: []uint16{53, 853, 5353}},
|
||||||
|
{name: "override empty disables", ports: "", setPorts: true, want: nil},
|
||||||
|
{name: "override invalid skipped", ports: "53,not-a-port,853", setPorts: true, want: []uint16{53, 853}},
|
||||||
|
{name: "override zero skipped", ports: "53,0,853", setPorts: true, want: []uint16{53, 853}},
|
||||||
|
{name: "override only invalid disables", ports: "abc", setPorts: true, want: nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Setenv(EnvDisable, tc.disable)
|
||||||
|
if tc.setPorts {
|
||||||
|
t.Setenv(EnvPorts, tc.ports)
|
||||||
|
}
|
||||||
|
got := blockedPorts()
|
||||||
|
if !reflect.DeepEqual(got, tc.want) {
|
||||||
|
t.Fatalf("blockedPorts() = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
16
client/internal/dns/dnsfw/dnsfw.go
Normal file
16
client/internal/dns/dnsfw/dnsfw.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Package dnsfw blocks DNS traffic from non-netbird processes when netbird is
|
||||||
|
// managing the host's DNS, so that resolvers running on apps or libraries
|
||||||
|
// outside netbird cannot bypass the configured DNS path.
|
||||||
|
//
|
||||||
|
// Implementation is Windows-only (uses WFP). On other platforms New returns
|
||||||
|
// a no-op manager.
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
|
// Manager controls the per-tunnel DNS firewall. Both methods must be safe
|
||||||
|
// to call multiple times.
|
||||||
|
type Manager interface {
|
||||||
|
Enable(ifaceGUID string, virtualDNSIP netip.Addr) error
|
||||||
|
Disable() error
|
||||||
|
}
|
||||||
15
client/internal/dns/dnsfw/dnsfw_other.go
Normal file
15
client/internal/dns/dnsfw/dnsfw_other.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
|
type noopManager struct{}
|
||||||
|
|
||||||
|
func (noopManager) Enable(string, netip.Addr) error { return nil }
|
||||||
|
func (noopManager) Disable() error { return nil }
|
||||||
|
|
||||||
|
// New returns a no-op manager on non-Windows platforms.
|
||||||
|
func New() Manager {
|
||||||
|
return noopManager{}
|
||||||
|
}
|
||||||
144
client/internal/dns/dnsfw/dnsfw_windows.go
Normal file
144
client/internal/dns/dnsfw/dnsfw_windows.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
modIphlpapi = windows.NewLazyDLL("iphlpapi.dll")
|
||||||
|
procConvertInterfaceGuidToLuid = modIphlpapi.NewProc("ConvertInterfaceGuidToLuid")
|
||||||
|
)
|
||||||
|
|
||||||
|
type windowsManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
// session is the WFP engine handle. Zero when disabled.
|
||||||
|
session uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable installs the dns firewall. Strict mode propagates failures;
|
||||||
|
// non-strict mode logs and returns nil so partial protection is preserved.
|
||||||
|
func (m *windowsManager) Enable(ifaceGUID string, virtualDNSIP netip.Addr) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
ports := blockedPorts()
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.session != 0 {
|
||||||
|
if err := m.disableLocked(); err != nil {
|
||||||
|
return fmt.Errorf("reset existing dns firewall session: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strict := strictMode()
|
||||||
|
|
||||||
|
luid, err := luidFromGUID(ifaceGUID)
|
||||||
|
if err != nil {
|
||||||
|
return m.failOrLog(strict, fmt.Errorf("resolve tun luid from guid %s: %w", ifaceGUID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return m.failOrLog(strict, fmt.Errorf("resolve daemon executable path: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := installConfig{
|
||||||
|
tunLUID: luid,
|
||||||
|
daemonExe: exe,
|
||||||
|
blockedPorts: ports,
|
||||||
|
strict: strict,
|
||||||
|
virtualDNSIP: virtualDNSIP,
|
||||||
|
}
|
||||||
|
// session==0 signals a hard failure; non-zero with non-nil err is a partial install.
|
||||||
|
session, installErr := installFilters(cfg)
|
||||||
|
if session == 0 {
|
||||||
|
return m.failOrLog(strict, fmt.Errorf("install dns firewall filters: %w", installErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
if installErr != nil && strict {
|
||||||
|
_ = closeSession(session)
|
||||||
|
return fmt.Errorf("strict dns firewall: partial install: %w", installErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.session = session
|
||||||
|
log.Infof("dns firewall installed: iface=%s daemon=%s ports=%v strict=%v virtual_dns=%s",
|
||||||
|
ifaceGUID, exe, ports, strict, virtualDNSIP)
|
||||||
|
if installErr != nil {
|
||||||
|
log.Warnf("dns firewall partially installed (some filters failed): %v", installErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *windowsManager) Disable() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.disableLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *windowsManager) disableLocked() error {
|
||||||
|
if m.session == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
session := m.session
|
||||||
|
m.session = 0
|
||||||
|
if err := closeSession(session); err != nil {
|
||||||
|
return fmt.Errorf("close wfp session: %w", err)
|
||||||
|
}
|
||||||
|
log.Info("dns firewall removed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// failOrLog returns err unchanged in strict mode. In non-strict mode the
|
||||||
|
// error is logged and nil is returned.
|
||||||
|
func (m *windowsManager) failOrLog(strict bool, err error) error {
|
||||||
|
if strict {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Errorf("dns firewall: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Windows DNS firewall manager backed by WFP.
|
||||||
|
func New() Manager {
|
||||||
|
return &windowsManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// strictMode reports whether strict mode is enabled via env.
|
||||||
|
func strictMode() bool {
|
||||||
|
v, _ := strconv.ParseBool(os.Getenv(EnvStrict))
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// luidFromGUID converts a Windows interface GUID string to its LUID.
|
||||||
|
func luidFromGUID(ifaceGUID string) (luid uint64, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic in luidFromGUID: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guid, err := windows.GUIDFromString(ifaceGUID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parse guid: %w", err)
|
||||||
|
}
|
||||||
|
rc, _, _ := procConvertInterfaceGuidToLuid.Call(
|
||||||
|
uintptr(unsafe.Pointer(&guid)),
|
||||||
|
uintptr(unsafe.Pointer(&luid)),
|
||||||
|
)
|
||||||
|
if rc != 0 {
|
||||||
|
return 0, fmt.Errorf("ConvertInterfaceGuidToLuid returned %d", rc)
|
||||||
|
}
|
||||||
|
return luid, nil
|
||||||
|
}
|
||||||
72
client/internal/dns/dnsfw/dnsfw_windows_test.go
Normal file
72
client/internal/dns/dnsfw/dnsfw_windows_test.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStrictMode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
val string
|
||||||
|
set bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "unset", want: false},
|
||||||
|
{name: "true", val: "true", set: true, want: true},
|
||||||
|
{name: "1", val: "1", set: true, want: true},
|
||||||
|
{name: "false", val: "false", set: true, want: false},
|
||||||
|
{name: "invalid is false", val: "garbage", set: true, want: false},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Setenv(EnvStrict, tc.val)
|
||||||
|
if !tc.set {
|
||||||
|
os.Unsetenv(EnvStrict)
|
||||||
|
}
|
||||||
|
if got := strictMode(); got != tc.want {
|
||||||
|
t.Fatalf("strictMode() = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowsManagerDisableIdempotent(t *testing.T) {
|
||||||
|
m := &windowsManager{}
|
||||||
|
if err := m.Disable(); err != nil {
|
||||||
|
t.Fatalf("first Disable on fresh manager: %v", err)
|
||||||
|
}
|
||||||
|
if err := m.Disable(); err != nil {
|
||||||
|
t.Fatalf("second Disable on fresh manager: %v", err)
|
||||||
|
}
|
||||||
|
if m.session != 0 {
|
||||||
|
t.Fatalf("session should remain zero, got %d", m.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowsManagerEnableNoOpWhenDisabledByEnv(t *testing.T) {
|
||||||
|
t.Setenv(EnvDisable, "true")
|
||||||
|
|
||||||
|
m := &windowsManager{}
|
||||||
|
if err := m.Enable("00000000-0000-0000-0000-000000000000", netip.Addr{}); err != nil {
|
||||||
|
t.Fatalf("Enable should be a no-op when firewall disabled by env: %v", err)
|
||||||
|
}
|
||||||
|
if m.session != 0 {
|
||||||
|
t.Fatalf("session must remain zero when env disables firewall, got %d", m.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowsManagerEnableNoOpWhenPortsEmpty(t *testing.T) {
|
||||||
|
t.Setenv(EnvPorts, "")
|
||||||
|
|
||||||
|
m := &windowsManager{}
|
||||||
|
if err := m.Enable("00000000-0000-0000-0000-000000000000", netip.Addr{}); err != nil {
|
||||||
|
t.Fatalf("Enable should be a no-op when ports list is empty: %v", err)
|
||||||
|
}
|
||||||
|
if m.session != 0 {
|
||||||
|
t.Fatalf("session must remain zero when ports list is empty, got %d", m.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
client/internal/dns/dnsfw/helpers_windows.go
Normal file
53
client/internal/dns/dnsfw/helpers_windows.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Adapted from wireguard-windows tunnel/firewall/helpers.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createWtFwpmDisplayData0(name, description string) (*wtFwpmDisplayData0, error) {
|
||||||
|
namePtr, err := windows.UTF16PtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionPtr, err := windows.UTF16PtrFromString(description)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &wtFwpmDisplayData0{
|
||||||
|
name: namePtr,
|
||||||
|
description: descriptionPtr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterWeight(weight uint8) wtFwpValue0 {
|
||||||
|
return wtFwpValue0{
|
||||||
|
_type: cFWP_UINT8,
|
||||||
|
value: uintptr(weight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapErr(err error) error {
|
||||||
|
var errno syscall.Errno
|
||||||
|
if !errors.As(err, &errno) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, file, line, ok := runtime.Caller(1)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("wfp error at unknown location: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("wfp error at %s:%d: %w", file, line, err)
|
||||||
|
}
|
||||||
249
client/internal/dns/dnsfw/rules_windows.go
Normal file
249
client/internal/dns/dnsfw/rules_windows.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
* Copyright (C) 2026 NetBird GmbH. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Filter installers adapted from wireguard-windows tunnel/firewall/rules.go.
|
||||||
|
* The block-DNS approach (port 53 + UDP/TCP) matches what wireguard-windows
|
||||||
|
* uses for its kill-switch DNS leak protection. We extend it with a
|
||||||
|
* configurable port set so we also cover :853 (DoT) and any future ports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filters install at outbound ALE_AUTH_CONNECT layers only; inbound replies
|
||||||
|
// follow the authorized outbound flow.
|
||||||
|
|
||||||
|
// permitTunInterface installs a permit filter for any traffic whose local
|
||||||
|
// interface is the netbird tunnel.
|
||||||
|
func permitTunInterface(session uintptr, base *baseObjects, weight uint8, ifLUID uint64) error {
|
||||||
|
cond := wtFwpmFilterCondition0{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_LOCAL_INTERFACE,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_UINT64,
|
||||||
|
value: uintptr(unsafe.Pointer(&ifLUID)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := wtFwpmFilter0{
|
||||||
|
providerKey: &base.provider,
|
||||||
|
subLayerKey: base.filters,
|
||||||
|
weight: filterWeight(weight),
|
||||||
|
numFilterConditions: 1,
|
||||||
|
filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&cond)),
|
||||||
|
action: wtFwpmAction0{_type: cFWP_ACTION_PERMIT},
|
||||||
|
}
|
||||||
|
|
||||||
|
return addOutboundFilters(session, &filter, "Permit netbird tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// permitDaemonByAppID installs a permit filter matching the netbird daemon
|
||||||
|
// executable by App-ID. App-ID alone is sufficient because netbird.exe is a
|
||||||
|
// dedicated binary.
|
||||||
|
func permitDaemonByAppID(session uintptr, base *baseObjects, daemonExe string, weight uint8) error {
|
||||||
|
appID, err := daemonAppID(daemonExe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fwpmFreeMemory0(unsafe.Pointer(&appID))
|
||||||
|
|
||||||
|
cond := wtFwpmFilterCondition0{
|
||||||
|
fieldKey: cFWPM_CONDITION_ALE_APP_ID,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_BYTE_BLOB_TYPE,
|
||||||
|
value: uintptr(unsafe.Pointer(appID)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := wtFwpmFilter0{
|
||||||
|
providerKey: &base.provider,
|
||||||
|
subLayerKey: base.filters,
|
||||||
|
weight: filterWeight(weight),
|
||||||
|
numFilterConditions: 1,
|
||||||
|
filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&cond)),
|
||||||
|
action: wtFwpmAction0{_type: cFWP_ACTION_PERMIT},
|
||||||
|
}
|
||||||
|
|
||||||
|
return addOutboundFilters(session, &filter, "Permit netbird daemon")
|
||||||
|
}
|
||||||
|
|
||||||
|
// permitVirtualDNSIP installs a permit filter for DNS-port traffic destined
|
||||||
|
// for the in-tunnel virtual DNS IP. Used in strict mode in lieu of
|
||||||
|
// permitTunInterface.
|
||||||
|
func permitVirtualDNSIP(session uintptr, base *baseObjects, ip netip.Addr, ports []uint16, weight uint8) error {
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, port := range ports {
|
||||||
|
if err := permitDNSToHost(session, base, ip, port, weight); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("permit %s:%d: %w", ip, port, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func permitDNSToHost(session uintptr, base *baseObjects, ip netip.Addr, port uint16, weight uint8) error {
|
||||||
|
if !ip.IsValid() {
|
||||||
|
return fmt.Errorf("invalid address")
|
||||||
|
}
|
||||||
|
|
||||||
|
var addrCond wtFwpmFilterCondition0
|
||||||
|
var layer windows.GUID
|
||||||
|
// v6 backing must outlive fwpmFilterAdd0; keep it on this stack frame.
|
||||||
|
var v6 wtFwpByteArray16
|
||||||
|
|
||||||
|
if ip.Is4() {
|
||||||
|
v4 := ip.As4()
|
||||||
|
addrCond = wtFwpmFilterCondition0{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_REMOTE_ADDRESS,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_UINT32,
|
||||||
|
value: uintptr(binary.BigEndian.Uint32(v4[:])),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
layer = cFWPM_LAYER_ALE_AUTH_CONNECT_V4
|
||||||
|
} else {
|
||||||
|
v6 = wtFwpByteArray16{byteArray16: ip.As16()}
|
||||||
|
addrCond = wtFwpmFilterCondition0{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_REMOTE_ADDRESS,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_BYTE_ARRAY16_TYPE,
|
||||||
|
value: uintptr(unsafe.Pointer(&v6)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
layer = cFWPM_LAYER_ALE_AUTH_CONNECT_V6
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions := [2]wtFwpmFilterCondition0{
|
||||||
|
addrCond,
|
||||||
|
{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_REMOTE_PORT,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_UINT16,
|
||||||
|
value: uintptr(port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
filter := wtFwpmFilter0{
|
||||||
|
providerKey: &base.provider,
|
||||||
|
subLayerKey: base.filters,
|
||||||
|
weight: filterWeight(weight),
|
||||||
|
numFilterConditions: uint32(len(conditions)),
|
||||||
|
filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions[0])),
|
||||||
|
action: wtFwpmAction0{_type: cFWP_ACTION_PERMIT},
|
||||||
|
}
|
||||||
|
|
||||||
|
display, err := createWtFwpmDisplayData0(fmt.Sprintf("Permit DNS to %s:%d", ip, port), "")
|
||||||
|
if err != nil {
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
filter.displayData = *display
|
||||||
|
filter.layerKey = layer
|
||||||
|
|
||||||
|
var filterID uint64
|
||||||
|
if err := fwpmFilterAdd0(session, &filter, 0, &filterID); err != nil {
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
_ = v6
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockDNSPorts installs a deny filter for outbound traffic to each of the
|
||||||
|
// given remote ports over UDP or TCP. Per-port and per-layer failures are
|
||||||
|
// accumulated; partial coverage is preferred over zero coverage.
|
||||||
|
func blockDNSPorts(session uintptr, base *baseObjects, ports []uint16, weight uint8) error {
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, port := range ports {
|
||||||
|
if err := blockDNSPort(session, base, port, weight); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("block port %d: %w", port, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func blockDNSPort(session uintptr, base *baseObjects, port uint16, weight uint8) error {
|
||||||
|
conditions := [3]wtFwpmFilterCondition0{
|
||||||
|
{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_REMOTE_PORT,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_UINT16,
|
||||||
|
value: uintptr(port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_PROTOCOL,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_UINT8,
|
||||||
|
value: uintptr(cIPPROTO_UDP),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Repeat the IP_PROTOCOL condition for logical OR with TCP.
|
||||||
|
{
|
||||||
|
fieldKey: cFWPM_CONDITION_IP_PROTOCOL,
|
||||||
|
matchType: cFWP_MATCH_EQUAL,
|
||||||
|
conditionValue: wtFwpConditionValue0{
|
||||||
|
_type: cFWP_UINT8,
|
||||||
|
value: uintptr(cIPPROTO_TCP),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := wtFwpmFilter0{
|
||||||
|
providerKey: &base.provider,
|
||||||
|
subLayerKey: base.filters,
|
||||||
|
weight: filterWeight(weight),
|
||||||
|
numFilterConditions: uint32(len(conditions)),
|
||||||
|
filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions[0])),
|
||||||
|
action: wtFwpmAction0{_type: cFWP_ACTION_BLOCK},
|
||||||
|
}
|
||||||
|
|
||||||
|
return addOutboundFilters(session, &filter, fmt.Sprintf("Block DNS port %d", port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addOutboundFilters installs the same filter on the v4 and v6 outbound ALE
|
||||||
|
// connect layers. v4 and v6 are installed independently: failure on one
|
||||||
|
// layer does not abort the other, and the accumulated errors are returned.
|
||||||
|
// Partial coverage is preferred over zero coverage.
|
||||||
|
func addOutboundFilters(session uintptr, filter *wtFwpmFilter0, name string) error {
|
||||||
|
layers := [...]struct {
|
||||||
|
layer windows.GUID
|
||||||
|
label string
|
||||||
|
}{
|
||||||
|
{cFWPM_LAYER_ALE_AUTH_CONNECT_V4, name + " (IPv4)"},
|
||||||
|
{cFWPM_LAYER_ALE_AUTH_CONNECT_V6, name + " (IPv6)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, l := range layers {
|
||||||
|
display, err := createWtFwpmDisplayData0(l.label, "")
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("%s: %w", l.label, wrapErr(err)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filter.displayData = *display
|
||||||
|
filter.layerKey = l.layer
|
||||||
|
|
||||||
|
var filterID uint64
|
||||||
|
if err := fwpmFilterAdd0(session, filter, 0, &filterID); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("%s: %w", l.label, wrapErr(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
177
client/internal/dns/dnsfw/session_windows.go
Normal file
177
client/internal/dns/dnsfw/session_windows.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
* Copyright (C) 2026 NetBird GmbH. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Session lifecycle and the high-level Install/Close entry points adapted
|
||||||
|
* from wireguard-windows tunnel/firewall.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// installConfig is the input to installFilters.
|
||||||
|
type installConfig struct {
|
||||||
|
tunLUID uint64
|
||||||
|
daemonExe string
|
||||||
|
blockedPorts []uint16
|
||||||
|
// strict, when true, narrows the carve-out from "anything on tun" to
|
||||||
|
// "DNS only to virtualDNSIP". virtualDNSIP must be valid in this case.
|
||||||
|
strict bool
|
||||||
|
virtualDNSIP netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseObjects holds the GUIDs of the WFP provider and sublayer registered
|
||||||
|
// for our session. Both are randomly generated per session.
|
||||||
|
type baseObjects struct {
|
||||||
|
provider windows.GUID
|
||||||
|
filters windows.GUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// installFilters opens a dynamic WFP session and installs the netbird DNS
|
||||||
|
// firewall filters. Returns a zero session on hard failure (session create,
|
||||||
|
// base objects); a non-zero session with a non-nil error is a partial install
|
||||||
|
// (some per-filter installs failed) and is safe to close.
|
||||||
|
func installFilters(cfg installConfig) (session uintptr, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Dynamic session: kernel will clean up on process exit even
|
||||||
|
// if we leave the handle dangling here.
|
||||||
|
err = fmt.Errorf("panic in installFilters: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(cfg.blockedPorts) == 0 {
|
||||||
|
return 0, errors.New("dns firewall: no blocked ports configured")
|
||||||
|
}
|
||||||
|
if cfg.strict && !cfg.virtualDNSIP.IsValid() {
|
||||||
|
return 0, errors.New("dns firewall: strict mode requires a valid virtual DNS IP")
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err = createSession()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
base, err := registerBaseObjects(session)
|
||||||
|
if err != nil {
|
||||||
|
_ = fwpmEngineClose0(session)
|
||||||
|
return 0, fmt.Errorf("register base objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
if cfg.strict {
|
||||||
|
if err := permitVirtualDNSIP(session, base, cfg.virtualDNSIP, cfg.blockedPorts, 15); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("permit virtual dns: %w", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := permitTunInterface(session, base, 15, cfg.tunLUID); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("permit tun interface: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := permitDaemonByAppID(session, base, cfg.daemonExe, 14); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("permit netbird daemon: %w", err))
|
||||||
|
}
|
||||||
|
if err := blockDNSPorts(session, base, cfg.blockedPorts, 10); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("block dns ports: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeSession tears down a WFP session previously opened by installFilters.
|
||||||
|
// All filters owned by the session are removed.
|
||||||
|
func closeSession(session uintptr) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic in closeSession: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if session == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := fwpmEngineClose0(session); err != nil {
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSession() (uintptr, error) {
|
||||||
|
displayData, err := createWtFwpmDisplayData0("NetBird DNS firewall", "NetBird DNS firewall dynamic session")
|
||||||
|
if err != nil {
|
||||||
|
return 0, wrapErr(err)
|
||||||
|
}
|
||||||
|
session := wtFwpmSession0{
|
||||||
|
displayData: *displayData,
|
||||||
|
flags: cFWPM_SESSION_FLAG_DYNAMIC,
|
||||||
|
txnWaitTimeoutInMSec: windows.INFINITE,
|
||||||
|
}
|
||||||
|
var handle uintptr
|
||||||
|
if err := fwpmEngineOpen0(nil, cRPC_C_AUTHN_WINNT, nil, &session, unsafe.Pointer(&handle)); err != nil {
|
||||||
|
return 0, wrapErr(err)
|
||||||
|
}
|
||||||
|
return handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerBaseObjects(session uintptr) (*baseObjects, error) {
|
||||||
|
bo := &baseObjects{}
|
||||||
|
var err error
|
||||||
|
if bo.provider, err = windows.GenerateGUID(); err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
if bo.filters, err = windows.GenerateGUID(); err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
displayData, err := createWtFwpmDisplayData0("NetBird DNS firewall", "NetBird DNS firewall provider")
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
provider := wtFwpmProvider0{
|
||||||
|
providerKey: bo.provider,
|
||||||
|
displayData: *displayData,
|
||||||
|
}
|
||||||
|
if err := fwpmProviderAdd0(session, &provider, 0); err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subDisplay, err := createWtFwpmDisplayData0("NetBird DNS firewall filters", "Permit and block filters")
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
sublayer := wtFwpmSublayer0{
|
||||||
|
subLayerKey: bo.filters,
|
||||||
|
displayData: *subDisplay,
|
||||||
|
providerKey: &bo.provider,
|
||||||
|
weight: ^uint16(0),
|
||||||
|
}
|
||||||
|
if err := fwpmSubLayerAdd0(session, &sublayer, 0); err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
return bo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// daemonAppID returns the WFP App-ID byte blob for the given executable path.
|
||||||
|
func daemonAppID(path string) (*wtFwpByteBlob, error) {
|
||||||
|
pathPtr, err := windows.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
var appID *wtFwpByteBlob
|
||||||
|
if err := fwpmGetAppIdFromFileName0(pathPtr, unsafe.Pointer(&appID)); err != nil {
|
||||||
|
return nil, wrapErr(err)
|
||||||
|
}
|
||||||
|
return appID, nil
|
||||||
|
}
|
||||||
38
client/internal/dns/dnsfw/syscall_windows.go
Normal file
38
client/internal/dns/dnsfw/syscall_windows.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Adapted from wireguard-windows tunnel/firewall/syscall_windows.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineopen0
|
||||||
|
//sys fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmEngineOpen0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineclose0
|
||||||
|
//sys fwpmEngineClose0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmEngineClose0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmsublayeradd0
|
||||||
|
//sys fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmSubLayerAdd0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmgetappidfromfilename0
|
||||||
|
//sys fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmGetAppIdFromFileName0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfreememory0
|
||||||
|
//sys fwpmFreeMemory0(p unsafe.Pointer) = fwpuclnt.FwpmFreeMemory0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfilteradd0
|
||||||
|
//sys fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) [failretval!=0] = fwpuclnt.FwpmFilterAdd0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/Fwpmu/nf-fwpmu-fwpmtransactionbegin0
|
||||||
|
//sys fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionBegin0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactioncommit0
|
||||||
|
//sys fwpmTransactionCommit0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionCommit0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactionabort0
|
||||||
|
//sys fwpmTransactionAbort0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionAbort0
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmprovideradd0
|
||||||
|
//sys fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmProviderAdd0
|
||||||
414
client/internal/dns/dnsfw/types_windows.go
Normal file
414
client/internal/dns/dnsfw/types_windows.go
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Adapted from wireguard-windows tunnel/firewall/types_windows.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import "golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
const (
|
||||||
|
anysizeArray = 1 // ANYSIZE_ARRAY defined in winnt.h
|
||||||
|
|
||||||
|
wtFwpBitmapArray64_Size = 8
|
||||||
|
|
||||||
|
wtFwpByteArray16_Size = 16
|
||||||
|
|
||||||
|
wtFwpByteArray6_Size = 6
|
||||||
|
|
||||||
|
wtFwpmAction0_Size = 20
|
||||||
|
wtFwpmAction0_filterType_Offset = 4
|
||||||
|
|
||||||
|
wtFwpV4AddrAndMask_Size = 8
|
||||||
|
wtFwpV4AddrAndMask_mask_Offset = 4
|
||||||
|
|
||||||
|
wtFwpV6AddrAndMask_Size = 17
|
||||||
|
wtFwpV6AddrAndMask_prefixLength_Offset = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
type wtFwpActionFlag uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWP_ACTION_FLAG_TERMINATING wtFwpActionFlag = 0x00001000
|
||||||
|
cFWP_ACTION_FLAG_NON_TERMINATING wtFwpActionFlag = 0x00002000
|
||||||
|
cFWP_ACTION_FLAG_CALLOUT wtFwpActionFlag = 0x00004000
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWP_ACTION_TYPE defined in fwptypes.h
|
||||||
|
type wtFwpActionType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWP_ACTION_BLOCK wtFwpActionType = wtFwpActionType(0x00000001 | cFWP_ACTION_FLAG_TERMINATING)
|
||||||
|
cFWP_ACTION_PERMIT wtFwpActionType = wtFwpActionType(0x00000002 | cFWP_ACTION_FLAG_TERMINATING)
|
||||||
|
cFWP_ACTION_CALLOUT_TERMINATING wtFwpActionType = wtFwpActionType(0x00000003 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_TERMINATING)
|
||||||
|
cFWP_ACTION_CALLOUT_INSPECTION wtFwpActionType = wtFwpActionType(0x00000004 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_NON_TERMINATING)
|
||||||
|
cFWP_ACTION_CALLOUT_UNKNOWN wtFwpActionType = wtFwpActionType(0x00000005 | cFWP_ACTION_FLAG_CALLOUT)
|
||||||
|
cFWP_ACTION_CONTINUE wtFwpActionType = wtFwpActionType(0x00000006 | cFWP_ACTION_FLAG_NON_TERMINATING)
|
||||||
|
cFWP_ACTION_NONE wtFwpActionType = 0x00000007
|
||||||
|
cFWP_ACTION_NONE_NO_MATCH wtFwpActionType = 0x00000008
|
||||||
|
cFWP_ACTION_BITMAP_INDEX_SET wtFwpActionType = 0x00000009
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWP_BYTE_BLOB defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_blob_)
|
||||||
|
type wtFwpByteBlob struct {
|
||||||
|
size uint32
|
||||||
|
data *uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_MATCH_TYPE defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_match_type_)
|
||||||
|
type wtFwpMatchType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWP_MATCH_EQUAL wtFwpMatchType = 0
|
||||||
|
cFWP_MATCH_GREATER wtFwpMatchType = cFWP_MATCH_EQUAL + 1
|
||||||
|
cFWP_MATCH_LESS wtFwpMatchType = cFWP_MATCH_GREATER + 1
|
||||||
|
cFWP_MATCH_GREATER_OR_EQUAL wtFwpMatchType = cFWP_MATCH_LESS + 1
|
||||||
|
cFWP_MATCH_LESS_OR_EQUAL wtFwpMatchType = cFWP_MATCH_GREATER_OR_EQUAL + 1
|
||||||
|
cFWP_MATCH_RANGE wtFwpMatchType = cFWP_MATCH_LESS_OR_EQUAL + 1
|
||||||
|
cFWP_MATCH_FLAGS_ALL_SET wtFwpMatchType = cFWP_MATCH_RANGE + 1
|
||||||
|
cFWP_MATCH_FLAGS_ANY_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ALL_SET + 1
|
||||||
|
cFWP_MATCH_FLAGS_NONE_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ANY_SET + 1
|
||||||
|
cFWP_MATCH_EQUAL_CASE_INSENSITIVE wtFwpMatchType = cFWP_MATCH_FLAGS_NONE_SET + 1
|
||||||
|
cFWP_MATCH_NOT_EQUAL wtFwpMatchType = cFWP_MATCH_EQUAL_CASE_INSENSITIVE + 1
|
||||||
|
cFWP_MATCH_PREFIX wtFwpMatchType = cFWP_MATCH_NOT_EQUAL + 1
|
||||||
|
cFWP_MATCH_NOT_PREFIX wtFwpMatchType = cFWP_MATCH_PREFIX + 1
|
||||||
|
cFWP_MATCH_TYPE_MAX wtFwpMatchType = cFWP_MATCH_NOT_PREFIX + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_ACTION0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_action0_)
|
||||||
|
type wtFwpmAction0 struct {
|
||||||
|
_type wtFwpActionType
|
||||||
|
filterType windows.GUID // Windows type: GUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in fwpmu.h. 4cd62a49-59c3-4969-b7f3-bda5d32890a4
|
||||||
|
var cFWPM_CONDITION_IP_LOCAL_INTERFACE = windows.GUID{
|
||||||
|
Data1: 0x4cd62a49,
|
||||||
|
Data2: 0x59c3,
|
||||||
|
Data3: 0x4969,
|
||||||
|
Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in fwpmu.h. b235ae9a-1d64-49b8-a44c-5ff3d9095045
|
||||||
|
var cFWPM_CONDITION_IP_REMOTE_ADDRESS = windows.GUID{
|
||||||
|
Data1: 0xb235ae9a,
|
||||||
|
Data2: 0x1d64,
|
||||||
|
Data3: 0x49b8,
|
||||||
|
Data4: [8]byte{0xa4, 0x4c, 0x5f, 0xf3, 0xd9, 0x09, 0x50, 0x45},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in fwpmu.h. 3971ef2b-623e-4f9a-8cb1-6e79b806b9a7
|
||||||
|
var cFWPM_CONDITION_IP_PROTOCOL = windows.GUID{
|
||||||
|
Data1: 0x3971ef2b,
|
||||||
|
Data2: 0x623e,
|
||||||
|
Data3: 0x4f9a,
|
||||||
|
Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in fwpmu.h. 0c1ba1af-5765-453f-af22-a8f791ac775b
|
||||||
|
var cFWPM_CONDITION_IP_LOCAL_PORT = windows.GUID{
|
||||||
|
Data1: 0x0c1ba1af,
|
||||||
|
Data2: 0x5765,
|
||||||
|
Data3: 0x453f,
|
||||||
|
Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in fwpmu.h. c35a604d-d22b-4e1a-91b4-68f674ee674b
|
||||||
|
var cFWPM_CONDITION_IP_REMOTE_PORT = windows.GUID{
|
||||||
|
Data1: 0xc35a604d,
|
||||||
|
Data2: 0xd22b,
|
||||||
|
Data3: 0x4e1a,
|
||||||
|
Data4: [8]byte{0x91, 0xb4, 0x68, 0xf6, 0x74, 0xee, 0x67, 0x4b},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in fwpmu.h. d78e1e87-8644-4ea5-9437-d809ecefc971
|
||||||
|
var cFWPM_CONDITION_ALE_APP_ID = windows.GUID{
|
||||||
|
Data1: 0xd78e1e87,
|
||||||
|
Data2: 0x8644,
|
||||||
|
Data3: 0x4ea5,
|
||||||
|
Data4: [8]byte{0x94, 0x37, 0xd8, 0x09, 0xec, 0xef, 0xc9, 0x71},
|
||||||
|
}
|
||||||
|
|
||||||
|
// af043a0a-b34d-4f86-979c-c90371af6e66
|
||||||
|
var cFWPM_CONDITION_ALE_USER_ID = windows.GUID{
|
||||||
|
Data1: 0xaf043a0a,
|
||||||
|
Data2: 0xb34d,
|
||||||
|
Data3: 0x4f86,
|
||||||
|
Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66},
|
||||||
|
}
|
||||||
|
|
||||||
|
// d9ee00de-c1ef-4617-bfe3-ffd8f5a08957
|
||||||
|
var cFWPM_CONDITION_IP_LOCAL_ADDRESS = windows.GUID{
|
||||||
|
Data1: 0xd9ee00de,
|
||||||
|
Data2: 0xc1ef,
|
||||||
|
Data3: 0x4617,
|
||||||
|
Data4: [8]byte{0xbf, 0xe3, 0xff, 0xd8, 0xf5, 0xa0, 0x89, 0x57},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cFWPM_CONDITION_ICMP_TYPE = cFWPM_CONDITION_IP_LOCAL_PORT
|
||||||
|
cFWPM_CONDITION_ICMP_CODE = cFWPM_CONDITION_IP_REMOTE_PORT
|
||||||
|
)
|
||||||
|
|
||||||
|
// 7bc43cbf-37ba-45f1-b74a-82ff518eeb10
|
||||||
|
var cFWPM_CONDITION_L2_FLAGS = windows.GUID{
|
||||||
|
Data1: 0x7bc43cbf,
|
||||||
|
Data2: 0x37ba,
|
||||||
|
Data3: 0x45f1,
|
||||||
|
Data4: [8]byte{0xb7, 0x4a, 0x82, 0xff, 0x51, 0x8e, 0xeb, 0x10},
|
||||||
|
}
|
||||||
|
|
||||||
|
type wtFwpmL2Flags uint32
|
||||||
|
|
||||||
|
const cFWP_CONDITION_L2_IS_VM2VM wtFwpmL2Flags = 0x00000010
|
||||||
|
|
||||||
|
var cFWPM_CONDITION_FLAGS = windows.GUID{
|
||||||
|
Data1: 0x632ce23b,
|
||||||
|
Data2: 0x5167,
|
||||||
|
Data3: 0x435c,
|
||||||
|
Data4: [8]byte{0x86, 0xd7, 0xe9, 0x03, 0x68, 0x4a, 0xa8, 0x0c},
|
||||||
|
}
|
||||||
|
|
||||||
|
type wtFwpmFlags uint32
|
||||||
|
|
||||||
|
const cFWP_CONDITION_FLAG_IS_LOOPBACK wtFwpmFlags = 0x00000001
|
||||||
|
|
||||||
|
// Defined in fwpmtypes.h
|
||||||
|
type wtFwpmFilterFlags uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWPM_FILTER_FLAG_NONE wtFwpmFilterFlags = 0x00000000
|
||||||
|
cFWPM_FILTER_FLAG_PERSISTENT wtFwpmFilterFlags = 0x00000001
|
||||||
|
cFWPM_FILTER_FLAG_BOOTTIME wtFwpmFilterFlags = 0x00000002
|
||||||
|
cFWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000004
|
||||||
|
cFWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT wtFwpmFilterFlags = 0x00000008
|
||||||
|
cFWPM_FILTER_FLAG_PERMIT_IF_CALLOUT_UNREGISTERED wtFwpmFilterFlags = 0x00000010
|
||||||
|
cFWPM_FILTER_FLAG_DISABLED wtFwpmFilterFlags = 0x00000020
|
||||||
|
cFWPM_FILTER_FLAG_INDEXED wtFwpmFilterFlags = 0x00000040
|
||||||
|
cFWPM_FILTER_FLAG_HAS_SECURITY_REALM_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000080
|
||||||
|
cFWPM_FILTER_FLAG_SYSTEMOS_ONLY wtFwpmFilterFlags = 0x00000100
|
||||||
|
cFWPM_FILTER_FLAG_GAMEOS_ONLY wtFwpmFilterFlags = 0x00000200
|
||||||
|
cFWPM_FILTER_FLAG_SILENT_MODE wtFwpmFilterFlags = 0x00000400
|
||||||
|
cFWPM_FILTER_FLAG_IPSEC_NO_ACQUIRE_INITIATE wtFwpmFilterFlags = 0x00000800
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_LAYER_ALE_AUTH_CONNECT_V4 (c38d57d1-05a7-4c33-904f-7fbceee60e82) defined in fwpmu.h
|
||||||
|
var cFWPM_LAYER_ALE_AUTH_CONNECT_V4 = windows.GUID{
|
||||||
|
Data1: 0xc38d57d1,
|
||||||
|
Data2: 0x05a7,
|
||||||
|
Data3: 0x4c33,
|
||||||
|
Data4: [8]byte{0x90, 0x4f, 0x7f, 0xbc, 0xee, 0xe6, 0x0e, 0x82},
|
||||||
|
}
|
||||||
|
|
||||||
|
// e1cd9fe7-f4b5-4273-96c0-592e487b8650
|
||||||
|
var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 = windows.GUID{
|
||||||
|
Data1: 0xe1cd9fe7,
|
||||||
|
Data2: 0xf4b5,
|
||||||
|
Data3: 0x4273,
|
||||||
|
Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50},
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWPM_LAYER_ALE_AUTH_CONNECT_V6 (4a72393b-319f-44bc-84c3-ba54dcb3b6b4) defined in fwpmu.h
|
||||||
|
var cFWPM_LAYER_ALE_AUTH_CONNECT_V6 = windows.GUID{
|
||||||
|
Data1: 0x4a72393b,
|
||||||
|
Data2: 0x319f,
|
||||||
|
Data3: 0x44bc,
|
||||||
|
Data4: [8]byte{0x84, 0xc3, 0xba, 0x54, 0xdc, 0xb3, 0xb6, 0xb4},
|
||||||
|
}
|
||||||
|
|
||||||
|
// a3b42c97-9f04-4672-b87e-cee9c483257f
|
||||||
|
var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 = windows.GUID{
|
||||||
|
Data1: 0xa3b42c97,
|
||||||
|
Data2: 0x9f04,
|
||||||
|
Data3: 0x4672,
|
||||||
|
Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 94c44912-9d6f-4ebf-b995-05ab8a088d1b
|
||||||
|
var cFWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE = windows.GUID{
|
||||||
|
Data1: 0x94c44912,
|
||||||
|
Data2: 0x9d6f,
|
||||||
|
Data3: 0x4ebf,
|
||||||
|
Data4: [8]byte{0xb9, 0x95, 0x05, 0xab, 0x8a, 0x08, 0x8d, 0x1b},
|
||||||
|
}
|
||||||
|
|
||||||
|
// d4220bd3-62ce-4f08-ae88-b56e8526df50
|
||||||
|
var cFWPM_LAYER_INBOUND_MAC_FRAME_NATIVE = windows.GUID{
|
||||||
|
Data1: 0xd4220bd3,
|
||||||
|
Data2: 0x62ce,
|
||||||
|
Data3: 0x4f08,
|
||||||
|
Data4: [8]byte{0xae, 0x88, 0xb5, 0x6e, 0x85, 0x26, 0xdf, 0x50},
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_BITMAP_ARRAY64 defined in fwtypes.h
|
||||||
|
type wtFwpBitmapArray64 struct {
|
||||||
|
bitmapArray64 [8]uint8 // Windows type: [8]UINT8
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_BYTE_ARRAY6 defined in fwtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array6_)
|
||||||
|
type wtFwpByteArray6 struct {
|
||||||
|
byteArray6 [6]uint8 // Windows type: [6]UINT8
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_BYTE_ARRAY16 defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array16_)
|
||||||
|
type wtFwpByteArray16 struct {
|
||||||
|
byteArray16 [16]uint8 // Windows type [16]UINT8
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_CONDITION_VALUE0 defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_condition_value0).
|
||||||
|
type wtFwpConditionValue0 wtFwpValue0
|
||||||
|
|
||||||
|
// FWP_DATA_TYPE defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_data_type_)
|
||||||
|
type wtFwpDataType uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWP_EMPTY wtFwpDataType = 0
|
||||||
|
cFWP_UINT8 wtFwpDataType = cFWP_EMPTY + 1
|
||||||
|
cFWP_UINT16 wtFwpDataType = cFWP_UINT8 + 1
|
||||||
|
cFWP_UINT32 wtFwpDataType = cFWP_UINT16 + 1
|
||||||
|
cFWP_UINT64 wtFwpDataType = cFWP_UINT32 + 1
|
||||||
|
cFWP_INT8 wtFwpDataType = cFWP_UINT64 + 1
|
||||||
|
cFWP_INT16 wtFwpDataType = cFWP_INT8 + 1
|
||||||
|
cFWP_INT32 wtFwpDataType = cFWP_INT16 + 1
|
||||||
|
cFWP_INT64 wtFwpDataType = cFWP_INT32 + 1
|
||||||
|
cFWP_FLOAT wtFwpDataType = cFWP_INT64 + 1
|
||||||
|
cFWP_DOUBLE wtFwpDataType = cFWP_FLOAT + 1
|
||||||
|
cFWP_BYTE_ARRAY16_TYPE wtFwpDataType = cFWP_DOUBLE + 1
|
||||||
|
cFWP_BYTE_BLOB_TYPE wtFwpDataType = cFWP_BYTE_ARRAY16_TYPE + 1
|
||||||
|
cFWP_SID wtFwpDataType = cFWP_BYTE_BLOB_TYPE + 1
|
||||||
|
cFWP_SECURITY_DESCRIPTOR_TYPE wtFwpDataType = cFWP_SID + 1
|
||||||
|
cFWP_TOKEN_INFORMATION_TYPE wtFwpDataType = cFWP_SECURITY_DESCRIPTOR_TYPE + 1
|
||||||
|
cFWP_TOKEN_ACCESS_INFORMATION_TYPE wtFwpDataType = cFWP_TOKEN_INFORMATION_TYPE + 1
|
||||||
|
cFWP_UNICODE_STRING_TYPE wtFwpDataType = cFWP_TOKEN_ACCESS_INFORMATION_TYPE + 1
|
||||||
|
cFWP_BYTE_ARRAY6_TYPE wtFwpDataType = cFWP_UNICODE_STRING_TYPE + 1
|
||||||
|
cFWP_BITMAP_INDEX_TYPE wtFwpDataType = cFWP_BYTE_ARRAY6_TYPE + 1
|
||||||
|
cFWP_BITMAP_ARRAY64_TYPE wtFwpDataType = cFWP_BITMAP_INDEX_TYPE + 1
|
||||||
|
cFWP_SINGLE_DATA_TYPE_MAX wtFwpDataType = 0xff
|
||||||
|
cFWP_V4_ADDR_MASK wtFwpDataType = cFWP_SINGLE_DATA_TYPE_MAX + 1
|
||||||
|
cFWP_V6_ADDR_MASK wtFwpDataType = cFWP_V4_ADDR_MASK + 1
|
||||||
|
cFWP_RANGE_TYPE wtFwpDataType = cFWP_V6_ADDR_MASK + 1
|
||||||
|
cFWP_DATA_TYPE_MAX wtFwpDataType = cFWP_RANGE_TYPE + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWP_V4_ADDR_AND_MASK defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v4_addr_and_mask).
|
||||||
|
type wtFwpV4AddrAndMask struct {
|
||||||
|
addr uint32
|
||||||
|
mask uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_V6_ADDR_AND_MASK defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v6_addr_and_mask).
|
||||||
|
type wtFwpV6AddrAndMask struct {
|
||||||
|
addr [16]uint8
|
||||||
|
prefixLength uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWP_VALUE0 defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_value0_)
|
||||||
|
type wtFwpValue0 struct {
|
||||||
|
_type wtFwpDataType
|
||||||
|
value uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWPM_DISPLAY_DATA0 defined in fwptypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwpm_display_data0).
|
||||||
|
type wtFwpmDisplayData0 struct {
|
||||||
|
name *uint16 // Windows type: *wchar_t
|
||||||
|
description *uint16 // Windows type: *wchar_t
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWPM_FILTER_CONDITION0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter_condition0).
|
||||||
|
type wtFwpmFilterCondition0 struct {
|
||||||
|
fieldKey windows.GUID // Windows type: GUID
|
||||||
|
matchType wtFwpMatchType
|
||||||
|
conditionValue wtFwpConditionValue0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWPM_PROVIDER0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0_)
|
||||||
|
type wtFwpProvider0 struct {
|
||||||
|
providerKey windows.GUID // Windows type: GUID
|
||||||
|
displayData wtFwpmDisplayData0
|
||||||
|
flags uint32
|
||||||
|
providerData wtFwpByteBlob
|
||||||
|
serviceName *uint16 // Windows type: *wchar_t
|
||||||
|
}
|
||||||
|
|
||||||
|
type wtFwpmSessionFlagsValue uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWPM_SESSION_FLAG_DYNAMIC wtFwpmSessionFlagsValue = 0x00000001 // FWPM_SESSION_FLAG_DYNAMIC defined in fwpmtypes.h
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_SESSION0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_session0).
|
||||||
|
type wtFwpmSession0 struct {
|
||||||
|
sessionKey windows.GUID // Windows type: GUID
|
||||||
|
displayData wtFwpmDisplayData0
|
||||||
|
flags wtFwpmSessionFlagsValue // Windows type UINT32
|
||||||
|
txnWaitTimeoutInMSec uint32
|
||||||
|
processId uint32 // Windows type: DWORD
|
||||||
|
sid *windows.SID
|
||||||
|
username *uint16 // Windows type: *wchar_t
|
||||||
|
kernelMode uint8 // Windows type: BOOL
|
||||||
|
}
|
||||||
|
|
||||||
|
type wtFwpmSublayerFlags uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWPM_SUBLAYER_FLAG_PERSISTENT wtFwpmSublayerFlags = 0x00000001 // FWPM_SUBLAYER_FLAG_PERSISTENT defined in fwpmtypes.h
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_SUBLAYER0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_sublayer0_)
|
||||||
|
type wtFwpmSublayer0 struct {
|
||||||
|
subLayerKey windows.GUID // Windows type: GUID
|
||||||
|
displayData wtFwpmDisplayData0
|
||||||
|
flags wtFwpmSublayerFlags
|
||||||
|
providerKey *windows.GUID // Windows type: *GUID
|
||||||
|
providerData wtFwpByteBlob
|
||||||
|
weight uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined in rpcdce.h
|
||||||
|
type wtRpcCAuthN uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cRPC_C_AUTHN_NONE wtRpcCAuthN = 0
|
||||||
|
cRPC_C_AUTHN_WINNT wtRpcCAuthN = 10
|
||||||
|
cRPC_C_AUTHN_DEFAULT wtRpcCAuthN = 0xFFFFFFFF
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_PROVIDER0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/sv-se/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0).
|
||||||
|
type wtFwpmProvider0 struct {
|
||||||
|
providerKey windows.GUID
|
||||||
|
displayData wtFwpmDisplayData0
|
||||||
|
flags uint32
|
||||||
|
providerData wtFwpByteBlob
|
||||||
|
serviceName *uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type wtIPProto uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
cIPPROTO_ICMP wtIPProto = 1
|
||||||
|
cIPPROTO_ICMPV6 wtIPProto = 58
|
||||||
|
cIPPROTO_TCP wtIPProto = 6
|
||||||
|
cIPPROTO_UDP wtIPProto = 17
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFWP_ACTRL_MATCH_FILTER = 1
|
||||||
|
)
|
||||||
92
client/internal/dns/dnsfw/types_windows_32.go
Normal file
92
client/internal/dns/dnsfw/types_windows_32.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
//go:build windows && (386 || arm)
|
||||||
|
|
||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Adapted from wireguard-windows tunnel/firewall/types_windows_32.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import "golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
const (
|
||||||
|
wtFwpByteBlob_Size = 8
|
||||||
|
wtFwpByteBlob_data_Offset = 4
|
||||||
|
|
||||||
|
wtFwpConditionValue0_Size = 8
|
||||||
|
wtFwpConditionValue0_uint8_Offset = 4
|
||||||
|
|
||||||
|
wtFwpmDisplayData0_Size = 8
|
||||||
|
wtFwpmDisplayData0_description_Offset = 4
|
||||||
|
|
||||||
|
wtFwpmFilter0_Size = 152
|
||||||
|
wtFwpmFilter0_displayData_Offset = 16
|
||||||
|
wtFwpmFilter0_flags_Offset = 24
|
||||||
|
wtFwpmFilter0_providerKey_Offset = 28
|
||||||
|
wtFwpmFilter0_providerData_Offset = 32
|
||||||
|
wtFwpmFilter0_layerKey_Offset = 40
|
||||||
|
wtFwpmFilter0_subLayerKey_Offset = 56
|
||||||
|
wtFwpmFilter0_weight_Offset = 72
|
||||||
|
wtFwpmFilter0_numFilterConditions_Offset = 80
|
||||||
|
wtFwpmFilter0_filterCondition_Offset = 84
|
||||||
|
wtFwpmFilter0_action_Offset = 88
|
||||||
|
wtFwpmFilter0_providerContextKey_Offset = 112
|
||||||
|
wtFwpmFilter0_reserved_Offset = 128
|
||||||
|
wtFwpmFilter0_filterID_Offset = 136
|
||||||
|
wtFwpmFilter0_effectiveWeight_Offset = 144
|
||||||
|
|
||||||
|
wtFwpmFilterCondition0_Size = 28
|
||||||
|
wtFwpmFilterCondition0_matchType_Offset = 16
|
||||||
|
wtFwpmFilterCondition0_conditionValue_Offset = 20
|
||||||
|
|
||||||
|
wtFwpmSession0_Size = 48
|
||||||
|
wtFwpmSession0_displayData_Offset = 16
|
||||||
|
wtFwpmSession0_flags_Offset = 24
|
||||||
|
wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 28
|
||||||
|
wtFwpmSession0_processId_Offset = 32
|
||||||
|
wtFwpmSession0_sid_Offset = 36
|
||||||
|
wtFwpmSession0_username_Offset = 40
|
||||||
|
wtFwpmSession0_kernelMode_Offset = 44
|
||||||
|
|
||||||
|
wtFwpmSublayer0_Size = 44
|
||||||
|
wtFwpmSublayer0_displayData_Offset = 16
|
||||||
|
wtFwpmSublayer0_flags_Offset = 24
|
||||||
|
wtFwpmSublayer0_providerKey_Offset = 28
|
||||||
|
wtFwpmSublayer0_providerData_Offset = 32
|
||||||
|
wtFwpmSublayer0_weight_Offset = 40
|
||||||
|
|
||||||
|
wtFwpProvider0_Size = 40
|
||||||
|
wtFwpProvider0_displayData_Offset = 16
|
||||||
|
wtFwpProvider0_flags_Offset = 24
|
||||||
|
wtFwpProvider0_providerData_Offset = 28
|
||||||
|
wtFwpProvider0_serviceName_Offset = 36
|
||||||
|
|
||||||
|
wtFwpTokenInformation_Size = 16
|
||||||
|
|
||||||
|
wtFwpValue0_Size = 8
|
||||||
|
wtFwpValue0_value_Offset = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_FILTER0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0).
|
||||||
|
type wtFwpmFilter0 struct {
|
||||||
|
filterKey windows.GUID // Windows type: GUID
|
||||||
|
displayData wtFwpmDisplayData0
|
||||||
|
flags wtFwpmFilterFlags
|
||||||
|
providerKey *windows.GUID // Windows type: *GUID
|
||||||
|
providerData wtFwpByteBlob
|
||||||
|
layerKey windows.GUID // Windows type: GUID
|
||||||
|
subLayerKey windows.GUID // Windows type: GUID
|
||||||
|
weight wtFwpValue0
|
||||||
|
numFilterConditions uint32
|
||||||
|
filterCondition *wtFwpmFilterCondition0
|
||||||
|
action wtFwpmAction0
|
||||||
|
offset1 [4]byte // Layout correction field
|
||||||
|
providerContextKey windows.GUID // Windows type: GUID
|
||||||
|
reserved *windows.GUID // Windows type: *GUID
|
||||||
|
offset2 [4]byte // Layout correction field
|
||||||
|
filterID uint64
|
||||||
|
effectiveWeight wtFwpValue0
|
||||||
|
}
|
||||||
89
client/internal/dns/dnsfw/types_windows_64.go
Normal file
89
client/internal/dns/dnsfw/types_windows_64.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//go:build windows && (amd64 || arm64)
|
||||||
|
|
||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Adapted from wireguard-windows tunnel/firewall/types_windows_64.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import "golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
const (
|
||||||
|
wtFwpByteBlob_Size = 16
|
||||||
|
wtFwpByteBlob_data_Offset = 8
|
||||||
|
|
||||||
|
wtFwpConditionValue0_Size = 16
|
||||||
|
wtFwpConditionValue0_uint8_Offset = 8
|
||||||
|
|
||||||
|
wtFwpmDisplayData0_Size = 16
|
||||||
|
wtFwpmDisplayData0_description_Offset = 8
|
||||||
|
|
||||||
|
wtFwpmFilter0_Size = 200
|
||||||
|
wtFwpmFilter0_displayData_Offset = 16
|
||||||
|
wtFwpmFilter0_flags_Offset = 32
|
||||||
|
wtFwpmFilter0_providerKey_Offset = 40
|
||||||
|
wtFwpmFilter0_providerData_Offset = 48
|
||||||
|
wtFwpmFilter0_layerKey_Offset = 64
|
||||||
|
wtFwpmFilter0_subLayerKey_Offset = 80
|
||||||
|
wtFwpmFilter0_weight_Offset = 96
|
||||||
|
wtFwpmFilter0_numFilterConditions_Offset = 112
|
||||||
|
wtFwpmFilter0_filterCondition_Offset = 120
|
||||||
|
wtFwpmFilter0_action_Offset = 128
|
||||||
|
wtFwpmFilter0_providerContextKey_Offset = 152
|
||||||
|
wtFwpmFilter0_reserved_Offset = 168
|
||||||
|
wtFwpmFilter0_filterID_Offset = 176
|
||||||
|
wtFwpmFilter0_effectiveWeight_Offset = 184
|
||||||
|
|
||||||
|
wtFwpmFilterCondition0_Size = 40
|
||||||
|
wtFwpmFilterCondition0_matchType_Offset = 16
|
||||||
|
wtFwpmFilterCondition0_conditionValue_Offset = 24
|
||||||
|
|
||||||
|
wtFwpmSession0_Size = 72
|
||||||
|
wtFwpmSession0_displayData_Offset = 16
|
||||||
|
wtFwpmSession0_flags_Offset = 32
|
||||||
|
wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 36
|
||||||
|
wtFwpmSession0_processId_Offset = 40
|
||||||
|
wtFwpmSession0_sid_Offset = 48
|
||||||
|
wtFwpmSession0_username_Offset = 56
|
||||||
|
wtFwpmSession0_kernelMode_Offset = 64
|
||||||
|
|
||||||
|
wtFwpmSublayer0_Size = 72
|
||||||
|
wtFwpmSublayer0_displayData_Offset = 16
|
||||||
|
wtFwpmSublayer0_flags_Offset = 32
|
||||||
|
wtFwpmSublayer0_providerKey_Offset = 40
|
||||||
|
wtFwpmSublayer0_providerData_Offset = 48
|
||||||
|
wtFwpmSublayer0_weight_Offset = 64
|
||||||
|
|
||||||
|
wtFwpProvider0_Size = 64
|
||||||
|
wtFwpProvider0_displayData_Offset = 16
|
||||||
|
wtFwpProvider0_flags_Offset = 32
|
||||||
|
wtFwpProvider0_providerData_Offset = 40
|
||||||
|
wtFwpProvider0_serviceName_Offset = 56
|
||||||
|
|
||||||
|
wtFwpValue0_Size = 16
|
||||||
|
wtFwpValue0_value_Offset = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_FILTER0 defined in fwpmtypes.h
|
||||||
|
// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0).
|
||||||
|
type wtFwpmFilter0 struct {
|
||||||
|
filterKey windows.GUID // Windows type: GUID
|
||||||
|
displayData wtFwpmDisplayData0
|
||||||
|
flags wtFwpmFilterFlags // Windows type: UINT32
|
||||||
|
providerKey *windows.GUID // Windows type: *GUID
|
||||||
|
providerData wtFwpByteBlob
|
||||||
|
layerKey windows.GUID // Windows type: GUID
|
||||||
|
subLayerKey windows.GUID // Windows type: GUID
|
||||||
|
weight wtFwpValue0
|
||||||
|
numFilterConditions uint32
|
||||||
|
filterCondition *wtFwpmFilterCondition0
|
||||||
|
action wtFwpmAction0
|
||||||
|
offset1 [4]byte // Layout correction field
|
||||||
|
providerContextKey windows.GUID // Windows type: GUID
|
||||||
|
reserved *windows.GUID // Windows type: *GUID
|
||||||
|
filterID uint64
|
||||||
|
effectiveWeight wtFwpValue0
|
||||||
|
}
|
||||||
130
client/internal/dns/dnsfw/zsyscall_windows.go
Normal file
130
client/internal/dns/dnsfw/zsyscall_windows.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// Code generated by 'go generate'; DO NOT EDIT.
|
||||||
|
|
||||||
|
package dnsfw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ unsafe.Pointer
|
||||||
|
|
||||||
|
// Do the interface allocations only once for common
|
||||||
|
// Errno values.
|
||||||
|
const (
|
||||||
|
errnoERROR_IO_PENDING = 997
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||||
|
errERROR_EINVAL error = syscall.EINVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
// errnoErr returns common boxed Errno values, to prevent
|
||||||
|
// allocations at runtime.
|
||||||
|
func errnoErr(e syscall.Errno) error {
|
||||||
|
switch e {
|
||||||
|
case 0:
|
||||||
|
return errERROR_EINVAL
|
||||||
|
case errnoERROR_IO_PENDING:
|
||||||
|
return errERROR_IO_PENDING
|
||||||
|
}
|
||||||
|
// TODO: add more here, after collecting data on the common
|
||||||
|
// error values see on Windows. (perhaps when running
|
||||||
|
// all.bat?)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
modfwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll")
|
||||||
|
|
||||||
|
procFwpmEngineClose0 = modfwpuclnt.NewProc("FwpmEngineClose0")
|
||||||
|
procFwpmEngineOpen0 = modfwpuclnt.NewProc("FwpmEngineOpen0")
|
||||||
|
procFwpmFilterAdd0 = modfwpuclnt.NewProc("FwpmFilterAdd0")
|
||||||
|
procFwpmFreeMemory0 = modfwpuclnt.NewProc("FwpmFreeMemory0")
|
||||||
|
procFwpmGetAppIdFromFileName0 = modfwpuclnt.NewProc("FwpmGetAppIdFromFileName0")
|
||||||
|
procFwpmProviderAdd0 = modfwpuclnt.NewProc("FwpmProviderAdd0")
|
||||||
|
procFwpmSubLayerAdd0 = modfwpuclnt.NewProc("FwpmSubLayerAdd0")
|
||||||
|
procFwpmTransactionAbort0 = modfwpuclnt.NewProc("FwpmTransactionAbort0")
|
||||||
|
procFwpmTransactionBegin0 = modfwpuclnt.NewProc("FwpmTransactionBegin0")
|
||||||
|
procFwpmTransactionCommit0 = modfwpuclnt.NewProc("FwpmTransactionCommit0")
|
||||||
|
)
|
||||||
|
|
||||||
|
func fwpmEngineClose0(engineHandle uintptr) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmEngineClose0.Addr(), 1, uintptr(engineHandle), 0, 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall6(procFwpmEngineOpen0.Addr(), 5, uintptr(unsafe.Pointer(serverName)), uintptr(authnService), uintptr(unsafe.Pointer(authIdentity)), uintptr(unsafe.Pointer(session)), uintptr(engineHandle), 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall6(procFwpmFilterAdd0.Addr(), 4, uintptr(engineHandle), uintptr(unsafe.Pointer(filter)), uintptr(sd), uintptr(unsafe.Pointer(id)), 0, 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmFreeMemory0(p unsafe.Pointer) {
|
||||||
|
syscall.Syscall(procFwpmFreeMemory0.Addr(), 1, uintptr(p), 0, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmGetAppIdFromFileName0.Addr(), 2, uintptr(unsafe.Pointer(fileName)), uintptr(appID), 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmProviderAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(provider)), uintptr(sd))
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmSubLayerAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(subLayer)), uintptr(sd))
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmTransactionAbort0(engineHandle uintptr) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmTransactionAbort0.Addr(), 1, uintptr(engineHandle), 0, 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmTransactionBegin0.Addr(), 2, uintptr(engineHandle), uintptr(flags), 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fwpmTransactionCommit0(engineHandle uintptr) (err error) {
|
||||||
|
r1, _, e1 := syscall.Syscall(procFwpmTransactionCommit0.Addr(), 1, uintptr(engineHandle), 0, 0)
|
||||||
|
if r1 != 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/dnsfw"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/winregistry"
|
"github.com/netbirdio/netbird/client/internal/winregistry"
|
||||||
)
|
)
|
||||||
@@ -71,6 +72,7 @@ type registryConfigurator struct {
|
|||||||
routingAll bool
|
routingAll bool
|
||||||
gpo bool
|
gpo bool
|
||||||
nrptEntryCount int
|
nrptEntryCount int
|
||||||
|
dnsFirewall dnsfw.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
||||||
@@ -90,8 +92,9 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configurator := ®istryConfigurator{
|
configurator := ®istryConfigurator{
|
||||||
guid: guid,
|
guid: guid,
|
||||||
gpo: useGPO,
|
gpo: useGPO,
|
||||||
|
dnsFirewall: dnsfw.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := configurator.configureInterface(); err != nil {
|
if err := configurator.configureInterface(); err != nil {
|
||||||
@@ -169,16 +172,8 @@ func (r *registryConfigurator) disableWINSForInterface() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
|
func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
|
||||||
if config.RouteAll {
|
if err := r.applyRouteAll(config); err != nil {
|
||||||
if err := r.addDNSSetupForAll(config.ServerIP); err != nil {
|
return err
|
||||||
return fmt.Errorf("add dns setup: %w", err)
|
|
||||||
}
|
|
||||||
} else if r.routingAll {
|
|
||||||
if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil {
|
|
||||||
return fmt.Errorf("delete interface registry key property: %w", err)
|
|
||||||
}
|
|
||||||
r.routingAll = false
|
|
||||||
log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.updateState(stateManager)
|
r.updateState(stateManager)
|
||||||
@@ -220,6 +215,35 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) applyRouteAll(config HostDNSConfig) error {
|
||||||
|
if config.RouteAll {
|
||||||
|
if err := r.dnsFirewall.Enable(r.guid, config.ServerIP); err != nil {
|
||||||
|
return fmt.Errorf("dns firewall: %w", err)
|
||||||
|
}
|
||||||
|
if err := r.addDNSSetupForAll(config.ServerIP); err != nil {
|
||||||
|
merr := multierror.Append(nil, fmt.Errorf("add dns setup: %w", err))
|
||||||
|
if dErr := r.dnsFirewall.Disable(); dErr != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("rollback dns firewall: %w", dErr))
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.dnsFirewall.Disable(); err != nil {
|
||||||
|
log.Errorf("disable dns firewall: %v", err)
|
||||||
|
}
|
||||||
|
if !r.routingAll {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil {
|
||||||
|
return fmt.Errorf("delete interface registry key property: %w", err)
|
||||||
|
}
|
||||||
|
r.routingAll = false
|
||||||
|
log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) updateState(stateManager *statemanager.Manager) {
|
func (r *registryConfigurator) updateState(stateManager *statemanager.Manager) {
|
||||||
if err := stateManager.UpdateState(&ShutdownState{
|
if err := stateManager.UpdateState(&ShutdownState{
|
||||||
Guid: r.guid,
|
Guid: r.guid,
|
||||||
@@ -406,6 +430,10 @@ func (r *registryConfigurator) restoreHostDNS() error {
|
|||||||
return fmt.Errorf("remove interface registry key: %w", err)
|
return fmt.Errorf("remove interface registry key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.dnsFirewall.Disable(); err != nil {
|
||||||
|
log.Errorf("disable dns firewall: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
go r.flushDNSCache()
|
go r.flushDNSCache()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/firewall"
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||||
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
|
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
@@ -68,6 +69,7 @@ import (
|
|||||||
signal "github.com/netbirdio/netbird/shared/signal/client"
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
sProto "github.com/netbirdio/netbird/shared/signal/proto"
|
sProto "github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/netbirdio/netbird/util/capture"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
|
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
|
||||||
@@ -218,6 +220,8 @@ type Engine struct {
|
|||||||
portForwardManager *portforward.Manager
|
portForwardManager *portforward.Manager
|
||||||
srWatcher *guard.SRWatcher
|
srWatcher *guard.SRWatcher
|
||||||
|
|
||||||
|
afpacketCapture *capture.AFPacketCapture
|
||||||
|
|
||||||
// Sync response persistence (protected by syncRespMux)
|
// Sync response persistence (protected by syncRespMux)
|
||||||
syncRespMux sync.RWMutex
|
syncRespMux sync.RWMutex
|
||||||
persistSyncResponse bool
|
persistSyncResponse bool
|
||||||
@@ -944,7 +948,12 @@ func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
|
|||||||
return fmt.Errorf("update relay token: %w", err)
|
return fmt.Errorf("update relay token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.relayManager.UpdateServerURLs(update.Urls)
|
urls := update.Urls
|
||||||
|
if override, ok := peer.OverrideRelayURLs(); ok {
|
||||||
|
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
|
||||||
|
urls = override
|
||||||
|
}
|
||||||
|
e.relayManager.UpdateServerURLs(urls)
|
||||||
|
|
||||||
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
||||||
// We can ignore all errors because the guard will manage the reconnection retries.
|
// We can ignore all errors because the guard will manage the reconnection retries.
|
||||||
@@ -1698,6 +1707,11 @@ func (e *Engine) parseNATExternalIPMappings() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) close() {
|
func (e *Engine) close() {
|
||||||
|
if e.afpacketCapture != nil {
|
||||||
|
e.afpacketCapture.Stop()
|
||||||
|
e.afpacketCapture = nil
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("removing Netbird interface %s", e.config.WgIfaceName)
|
log.Debugf("removing Netbird interface %s", e.config.WgIfaceName)
|
||||||
|
|
||||||
if e.wgInterface != nil {
|
if e.wgInterface != nil {
|
||||||
@@ -2163,6 +2177,62 @@ func (e *Engine) Address() (netip.Addr, error) {
|
|||||||
return e.wgInterface.Address().IP, nil
|
return e.wgInterface.Address().IP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCapture sets or clears packet capture on the WireGuard device.
|
||||||
|
// On userspace WireGuard, it taps the FilteredDevice directly.
|
||||||
|
// On kernel WireGuard (Linux), it falls back to AF_PACKET raw socket capture.
|
||||||
|
// Pass nil to disable capture.
|
||||||
|
func (e *Engine) SetCapture(pc device.PacketCapture) error {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
intf := e.wgInterface
|
||||||
|
if intf == nil {
|
||||||
|
return errors.New("wireguard interface not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.afpacketCapture != nil {
|
||||||
|
e.afpacketCapture.Stop()
|
||||||
|
e.afpacketCapture = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := intf.GetDevice()
|
||||||
|
if dev != nil {
|
||||||
|
dev.SetCapture(pc)
|
||||||
|
e.setForwarderCapture(pc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kernel mode: no FilteredDevice. Use AF_PACKET on Linux.
|
||||||
|
if pc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sess, ok := pc.(*capture.Session)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("filtered device not available and AF_PACKET requires *capture.Session")
|
||||||
|
}
|
||||||
|
|
||||||
|
afc := capture.NewAFPacketCapture(intf.Name(), sess)
|
||||||
|
if err := afc.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start AF_PACKET capture on %s: %w", intf.Name(), err)
|
||||||
|
}
|
||||||
|
e.afpacketCapture = afc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setForwarderCapture propagates capture to the USP filter's forwarder endpoint.
|
||||||
|
// This captures outbound response packets that bypass the FilteredDevice in netstack mode.
|
||||||
|
func (e *Engine) setForwarderCapture(pc device.PacketCapture) {
|
||||||
|
if e.firewall == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type forwarderCapturer interface {
|
||||||
|
SetPacketCapture(pc forwarder.PacketCapture)
|
||||||
|
}
|
||||||
|
if fc, ok := e.firewall.(forwarderCapturer); ok {
|
||||||
|
fc.SetPacketCapture(pc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewallManager.ForwardRule, error) {
|
func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewallManager.ForwardRule, error) {
|
||||||
if e.firewall == nil {
|
if e.firewall == nil {
|
||||||
log.Warn("firewall is disabled, not updating forwarding rules")
|
log.Warn("firewall is disabled, not updating forwarding rules")
|
||||||
@@ -2384,6 +2454,8 @@ func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relayIP := decodeRelayIP(msg.GetBody().GetRelayServerIP())
|
||||||
|
|
||||||
offerAnswer := peer.OfferAnswer{
|
offerAnswer := peer.OfferAnswer{
|
||||||
IceCredentials: peer.IceCredentials{
|
IceCredentials: peer.IceCredentials{
|
||||||
UFrag: remoteCred.UFrag,
|
UFrag: remoteCred.UFrag,
|
||||||
@@ -2394,7 +2466,23 @@ func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) {
|
|||||||
RosenpassPubKey: rosenpassPubKey,
|
RosenpassPubKey: rosenpassPubKey,
|
||||||
RosenpassAddr: rosenpassAddr,
|
RosenpassAddr: rosenpassAddr,
|
||||||
RelaySrvAddress: msg.GetBody().GetRelayServerAddress(),
|
RelaySrvAddress: msg.GetBody().GetRelayServerAddress(),
|
||||||
|
RelaySrvIP: relayIP,
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
}
|
}
|
||||||
return &offerAnswer, nil
|
return &offerAnswer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeRelayIP decodes the proto relayServerIP bytes (4 or 16) into a
|
||||||
|
// netip.Addr. Returns the zero value for empty input and logs a warning
|
||||||
|
// for malformed payloads.
|
||||||
|
func decodeRelayIP(b []byte) netip.Addr {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
ip, ok := netip.AddrFromSlice(b)
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("invalid relayServerIP in signal message (%d bytes), ignoring", len(b))
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
return ip.Unmap()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1671,7 +1671,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package activity
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,10 +17,6 @@ import (
|
|||||||
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isBindListenerPlatform() bool {
|
|
||||||
return runtime.GOOS == "windows" || runtime.GOOS == "js"
|
|
||||||
}
|
|
||||||
|
|
||||||
// mockEndpointManager implements device.EndpointManager for testing
|
// mockEndpointManager implements device.EndpointManager for testing
|
||||||
type mockEndpointManager struct {
|
type mockEndpointManager struct {
|
||||||
endpoints map[netip.Addr]net.Conn
|
endpoints map[netip.Addr]net.Conn
|
||||||
@@ -181,10 +176,6 @@ func TestBindListener_Close(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_BindMode(t *testing.T) {
|
func TestManager_BindMode(t *testing.T) {
|
||||||
if !isBindListenerPlatform() {
|
|
||||||
t.Skip("BindListener only used on Windows/JS platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEndpointMgr := newMockEndpointManager()
|
mockEndpointMgr := newMockEndpointManager()
|
||||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||||
|
|
||||||
@@ -226,10 +217,6 @@ func TestManager_BindMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
||||||
if !isBindListenerPlatform() {
|
|
||||||
t.Skip("BindListener only used on Windows/JS platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEndpointMgr := newMockEndpointManager()
|
mockEndpointMgr := newMockEndpointManager()
|
||||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
@@ -75,16 +73,6 @@ func (m *Manager) createListener(peerCfg lazyconn.PeerConfig) (listener, error)
|
|||||||
return NewUDPListener(m.wgIface, peerCfg)
|
return NewUDPListener(m.wgIface, peerCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindListener is used on Windows, JS, and netstack platforms:
|
|
||||||
// - JS: Cannot listen to UDP sockets
|
|
||||||
// - Windows: IP_UNICAST_IF socket option forces packets out the interface the default
|
|
||||||
// gateway points to, preventing them from reaching the loopback interface.
|
|
||||||
// - Netstack: Allows multiple instances on the same host without port conflicts.
|
|
||||||
// BindListener bypasses these issues by passing data directly through the bind.
|
|
||||||
if runtime.GOOS != "windows" && runtime.GOOS != "js" && !netstack.IsEnabled() {
|
|
||||||
return NewUDPListener(m.wgIface, peerCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider, ok := m.wgIface.(bindProvider)
|
provider, ok := m.wgIface.(bindProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn/activity"
|
"github.com/netbirdio/netbird/client/internal/lazyconn/activity"
|
||||||
@@ -91,8 +90,8 @@ func (m *Manager) UpdateRouteHAMap(haMap route.HAMap) {
|
|||||||
m.routesMu.Lock()
|
m.routesMu.Lock()
|
||||||
defer m.routesMu.Unlock()
|
defer m.routesMu.Unlock()
|
||||||
|
|
||||||
maps.Clear(m.peerToHAGroups)
|
clear(m.peerToHAGroups)
|
||||||
maps.Clear(m.haGroupToPeers)
|
clear(m.haGroupToPeers)
|
||||||
|
|
||||||
for haUniqueID, routes := range haMap {
|
for haUniqueID, routes := range haMap {
|
||||||
var peers []string
|
var peers []string
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package store
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/netflow/types"
|
"github.com/netbirdio/netbird/client/internal/netflow/types"
|
||||||
@@ -30,7 +28,7 @@ func (m *Memory) StoreEvent(event *types.Event) {
|
|||||||
func (m *Memory) Close() {
|
func (m *Memory) Close() {
|
||||||
m.mux.Lock()
|
m.mux.Lock()
|
||||||
defer m.mux.Unlock()
|
defer m.mux.Unlock()
|
||||||
maps.Clear(m.events)
|
clear(m.events)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) GetEvents() []*types.Event {
|
func (m *Memory) GetEvents() []*types.Event {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
||||||
|
EnvKeyNBHomeRelayServers = "NB_HOME_RELAY_SERVERS"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsForceRelayed() bool {
|
func IsForceRelayed() bool {
|
||||||
@@ -16,3 +17,28 @@ func IsForceRelayed() bool {
|
|||||||
}
|
}
|
||||||
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OverrideRelayURLs returns the relay server URL list set in
|
||||||
|
// NB_HOME_RELAY_SERVERS (comma-separated) and a boolean indicating whether
|
||||||
|
// the override is active. When the env var is unset, the boolean is false
|
||||||
|
// and the caller should keep the list received from the management server.
|
||||||
|
// Intended for lab/debug scenarios where a peer must pin to a specific home
|
||||||
|
// relay regardless of what management offers.
|
||||||
|
func OverrideRelayURLs() ([]string, bool) {
|
||||||
|
raw := os.Getenv(EnvKeyNBHomeRelayServers)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
urls := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p != "" {
|
||||||
|
urls = append(urls, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return urls, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package peer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@@ -40,6 +41,10 @@ type OfferAnswer struct {
|
|||||||
|
|
||||||
// relay server address
|
// relay server address
|
||||||
RelaySrvAddress string
|
RelaySrvAddress string
|
||||||
|
// RelaySrvIP is the IP the remote peer is connected to on its
|
||||||
|
// relay server. Used as a dial target if DNS for RelaySrvAddress
|
||||||
|
// fails. Zero value if the peer did not advertise an IP.
|
||||||
|
RelaySrvIP netip.Addr
|
||||||
// SessionID is the unique identifier of the session, used to discard old messages
|
// SessionID is the unique identifier of the session, used to discard old messages
|
||||||
SessionID *ICESessionID
|
SessionID *ICESessionID
|
||||||
}
|
}
|
||||||
@@ -217,8 +222,9 @@ func (h *Handshaker) buildOfferAnswer() OfferAnswer {
|
|||||||
answer.SessionID = &sid
|
answer.SessionID = &sid
|
||||||
}
|
}
|
||||||
|
|
||||||
if addr, err := h.relay.RelayInstanceAddress(); err == nil {
|
if addr, ip, err := h.relay.RelayInstanceAddress(); err == nil {
|
||||||
answer.RelaySrvAddress = addr
|
answer.RelaySrvAddress = addr
|
||||||
|
answer.RelaySrvIP = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
return answer
|
return answer
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type mocListener struct {
|
type mocListener struct {
|
||||||
lastState int
|
lastState int
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
peersWg sync.WaitGroup
|
||||||
peers int
|
peers int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ func (l *mocListener) OnAddressChanged(host, addr string) {
|
|||||||
}
|
}
|
||||||
func (l *mocListener) OnPeersListChanged(size int) {
|
func (l *mocListener) OnPeersListChanged(size int) {
|
||||||
l.peers = size
|
l.peers = size
|
||||||
|
l.peersWg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *mocListener) setWaiter() {
|
func (l *mocListener) setWaiter() {
|
||||||
@@ -43,6 +45,14 @@ func (l *mocListener) wait() {
|
|||||||
l.wg.Wait()
|
l.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *mocListener) setPeersWaiter() {
|
||||||
|
l.peersWg.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *mocListener) waitPeers() {
|
||||||
|
l.peersWg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func Test_notifier_serverState(t *testing.T) {
|
func Test_notifier_serverState(t *testing.T) {
|
||||||
|
|
||||||
type scenario struct {
|
type scenario struct {
|
||||||
@@ -72,11 +82,13 @@ func Test_notifier_serverState(t *testing.T) {
|
|||||||
func Test_notifier_SetListener(t *testing.T) {
|
func Test_notifier_SetListener(t *testing.T) {
|
||||||
listener := &mocListener{}
|
listener := &mocListener{}
|
||||||
listener.setWaiter()
|
listener.setWaiter()
|
||||||
|
listener.setPeersWaiter()
|
||||||
|
|
||||||
n := newNotifier()
|
n := newNotifier()
|
||||||
n.lastNotification = stateConnecting
|
n.lastNotification = stateConnecting
|
||||||
n.setListener(listener)
|
n.setListener(listener)
|
||||||
listener.wait()
|
listener.wait()
|
||||||
|
listener.waitPeers()
|
||||||
if listener.lastState != n.lastNotification {
|
if listener.lastState != n.lastNotification {
|
||||||
t.Errorf("invalid state: %d, expected: %d", listener.lastState, n.lastNotification)
|
t.Errorf("invalid state: %d, expected: %d", listener.lastState, n.lastNotification)
|
||||||
}
|
}
|
||||||
@@ -85,9 +97,14 @@ func Test_notifier_SetListener(t *testing.T) {
|
|||||||
func Test_notifier_RemoveListener(t *testing.T) {
|
func Test_notifier_RemoveListener(t *testing.T) {
|
||||||
listener := &mocListener{}
|
listener := &mocListener{}
|
||||||
listener.setWaiter()
|
listener.setWaiter()
|
||||||
|
listener.setPeersWaiter()
|
||||||
n := newNotifier()
|
n := newNotifier()
|
||||||
n.lastNotification = stateConnecting
|
n.lastNotification = stateConnecting
|
||||||
n.setListener(listener)
|
n.setListener(listener)
|
||||||
|
// setListener replays cached state on a goroutine; wait for both the state
|
||||||
|
// and peers callbacks to finish so we don't race on listener.peers.
|
||||||
|
listener.wait()
|
||||||
|
listener.waitPeers()
|
||||||
n.removeListener()
|
n.removeListener()
|
||||||
n.peerListChanged(1)
|
n.peerListChanged(1)
|
||||||
|
|
||||||
|
|||||||
@@ -54,19 +54,19 @@ func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string,
|
|||||||
log.Warnf("failed to get session ID bytes: %v", err)
|
log.Warnf("failed to get session ID bytes: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msg, err := signal.MarshalCredential(
|
msg, err := signal.MarshalCredential(s.wgPrivateKey, remoteKey, signal.CredentialPayload{
|
||||||
s.wgPrivateKey,
|
Type: bodyType,
|
||||||
offerAnswer.WgListenPort,
|
WgListenPort: offerAnswer.WgListenPort,
|
||||||
remoteKey,
|
Credential: &signal.Credential{
|
||||||
&signal.Credential{
|
|
||||||
UFrag: offerAnswer.IceCredentials.UFrag,
|
UFrag: offerAnswer.IceCredentials.UFrag,
|
||||||
Pwd: offerAnswer.IceCredentials.Pwd,
|
Pwd: offerAnswer.IceCredentials.Pwd,
|
||||||
},
|
},
|
||||||
bodyType,
|
RosenpassPubKey: offerAnswer.RosenpassPubKey,
|
||||||
offerAnswer.RosenpassPubKey,
|
RosenpassAddr: offerAnswer.RosenpassAddr,
|
||||||
offerAnswer.RosenpassAddr,
|
RelaySrvAddress: offerAnswer.RelaySrvAddress,
|
||||||
offerAnswer.RelaySrvAddress,
|
RelaySrvIP: offerAnswer.RelaySrvIP,
|
||||||
sessionIDBytes)
|
SessionID: sessionIDBytes,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,10 +320,10 @@ func (d *Status) RemovePeer(peerPubKey string) error {
|
|||||||
// UpdatePeerState updates peer status
|
// UpdatePeerState updates peer status
|
||||||
func (d *Status) UpdatePeerState(receivedState State) error {
|
func (d *Status) UpdatePeerState(receivedState State) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[receivedState.PubKey]
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,23 +343,29 @@ func (d *Status) UpdatePeerState(receivedState State) error {
|
|||||||
|
|
||||||
d.peers[receivedState.PubKey] = peerState
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
|
notifyList := hasConnStatusChanged(oldState, receivedState.ConnStatus)
|
||||||
d.notifyPeerListChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
// when we close the connection we will not notify the router manager
|
// when we close the connection we will not notify the router manager
|
||||||
if receivedState.ConnStatus == StatusIdle {
|
notifyRouter := receivedState.ConnStatus == StatusIdle
|
||||||
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
routerSnapshot := d.snapshotRouterPeersLocked(receivedState.PubKey, notifyRouter)
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
if notifyList {
|
||||||
|
d.notifier.peerListChanged(numPeers)
|
||||||
|
}
|
||||||
|
if notifyRouter {
|
||||||
|
d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) AddPeerStateRoute(peer string, route string, resourceId route.ResID) error {
|
func (d *Status) AddPeerStateRoute(peer string, route string, resourceId route.ResID) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[peer]
|
peerState, ok := d.peers[peer]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,17 +377,20 @@ func (d *Status) AddPeerStateRoute(peer string, route string, resourceId route.R
|
|||||||
d.routeIDLookup.AddRemoteRouteID(resourceId, pref)
|
d.routeIDLookup.AddRemoteRouteID(resourceId, pref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
// todo: consider to make sense of this notification or not
|
// todo: consider to make sense of this notification or not
|
||||||
d.notifyPeerListChanged()
|
d.notifier.peerListChanged(numPeers)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) RemovePeerStateRoute(peer string, route string) error {
|
func (d *Status) RemovePeerStateRoute(peer string, route string) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[peer]
|
peerState, ok := d.peers[peer]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +402,11 @@ func (d *Status) RemovePeerStateRoute(peer string, route string) error {
|
|||||||
d.routeIDLookup.RemoveRemoteRouteID(pref)
|
d.routeIDLookup.RemoveRemoteRouteID(pref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
// todo: consider to make sense of this notification or not
|
// todo: consider to make sense of this notification or not
|
||||||
d.notifyPeerListChanged()
|
d.notifier.peerListChanged(numPeers)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,10 +422,10 @@ func (d *Status) CheckRoutes(ip netip.Addr) ([]byte, bool) {
|
|||||||
|
|
||||||
func (d *Status) UpdatePeerICEState(receivedState State) error {
|
func (d *Status) UpdatePeerICEState(receivedState State) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[receivedState.PubKey]
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,22 +443,28 @@ func (d *Status) UpdatePeerICEState(receivedState State) error {
|
|||||||
|
|
||||||
d.peers[receivedState.PubKey] = peerState
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
|
notifyList := hasConnStatusChanged(oldState, receivedState.ConnStatus)
|
||||||
d.notifyPeerListChanged()
|
notifyRouter := hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed)
|
||||||
}
|
routerSnapshot := d.snapshotRouterPeersLocked(receivedState.PubKey, notifyRouter)
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
|
||||||
if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
|
d.mux.Unlock()
|
||||||
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
|
||||||
|
if notifyList {
|
||||||
|
d.notifier.peerListChanged(numPeers)
|
||||||
|
}
|
||||||
|
if notifyRouter {
|
||||||
|
d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) UpdatePeerRelayedState(receivedState State) error {
|
func (d *Status) UpdatePeerRelayedState(receivedState State) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[receivedState.PubKey]
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,22 +479,28 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error {
|
|||||||
|
|
||||||
d.peers[receivedState.PubKey] = peerState
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
|
notifyList := hasConnStatusChanged(oldState, receivedState.ConnStatus)
|
||||||
d.notifyPeerListChanged()
|
notifyRouter := hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed)
|
||||||
}
|
routerSnapshot := d.snapshotRouterPeersLocked(receivedState.PubKey, notifyRouter)
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
|
||||||
if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
|
d.mux.Unlock()
|
||||||
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
|
||||||
|
if notifyList {
|
||||||
|
d.notifier.peerListChanged(numPeers)
|
||||||
|
}
|
||||||
|
if notifyRouter {
|
||||||
|
d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error {
|
func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[receivedState.PubKey]
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,22 +514,28 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error
|
|||||||
|
|
||||||
d.peers[receivedState.PubKey] = peerState
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
|
notifyList := hasConnStatusChanged(oldState, receivedState.ConnStatus)
|
||||||
d.notifyPeerListChanged()
|
notifyRouter := hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed)
|
||||||
}
|
routerSnapshot := d.snapshotRouterPeersLocked(receivedState.PubKey, notifyRouter)
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
|
||||||
if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
|
d.mux.Unlock()
|
||||||
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
|
||||||
|
if notifyList {
|
||||||
|
d.notifier.peerListChanged(numPeers)
|
||||||
|
}
|
||||||
|
if notifyRouter {
|
||||||
|
d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error {
|
func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
peerState, ok := d.peers[receivedState.PubKey]
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
d.mux.Unlock()
|
||||||
return errors.New("peer doesn't exist")
|
return errors.New("peer doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,12 +552,18 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error {
|
|||||||
|
|
||||||
d.peers[receivedState.PubKey] = peerState
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
|
notifyList := hasConnStatusChanged(oldState, receivedState.ConnStatus)
|
||||||
d.notifyPeerListChanged()
|
notifyRouter := hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed)
|
||||||
}
|
routerSnapshot := d.snapshotRouterPeersLocked(receivedState.PubKey, notifyRouter)
|
||||||
|
numPeers := d.numOfPeers()
|
||||||
|
|
||||||
if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
|
d.mux.Unlock()
|
||||||
d.notifyPeerStateChangeListeners(receivedState.PubKey)
|
|
||||||
|
if notifyList {
|
||||||
|
d.notifier.peerListChanged(numPeers)
|
||||||
|
}
|
||||||
|
if notifyRouter {
|
||||||
|
d.dispatchRouterPeers(receivedState.PubKey, routerSnapshot)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -594,17 +630,33 @@ func (d *Status) UpdatePeerSSHHostKey(peerPubKey string, sshHostKey []byte) erro
|
|||||||
// FinishPeerListModifications this event invoke the notification
|
// FinishPeerListModifications this event invoke the notification
|
||||||
func (d *Status) FinishPeerListModifications() {
|
func (d *Status) FinishPeerListModifications() {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
if !d.peerListChangedForNotification {
|
if !d.peerListChangedForNotification {
|
||||||
|
d.mux.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.peerListChangedForNotification = false
|
d.peerListChangedForNotification = false
|
||||||
|
|
||||||
d.notifyPeerListChanged()
|
numPeers := d.numOfPeers()
|
||||||
|
|
||||||
|
// snapshot per-peer router state to deliver after the lock is released
|
||||||
|
type routerDispatch struct {
|
||||||
|
peerID string
|
||||||
|
snapshot map[string]RouterState
|
||||||
|
}
|
||||||
|
dispatches := make([]routerDispatch, 0, len(d.peers))
|
||||||
for key := range d.peers {
|
for key := range d.peers {
|
||||||
d.notifyPeerStateChangeListeners(key)
|
snapshot := d.snapshotRouterPeersLocked(key, true)
|
||||||
|
if snapshot != nil {
|
||||||
|
dispatches = append(dispatches, routerDispatch{peerID: key, snapshot: snapshot})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.peerListChanged(numPeers)
|
||||||
|
for _, rd := range dispatches {
|
||||||
|
d.dispatchRouterPeers(rd.peerID, rd.snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,10 +707,12 @@ func (d *Status) GetLocalPeerState() LocalPeerState {
|
|||||||
// UpdateLocalPeerState updates local peer status
|
// UpdateLocalPeerState updates local peer status
|
||||||
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
|
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
d.localPeer = localPeerState
|
d.localPeer = localPeerState
|
||||||
d.notifyAddressChanged()
|
fqdn := d.localPeer.FQDN
|
||||||
|
ip := d.localPeer.IP
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.localAddressChanged(fqdn, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddLocalPeerStateRoute adds a route to the local peer state
|
// AddLocalPeerStateRoute adds a route to the local peer state
|
||||||
@@ -721,30 +775,36 @@ func (d *Status) CleanLocalPeerStateRoutes() {
|
|||||||
// CleanLocalPeerState cleans local peer status
|
// CleanLocalPeerState cleans local peer status
|
||||||
func (d *Status) CleanLocalPeerState() {
|
func (d *Status) CleanLocalPeerState() {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
|
|
||||||
d.localPeer = LocalPeerState{}
|
d.localPeer = LocalPeerState{}
|
||||||
d.notifyAddressChanged()
|
fqdn := d.localPeer.FQDN
|
||||||
|
ip := d.localPeer.IP
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.localAddressChanged(fqdn, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkManagementDisconnected sets ManagementState to disconnected
|
// MarkManagementDisconnected sets ManagementState to disconnected
|
||||||
func (d *Status) MarkManagementDisconnected(err error) {
|
func (d *Status) MarkManagementDisconnected(err error) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
defer d.onConnectionChanged()
|
|
||||||
|
|
||||||
d.managementState = false
|
d.managementState = false
|
||||||
d.managementError = err
|
d.managementError = err
|
||||||
|
mgm := d.managementState
|
||||||
|
sig := d.signalState
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.updateServerStates(mgm, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkManagementConnected sets ManagementState to connected
|
// MarkManagementConnected sets ManagementState to connected
|
||||||
func (d *Status) MarkManagementConnected() {
|
func (d *Status) MarkManagementConnected() {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
defer d.onConnectionChanged()
|
|
||||||
|
|
||||||
d.managementState = true
|
d.managementState = true
|
||||||
d.managementError = nil
|
d.managementError = nil
|
||||||
|
mgm := d.managementState
|
||||||
|
sig := d.signalState
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.updateServerStates(mgm, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSignalAddress update the address of the signal server
|
// UpdateSignalAddress update the address of the signal server
|
||||||
@@ -778,21 +838,25 @@ func (d *Status) UpdateLazyConnection(enabled bool) {
|
|||||||
// MarkSignalDisconnected sets SignalState to disconnected
|
// MarkSignalDisconnected sets SignalState to disconnected
|
||||||
func (d *Status) MarkSignalDisconnected(err error) {
|
func (d *Status) MarkSignalDisconnected(err error) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
defer d.onConnectionChanged()
|
|
||||||
|
|
||||||
d.signalState = false
|
d.signalState = false
|
||||||
d.signalError = err
|
d.signalError = err
|
||||||
|
mgm := d.managementState
|
||||||
|
sig := d.signalState
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.updateServerStates(mgm, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkSignalConnected sets SignalState to connected
|
// MarkSignalConnected sets SignalState to connected
|
||||||
func (d *Status) MarkSignalConnected() {
|
func (d *Status) MarkSignalConnected() {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
|
||||||
defer d.onConnectionChanged()
|
|
||||||
|
|
||||||
d.signalState = true
|
d.signalState = true
|
||||||
d.signalError = nil
|
d.signalError = nil
|
||||||
|
mgm := d.managementState
|
||||||
|
sig := d.signalState
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
|
d.notifier.updateServerStates(mgm, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) {
|
func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) {
|
||||||
@@ -919,7 +983,7 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
|
|||||||
|
|
||||||
// if the server connection is not established then we will use the general address
|
// if the server connection is not established then we will use the general address
|
||||||
// in case of connection we will use the instance specific address
|
// in case of connection we will use the instance specific address
|
||||||
instanceAddr, err := d.relayMgr.RelayInstanceAddress()
|
instanceAddr, _, err := d.relayMgr.RelayInstanceAddress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO add their status
|
// TODO add their status
|
||||||
for _, r := range d.relayMgr.ServerURLs() {
|
for _, r := range d.relayMgr.ServerURLs() {
|
||||||
@@ -1012,18 +1076,17 @@ func (d *Status) RemoveConnectionListener() {
|
|||||||
d.notifier.removeListener()
|
d.notifier.removeListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) onConnectionChanged() {
|
// snapshotRouterPeersLocked builds the RouterState map for a peer's subscribers.
|
||||||
d.notifier.updateServerStates(d.managementState, d.signalState)
|
// Caller MUST hold d.mux. Returns nil when there are no subscribers for peerID
|
||||||
}
|
// or when notify is false. The snapshot is consumed later by dispatchRouterPeers
|
||||||
|
// outside the lock so the channel send cannot stall any d.mux holder.
|
||||||
// notifyPeerStateChangeListeners notifies route manager about the change in peer state
|
func (d *Status) snapshotRouterPeersLocked(peerID string, notify bool) map[string]RouterState {
|
||||||
func (d *Status) notifyPeerStateChangeListeners(peerID string) {
|
if !notify {
|
||||||
subs, ok := d.changeNotify[peerID]
|
return nil
|
||||||
if !ok {
|
}
|
||||||
return
|
if _, ok := d.changeNotify[peerID]; !ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect the relevant data for router peers
|
|
||||||
routerPeers := make(map[string]RouterState, len(d.changeNotify))
|
routerPeers := make(map[string]RouterState, len(d.changeNotify))
|
||||||
for pid := range d.changeNotify {
|
for pid := range d.changeNotify {
|
||||||
s, ok := d.peers[pid]
|
s, ok := d.peers[pid]
|
||||||
@@ -1031,13 +1094,35 @@ func (d *Status) notifyPeerStateChangeListeners(peerID string) {
|
|||||||
log.Warnf("router peer not found in peers list: %s", pid)
|
log.Warnf("router peer not found in peers list: %s", pid)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
routerPeers[pid] = RouterState{
|
routerPeers[pid] = RouterState{
|
||||||
Status: s.ConnStatus,
|
Status: s.ConnStatus,
|
||||||
Relayed: s.Relayed,
|
Relayed: s.Relayed,
|
||||||
Latency: s.Latency,
|
Latency: s.Latency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return routerPeers
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchRouterPeers delivers a previously snapshotted router-state map to
|
||||||
|
// the peer's subscribers. Caller MUST NOT hold d.mux. The method takes a
|
||||||
|
// fresh, short read of d.changeNotify under the lock to grab subscriber
|
||||||
|
// channels, then sends outside the lock so a slow consumer cannot block other
|
||||||
|
// d.mux holders. The send itself stays blocking (only short-circuited by the
|
||||||
|
// subscriber's context) so peer state transitions are not silently dropped.
|
||||||
|
func (d *Status) dispatchRouterPeers(peerID string, routerPeers map[string]RouterState) {
|
||||||
|
if routerPeers == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mux.Lock()
|
||||||
|
subsMap, ok := d.changeNotify[peerID]
|
||||||
|
subs := make([]*StatusChangeSubscription, 0, len(subsMap))
|
||||||
|
if ok {
|
||||||
|
for _, sub := range subsMap {
|
||||||
|
subs = append(subs, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.mux.Unlock()
|
||||||
|
|
||||||
for _, sub := range subs {
|
for _, sub := range subs {
|
||||||
select {
|
select {
|
||||||
@@ -1047,14 +1132,6 @@ func (d *Status) notifyPeerStateChangeListeners(peerID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) notifyPeerListChanged() {
|
|
||||||
d.notifier.peerListChanged(d.numOfPeers())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Status) notifyAddressChanged() {
|
|
||||||
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Status) numOfPeers() int {
|
func (d *Status) numOfPeers() int {
|
||||||
return len(d.peers) + len(d.offlinePeers)
|
return len(d.peers) + len(d.offlinePeers)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@@ -53,15 +54,19 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
w.relaySupportedOnRemotePeer.Store(true)
|
w.relaySupportedOnRemotePeer.Store(true)
|
||||||
|
|
||||||
// the relayManager will return with error in case if the connection has lost with relay server
|
// the relayManager will return with error in case if the connection has lost with relay server
|
||||||
currentRelayAddress, err := w.relayManager.RelayInstanceAddress()
|
currentRelayAddress, _, err := w.relayManager.RelayInstanceAddress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.log.Errorf("failed to handle new offer: %s", err)
|
w.log.Errorf("failed to handle new offer: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := w.preferredRelayServer(currentRelayAddress, remoteOfferAnswer.RelaySrvAddress)
|
srv := w.preferredRelayServer(currentRelayAddress, remoteOfferAnswer.RelaySrvAddress)
|
||||||
|
var serverIP netip.Addr
|
||||||
|
if srv == remoteOfferAnswer.RelaySrvAddress {
|
||||||
|
serverIP = remoteOfferAnswer.RelaySrvIP
|
||||||
|
}
|
||||||
|
|
||||||
relayedConn, err := w.relayManager.OpenConn(w.peerCtx, srv, w.config.Key)
|
relayedConn, err := w.relayManager.OpenConn(w.peerCtx, srv, w.config.Key, serverIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, relayClient.ErrConnAlreadyExists) {
|
if errors.Is(err, relayClient.ErrConnAlreadyExists) {
|
||||||
w.log.Debugf("handled offer by reusing existing relay connection")
|
w.log.Debugf("handled offer by reusing existing relay connection")
|
||||||
@@ -90,7 +95,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerRelay) RelayInstanceAddress() (string, error) {
|
func (w *WorkerRelay) RelayInstanceAddress() (string, netip.Addr, error) {
|
||||||
return w.relayManager.RelayInstanceAddress()
|
return w.relayManager.RelayInstanceAddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -177,7 +178,12 @@ func getDefaultGateway() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, gateway, localIP, err = router.Route(net.IPv4zero)
|
dst := net.IPv4zero
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux.
|
||||||
|
dst = net.IPv4(0, 0, 0, 1)
|
||||||
|
}
|
||||||
|
_, gateway, localIP, err = router.Route(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -196,7 +202,12 @@ func getDefaultGateway6() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, gateway, localIP, err = router.Route(net.IPv6zero)
|
dst := net.IPv6zero
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// ::2
|
||||||
|
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||||
|
}
|
||||||
|
_, gateway, localIP, err = router.Route(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,16 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
|||||||
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reused := false
|
||||||
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
||||||
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
if !errors.Is(err, unix.EEXIST) {
|
||||||
|
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
||||||
|
}
|
||||||
|
// macOS installs its own RTF_IFSCOPE defaults for primary service
|
||||||
|
// selection on multi-NIC setups, so a route on this ifindex can
|
||||||
|
// already exist before we try. Binding to it via IP[V6]_BOUND_IF
|
||||||
|
// still produces the scoped lookup we need.
|
||||||
|
reused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
af := unix.AF_INET
|
af := unix.AF_INET
|
||||||
@@ -102,7 +110,11 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
|||||||
if nexthop.IP.IsValid() {
|
if nexthop.IP.IsValid() {
|
||||||
via = nexthop.IP.String()
|
via = nexthop.IP.String()
|
||||||
}
|
}
|
||||||
log.Infof("installed scoped default route via %s on %s for %s", via, nexthop.Intf.Name, afOf(unspec))
|
verb := "installed"
|
||||||
|
if reused {
|
||||||
|
verb = "reused existing"
|
||||||
|
}
|
||||||
|
log.Infof("%s scoped default route via %s on %s for %s", verb, via, nexthop.Intf.Name, afOf(unspec))
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,6 +342,22 @@ func GetNextHop(ip netip.Addr) (Nexthop, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Nexthop{}, fmt.Errorf("new netroute: %w", err)
|
return Nexthop{}, fmt.Errorf("new netroute: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// go-netroute v0.4.0 rejects unspecified destinations on Linux with a hard
|
||||||
|
// client-side check. Substitute the lowest non-loopback address so the
|
||||||
|
// lookup falls through to the default route (::1 / 127.0.0.1 would match
|
||||||
|
// loopback, ::/0.0.0.0 are unspec). BSD/Windows pass the query straight to
|
||||||
|
// the kernel and need no substitution.
|
||||||
|
if runtime.GOOS == "linux" && ip.IsUnspecified() {
|
||||||
|
if ip.Is6() {
|
||||||
|
// ::2
|
||||||
|
ip = netip.AddrFrom16([16]byte{15: 2})
|
||||||
|
} else {
|
||||||
|
// 0.0.0.1
|
||||||
|
ip = netip.AddrFrom4([4]byte{0, 0, 0, 1})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
intf, gateway, preferredSrc, err := r.Route(ip.AsSlice())
|
intf, gateway, preferredSrc, err := r.Route(ip.AsSlice())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Failed to get route for %s: %v", ip, err)
|
log.Debugf("Failed to get route for %s: %v", ip, err)
|
||||||
|
|||||||
@@ -354,9 +354,13 @@ func TestAddRouteToNonVPNIntf(t *testing.T) {
|
|||||||
require.NoError(t, err, "Should be able to get IPv4 default route")
|
require.NoError(t, err, "Should be able to get IPv4 default route")
|
||||||
t.Logf("Initial IPv4 next hop: %s", initialNextHopV4)
|
t.Logf("Initial IPv4 next hop: %s", initialNextHopV4)
|
||||||
|
|
||||||
|
if testCase.prefix.Addr().Is6() && !testCase.expectError {
|
||||||
|
ensureIPv6DefaultRoute(t)
|
||||||
|
}
|
||||||
|
|
||||||
initialNextHopV6, err := GetNextHop(netip.IPv6Unspecified())
|
initialNextHopV6, err := GetNextHop(netip.IPv6Unspecified())
|
||||||
if testCase.prefix.Addr().Is6() &&
|
if testCase.prefix.Addr().Is6() &&
|
||||||
(errors.Is(err, vars.ErrRouteNotFound) || initialNextHopV6.Intf != nil && strings.HasPrefix(initialNextHopV6.Intf.Name, "utun")) {
|
initialNextHopV6.Intf != nil && strings.HasPrefix(initialNextHopV6.Intf.Name, "utun") {
|
||||||
t.Skip("Skipping test as no ipv6 default route is available")
|
t.Skip("Skipping test as no ipv6 default route is available")
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, vars.ErrRouteNotFound) {
|
if err != nil && !errors.Is(err, vars.ErrRouteNotFound) {
|
||||||
|
|||||||
30
client/internal/routemanager/systemops/v6route_bsd_test.go
Normal file
30
client/internal/routemanager/systemops/v6route_bsd_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ensureIPv6DefaultRoute installs an IPv6 default route via the loopback
|
||||||
|
// interface so route lookups for global IPv6 prefixes resolve in environments
|
||||||
|
// without v6 connectivity. If a default already exists it is left alone.
|
||||||
|
func ensureIPv6DefaultRoute(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
out, err := exec.Command("route", "-6", "add", "default", "-iface", "lo0").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// Existing default; nothing to install or clean up.
|
||||||
|
if bytes.Contains(out, []byte("route already in table")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Skipf("install IPv6 fallback default route: %v: %s", err, out)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if out, err := exec.Command("route", "-6", "delete", "default").CombinedOutput(); err != nil {
|
||||||
|
t.Logf("delete IPv6 fallback default route: %v: %s", err, out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
41
client/internal/routemanager/systemops/v6route_linux_test.go
Normal file
41
client/internal/routemanager/systemops/v6route_linux_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build linux && !android
|
||||||
|
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ensureIPv6DefaultRoute installs a low-preference IPv6 default route via the
|
||||||
|
// loopback interface so route lookups for global IPv6 prefixes resolve in
|
||||||
|
// environments without v6 connectivity. Any pre-existing default route wins
|
||||||
|
// because of its lower metric.
|
||||||
|
func ensureIPv6DefaultRoute(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
lo, err := netlink.LinkByName("lo")
|
||||||
|
require.NoError(t, err, "find loopback interface")
|
||||||
|
|
||||||
|
route := &netlink.Route{
|
||||||
|
Dst: &net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)},
|
||||||
|
LinkIndex: lo.Attrs().Index,
|
||||||
|
Priority: 1 << 20,
|
||||||
|
}
|
||||||
|
if err := netlink.RouteAdd(route); err != nil {
|
||||||
|
if errors.Is(err, syscall.EEXIST) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Skipf("install IPv6 fallback default route: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := netlink.RouteDel(route); err != nil && !errors.Is(err, syscall.ESRCH) {
|
||||||
|
t.Logf("delete IPv6 fallback default route: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const loopbackIfaceWindows = "Loopback Pseudo-Interface 1"
|
||||||
|
|
||||||
|
// ensureIPv6DefaultRoute installs an IPv6 default route via the loopback
|
||||||
|
// interface so route lookups for global IPv6 prefixes resolve in environments
|
||||||
|
// without v6 connectivity. If a default already exists it is left alone.
|
||||||
|
func ensureIPv6DefaultRoute(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
script := `New-NetRoute -DestinationPrefix "::/0" -InterfaceAlias "` + loopbackIfaceWindows + `" -RouteMetric 9999 -PolicyStore ActiveStore -ErrorAction Stop`
|
||||||
|
out, err := exec.Command("powershell", "-Command", script).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// Existing default; nothing to install or clean up.
|
||||||
|
if bytes.Contains(out, []byte("already exists")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Skipf("install IPv6 fallback default route: %v: %s", err, out)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
script := `Remove-NetRoute -DestinationPrefix "::/0" -InterfaceAlias "` + loopbackIfaceWindows + `" -Confirm:$false -ErrorAction Stop`
|
||||||
|
if out, err := exec.Command("powershell", "-Command", script).CombinedOutput(); err != nil {
|
||||||
|
t.Logf("delete IPv6 fallback default route: %v: %s", err, out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/errors"
|
"github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
@@ -44,8 +43,8 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al
|
|||||||
if rs.selectedRoutes == nil {
|
if rs.selectedRoutes == nil {
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
}
|
}
|
||||||
maps.Clear(rs.deselectedRoutes)
|
clear(rs.deselectedRoutes)
|
||||||
maps.Clear(rs.selectedRoutes)
|
clear(rs.selectedRoutes)
|
||||||
for _, r := range allRoutes {
|
for _, r := range allRoutes {
|
||||||
rs.deselectedRoutes[r] = struct{}{}
|
rs.deselectedRoutes[r] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -78,8 +77,8 @@ func (rs *RouteSelector) SelectAllRoutes() {
|
|||||||
if rs.selectedRoutes == nil {
|
if rs.selectedRoutes == nil {
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
}
|
}
|
||||||
maps.Clear(rs.deselectedRoutes)
|
clear(rs.deselectedRoutes)
|
||||||
maps.Clear(rs.selectedRoutes)
|
clear(rs.selectedRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeselectRoutes removes specific routes from the selection.
|
// DeselectRoutes removes specific routes from the selection.
|
||||||
@@ -116,8 +115,8 @@ func (rs *RouteSelector) DeselectAllRoutes() {
|
|||||||
if rs.selectedRoutes == nil {
|
if rs.selectedRoutes == nil {
|
||||||
rs.selectedRoutes = map[route.NetID]struct{}{}
|
rs.selectedRoutes = map[route.NetID]struct{}{}
|
||||||
}
|
}
|
||||||
maps.Clear(rs.deselectedRoutes)
|
clear(rs.deselectedRoutes)
|
||||||
maps.Clear(rs.selectedRoutes)
|
clear(rs.selectedRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSelected checks if a specific route is selected.
|
// IsSelected checks if a specific route is selected.
|
||||||
|
|||||||
@@ -2,217 +2,358 @@
|
|||||||
|
|
||||||
package sleep
|
package sleep
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo LDFLAGS: -framework IOKit -framework CoreFoundation
|
|
||||||
#include <IOKit/pwr_mgt/IOPMLib.h>
|
|
||||||
#include <IOKit/IOMessage.h>
|
|
||||||
#include <CoreFoundation/CoreFoundation.h>
|
|
||||||
|
|
||||||
extern void sleepCallbackBridge();
|
|
||||||
extern void poweredOnCallbackBridge();
|
|
||||||
extern void suspendedCallbackBridge();
|
|
||||||
extern void resumedCallbackBridge();
|
|
||||||
|
|
||||||
|
|
||||||
// C global variables for IOKit state
|
|
||||||
static IONotificationPortRef g_notifyPortRef = NULL;
|
|
||||||
static io_object_t g_notifierObject = 0;
|
|
||||||
static io_object_t g_generalInterestNotifier = 0;
|
|
||||||
static io_connect_t g_rootPort = 0;
|
|
||||||
static CFRunLoopRef g_runLoop = NULL;
|
|
||||||
|
|
||||||
static void sleepCallback(void* refCon, io_service_t service, natural_t messageType, void* messageArgument) {
|
|
||||||
switch (messageType) {
|
|
||||||
case kIOMessageSystemWillSleep:
|
|
||||||
sleepCallbackBridge();
|
|
||||||
IOAllowPowerChange(g_rootPort, (long)messageArgument);
|
|
||||||
break;
|
|
||||||
case kIOMessageSystemHasPoweredOn:
|
|
||||||
poweredOnCallbackBridge();
|
|
||||||
break;
|
|
||||||
case kIOMessageServiceIsSuspended:
|
|
||||||
suspendedCallbackBridge();
|
|
||||||
break;
|
|
||||||
case kIOMessageServiceIsResumed:
|
|
||||||
resumedCallbackBridge();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void registerNotifications() {
|
|
||||||
g_rootPort = IORegisterForSystemPower(
|
|
||||||
NULL,
|
|
||||||
&g_notifyPortRef,
|
|
||||||
(IOServiceInterestCallback)sleepCallback,
|
|
||||||
&g_notifierObject
|
|
||||||
);
|
|
||||||
|
|
||||||
if (g_rootPort == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFRunLoopAddSource(CFRunLoopGetCurrent(),
|
|
||||||
IONotificationPortGetRunLoopSource(g_notifyPortRef),
|
|
||||||
kCFRunLoopCommonModes);
|
|
||||||
|
|
||||||
g_runLoop = CFRunLoopGetCurrent();
|
|
||||||
CFRunLoopRun();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void unregisterNotifications() {
|
|
||||||
CFRunLoopRemoveSource(g_runLoop,
|
|
||||||
IONotificationPortGetRunLoopSource(g_notifyPortRef),
|
|
||||||
kCFRunLoopCommonModes);
|
|
||||||
|
|
||||||
IODeregisterForSystemPower(&g_notifierObject);
|
|
||||||
IOServiceClose(g_rootPort);
|
|
||||||
IONotificationPortDestroy(g_notifyPortRef);
|
|
||||||
CFRunLoopStop(g_runLoop);
|
|
||||||
|
|
||||||
g_notifyPortRef = NULL;
|
|
||||||
g_notifierObject = 0;
|
|
||||||
g_rootPort = 0;
|
|
||||||
g_runLoop = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/ebitengine/purego"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// IOKit message types from IOKit/IOMessage.h.
|
||||||
serviceRegistry = make(map[*Detector]struct{})
|
const (
|
||||||
serviceRegistryMu sync.Mutex
|
kIOMessageCanSystemSleep uintptr = 0xe0000270
|
||||||
|
kIOMessageSystemWillSleep uintptr = 0xe0000280
|
||||||
|
kIOMessageSystemHasPoweredOn uintptr = 0xe0000300
|
||||||
)
|
)
|
||||||
|
|
||||||
//export sleepCallbackBridge
|
var (
|
||||||
func sleepCallbackBridge() {
|
ioKit iokitFuncs
|
||||||
log.Info("sleepCallbackBridge event triggered")
|
cf cfFuncs
|
||||||
|
cfCommonModes uintptr
|
||||||
|
|
||||||
serviceRegistryMu.Lock()
|
libInitOnce sync.Once
|
||||||
defer serviceRegistryMu.Unlock()
|
libInitErr error
|
||||||
|
|
||||||
for svc := range serviceRegistry {
|
// callbackThunk is the single C-callable trampoline registered with IOKit.
|
||||||
svc.triggerCallback(EventTypeSleep)
|
callbackThunk uintptr
|
||||||
}
|
|
||||||
|
serviceRegistry = make(map[*Detector]struct{})
|
||||||
|
serviceRegistryMu sync.Mutex
|
||||||
|
session *runLoopSession
|
||||||
|
|
||||||
|
// lifecycleMu serializes Register/Deregister so a new registration can't
|
||||||
|
// start a second runloop while a previous teardown is still pending.
|
||||||
|
lifecycleMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// iokitFuncs holds IOKit symbols resolved once at init.
|
||||||
|
type iokitFuncs struct {
|
||||||
|
IORegisterForSystemPower func(refcon uintptr, portRef *uintptr, callback uintptr, notifier *uintptr) uintptr
|
||||||
|
IODeregisterForSystemPower func(notifier *uintptr) int32
|
||||||
|
IOAllowPowerChange func(kernelPort uintptr, notificationID uintptr) int32
|
||||||
|
IOServiceClose func(connect uintptr) int32
|
||||||
|
IONotificationPortGetRunLoopSource func(port uintptr) uintptr
|
||||||
|
IONotificationPortDestroy func(port uintptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export resumedCallbackBridge
|
// cfFuncs holds CoreFoundation symbols resolved once at init.
|
||||||
func resumedCallbackBridge() {
|
type cfFuncs struct {
|
||||||
log.Info("resumedCallbackBridge event triggered")
|
CFRunLoopGetCurrent func() uintptr
|
||||||
|
CFRunLoopRun func()
|
||||||
|
CFRunLoopStop func(rl uintptr)
|
||||||
|
CFRunLoopAddSource func(rl, source, mode uintptr)
|
||||||
|
CFRunLoopRemoveSource func(rl, source, mode uintptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export suspendedCallbackBridge
|
// runLoopSession bundles the handles owned by one CFRunLoop lifetime. A nil
|
||||||
func suspendedCallbackBridge() {
|
// session means no runloop is active and the next Register must start one.
|
||||||
log.Info("suspendedCallbackBridge event triggered")
|
type runLoopSession struct {
|
||||||
|
rl uintptr
|
||||||
|
port uintptr
|
||||||
|
notifier uintptr
|
||||||
|
rp uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
//export poweredOnCallbackBridge
|
// detectorSnapshot pins a detector's callback and done channel so dispatch
|
||||||
func poweredOnCallbackBridge() {
|
// runs with values valid at snapshot time, even if a concurrent
|
||||||
log.Info("poweredOnCallbackBridge event triggered")
|
// Deregister/Register rewrites the detector's fields.
|
||||||
serviceRegistryMu.Lock()
|
type detectorSnapshot struct {
|
||||||
defer serviceRegistryMu.Unlock()
|
detector *Detector
|
||||||
|
callback func(event EventType)
|
||||||
for svc := range serviceRegistry {
|
done <-chan struct{}
|
||||||
svc.triggerCallback(EventTypeWakeUp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detector delivers sleep and wake events to a registered callback.
|
||||||
type Detector struct {
|
type Detector struct {
|
||||||
callback func(event EventType)
|
callback func(event EventType)
|
||||||
ctx context.Context
|
done chan struct{}
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDetector() (*Detector, error) {
|
|
||||||
return &Detector{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register installs callback for power events. The first registration starts
|
||||||
|
// the CFRunLoop on a dedicated OS-locked thread and blocks until IOKit
|
||||||
|
// registration succeeds or fails; subsequent registrations just add to the
|
||||||
|
// dispatch set.
|
||||||
func (d *Detector) Register(callback func(event EventType)) error {
|
func (d *Detector) Register(callback func(event EventType)) error {
|
||||||
serviceRegistryMu.Lock()
|
lifecycleMu.Lock()
|
||||||
defer serviceRegistryMu.Unlock()
|
defer lifecycleMu.Unlock()
|
||||||
|
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
if _, exists := serviceRegistry[d]; exists {
|
if _, exists := serviceRegistry[d]; exists {
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
return fmt.Errorf("detector service already registered")
|
return fmt.Errorf("detector service already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.callback = callback
|
d.callback = callback
|
||||||
|
d.done = make(chan struct{})
|
||||||
|
serviceRegistry[d] = struct{}{}
|
||||||
|
needSetup := session == nil
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
d.ctx, d.cancel = context.WithCancel(context.Background())
|
if !needSetup {
|
||||||
|
|
||||||
if len(serviceRegistry) > 0 {
|
|
||||||
serviceRegistry[d] = struct{}{}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceRegistry[d] = struct{}{}
|
errCh := make(chan error, 1)
|
||||||
|
go runRunLoop(errCh)
|
||||||
// CFRunLoop must run on a single fixed OS thread
|
if err := <-errCh; err != nil {
|
||||||
go func() {
|
serviceRegistryMu.Lock()
|
||||||
runtime.LockOSThread()
|
delete(serviceRegistry, d)
|
||||||
defer runtime.UnlockOSThread()
|
close(d.done)
|
||||||
|
d.done = nil
|
||||||
C.registerNotifications()
|
serviceRegistryMu.Unlock()
|
||||||
}()
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("sleep detection service started on macOS")
|
log.Info("sleep detection service started on macOS")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deregister removes the detector. When the last detector is removed, IOKit registration is torn down
|
// Deregister removes the detector. When the last detector leaves, IOKit
|
||||||
// and the runloop is stopped and cleaned up.
|
// notifications are torn down and the runloop is stopped.
|
||||||
func (d *Detector) Deregister() error {
|
func (d *Detector) Deregister() error {
|
||||||
|
lifecycleMu.Lock()
|
||||||
|
defer lifecycleMu.Unlock()
|
||||||
|
|
||||||
serviceRegistryMu.Lock()
|
serviceRegistryMu.Lock()
|
||||||
defer serviceRegistryMu.Unlock()
|
if _, exists := serviceRegistry[d]; !exists {
|
||||||
_, exists := serviceRegistry[d]
|
serviceRegistryMu.Unlock()
|
||||||
if !exists {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
close(d.done)
|
||||||
// cancel and remove this detector
|
|
||||||
d.cancel()
|
|
||||||
delete(serviceRegistry, d)
|
delete(serviceRegistry, d)
|
||||||
|
|
||||||
// If other Detectors still exist, leave IOKit running
|
|
||||||
if len(serviceRegistry) > 0 {
|
if len(serviceRegistry) > 0 {
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
sess := session
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
log.Info("sleep detection service stopping (deregister)")
|
log.Info("sleep detection service stopping (deregister)")
|
||||||
|
|
||||||
// Deregister IOKit notifications, stop runloop, and free resources
|
if sess == nil {
|
||||||
C.unregisterNotifications()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if sess.rl != 0 && sess.port != 0 {
|
||||||
|
source := ioKit.IONotificationPortGetRunLoopSource(sess.port)
|
||||||
|
cf.CFRunLoopRemoveSource(sess.rl, source, cfCommonModes)
|
||||||
|
}
|
||||||
|
if sess.notifier != 0 {
|
||||||
|
n := sess.notifier
|
||||||
|
ioKit.IODeregisterForSystemPower(&n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear session only after IODeregisterForSystemPower returns so any
|
||||||
|
// in-flight powerCallback can still look up session.rp to ack sleep.
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
session = nil
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
|
if sess.rp != 0 {
|
||||||
|
ioKit.IOServiceClose(sess.rp)
|
||||||
|
}
|
||||||
|
if sess.port != 0 {
|
||||||
|
ioKit.IONotificationPortDestroy(sess.port)
|
||||||
|
}
|
||||||
|
if sess.rl != 0 {
|
||||||
|
cf.CFRunLoopStop(sess.rl)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Detector) triggerCallback(event EventType) {
|
func (d *Detector) triggerCallback(event EventType, cb func(event EventType), done <-chan struct{}) {
|
||||||
doneChan := make(chan struct{})
|
if cb == nil || done == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
doneChan := make(chan struct{})
|
||||||
timeout := time.NewTimer(500 * time.Millisecond)
|
timeout := time.NewTimer(500 * time.Millisecond)
|
||||||
defer timeout.Stop()
|
defer timeout.Stop()
|
||||||
|
|
||||||
cb := d.callback
|
go func() {
|
||||||
go func(callback func(event EventType)) {
|
defer close(doneChan)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("panic in sleep callback: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
log.Info("sleep detection event fired")
|
log.Info("sleep detection event fired")
|
||||||
callback(event)
|
cb(event)
|
||||||
close(doneChan)
|
}()
|
||||||
}(cb)
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-doneChan:
|
case <-doneChan:
|
||||||
case <-d.ctx.Done():
|
case <-done:
|
||||||
case <-timeout.C:
|
case <-timeout.C:
|
||||||
log.Warnf("sleep callback timed out")
|
log.Warn("sleep callback timed out")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDetector initializes IOKit/CoreFoundation bindings and returns a Detector.
|
||||||
|
func NewDetector() (*Detector, error) {
|
||||||
|
if err := initLibs(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Detector{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLibs() error {
|
||||||
|
libInitOnce.Do(func() {
|
||||||
|
iokit, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||||
|
if err != nil {
|
||||||
|
libInitErr = fmt.Errorf("dlopen IOKit: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfLib, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||||
|
if err != nil {
|
||||||
|
libInitErr = fmt.Errorf("dlopen CoreFoundation: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
purego.RegisterLibFunc(&ioKit.IORegisterForSystemPower, iokit, "IORegisterForSystemPower")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IODeregisterForSystemPower, iokit, "IODeregisterForSystemPower")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IOAllowPowerChange, iokit, "IOAllowPowerChange")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IOServiceClose, iokit, "IOServiceClose")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IONotificationPortGetRunLoopSource, iokit, "IONotificationPortGetRunLoopSource")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IONotificationPortDestroy, iokit, "IONotificationPortDestroy")
|
||||||
|
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopGetCurrent, cfLib, "CFRunLoopGetCurrent")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopRun, cfLib, "CFRunLoopRun")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopStop, cfLib, "CFRunLoopStop")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopAddSource, cfLib, "CFRunLoopAddSource")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopRemoveSource, cfLib, "CFRunLoopRemoveSource")
|
||||||
|
|
||||||
|
modeAddr, err := purego.Dlsym(cfLib, "kCFRunLoopCommonModes")
|
||||||
|
if err != nil {
|
||||||
|
libInitErr = fmt.Errorf("dlsym kCFRunLoopCommonModes: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Launder the uintptr-to-pointer conversion through a Go variable so
|
||||||
|
// go vet's unsafeptr analyzer doesn't flag a system-library global.
|
||||||
|
cfCommonModes = **(**uintptr)(unsafe.Pointer(&modeAddr))
|
||||||
|
|
||||||
|
// NewCallback slots are a finite, non-reclaimable resource, so register
|
||||||
|
// a single thunk that dispatches to the current Detector set.
|
||||||
|
callbackThunk = purego.NewCallback(powerCallback)
|
||||||
|
})
|
||||||
|
return libInitErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// powerCallback is the IOServiceInterestCallback trampoline, invoked on the
|
||||||
|
// runloop thread. A Go panic crossing the purego boundary has undefined
|
||||||
|
// behavior, so contain it here.
|
||||||
|
func powerCallback(refcon, service, messageType, messageArgument uintptr) uintptr {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("panic in sleep powerCallback: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
switch messageType {
|
||||||
|
case kIOMessageCanSystemSleep:
|
||||||
|
// Not acknowledging forces a 30s IOKit timeout before idle sleep.
|
||||||
|
allowPowerChange(messageArgument)
|
||||||
|
case kIOMessageSystemWillSleep:
|
||||||
|
dispatchEvent(EventTypeSleep)
|
||||||
|
allowPowerChange(messageArgument)
|
||||||
|
case kIOMessageSystemHasPoweredOn:
|
||||||
|
dispatchEvent(EventTypeWakeUp)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowPowerChange(messageArgument uintptr) {
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
var port uintptr
|
||||||
|
if session != nil {
|
||||||
|
port = session.rp
|
||||||
|
}
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
if port != 0 {
|
||||||
|
ioKit.IOAllowPowerChange(port, messageArgument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dispatchEvent(event EventType) {
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
snaps := make([]detectorSnapshot, 0, len(serviceRegistry))
|
||||||
|
for d := range serviceRegistry {
|
||||||
|
snaps = append(snaps, detectorSnapshot{
|
||||||
|
detector: d,
|
||||||
|
callback: d.callback,
|
||||||
|
done: d.done,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
|
for _, s := range snaps {
|
||||||
|
s.detector.triggerCallback(event, s.callback, s.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runRunLoop owns the OS-locked thread that CFRunLoop is pinned to. Setup
|
||||||
|
// result is reported on errCh so Register can surface failures synchronously.
|
||||||
|
func runRunLoop(errCh chan<- error) {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
sess, err := setupSession()
|
||||||
|
if err == nil {
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
session = sess
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
}
|
||||||
|
errCh <- err
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("panic in sleep runloop: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cf.CFRunLoopRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupSession performs the IOKit registration on the current thread. Panics
|
||||||
|
// are converted to errors so runRunLoop never leaves errCh unsent.
|
||||||
|
func setupSession() (s *runLoopSession, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic during runloop setup: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var portRef, notifier uintptr
|
||||||
|
rp := ioKit.IORegisterForSystemPower(0, &portRef, callbackThunk, ¬ifier)
|
||||||
|
if rp == 0 {
|
||||||
|
return nil, fmt.Errorf("IORegisterForSystemPower returned zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
rl := cf.CFRunLoopGetCurrent()
|
||||||
|
source := ioKit.IONotificationPortGetRunLoopSource(portRef)
|
||||||
|
cf.CFRunLoopAddSource(rl, source, cfCommonModes)
|
||||||
|
|
||||||
|
return &runLoopSession{rl: rl, port: portRef, notifier: notifier, rp: rp}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -28,6 +27,10 @@ func NewWGIfaceMonitor() *WGIfaceMonitor {
|
|||||||
|
|
||||||
// Start begins monitoring the WireGuard interface.
|
// Start begins monitoring the WireGuard interface.
|
||||||
// It relies on the provided context cancellation to stop.
|
// It relies on the provided context cancellation to stop.
|
||||||
|
//
|
||||||
|
// On Linux the watcher is event-driven (RTNLGRP_LINK netlink subscription)
|
||||||
|
// to avoid the allocation churn of repeatedly dumping the kernel link
|
||||||
|
// table; on other platforms it falls back to a low-frequency poll.
|
||||||
func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) (shouldRestart bool, err error) {
|
func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) (shouldRestart bool, err error) {
|
||||||
defer close(m.done)
|
defer close(m.done)
|
||||||
|
|
||||||
@@ -56,31 +59,7 @@ func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) (shouldRes
|
|||||||
|
|
||||||
log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex)
|
log.Infof("Interface monitor: watching %s (index: %d)", ifaceName, expectedIndex)
|
||||||
|
|
||||||
ticker := time.NewTicker(2 * time.Second)
|
return watchInterface(ctx, ifaceName, expectedIndex)
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Infof("Interface monitor: stopped for %s", ifaceName)
|
|
||||||
return false, fmt.Errorf("wg interface monitor stopped: %v", ctx.Err())
|
|
||||||
case <-ticker.C:
|
|
||||||
currentIndex, err := getInterfaceIndex(ifaceName)
|
|
||||||
if err != nil {
|
|
||||||
// Interface was deleted
|
|
||||||
log.Infof("Interface monitor: %s deleted", ifaceName)
|
|
||||||
return true, fmt.Errorf("interface %s deleted: %w", ifaceName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if interface index changed (interface was recreated)
|
|
||||||
if currentIndex != expectedIndex {
|
|
||||||
log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine",
|
|
||||||
ifaceName, expectedIndex, currentIndex)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInterfaceIndex returns the index of a network interface by name.
|
// getInterfaceIndex returns the index of a network interface by name.
|
||||||
|
|||||||
134
client/internal/wg_iface_monitor_linux.go
Normal file
134
client/internal/wg_iface_monitor_linux.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// watchInterface uses an RTNLGRP_LINK netlink subscription to detect
|
||||||
|
// deletion or recreation of the WireGuard interface.
|
||||||
|
//
|
||||||
|
// The previous implementation polled net.InterfaceByName every 2 s, which
|
||||||
|
// on Linux issues syscall.NetlinkRIB(RTM_GETLINK, ...) and dumps the
|
||||||
|
// entire kernel link table on every call. On hosts with many veth
|
||||||
|
// interfaces (containers, bridges) the resulting allocation churn was on
|
||||||
|
// the order of ~1 GB/day from this single ticker, which on small ARM
|
||||||
|
// hosts manifested as a slow RSS climb (see netbirdio/netbird#3678).
|
||||||
|
//
|
||||||
|
// The event-driven version below allocates only when the kernel actually
|
||||||
|
// publishes a link event for the tracked interface — typically zero
|
||||||
|
// allocations between events.
|
||||||
|
func watchInterface(ctx context.Context, ifaceName string, expectedIndex int) (bool, error) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
// Buffer the channel to absorb event bursts (e.g. when many veth
|
||||||
|
// pairs are created/destroyed at once by container runtimes).
|
||||||
|
linkChan := make(chan netlink.LinkUpdate, 32)
|
||||||
|
if err := netlink.LinkSubscribe(linkChan, done); err != nil {
|
||||||
|
// Return shouldRestart=true so the engine recovers monitoring
|
||||||
|
// via triggerClientRestart instead of silently losing it for
|
||||||
|
// the rest of the process lifetime.
|
||||||
|
return true, fmt.Errorf("subscribe to link updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Race window: the interface could have been deleted (or recreated)
|
||||||
|
// between the initial getInterfaceIndex() in Start and LinkSubscribe
|
||||||
|
// completing its handshake with the kernel. Re-check explicitly so we
|
||||||
|
// do not block forever waiting for an event that already fired.
|
||||||
|
if currentIndex, err := getInterfaceIndex(ifaceName); err != nil {
|
||||||
|
log.Infof("Interface monitor: %s deleted before subscription completed", ifaceName)
|
||||||
|
return true, fmt.Errorf("interface %s deleted: %w", ifaceName, err)
|
||||||
|
} else if currentIndex != expectedIndex {
|
||||||
|
log.Infof("Interface monitor: %s recreated (index changed from %d to %d) before subscription completed",
|
||||||
|
ifaceName, expectedIndex, currentIndex)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Infof("Interface monitor: stopped for %s", ifaceName)
|
||||||
|
return false, fmt.Errorf("wg interface monitor stopped: %w", ctx.Err())
|
||||||
|
|
||||||
|
case update, ok := <-linkChan:
|
||||||
|
if !ok {
|
||||||
|
// The vishvananda/netlink subscription goroutine closes
|
||||||
|
// the channel on receive errors. Signal the engine to
|
||||||
|
// restart so monitoring is re-established instead of
|
||||||
|
// silently ending.
|
||||||
|
log.Warnf("Interface monitor: link subscription channel closed unexpectedly for %s", ifaceName)
|
||||||
|
return true, fmt.Errorf("link subscription channel closed unexpectedly")
|
||||||
|
}
|
||||||
|
if restart, err := inspectLinkEvent(update, ifaceName, expectedIndex); restart {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspectLinkEvent classifies a single netlink link update against the
|
||||||
|
// tracked WireGuard interface. It returns (true, err) when the engine
|
||||||
|
// should restart monitoring; (false, nil) means the event is unrelated
|
||||||
|
// and the caller should keep waiting.
|
||||||
|
//
|
||||||
|
// The error component, when non-nil, describes the kernel-side reason
|
||||||
|
// (deletion or rename); the recreation case returns (true, nil) since
|
||||||
|
// no error condition is reported.
|
||||||
|
func inspectLinkEvent(update netlink.LinkUpdate, ifaceName string, expectedIndex int) (bool, error) {
|
||||||
|
eventIndex := int(update.Index)
|
||||||
|
eventName := ""
|
||||||
|
if attrs := update.Attrs(); attrs != nil {
|
||||||
|
eventName = attrs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
switch update.Header.Type {
|
||||||
|
case syscall.RTM_DELLINK:
|
||||||
|
return inspectDelLink(eventIndex, ifaceName, expectedIndex)
|
||||||
|
case syscall.RTM_NEWLINK:
|
||||||
|
return inspectNewLink(eventIndex, eventName, ifaceName, expectedIndex)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspectDelLink reports a restart when an RTM_DELLINK arrives for the
|
||||||
|
// tracked interface index.
|
||||||
|
func inspectDelLink(eventIndex int, ifaceName string, expectedIndex int) (bool, error) {
|
||||||
|
if eventIndex != expectedIndex {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
log.Infof("Interface monitor: %s deleted", ifaceName)
|
||||||
|
return true, fmt.Errorf("interface %s deleted", ifaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspectNewLink reports a restart when an RTM_NEWLINK either:
|
||||||
|
//
|
||||||
|
// 1. Introduces a link with our name at a different index (recreation
|
||||||
|
// after a delete), or
|
||||||
|
//
|
||||||
|
// 2. Reports a link still at our index but with a different name
|
||||||
|
// (in-place rename). The previous polling implementation caught
|
||||||
|
// this implicitly because net.InterfaceByName(ifaceName) would
|
||||||
|
// start failing; the event-driven version has to test it.
|
||||||
|
//
|
||||||
|
// Same name + same index is just a flag/state change on the existing
|
||||||
|
// interface and is ignored.
|
||||||
|
func inspectNewLink(eventIndex int, eventName, ifaceName string, expectedIndex int) (bool, error) {
|
||||||
|
if eventName == ifaceName && eventIndex != expectedIndex {
|
||||||
|
log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine",
|
||||||
|
ifaceName, expectedIndex, eventIndex)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if eventIndex == expectedIndex && eventName != "" && eventName != ifaceName {
|
||||||
|
log.Infof("Interface monitor: %s renamed to %s (index %d), restarting engine",
|
||||||
|
ifaceName, eventName, expectedIndex)
|
||||||
|
return true, fmt.Errorf("interface %s renamed to %s", ifaceName, eventName)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
56
client/internal/wg_iface_monitor_other.go
Normal file
56
client/internal/wg_iface_monitor_other.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// watchInterface polls net.InterfaceByName at a fixed interval to detect
|
||||||
|
// deletion or recreation of the WireGuard interface.
|
||||||
|
//
|
||||||
|
// This is the fallback used on non-Linux desktop and server platforms
|
||||||
|
// (darwin, windows, freebsd). It is also compiled on android and ios so
|
||||||
|
// the package builds on every supported GOOS, but it is never reached
|
||||||
|
// at runtime there because Start() in wg_iface_monitor.go exits early
|
||||||
|
// on mobile platforms.
|
||||||
|
//
|
||||||
|
// The Linux build (see wg_iface_monitor_linux.go) uses an event-driven
|
||||||
|
// RTNLGRP_LINK netlink subscription instead, because on Linux
|
||||||
|
// net.InterfaceByName issues syscall.NetlinkRIB(RTM_GETLINK, ...) which
|
||||||
|
// dumps the entire kernel link table on every call and produces
|
||||||
|
// significant allocation churn (netbirdio/netbird#3678).
|
||||||
|
//
|
||||||
|
// Windows is also reported in #3678 as affected by RSS climb. A future
|
||||||
|
// follow-up could implement an event-driven watcher there using
|
||||||
|
// NotifyIpInterfaceChange from iphlpapi.
|
||||||
|
func watchInterface(ctx context.Context, ifaceName string, expectedIndex int) (bool, error) {
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Infof("Interface monitor: stopped for %s", ifaceName)
|
||||||
|
return false, fmt.Errorf("wg interface monitor stopped: %w", ctx.Err())
|
||||||
|
case <-ticker.C:
|
||||||
|
currentIndex, err := getInterfaceIndex(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
// Interface was deleted
|
||||||
|
log.Infof("Interface monitor: %s deleted", ifaceName)
|
||||||
|
return true, fmt.Errorf("interface %s deleted: %w", ifaceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if interface index changed (interface was recreated)
|
||||||
|
if currentIndex != expectedIndex {
|
||||||
|
log.Infof("Interface monitor: %s recreated (index changed from %d to %d), restarting engine",
|
||||||
|
ifaceName, expectedIndex, currentIndex)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,15 +13,25 @@
|
|||||||
|
|
||||||
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit."/>
|
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit."/>
|
||||||
|
|
||||||
|
<!-- Autostart: enabled by default, disable with AUTOSTART=0 on the msiexec command line -->
|
||||||
|
<Property Id="AUTOSTART" Value="1" />
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramFiles64Folder">
|
<StandardDirectory Id="ProgramFiles64Folder">
|
||||||
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
||||||
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
||||||
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
|
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
</File>
|
</File>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
||||||
|
<File Id="NetbirdToastIcon" Name="netbird.png" Source=".\client\ui\assets\netbird.png" />
|
||||||
<?if $(var.ArchSuffix) = "amd64" ?>
|
<?if $(var.ArchSuffix) = "amd64" ?>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
||||||
<?endif ?>
|
<?endif ?>
|
||||||
@@ -46,8 +56,30 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<!-- Per-user component: HKCU keypath (auto GUID via "*"), separate from
|
||||||
|
the per-machine NetbirdFiles component to satisfy ICE57. -->
|
||||||
|
<StandardDirectory Id="ProgramMenuFolder">
|
||||||
|
<Component Id="NetbirdAumidRegistry" Guid="*">
|
||||||
|
<RegistryKey Root="HKCU" Key="Software\Classes\AppUserModelId\NetBird" ForceDeleteOnUninstall="yes">
|
||||||
|
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
</RegistryKey>
|
||||||
|
</Component>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="CommonAppDataFolder">
|
||||||
|
<Directory Id="NetbirdAutoStartDir" Name="Netbird">
|
||||||
|
<Component Id="NetbirdAutoStart" Guid="b199eaca-b0dd-4032-af19-679cfad48eb3" Bitness="always64" Condition='AUTOSTART = "1"'>
|
||||||
|
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
Name="Netbird" Value=""[NetbirdInstallDir]netbird-ui.exe""
|
||||||
|
Type="string" KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
<ComponentGroup Id="NetbirdFilesComponent">
|
<ComponentGroup Id="NetbirdFilesComponent">
|
||||||
<ComponentRef Id="NetbirdFiles" />
|
<ComponentRef Id="NetbirdFiles" />
|
||||||
|
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||||
|
<ComponentRef Id="NetbirdAutoStart" />
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
|
|
||||||
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,17 @@ service DaemonService {
|
|||||||
|
|
||||||
rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {}
|
rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {}
|
||||||
|
|
||||||
|
// StartCapture begins streaming packet capture on the WireGuard interface.
|
||||||
|
// Requires --enable-capture set at service install/reconfigure time.
|
||||||
|
rpc StartCapture(StartCaptureRequest) returns (stream CapturePacket) {}
|
||||||
|
|
||||||
|
// StartBundleCapture begins capturing packets to a server-side temp file
|
||||||
|
// for inclusion in the next debug bundle. Auto-stops after the given timeout.
|
||||||
|
rpc StartBundleCapture(StartBundleCaptureRequest) returns (StartBundleCaptureResponse) {}
|
||||||
|
|
||||||
|
// StopBundleCapture stops the running bundle capture. Idempotent.
|
||||||
|
rpc StopBundleCapture(StopBundleCaptureRequest) returns (StopBundleCaptureResponse) {}
|
||||||
|
|
||||||
rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {}
|
rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {}
|
||||||
|
|
||||||
rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {}
|
rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {}
|
||||||
@@ -104,8 +115,6 @@ service DaemonService {
|
|||||||
// StopCPUProfile stops CPU profiling in the daemon
|
// StopCPUProfile stops CPU profiling in the daemon
|
||||||
rpc StopCPUProfile(StopCPUProfileRequest) returns (StopCPUProfileResponse) {}
|
rpc StopCPUProfile(StopCPUProfileRequest) returns (StopCPUProfileResponse) {}
|
||||||
|
|
||||||
rpc NotifyOSLifecycle(OSLifecycleRequest) returns(OSLifecycleResponse) {}
|
|
||||||
|
|
||||||
rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {}
|
rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {}
|
||||||
|
|
||||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
@@ -114,20 +123,6 @@ service DaemonService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
message OSLifecycleRequest {
|
|
||||||
// avoid collision with loglevel enum
|
|
||||||
enum CycleType {
|
|
||||||
UNKNOWN = 0;
|
|
||||||
SLEEP = 1;
|
|
||||||
WAKEUP = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
CycleType type = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message OSLifecycleResponse {}
|
|
||||||
|
|
||||||
|
|
||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
// setupKey netbird setup key.
|
// setupKey netbird setup key.
|
||||||
string setupKey = 1;
|
string setupKey = 1;
|
||||||
@@ -848,3 +843,26 @@ message ExposeServiceReady {
|
|||||||
string domain = 3;
|
string domain = 3;
|
||||||
bool port_auto_assigned = 4;
|
bool port_auto_assigned = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StartCaptureRequest {
|
||||||
|
bool text_output = 1;
|
||||||
|
uint32 snap_len = 2;
|
||||||
|
google.protobuf.Duration duration = 3;
|
||||||
|
string filter_expr = 4;
|
||||||
|
bool verbose = 5;
|
||||||
|
bool ascii = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CapturePacket {
|
||||||
|
bytes data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StartBundleCaptureRequest {
|
||||||
|
// timeout auto-stops the capture after this duration.
|
||||||
|
// Clamped to a server-side maximum (10 minutes). Zero or unset defaults to the maximum.
|
||||||
|
google.protobuf.Duration timeout = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StartBundleCaptureResponse {}
|
||||||
|
message StopBundleCaptureRequest {}
|
||||||
|
message StopBundleCaptureResponse {}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
365
client/server/capture.go
Normal file
365
client/server/capture.go
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/util/capture"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxBundleCaptureDuration = 10 * time.Minute
|
||||||
|
|
||||||
|
// bundleCapture holds the state of an in-progress capture destined for the
|
||||||
|
// debug bundle. The lifecycle is:
|
||||||
|
//
|
||||||
|
// StartBundleCapture → capture running, writing to temp file
|
||||||
|
// StopBundleCapture → capture stopped, temp file available
|
||||||
|
// DebugBundle → temp file included in zip, then cleaned up
|
||||||
|
type bundleCapture struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
sess *capture.Session
|
||||||
|
file *os.File
|
||||||
|
engine *internal.Engine
|
||||||
|
cancel context.CancelFunc
|
||||||
|
stopped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop halts the capture session and closes the pcap writer. Idempotent.
|
||||||
|
func (bc *bundleCapture) stop() {
|
||||||
|
bc.mu.Lock()
|
||||||
|
defer bc.mu.Unlock()
|
||||||
|
|
||||||
|
if bc.stopped {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bc.stopped = true
|
||||||
|
|
||||||
|
if bc.cancel != nil {
|
||||||
|
bc.cancel()
|
||||||
|
}
|
||||||
|
if bc.sess != nil {
|
||||||
|
bc.sess.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// path returns the temp file path, or "" if no file exists.
|
||||||
|
func (bc *bundleCapture) path() string {
|
||||||
|
if bc.file == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return bc.file.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup removes the temp file.
|
||||||
|
func (bc *bundleCapture) cleanup() {
|
||||||
|
if bc.file == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := bc.file.Name()
|
||||||
|
if err := bc.file.Close(); err != nil {
|
||||||
|
log.Debugf("close bundle capture file: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Debugf("remove bundle capture file: %v", err)
|
||||||
|
}
|
||||||
|
bc.file = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartCapture streams a pcap or text packet capture over gRPC.
|
||||||
|
// Gated by the --enable-capture service flag.
|
||||||
|
func (s *Server) StartCapture(req *proto.StartCaptureRequest, stream proto.DaemonService_StartCaptureServer) error {
|
||||||
|
if !s.captureEnabled {
|
||||||
|
return status.Error(codes.PermissionDenied,
|
||||||
|
"packet capture is disabled; reinstall or reconfigure the service with --enable-capture")
|
||||||
|
}
|
||||||
|
|
||||||
|
if d := req.GetDuration(); d != nil && d.AsDuration() < 0 {
|
||||||
|
return status.Error(codes.InvalidArgument, "duration must not be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher, err := parseCaptureFilter(req)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
|
||||||
|
opts := capture.Options{
|
||||||
|
Matcher: matcher,
|
||||||
|
SnapLen: req.GetSnapLen(),
|
||||||
|
Verbose: req.GetVerbose(),
|
||||||
|
ASCII: req.GetAscii(),
|
||||||
|
}
|
||||||
|
if req.GetTextOutput() {
|
||||||
|
opts.TextOutput = pw
|
||||||
|
} else {
|
||||||
|
opts.Output = pw
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := capture.NewSession(opts)
|
||||||
|
if err != nil {
|
||||||
|
pw.Close()
|
||||||
|
return status.Errorf(codes.Internal, "create capture session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := s.claimCapture(sess)
|
||||||
|
if err != nil {
|
||||||
|
sess.Stop()
|
||||||
|
pw.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := engine.SetCapture(sess); err != nil {
|
||||||
|
s.releaseCapture(sess)
|
||||||
|
sess.Stop()
|
||||||
|
pw.Close()
|
||||||
|
return status.Errorf(codes.Internal, "set capture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an empty initial message to signal that the capture was accepted.
|
||||||
|
// The client waits for this before printing the banner, so it must arrive
|
||||||
|
// before any packet data.
|
||||||
|
if err := stream.Send(&proto.CapturePacket{}); err != nil {
|
||||||
|
s.clearCaptureIfOwner(sess, engine)
|
||||||
|
sess.Stop()
|
||||||
|
pw.Close()
|
||||||
|
return status.Errorf(codes.Internal, "send initial message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := stream.Context()
|
||||||
|
if d := req.GetDuration(); d != nil {
|
||||||
|
if dur := d.AsDuration(); dur > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, dur)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
s.clearCaptureIfOwner(sess, engine)
|
||||||
|
sess.Stop()
|
||||||
|
pw.Close()
|
||||||
|
}()
|
||||||
|
defer pr.Close()
|
||||||
|
|
||||||
|
log.Infof("packet capture started (text=%v, expr=%q)", req.GetTextOutput(), req.GetFilterExpr())
|
||||||
|
defer func() {
|
||||||
|
stats := sess.Stats()
|
||||||
|
log.Infof("packet capture stopped: %d packets, %d bytes, %d dropped",
|
||||||
|
stats.Packets, stats.Bytes, stats.Dropped)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return streamToGRPC(pr, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamToGRPC(r io.Reader, stream proto.DaemonService_StartCaptureServer) error {
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
for {
|
||||||
|
n, readErr := r.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if err := stream.Send(&proto.CapturePacket{Data: buf[:n]}); err != nil {
|
||||||
|
log.Debugf("capture stream send: %v", err)
|
||||||
|
return nil //nolint:nilerr // client disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if readErr != nil {
|
||||||
|
return nil //nolint:nilerr // pipe closed, capture stopped normally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBundleCapture begins capturing packets to a server-side temp file for
|
||||||
|
// inclusion in the next debug bundle. Not gated by --enable-capture since the
|
||||||
|
// output stays on the server (same trust level as CPU profiling).
|
||||||
|
//
|
||||||
|
// A timeout auto-stops the capture as a safety net if StopBundleCapture is
|
||||||
|
// never called (e.g. CLI crash).
|
||||||
|
func (s *Server) StartBundleCapture(_ context.Context, req *proto.StartBundleCaptureRequest) (*proto.StartBundleCaptureResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.stopBundleCaptureLocked()
|
||||||
|
s.cleanupBundleCapture()
|
||||||
|
|
||||||
|
if s.activeCapture != nil {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "another capture is already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := s.getCaptureEngineLocked()
|
||||||
|
if err != nil {
|
||||||
|
// Not fatal: kernel mode or not connected. Log and return success
|
||||||
|
// so the debug bundle still generates without capture data.
|
||||||
|
log.Warnf("packet capture unavailable, skipping: %v", err)
|
||||||
|
return &proto.StartBundleCaptureResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := req.GetTimeout().AsDuration()
|
||||||
|
if timeout <= 0 || timeout > maxBundleCaptureDuration {
|
||||||
|
timeout = maxBundleCaptureDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.CreateTemp("", "netbird.capture.*.pcap")
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "create temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := capture.NewSession(capture.Options{Output: f})
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(f.Name())
|
||||||
|
return nil, status.Errorf(codes.Internal, "create capture session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := engine.SetCapture(sess); err != nil {
|
||||||
|
sess.Stop()
|
||||||
|
f.Close()
|
||||||
|
os.Remove(f.Name())
|
||||||
|
log.Warnf("packet capture unavailable (no filtered device), skipping: %v", err)
|
||||||
|
return &proto.StartBundleCaptureResponse{}, nil
|
||||||
|
}
|
||||||
|
s.activeCapture = sess
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
bc := &bundleCapture{
|
||||||
|
sess: sess,
|
||||||
|
file: f,
|
||||||
|
engine: engine,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.bundleCapture = bc
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
s.mutex.Lock()
|
||||||
|
if s.bundleCapture == bc {
|
||||||
|
s.stopBundleCaptureLocked()
|
||||||
|
} else {
|
||||||
|
bc.stop()
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
log.Infof("bundle capture auto-stopped after timeout")
|
||||||
|
}()
|
||||||
|
log.Infof("bundle capture started (timeout=%s, file=%s)", timeout, f.Name())
|
||||||
|
|
||||||
|
return &proto.StartBundleCaptureResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopBundleCapture stops the running bundle capture. Idempotent.
|
||||||
|
func (s *Server) StopBundleCapture(_ context.Context, _ *proto.StopBundleCaptureRequest) (*proto.StopBundleCaptureResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.stopBundleCaptureLocked()
|
||||||
|
return &proto.StopBundleCaptureResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopBundleCaptureLocked stops the bundle capture if running. Must hold s.mutex.
|
||||||
|
func (s *Server) stopBundleCaptureLocked() {
|
||||||
|
if s.bundleCapture == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bc := s.bundleCapture
|
||||||
|
if bc.engine != nil && s.activeCapture == bc.sess {
|
||||||
|
if err := bc.engine.SetCapture(nil); err != nil {
|
||||||
|
log.Debugf("clear bundle capture: %v", err)
|
||||||
|
}
|
||||||
|
s.activeCapture = nil
|
||||||
|
}
|
||||||
|
bc.stop()
|
||||||
|
|
||||||
|
stats := bc.sess.Stats()
|
||||||
|
log.Infof("bundle capture stopped: %d packets, %d bytes, %d dropped",
|
||||||
|
stats.Packets, stats.Bytes, stats.Dropped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bundleCapturePath returns the temp file path if a capture has been taken,
|
||||||
|
// stops any running capture, and returns "". Called from DebugBundle.
|
||||||
|
// Must hold s.mutex.
|
||||||
|
func (s *Server) bundleCapturePath() string {
|
||||||
|
if s.bundleCapture == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
s.bundleCapture.stop()
|
||||||
|
return s.bundleCapture.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupBundleCapture removes the temp file and clears state. Must hold s.mutex.
|
||||||
|
func (s *Server) cleanupBundleCapture() {
|
||||||
|
if s.bundleCapture == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.bundleCapture.cleanup()
|
||||||
|
s.bundleCapture = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// claimCapture reserves the engine's capture slot for sess. Returns
|
||||||
|
// FailedPrecondition if another capture is already active.
|
||||||
|
func (s *Server) claimCapture(sess *capture.Session) (*internal.Engine, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.activeCapture != nil {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "another capture is already running")
|
||||||
|
}
|
||||||
|
engine, err := s.getCaptureEngineLocked()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.activeCapture = sess
|
||||||
|
return engine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseCapture clears the active-capture owner if it still matches sess.
|
||||||
|
func (s *Server) releaseCapture(sess *capture.Session) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
if s.activeCapture == sess {
|
||||||
|
s.activeCapture = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearCaptureIfOwner clears engine's capture slot only if sess still owns it.
|
||||||
|
func (s *Server) clearCaptureIfOwner(sess *capture.Session, engine *internal.Engine) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
if s.activeCapture != sess {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := engine.SetCapture(nil); err != nil {
|
||||||
|
log.Debugf("clear capture: %v", err)
|
||||||
|
}
|
||||||
|
s.activeCapture = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getCaptureEngineLocked() (*internal.Engine, error) {
|
||||||
|
if s.connectClient == nil {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "client not connected")
|
||||||
|
}
|
||||||
|
engine := s.connectClient.Engine()
|
||||||
|
if engine == nil {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "engine not initialized")
|
||||||
|
}
|
||||||
|
return engine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCaptureFilter returns a Matcher from the request.
|
||||||
|
// Returns nil (match all) when no filter expression is set.
|
||||||
|
func parseCaptureFilter(req *proto.StartCaptureRequest) (capture.Matcher, error) {
|
||||||
|
expr := req.GetFilterExpr()
|
||||||
|
if expr == "" {
|
||||||
|
return nil, nil //nolint:nilnil // nil Matcher means "match all"
|
||||||
|
}
|
||||||
|
return capture.ParseFilter(expr)
|
||||||
|
}
|
||||||
@@ -43,7 +43,9 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare refresh callback for health probes
|
capturePath := s.bundleCapturePath()
|
||||||
|
defer s.cleanupBundleCapture()
|
||||||
|
|
||||||
var refreshStatus func()
|
var refreshStatus func()
|
||||||
if s.connectClient != nil {
|
if s.connectClient != nil {
|
||||||
engine := s.connectClient.Engine()
|
engine := s.connectClient.Engine()
|
||||||
@@ -62,6 +64,7 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (
|
|||||||
SyncResponse: syncResponse,
|
SyncResponse: syncResponse,
|
||||||
LogPath: s.logFile,
|
LogPath: s.logFile,
|
||||||
CPUProfile: cpuProfileData,
|
CPUProfile: cpuProfileData,
|
||||||
|
CapturePath: capturePath,
|
||||||
RefreshStatus: refreshStatus,
|
RefreshStatus: refreshStatus,
|
||||||
ClientMetrics: clientMetrics,
|
ClientMetrics: clientMetrics,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/updater"
|
"github.com/netbirdio/netbird/client/internal/updater"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/util/capture"
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,7 +90,11 @@ type Server struct {
|
|||||||
profileManager *profilemanager.ServiceManager
|
profileManager *profilemanager.ServiceManager
|
||||||
profilesDisabled bool
|
profilesDisabled bool
|
||||||
updateSettingsDisabled bool
|
updateSettingsDisabled bool
|
||||||
networksDisabled bool
|
captureEnabled bool
|
||||||
|
bundleCapture *bundleCapture
|
||||||
|
// activeCapture is the session currently installed on the engine; guarded by s.mutex.
|
||||||
|
activeCapture *capture.Session
|
||||||
|
networksDisabled bool
|
||||||
|
|
||||||
sleepHandler *sleephandler.SleepHandler
|
sleepHandler *sleephandler.SleepHandler
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@ type oauthAuthFlow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New server instance constructor.
|
// New server instance constructor.
|
||||||
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool, networksDisabled bool) *Server {
|
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool, captureEnabled bool, networksDisabled bool) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
rootCtx: ctx,
|
rootCtx: ctx,
|
||||||
logFile: logFile,
|
logFile: logFile,
|
||||||
@@ -115,11 +120,13 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable
|
|||||||
profileManager: profilemanager.NewServiceManager(configFile),
|
profileManager: profilemanager.NewServiceManager(configFile),
|
||||||
profilesDisabled: profilesDisabled,
|
profilesDisabled: profilesDisabled,
|
||||||
updateSettingsDisabled: updateSettingsDisabled,
|
updateSettingsDisabled: updateSettingsDisabled,
|
||||||
|
captureEnabled: captureEnabled,
|
||||||
networksDisabled: networksDisabled,
|
networksDisabled: networksDisabled,
|
||||||
jwtCache: newJWTCache(),
|
jwtCache: newJWTCache(),
|
||||||
}
|
}
|
||||||
agent := &serverAgent{s}
|
agent := &serverAgent{s}
|
||||||
s.sleepHandler = sleephandler.New(agent)
|
s.sleepHandler = sleephandler.New(agent)
|
||||||
|
s.startSleepDetector()
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
|
|||||||
t.Fatalf("failed to set active profile state: %v", err)
|
t.Fatalf("failed to set active profile state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := New(ctx, "debug", "", false, false, false)
|
s := New(ctx, "debug", "", false, false, false, false)
|
||||||
|
|
||||||
s.config = config
|
s.config = config
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ func TestServer_Up(t *testing.T) {
|
|||||||
t.Fatalf("failed to set active profile state: %v", err)
|
t.Fatalf("failed to set active profile state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := New(ctx, "console", "", false, false, false)
|
s := New(ctx, "console", "", false, false, false, false)
|
||||||
err = s.Start()
|
err = s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ func TestServer_SubcribeEvents(t *testing.T) {
|
|||||||
t.Fatalf("failed to set active profile state: %v", err)
|
t.Fatalf("failed to set active profile state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := New(ctx, "console", "", false, false, false)
|
s := New(ctx, "console", "", false, false, false, false)
|
||||||
|
|
||||||
err = s.Start()
|
err = s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -335,7 +335,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
s := New(ctx, "console", "", false, false, false)
|
s := New(ctx, "console", "", false, false, false, false)
|
||||||
|
|
||||||
rosenpassEnabled := true
|
rosenpassEnabled := true
|
||||||
rosenpassPermissive := true
|
rosenpassPermissive := true
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/sleep"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const envDisableSleepDetector = "NB_DISABLE_SLEEP_DETECTOR"
|
||||||
|
|
||||||
// serverAgent adapts Server to the handler.Agent and handler.StatusChecker interfaces
|
// serverAgent adapts Server to the handler.Agent and handler.StatusChecker interfaces
|
||||||
type serverAgent struct {
|
type serverAgent struct {
|
||||||
s *Server
|
s *Server
|
||||||
@@ -28,19 +33,61 @@ func (a *serverAgent) Status() (internal.StatusType, error) {
|
|||||||
return internal.CtxGetState(a.s.rootCtx).Status()
|
return internal.CtxGetState(a.s.rootCtx).Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyOSLifecycle handles operating system lifecycle events by executing appropriate logic based on the request type.
|
// startSleepDetector starts the OS sleep/wake detector and forwards events to
|
||||||
func (s *Server) NotifyOSLifecycle(callerCtx context.Context, req *proto.OSLifecycleRequest) (*proto.OSLifecycleResponse, error) {
|
// the sleep handler. On platforms without a supported detector the attempt
|
||||||
switch req.GetType() {
|
// logs a warning and returns. Setting NB_DISABLE_SLEEP_DETECTOR=true skips
|
||||||
case proto.OSLifecycleRequest_WAKEUP:
|
// registration entirely.
|
||||||
if err := s.sleepHandler.HandleWakeUp(callerCtx); err != nil {
|
func (s *Server) startSleepDetector() {
|
||||||
return &proto.OSLifecycleResponse{}, err
|
if sleepDetectorDisabled() {
|
||||||
}
|
log.Info("sleep detection disabled via " + envDisableSleepDetector)
|
||||||
case proto.OSLifecycleRequest_SLEEP:
|
return
|
||||||
if err := s.sleepHandler.HandleSleep(callerCtx); err != nil {
|
|
||||||
return &proto.OSLifecycleResponse{}, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Errorf("unknown OSLifecycleRequest type: %v", req.GetType())
|
|
||||||
}
|
}
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
|
||||||
|
svc, err := sleep.New()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to initialize sleep detection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.Register(func(event sleep.EventType) {
|
||||||
|
switch event {
|
||||||
|
case sleep.EventTypeSleep:
|
||||||
|
log.Info("handling sleep event")
|
||||||
|
if err := s.sleepHandler.HandleSleep(s.rootCtx); err != nil {
|
||||||
|
log.Errorf("failed to handle sleep event: %v", err)
|
||||||
|
}
|
||||||
|
case sleep.EventTypeWakeUp:
|
||||||
|
log.Info("handling wakeup event")
|
||||||
|
if err := s.sleepHandler.HandleWakeUp(s.rootCtx); err != nil {
|
||||||
|
log.Errorf("failed to handle wakeup event: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to register sleep detector: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("sleep detection service initialized")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-s.rootCtx.Done()
|
||||||
|
log.Info("stopping sleep event listener")
|
||||||
|
if err := svc.Deregister(); err != nil {
|
||||||
|
log.Errorf("failed to deregister sleep detector: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sleepDetectorDisabled() bool {
|
||||||
|
val := os.Getenv(envDisableSleepDetector)
|
||||||
|
if val == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
disabled, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s=%q: %v", envDisableSleepDetector, val, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return disabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,15 +224,20 @@ func (m *Manager) buildHostPatterns(peer PeerSSHInfo) []string {
|
|||||||
|
|
||||||
func (m *Manager) writeSSHConfig(sshConfig string) error {
|
func (m *Manager) writeSSHConfig(sshConfig string) error {
|
||||||
sshConfigPath := filepath.Join(m.sshConfigDir, m.sshConfigFile)
|
sshConfigPath := filepath.Join(m.sshConfigDir, m.sshConfigFile)
|
||||||
|
sshConfigPathTmp := sshConfigPath + ".tmp"
|
||||||
|
|
||||||
if err := os.MkdirAll(m.sshConfigDir, 0755); err != nil {
|
if err := os.MkdirAll(m.sshConfigDir, 0755); err != nil {
|
||||||
return fmt.Errorf("create SSH config directory %s: %w", m.sshConfigDir, err)
|
return fmt.Errorf("create SSH config directory %s: %w", m.sshConfigDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeFileWithTimeout(sshConfigPath, []byte(sshConfig), 0644); err != nil {
|
if err := writeFileWithTimeout(sshConfigPathTmp, []byte(sshConfig), 0644); err != nil {
|
||||||
return fmt.Errorf("write SSH config file %s: %w", sshConfigPath, err)
|
return fmt.Errorf("write SSH config file %s: %w", sshConfigPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(sshConfigPathTmp, sshConfigPath); err != nil {
|
||||||
|
return fmt.Errorf("rename ssh config %s -> %s: %w", sshConfigPathTmp, sshConfigPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("Created NetBird SSH client config: %s", sshConfigPath)
|
log.Infof("Created NetBird SSH client config: %s", sshConfigPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/sleep"
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||||
"github.com/netbirdio/netbird/client/ui/event"
|
"github.com/netbirdio/netbird/client/ui/event"
|
||||||
|
"github.com/netbirdio/netbird/client/ui/notifier"
|
||||||
"github.com/netbirdio/netbird/client/ui/process"
|
"github.com/netbirdio/netbird/client/ui/process"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
@@ -260,6 +260,7 @@ type serviceClient struct {
|
|||||||
|
|
||||||
// application with main windows.
|
// application with main windows.
|
||||||
app fyne.App
|
app fyne.App
|
||||||
|
notifier notifier.Notifier
|
||||||
wSettings fyne.Window
|
wSettings fyne.Window
|
||||||
showAdvancedSettings bool
|
showAdvancedSettings bool
|
||||||
sendNotification bool
|
sendNotification bool
|
||||||
@@ -364,6 +365,7 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
addr: args.addr,
|
addr: args.addr,
|
||||||
app: args.app,
|
app: args.app,
|
||||||
|
notifier: notifier.New(args.app),
|
||||||
logFile: args.logFile,
|
logFile: args.logFile,
|
||||||
sendNotification: false,
|
sendNotification: false,
|
||||||
|
|
||||||
@@ -892,7 +894,7 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("get service status: %v", err)
|
log.Errorf("get service status: %v", err)
|
||||||
if s.connected {
|
if s.connected {
|
||||||
s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost"))
|
s.notifier.Send("Error", "Connection to service lost")
|
||||||
}
|
}
|
||||||
s.setDisconnectedStatus()
|
s.setDisconnectedStatus()
|
||||||
return err
|
return err
|
||||||
@@ -1109,7 +1111,7 @@ func (s *serviceClient) onTrayReady() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.eventManager = event.NewManager(s.app, s.addr)
|
s.eventManager = event.NewManager(s.notifier, s.addr)
|
||||||
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
||||||
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
||||||
if event.Category == proto.SystemEvent_SYSTEM {
|
if event.Category == proto.SystemEvent_SYSTEM {
|
||||||
@@ -1146,9 +1148,6 @@ func (s *serviceClient) onTrayReady() {
|
|||||||
|
|
||||||
go s.eventManager.Start(s.ctx)
|
go s.eventManager.Start(s.ctx)
|
||||||
go s.eventHandler.listen(s.ctx)
|
go s.eventHandler.listen(s.ctx)
|
||||||
|
|
||||||
// Start sleep detection listener
|
|
||||||
go s.startSleepListener()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
|
func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
|
||||||
@@ -1209,62 +1208,6 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
|
|||||||
return s.conn, nil
|
return s.conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSleepListener initializes the sleep detection service and listens for sleep events
|
|
||||||
func (s *serviceClient) startSleepListener() {
|
|
||||||
sleepService, err := sleep.New()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sleepService.Register(s.handleSleepEvents); err != nil {
|
|
||||||
log.Errorf("failed to start sleep detection: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("sleep detection service initialized")
|
|
||||||
|
|
||||||
// Cleanup on context cancellation
|
|
||||||
go func() {
|
|
||||||
<-s.ctx.Done()
|
|
||||||
log.Info("stopping sleep event listener")
|
|
||||||
if err := sleepService.Deregister(); err != nil {
|
|
||||||
log.Errorf("failed to deregister sleep detection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSleepEvents sends a sleep notification to the daemon via gRPC
|
|
||||||
func (s *serviceClient) handleSleepEvents(event sleep.EventType) {
|
|
||||||
conn, err := s.getSrvClient(0)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get daemon client for sleep notification: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &proto.OSLifecycleRequest{}
|
|
||||||
|
|
||||||
switch event {
|
|
||||||
case sleep.EventTypeWakeUp:
|
|
||||||
log.Infof("handle wakeup event: %v", event)
|
|
||||||
req.Type = proto.OSLifecycleRequest_WAKEUP
|
|
||||||
case sleep.EventTypeSleep:
|
|
||||||
log.Infof("handle sleep event: %v", event)
|
|
||||||
req.Type = proto.OSLifecycleRequest_SLEEP
|
|
||||||
default:
|
|
||||||
log.Infof("unknown event: %v", event)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.NotifyOSLifecycle(s.ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to notify daemon about os lifecycle notification: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("successfully notified daemon about os lifecycle")
|
|
||||||
}
|
|
||||||
|
|
||||||
// setSettingsEnabled enables or disables the settings menu based on the provided state
|
// setSettingsEnabled enables or disables the settings menu based on the provided state
|
||||||
func (s *serviceClient) setSettingsEnabled(enabled bool) {
|
func (s *serviceClient) setSettingsEnabled(enabled bool) {
|
||||||
if s.mSettings != nil {
|
if s.mSettings != nil {
|
||||||
@@ -1548,7 +1491,7 @@ func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) {
|
|||||||
|
|
||||||
if enforced && s.lastNotifiedVersion != newVersion {
|
if enforced && s.lastNotifiedVersion != newVersion {
|
||||||
s.lastNotifiedVersion = newVersion
|
s.lastNotifiedVersion = newVersion
|
||||||
s.app.SendNotification(fyne.NewNotification("Update available", "A new version "+newVersion+" is ready to install"))
|
s.notifier.Send("Update available", "A new version "+newVersion+" is ready to install")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/skratchdot/open-golang/open"
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
@@ -38,6 +39,7 @@ type debugCollectionParams struct {
|
|||||||
upload bool
|
upload bool
|
||||||
uploadURL string
|
uploadURL string
|
||||||
enablePersistence bool
|
enablePersistence bool
|
||||||
|
capture bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI components for progress tracking
|
// UI components for progress tracking
|
||||||
@@ -51,25 +53,58 @@ type progressUI struct {
|
|||||||
func (s *serviceClient) showDebugUI() {
|
func (s *serviceClient) showDebugUI() {
|
||||||
w := s.app.NewWindow("NetBird Debug")
|
w := s.app.NewWindow("NetBird Debug")
|
||||||
w.SetOnClosed(s.cancel)
|
w.SetOnClosed(s.cancel)
|
||||||
|
|
||||||
w.Resize(fyne.NewSize(600, 500))
|
w.Resize(fyne.NewSize(600, 500))
|
||||||
w.SetFixedSize(true)
|
w.SetFixedSize(true)
|
||||||
|
|
||||||
anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil)
|
anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil)
|
||||||
systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil)
|
systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil)
|
||||||
systemInfoCheck.SetChecked(true)
|
systemInfoCheck.SetChecked(true)
|
||||||
|
captureCheck := widget.NewCheck("Include packet capture", nil)
|
||||||
uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil)
|
uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil)
|
||||||
uploadCheck.SetChecked(true)
|
uploadCheck.SetChecked(true)
|
||||||
|
|
||||||
uploadURLLabel := widget.NewLabel("Debug upload URL:")
|
uploadURLContainer, uploadURL := s.buildUploadSection(uploadCheck)
|
||||||
|
|
||||||
|
debugModeContainer, runForDurationCheck, durationInput, noteLabel := s.buildDurationSection()
|
||||||
|
|
||||||
|
statusLabel := widget.NewLabel("")
|
||||||
|
statusLabel.Hide()
|
||||||
|
progressBar := widget.NewProgressBar()
|
||||||
|
progressBar.Hide()
|
||||||
|
createButton := widget.NewButton("Create Debug Bundle", nil)
|
||||||
|
|
||||||
|
uiControls := []fyne.Disableable{
|
||||||
|
anonymizeCheck, systemInfoCheck, captureCheck,
|
||||||
|
uploadCheck, uploadURL, runForDurationCheck, durationInput, createButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
createButton.OnTapped = s.getCreateHandler(
|
||||||
|
statusLabel, progressBar, uploadCheck, uploadURL,
|
||||||
|
anonymizeCheck, systemInfoCheck, captureCheck,
|
||||||
|
runForDurationCheck, durationInput, uiControls, w,
|
||||||
|
)
|
||||||
|
|
||||||
|
content := container.NewVBox(
|
||||||
|
widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"),
|
||||||
|
widget.NewLabel(""),
|
||||||
|
anonymizeCheck, systemInfoCheck, captureCheck,
|
||||||
|
uploadCheck, uploadURLContainer,
|
||||||
|
widget.NewLabel(""),
|
||||||
|
debugModeContainer, noteLabel,
|
||||||
|
widget.NewLabel(""),
|
||||||
|
statusLabel, progressBar, createButton,
|
||||||
|
)
|
||||||
|
|
||||||
|
w.SetContent(container.NewPadded(content))
|
||||||
|
w.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) buildUploadSection(uploadCheck *widget.Check) (*fyne.Container, *widget.Entry) {
|
||||||
uploadURL := widget.NewEntry()
|
uploadURL := widget.NewEntry()
|
||||||
uploadURL.SetText(uptypes.DefaultBundleURL)
|
uploadURL.SetText(uptypes.DefaultBundleURL)
|
||||||
uploadURL.SetPlaceHolder("Enter upload URL")
|
uploadURL.SetPlaceHolder("Enter upload URL")
|
||||||
|
|
||||||
uploadURLContainer := container.NewVBox(
|
uploadURLContainer := container.NewVBox(widget.NewLabel("Debug upload URL:"), uploadURL)
|
||||||
uploadURLLabel,
|
|
||||||
uploadURL,
|
|
||||||
)
|
|
||||||
|
|
||||||
uploadCheck.OnChanged = func(checked bool) {
|
uploadCheck.OnChanged = func(checked bool) {
|
||||||
if checked {
|
if checked {
|
||||||
@@ -78,13 +113,14 @@ func (s *serviceClient) showDebugUI() {
|
|||||||
uploadURLContainer.Hide()
|
uploadURLContainer.Hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return uploadURLContainer, uploadURL
|
||||||
|
}
|
||||||
|
|
||||||
debugModeContainer := container.NewHBox()
|
func (s *serviceClient) buildDurationSection() (*fyne.Container, *widget.Check, *widget.Entry, *widget.Label) {
|
||||||
runForDurationCheck := widget.NewCheck("Run with trace logs before creating bundle", nil)
|
runForDurationCheck := widget.NewCheck("Run with trace logs before creating bundle", nil)
|
||||||
runForDurationCheck.SetChecked(true)
|
runForDurationCheck.SetChecked(true)
|
||||||
|
|
||||||
forLabel := widget.NewLabel("for")
|
forLabel := widget.NewLabel("for")
|
||||||
|
|
||||||
durationInput := widget.NewEntry()
|
durationInput := widget.NewEntry()
|
||||||
durationInput.SetText("1")
|
durationInput.SetText("1")
|
||||||
minutesLabel := widget.NewLabel("minute")
|
minutesLabel := widget.NewLabel("minute")
|
||||||
@@ -108,63 +144,8 @@ func (s *serviceClient) showDebugUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugModeContainer.Add(runForDurationCheck)
|
modeContainer := container.NewHBox(runForDurationCheck, forLabel, durationInput, minutesLabel)
|
||||||
debugModeContainer.Add(forLabel)
|
return modeContainer, runForDurationCheck, durationInput, noteLabel
|
||||||
debugModeContainer.Add(durationInput)
|
|
||||||
debugModeContainer.Add(minutesLabel)
|
|
||||||
|
|
||||||
statusLabel := widget.NewLabel("")
|
|
||||||
statusLabel.Hide()
|
|
||||||
|
|
||||||
progressBar := widget.NewProgressBar()
|
|
||||||
progressBar.Hide()
|
|
||||||
|
|
||||||
createButton := widget.NewButton("Create Debug Bundle", nil)
|
|
||||||
|
|
||||||
// UI controls that should be disabled during debug collection
|
|
||||||
uiControls := []fyne.Disableable{
|
|
||||||
anonymizeCheck,
|
|
||||||
systemInfoCheck,
|
|
||||||
uploadCheck,
|
|
||||||
uploadURL,
|
|
||||||
runForDurationCheck,
|
|
||||||
durationInput,
|
|
||||||
createButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
createButton.OnTapped = s.getCreateHandler(
|
|
||||||
statusLabel,
|
|
||||||
progressBar,
|
|
||||||
uploadCheck,
|
|
||||||
uploadURL,
|
|
||||||
anonymizeCheck,
|
|
||||||
systemInfoCheck,
|
|
||||||
runForDurationCheck,
|
|
||||||
durationInput,
|
|
||||||
uiControls,
|
|
||||||
w,
|
|
||||||
)
|
|
||||||
|
|
||||||
content := container.NewVBox(
|
|
||||||
widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"),
|
|
||||||
widget.NewLabel(""),
|
|
||||||
anonymizeCheck,
|
|
||||||
systemInfoCheck,
|
|
||||||
uploadCheck,
|
|
||||||
uploadURLContainer,
|
|
||||||
widget.NewLabel(""),
|
|
||||||
debugModeContainer,
|
|
||||||
noteLabel,
|
|
||||||
widget.NewLabel(""),
|
|
||||||
statusLabel,
|
|
||||||
progressBar,
|
|
||||||
createButton,
|
|
||||||
)
|
|
||||||
|
|
||||||
paddedContent := container.NewPadded(content)
|
|
||||||
w.SetContent(paddedContent)
|
|
||||||
|
|
||||||
w.Show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateMinute(s string, minutesLabel *widget.Label) error {
|
func validateMinute(s string, minutesLabel *widget.Label) error {
|
||||||
@@ -200,6 +181,7 @@ func (s *serviceClient) getCreateHandler(
|
|||||||
uploadURL *widget.Entry,
|
uploadURL *widget.Entry,
|
||||||
anonymizeCheck *widget.Check,
|
anonymizeCheck *widget.Check,
|
||||||
systemInfoCheck *widget.Check,
|
systemInfoCheck *widget.Check,
|
||||||
|
captureCheck *widget.Check,
|
||||||
runForDurationCheck *widget.Check,
|
runForDurationCheck *widget.Check,
|
||||||
duration *widget.Entry,
|
duration *widget.Entry,
|
||||||
uiControls []fyne.Disableable,
|
uiControls []fyne.Disableable,
|
||||||
@@ -222,6 +204,7 @@ func (s *serviceClient) getCreateHandler(
|
|||||||
params := &debugCollectionParams{
|
params := &debugCollectionParams{
|
||||||
anonymize: anonymizeCheck.Checked,
|
anonymize: anonymizeCheck.Checked,
|
||||||
systemInfo: systemInfoCheck.Checked,
|
systemInfo: systemInfoCheck.Checked,
|
||||||
|
capture: captureCheck.Checked,
|
||||||
upload: uploadCheck.Checked,
|
upload: uploadCheck.Checked,
|
||||||
uploadURL: url,
|
uploadURL: url,
|
||||||
enablePersistence: true,
|
enablePersistence: true,
|
||||||
@@ -253,10 +236,7 @@ func (s *serviceClient) getCreateHandler(
|
|||||||
|
|
||||||
statusLabel.SetText("Creating debug bundle...")
|
statusLabel.SetText("Creating debug bundle...")
|
||||||
go s.handleDebugCreation(
|
go s.handleDebugCreation(
|
||||||
anonymizeCheck.Checked,
|
params,
|
||||||
systemInfoCheck.Checked,
|
|
||||||
uploadCheck.Checked,
|
|
||||||
url,
|
|
||||||
statusLabel,
|
statusLabel,
|
||||||
uiControls,
|
uiControls,
|
||||||
w,
|
w,
|
||||||
@@ -371,7 +351,7 @@ func startProgressTracker(ctx context.Context, wg *sync.WaitGroup, duration time
|
|||||||
func (s *serviceClient) configureServiceForDebug(
|
func (s *serviceClient) configureServiceForDebug(
|
||||||
conn proto.DaemonServiceClient,
|
conn proto.DaemonServiceClient,
|
||||||
state *debugInitialState,
|
state *debugInitialState,
|
||||||
enablePersistence bool,
|
params *debugCollectionParams,
|
||||||
) {
|
) {
|
||||||
if state.wasDown {
|
if state.wasDown {
|
||||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||||
@@ -397,7 +377,7 @@ func (s *serviceClient) configureServiceForDebug(
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if enablePersistence {
|
if params.enablePersistence {
|
||||||
if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{
|
if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -417,6 +397,26 @@ func (s *serviceClient) configureServiceForDebug(
|
|||||||
if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil {
|
if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil {
|
||||||
log.Warnf("failed to start CPU profiling: %v", err)
|
log.Warnf("failed to start CPU profiling: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.startBundleCaptureIfEnabled(conn, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) startBundleCaptureIfEnabled(conn proto.DaemonServiceClient, params *debugCollectionParams) {
|
||||||
|
if !params.capture {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxCapture = 10 * time.Minute
|
||||||
|
timeout := params.duration + 30*time.Second
|
||||||
|
if timeout > maxCapture {
|
||||||
|
timeout = maxCapture
|
||||||
|
log.Warnf("packet capture clamped to %s (server maximum)", maxCapture)
|
||||||
|
}
|
||||||
|
if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{
|
||||||
|
Timeout: durationpb.New(timeout),
|
||||||
|
}); err != nil {
|
||||||
|
log.Warnf("failed to start bundle capture: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceClient) collectDebugData(
|
func (s *serviceClient) collectDebugData(
|
||||||
@@ -430,7 +430,7 @@ func (s *serviceClient) collectDebugData(
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
startProgressTracker(ctx, &wg, params.duration, progress)
|
startProgressTracker(ctx, &wg, params.duration, progress)
|
||||||
|
|
||||||
s.configureServiceForDebug(conn, state, params.enablePersistence)
|
s.configureServiceForDebug(conn, state, params)
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
progress.progressBar.Hide()
|
progress.progressBar.Hide()
|
||||||
@@ -440,6 +440,14 @@ func (s *serviceClient) collectDebugData(
|
|||||||
log.Warnf("failed to stop CPU profiling: %v", err)
|
log.Warnf("failed to stop CPU profiling: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.capture {
|
||||||
|
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
||||||
|
log.Warnf("failed to stop bundle capture: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,18 +528,37 @@ func handleError(progress *progressUI, errMsg string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceClient) handleDebugCreation(
|
func (s *serviceClient) handleDebugCreation(
|
||||||
anonymize bool,
|
params *debugCollectionParams,
|
||||||
systemInfo bool,
|
|
||||||
upload bool,
|
|
||||||
uploadURL string,
|
|
||||||
statusLabel *widget.Label,
|
statusLabel *widget.Label,
|
||||||
uiControls []fyne.Disableable,
|
uiControls []fyne.Disableable,
|
||||||
w fyne.Window,
|
w fyne.Window,
|
||||||
) {
|
) {
|
||||||
log.Infof("Creating debug bundle (Anonymized: %v, System Info: %v, Upload Attempt: %v)...",
|
conn, err := s.getSrvClient(failFastTimeout)
|
||||||
anonymize, systemInfo, upload)
|
if err != nil {
|
||||||
|
log.Errorf("Failed to get client for debug: %v", err)
|
||||||
|
statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
||||||
|
enableUIControls(uiControls)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := s.createDebugBundle(anonymize, systemInfo, uploadURL)
|
if params.capture {
|
||||||
|
if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{
|
||||||
|
Timeout: durationpb.New(30 * time.Second),
|
||||||
|
}); err != nil {
|
||||||
|
log.Warnf("failed to start bundle capture: %v", err)
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
||||||
|
log.Warnf("failed to stop bundle capture: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.createDebugBundle(params.anonymize, params.systemInfo, params.uploadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to create debug bundle: %v", err)
|
log.Errorf("Failed to create debug bundle: %v", err)
|
||||||
statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err))
|
statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err))
|
||||||
@@ -543,7 +570,7 @@ func (s *serviceClient) handleDebugCreation(
|
|||||||
uploadFailureReason := resp.GetUploadFailureReason()
|
uploadFailureReason := resp.GetUploadFailureReason()
|
||||||
uploadedKey := resp.GetUploadedKey()
|
uploadedKey := resp.GetUploadedKey()
|
||||||
|
|
||||||
if upload {
|
if params.upload {
|
||||||
if uploadFailureReason != "" {
|
if uploadFailureReason != "" {
|
||||||
showUploadFailedDialog(w, localPath, uploadFailureReason)
|
showUploadFailedDialog(w, localPath, uploadFailureReason)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -18,11 +17,17 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Notifier sends desktop notifications. Defined here so the event package
|
||||||
|
// does not depend on fyne or the platform-specific notifier implementation.
|
||||||
|
type Notifier interface {
|
||||||
|
Send(title, body string)
|
||||||
|
}
|
||||||
|
|
||||||
type Handler func(*proto.SystemEvent)
|
type Handler func(*proto.SystemEvent)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
app fyne.App
|
notifier Notifier
|
||||||
addr string
|
addr string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -31,10 +36,10 @@ type Manager struct {
|
|||||||
handlers []Handler
|
handlers []Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(app fyne.App, addr string) *Manager {
|
func NewManager(notifier Notifier, addr string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
app: app,
|
notifier: notifier,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) {
|
|||||||
if id != "" {
|
if id != "" {
|
||||||
body += fmt.Sprintf(" ID: %s", id)
|
body += fmt.Sprintf(" ID: %s", id)
|
||||||
}
|
}
|
||||||
e.app.SendNotification(fyne.NewNotification(title, body))
|
e.notifier.Send(title, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, handler := range handlers {
|
for _, handler := range handlers {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/systray"
|
"fyne.io/systray"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -87,7 +86,7 @@ func (h *eventHandler) handleConnectClick() {
|
|||||||
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
||||||
log.Debugf("connect operation cancelled by user")
|
log.Debugf("connect operation cancelled by user")
|
||||||
} else {
|
} else {
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to connect"))
|
h.client.notifier.Send("Error", "Failed to connect")
|
||||||
log.Errorf("connect failed: %v", err)
|
log.Errorf("connect failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +111,7 @@ func (h *eventHandler) handleDisconnectClick() {
|
|||||||
if err := h.client.menuDownClick(); err != nil {
|
if err := h.client.menuDownClick(); err != nil {
|
||||||
st, ok := status.FromError(err)
|
st, ok := status.FromError(err)
|
||||||
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to disconnect"))
|
h.client.notifier.Send("Error", "Failed to disconnect")
|
||||||
log.Errorf("disconnect failed: %v", err)
|
log.Errorf("disconnect failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("disconnect cancelled or already disconnecting")
|
log.Debugf("disconnect cancelled or already disconnecting")
|
||||||
@@ -130,7 +129,7 @@ func (h *eventHandler) handleAllowSSHClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update SSH settings"))
|
h.client.notifier.Send("Error", "Failed to update SSH settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -140,7 +139,7 @@ func (h *eventHandler) handleAutoConnectClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update auto-connect settings"))
|
h.client.notifier.Send("Error", "Failed to update auto-connect settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +148,7 @@ func (h *eventHandler) handleRosenpassClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update Rosenpass settings"))
|
h.client.notifier.Send("Error", "Failed to update Rosenpass settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ func (h *eventHandler) handleLazyConnectionClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update lazy connection settings"))
|
h.client.notifier.Send("Error", "Failed to update lazy connection settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +166,7 @@ func (h *eventHandler) handleBlockInboundClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update block inbound settings"))
|
h.client.notifier.Send("Error", "Failed to update block inbound settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ func (h *eventHandler) handleNotificationsClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update notifications settings"))
|
h.client.notifier.Send("Error", "Failed to update notifications settings")
|
||||||
} else if h.client.eventManager != nil {
|
} else if h.client.eventManager != nil {
|
||||||
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
||||||
}
|
}
|
||||||
|
|||||||
27
client/ui/notifier/notifier.go
Normal file
27
client/ui/notifier/notifier.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Package notifier sends desktop notifications. On Windows it uses the WinRT
|
||||||
|
// COM API directly via go-toast/v2 to avoid the PowerShell window flash that
|
||||||
|
// fyne's default implementation produces. On other platforms it delegates to
|
||||||
|
// fyne.
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
|
||||||
|
// Notifier sends desktop notifications.
|
||||||
|
type Notifier interface {
|
||||||
|
Send(title, body string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a platform-specific Notifier. The fyne app is used as the
|
||||||
|
// fallback notifier on platforms where no native implementation is wired up,
|
||||||
|
// and on Windows when the COM path fails to initialize.
|
||||||
|
func New(app fyne.App) Notifier {
|
||||||
|
return newNotifier(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fyneNotifier struct {
|
||||||
|
app fyne.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fyneNotifier) Send(title, body string) {
|
||||||
|
f.app.SendNotification(fyne.NewNotification(title, body))
|
||||||
|
}
|
||||||
9
client/ui/notifier/notifier_other.go
Normal file
9
client/ui/notifier/notifier_other.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
|
||||||
|
func newNotifier(app fyne.App) Notifier {
|
||||||
|
return &fyneNotifier{app: app}
|
||||||
|
}
|
||||||
88
client/ui/notifier/notifier_windows.go
Normal file
88
client/ui/notifier/notifier_windows.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||||
|
"git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// appID is the AppUserModelID shown in the Windows Action Center. It
|
||||||
|
// must match the System.AppUserModel.ID property set on the Start Menu
|
||||||
|
// shortcut by the MSI (see client/netbird.wxs); otherwise Windows
|
||||||
|
// groups toasts under a separate, unbranded entry.
|
||||||
|
appID = "NetBird"
|
||||||
|
|
||||||
|
// appGUID identifies the COM activation callback class. Generated once
|
||||||
|
// for NetBird; do not change without coordinating an installer bump,
|
||||||
|
// since old registry entries pointing at the previous GUID would orphan.
|
||||||
|
appGUID = "{0E1B4DE7-E148-432B-9814-544F941826EC}"
|
||||||
|
)
|
||||||
|
|
||||||
|
type comNotifier struct {
|
||||||
|
fallback *fyneNotifier
|
||||||
|
ready bool
|
||||||
|
iconPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
initOnce sync.Once
|
||||||
|
initErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func newNotifier(app fyne.App) Notifier {
|
||||||
|
n := &comNotifier{
|
||||||
|
fallback: &fyneNotifier{app: app},
|
||||||
|
iconPath: resolveIcon(),
|
||||||
|
}
|
||||||
|
initOnce.Do(func() {
|
||||||
|
initErr = wintoast.SetAppData(wintoast.AppData{
|
||||||
|
AppID: appID,
|
||||||
|
GUID: appGUID,
|
||||||
|
IconPath: n.iconPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if initErr != nil {
|
||||||
|
log.Warnf("toast: register app data failed, falling back to fyne notifications: %v", initErr)
|
||||||
|
return n.fallback
|
||||||
|
}
|
||||||
|
n.ready = true
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *comNotifier) Send(title, body string) {
|
||||||
|
if !n.ready {
|
||||||
|
n.fallback.Send(title, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification := toast.Notification{
|
||||||
|
AppID: appID,
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
Icon: n.iconPath,
|
||||||
|
}
|
||||||
|
if err := notification.Push(); err != nil {
|
||||||
|
log.Warnf("toast: push failed, using fyne fallback: %v", err)
|
||||||
|
n.fallback.Send(title, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveIcon returns an absolute path to the toast icon, or an empty string
|
||||||
|
// when no icon can be located. Windows requires a PNG/JPG for the
|
||||||
|
// AppUserModelId IconUri registry value; .ico is silently ignored.
|
||||||
|
func resolveIcon() string {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
candidate := filepath.Join(filepath.Dir(exe), "netbird.png")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -548,7 +548,7 @@ func (p *profileMenu) refresh() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to switch profile: %v", err)
|
log.Errorf("failed to switch profile: %v", err)
|
||||||
// show notification dialog
|
// show notification dialog
|
||||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile"))
|
p.serviceClient.notifier.Send("Error", "Failed to switch profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,9 +628,9 @@ func (p *profileMenu) refresh() {
|
|||||||
}
|
}
|
||||||
if err := p.eventHandler.logout(p.ctx); err != nil {
|
if err := p.eventHandler.logout(p.ctx); err != nil {
|
||||||
log.Errorf("logout failed: %v", err)
|
log.Errorf("logout failed: %v", err)
|
||||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to deregister"))
|
p.serviceClient.notifier.Send("Error", "Failed to deregister")
|
||||||
} else {
|
} else {
|
||||||
p.app.SendNotification(fyne.NewNotification("Success", "Deregistered successfully"))
|
p.serviceClient.notifier.Send("Success", "Deregistered successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
netbird "github.com/netbirdio/netbird/client/embed"
|
netbird "github.com/netbirdio/netbird/client/embed"
|
||||||
sshdetection "github.com/netbirdio/netbird/client/ssh/detection"
|
sshdetection "github.com/netbirdio/netbird/client/ssh/detection"
|
||||||
nbstatus "github.com/netbirdio/netbird/client/status"
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
wasmcapture "github.com/netbirdio/netbird/client/wasm/internal/capture"
|
||||||
"github.com/netbirdio/netbird/client/wasm/internal/http"
|
"github.com/netbirdio/netbird/client/wasm/internal/http"
|
||||||
"github.com/netbirdio/netbird/client/wasm/internal/rdp"
|
"github.com/netbirdio/netbird/client/wasm/internal/rdp"
|
||||||
"github.com/netbirdio/netbird/client/wasm/internal/ssh"
|
"github.com/netbirdio/netbird/client/wasm/internal/ssh"
|
||||||
@@ -459,6 +461,95 @@ func createSetLogLevelMethod(client *netbird.Client) js.Func {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createStartCaptureMethod creates the programmable packet capture method.
|
||||||
|
// Returns a JS interface with onpacket callback and stop() method.
|
||||||
|
//
|
||||||
|
// Usage from JavaScript:
|
||||||
|
//
|
||||||
|
// const cap = await client.startCapture({ filter: "tcp port 443", verbose: true })
|
||||||
|
// cap.onpacket = (line) => console.log(line)
|
||||||
|
// const stats = cap.stop()
|
||||||
|
func createStartCaptureMethod(client *netbird.Client) js.Func {
|
||||||
|
return js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||||
|
var opts js.Value
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPromise(func(resolve, reject js.Value) {
|
||||||
|
iface, err := wasmcapture.Start(client, opts)
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(js.ValueOf(fmt.Sprintf("start capture: %v", err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve.Invoke(iface)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureMethods returns capture() and stopCapture() that share state for
|
||||||
|
// the console-log shortcut. capture() logs packets to the browser console
|
||||||
|
// and stopCapture() ends it, like Ctrl+C on the CLI.
|
||||||
|
//
|
||||||
|
// Usage from browser devtools console:
|
||||||
|
//
|
||||||
|
// await client.capture() // capture all packets
|
||||||
|
// await client.capture("tcp") // capture with filter
|
||||||
|
// await client.capture({filter: "host 10.0.0.1", verbose: true})
|
||||||
|
// client.stopCapture() // stop and print stats
|
||||||
|
func captureMethods(client *netbird.Client) (startFn, stopFn js.Func) {
|
||||||
|
var mu sync.Mutex
|
||||||
|
var active *wasmcapture.Handle
|
||||||
|
|
||||||
|
startFn = js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||||
|
var opts js.Value
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPromise(func(resolve, reject js.Value) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if active != nil {
|
||||||
|
active.Stop()
|
||||||
|
active = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := wasmcapture.StartConsole(client, opts)
|
||||||
|
if err != nil {
|
||||||
|
reject.Invoke(js.ValueOf(fmt.Sprintf("start capture: %v", err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
active = h
|
||||||
|
|
||||||
|
console := js.Global().Get("console")
|
||||||
|
console.Call("log", "[capture] started, call client.stopCapture() to stop")
|
||||||
|
resolve.Invoke(js.Undefined())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
stopFn = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if active == nil {
|
||||||
|
js.Global().Get("console").Call("log", "[capture] no active capture")
|
||||||
|
return js.Undefined()
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := active.Stop()
|
||||||
|
active = nil
|
||||||
|
|
||||||
|
console := js.Global().Get("console")
|
||||||
|
console.Call("log", fmt.Sprintf("[capture] stopped: %d packets, %d bytes, %d dropped",
|
||||||
|
stats.Packets, stats.Bytes, stats.Dropped))
|
||||||
|
return js.Undefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
return startFn, stopFn
|
||||||
|
}
|
||||||
|
|
||||||
// createPromise is a helper to create JavaScript promises
|
// createPromise is a helper to create JavaScript promises
|
||||||
func createPromise(handler func(resolve, reject js.Value)) js.Value {
|
func createPromise(handler func(resolve, reject js.Value)) js.Value {
|
||||||
return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, promiseArgs []js.Value) any {
|
return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, promiseArgs []js.Value) any {
|
||||||
@@ -521,6 +612,11 @@ func createClientObject(client *netbird.Client) js.Value {
|
|||||||
obj["statusDetail"] = createStatusDetailMethod(client)
|
obj["statusDetail"] = createStatusDetailMethod(client)
|
||||||
obj["getSyncResponse"] = createGetSyncResponseMethod(client)
|
obj["getSyncResponse"] = createGetSyncResponseMethod(client)
|
||||||
obj["setLogLevel"] = createSetLogLevelMethod(client)
|
obj["setLogLevel"] = createSetLogLevelMethod(client)
|
||||||
|
obj["startCapture"] = createStartCaptureMethod(client)
|
||||||
|
|
||||||
|
capStart, capStop := captureMethods(client)
|
||||||
|
obj["capture"] = capStart
|
||||||
|
obj["stopCapture"] = capStop
|
||||||
|
|
||||||
return js.ValueOf(obj)
|
return js.ValueOf(obj)
|
||||||
}
|
}
|
||||||
|
|||||||
176
client/wasm/internal/capture/capture.go
Normal file
176
client/wasm/internal/capture/capture.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
//go:build js
|
||||||
|
|
||||||
|
// Package capture bridges the util/capture package to JavaScript via syscall/js.
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
netbird "github.com/netbirdio/netbird/client/embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle holds a running capture session so it can be stopped later.
|
||||||
|
type Handle struct {
|
||||||
|
cs *netbird.CaptureSession
|
||||||
|
stopFn js.Func
|
||||||
|
stopped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop ends the capture and returns stats.
|
||||||
|
func (h *Handle) Stop() netbird.CaptureStats {
|
||||||
|
if h.stopped {
|
||||||
|
return h.cs.Stats()
|
||||||
|
}
|
||||||
|
h.stopped = true
|
||||||
|
h.stopFn.Release()
|
||||||
|
|
||||||
|
h.cs.Stop()
|
||||||
|
return h.cs.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func statsToJS(s netbird.CaptureStats) js.Value {
|
||||||
|
obj := js.Global().Get("Object").Call("create", js.Null())
|
||||||
|
obj.Set("packets", js.ValueOf(s.Packets))
|
||||||
|
obj.Set("bytes", js.ValueOf(s.Bytes))
|
||||||
|
obj.Set("dropped", js.ValueOf(s.Dropped))
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOpts extracts filter/verbose/ascii from a JS options value.
|
||||||
|
func parseOpts(jsOpts js.Value) (filter string, verbose, ascii bool) {
|
||||||
|
if jsOpts.IsNull() || jsOpts.IsUndefined() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if jsOpts.Type() == js.TypeString {
|
||||||
|
filter = jsOpts.String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if jsOpts.Type() != js.TypeObject {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f := jsOpts.Get("filter"); !f.IsUndefined() && !f.IsNull() {
|
||||||
|
filter = f.String()
|
||||||
|
}
|
||||||
|
if v := jsOpts.Get("verbose"); !v.IsUndefined() {
|
||||||
|
verbose = v.Truthy()
|
||||||
|
}
|
||||||
|
if a := jsOpts.Get("ascii"); !a.IsUndefined() {
|
||||||
|
ascii = a.Truthy()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start creates a capture session and returns a JS interface for streaming text
|
||||||
|
// output. The returned object exposes:
|
||||||
|
//
|
||||||
|
// onpacket(callback) - set callback(string) for each text line
|
||||||
|
// stop() - stop capture and return stats { packets, bytes, dropped }
|
||||||
|
//
|
||||||
|
// Options: { filter: string, verbose: bool, ascii: bool } or just a filter string.
|
||||||
|
func Start(client *netbird.Client, jsOpts js.Value) (js.Value, error) {
|
||||||
|
filter, verbose, ascii := parseOpts(jsOpts)
|
||||||
|
|
||||||
|
cb := &jsCallbackWriter{}
|
||||||
|
|
||||||
|
cs, err := client.StartCapture(netbird.CaptureOptions{
|
||||||
|
TextOutput: cb,
|
||||||
|
Filter: filter,
|
||||||
|
Verbose: verbose,
|
||||||
|
ASCII: ascii,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return js.Undefined(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := &Handle{cs: cs}
|
||||||
|
|
||||||
|
iface := js.Global().Get("Object").Call("create", js.Null())
|
||||||
|
handle.stopFn = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
||||||
|
return statsToJS(handle.Stop())
|
||||||
|
})
|
||||||
|
iface.Set("stop", handle.stopFn)
|
||||||
|
iface.Set("onpacket", js.Undefined())
|
||||||
|
cb.setInterface(iface)
|
||||||
|
|
||||||
|
return iface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartConsole starts a capture that logs every packet line to console.log.
|
||||||
|
// Returns a Handle so the caller can stop it later.
|
||||||
|
func StartConsole(client *netbird.Client, jsOpts js.Value) (*Handle, error) {
|
||||||
|
filter, verbose, ascii := parseOpts(jsOpts)
|
||||||
|
|
||||||
|
cb := &jsCallbackWriter{}
|
||||||
|
|
||||||
|
cs, err := client.StartCapture(netbird.CaptureOptions{
|
||||||
|
TextOutput: cb,
|
||||||
|
Filter: filter,
|
||||||
|
Verbose: verbose,
|
||||||
|
ASCII: ascii,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := &Handle{cs: cs}
|
||||||
|
handle.stopFn = js.FuncOf(func(_ js.Value, _ []js.Value) any {
|
||||||
|
return statsToJS(handle.Stop())
|
||||||
|
})
|
||||||
|
|
||||||
|
iface := js.Global().Get("Object").Call("create", js.Null())
|
||||||
|
console := js.Global().Get("console")
|
||||||
|
iface.Set("onpacket", console.Get("log").Call("bind", console, js.ValueOf("[capture]")))
|
||||||
|
cb.setInterface(iface)
|
||||||
|
|
||||||
|
return handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsCallbackWriter is an io.Writer that buffers text until a newline, then
|
||||||
|
// invokes the JS onpacket callback with each complete line.
|
||||||
|
type jsCallbackWriter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
iface js.Value
|
||||||
|
buf strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsCallbackWriter) setInterface(iface js.Value) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.iface = iface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsCallbackWriter) Write(p []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
w.buf.Write(p)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
for {
|
||||||
|
str := w.buf.String()
|
||||||
|
idx := strings.IndexByte(str, '\n')
|
||||||
|
if idx < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, str[:idx])
|
||||||
|
w.buf.Reset()
|
||||||
|
if idx+1 < len(str) {
|
||||||
|
w.buf.WriteString(str[idx+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iface := w.iface
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if iface.IsUndefined() {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
cb := iface.Get("onpacket")
|
||||||
|
if cb.IsUndefined() || cb.IsNull() {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
cb.Invoke(js.ValueOf(line))
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
@@ -13,11 +13,9 @@ import (
|
|||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
nbgrpc "github.com/netbirdio/netbird/client/grpc"
|
nbgrpc "github.com/netbirdio/netbird/client/grpc"
|
||||||
"github.com/netbirdio/netbird/flow/proto"
|
"github.com/netbirdio/netbird/flow/proto"
|
||||||
@@ -301,12 +299,11 @@ func defaultBackoff(ctx context.Context, interval time.Duration) backoff.BackOff
|
|||||||
}, ctx)
|
}, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isContextDone reports whether the local context has been canceled or has
|
||||||
|
// exceeded its deadline. It deliberately does not inspect gRPC status codes:
|
||||||
|
// a server- or proxy-sent codes.Canceled / codes.DeadlineExceeded must not
|
||||||
|
// short-circuit our retry loop, since retrying is the correct response when
|
||||||
|
// the local context is still alive.
|
||||||
func isContextDone(err error) bool {
|
func isContextDone(err error) bool {
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
|
||||||
return true
|
|
||||||
}
|
|
||||||
if s, ok := status.FromError(err); ok {
|
|
||||||
return s.Code() == codes.Canceled || s.Code() == codes.DeadlineExceeded
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
go.mod
23
go.mod
@@ -17,8 +17,8 @@ require (
|
|||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/pflag v1.0.9
|
github.com/spf13/pflag v1.0.9
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
|
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
@@ -30,6 +30,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.7.0
|
fyne.io/fyne/v2 v2.7.0
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
|
||||||
github.com/awnumar/memguard v0.23.0
|
github.com/awnumar/memguard v0.23.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.38.3
|
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||||
@@ -46,6 +47,7 @@ require (
|
|||||||
github.com/crowdsecurity/go-cs-bouncer v0.0.21
|
github.com/crowdsecurity/go-cs-bouncer v0.0.21
|
||||||
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
||||||
github.com/dexidp/dex/api/v2 v2.4.0
|
github.com/dexidp/dex/api/v2 v2.4.0
|
||||||
|
github.com/ebitengine/purego v0.8.4
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0
|
github.com/eko/gocache/lib/v4 v4.2.0
|
||||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
||||||
github.com/eko/gocache/store/redis/v4 v4.2.2
|
github.com/eko/gocache/store/redis/v4 v4.2.2
|
||||||
@@ -66,9 +68,10 @@ require (
|
|||||||
github.com/jackc/pgx/v5 v5.5.5
|
github.com/jackc/pgx/v5 v5.5.5
|
||||||
github.com/libdns/route53 v1.5.0
|
github.com/libdns/route53 v1.5.0
|
||||||
github.com/libp2p/go-nat v0.2.0
|
github.com/libp2p/go-nat v0.2.0
|
||||||
github.com/libp2p/go-netroute v0.2.1
|
github.com/libp2p/go-netroute v0.4.0
|
||||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81
|
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81
|
||||||
github.com/mdlayher/socket v0.5.1
|
github.com/mdlayher/socket v0.5.1
|
||||||
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/miekg/dns v1.1.59
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42
|
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42
|
||||||
@@ -115,11 +118,11 @@ require (
|
|||||||
goauthentik.io/api/v3 v3.2023051.3
|
goauthentik.io/api/v3 v3.2023051.3
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||||
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
||||||
golang.org/x/mod v0.33.0
|
golang.org/x/mod v0.34.0
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/term v0.41.0
|
golang.org/x/term v0.42.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
google.golang.org/api v0.276.0
|
google.golang.org/api v0.276.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -178,7 +181,6 @@ require (
|
|||||||
github.com/docker/docker v28.0.1+incompatible // indirect
|
github.com/docker/docker v28.0.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fredbi/uri v1.1.1 // indirect
|
github.com/fredbi/uri v1.1.1 // indirect
|
||||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||||
@@ -301,13 +303,14 @@ require (
|
|||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/image v0.33.0 // indirect
|
golang.org/x/image v0.33.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502
|
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502
|
||||||
@@ -320,8 +323,6 @@ replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-2023080111
|
|||||||
|
|
||||||
replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51
|
replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51
|
||||||
|
|
||||||
replace github.com/libp2p/go-netroute => github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944
|
|
||||||
|
|
||||||
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.0
|
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.0
|
||||||
|
|
||||||
replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0
|
replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0
|
||||||
|
|||||||
38
go.sum
38
go.sum
@@ -15,6 +15,8 @@ fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
|||||||
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
||||||
@@ -393,6 +395,8 @@ github.com/libdns/route53 v1.5.0 h1:2SKdpPFl/qgWsXQvsLNJJAoX7rSxlk7zgoL4jnWdXVA=
|
|||||||
github.com/libdns/route53 v1.5.0/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
|
github.com/libdns/route53 v1.5.0/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
|
||||||
github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
|
github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
|
||||||
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
|
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
|
||||||
|
github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q=
|
||||||
|
github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA=
|
||||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 h1:J56rFEfUTFT9j9CiRXhi1r8lUJ4W5idG3CiaBZGojNU=
|
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 h1:J56rFEfUTFT9j9CiRXhi1r8lUJ4W5idG3CiaBZGojNU=
|
||||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/YdXctQ7qbcizZkw5mZ6l8Ogrl1dodBzVJduwI=
|
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/YdXctQ7qbcizZkw5mZ6l8Ogrl1dodBzVJduwI=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
@@ -413,6 +417,8 @@ github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0
|
|||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
|
github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
|
||||||
github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
|
github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
|
||||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||||
@@ -449,8 +455,6 @@ github.com/netbirdio/dex v0.244.0 h1:1GOvi8wnXYassnKGildzNqRHq0RbcfEUw7LKYpKIN7U
|
|||||||
github.com/netbirdio/dex v0.244.0/go.mod h1:STGInJhPcAflrHmDO7vyit2kSq03PdL+8zQPoGALtcU=
|
github.com/netbirdio/dex v0.244.0/go.mod h1:STGInJhPcAflrHmDO7vyit2kSq03PdL+8zQPoGALtcU=
|
||||||
github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6Aus=
|
github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6Aus=
|
||||||
github.com/netbirdio/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/netbirdio/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6Sf8uYFx/dMeqNOL90KUoRscdfpFZ3Im89uk=
|
|
||||||
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
|
|
||||||
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI=
|
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI=
|
||||||
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8=
|
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8=
|
||||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42 h1:F3zS5fT9xzD1OFLfcdAE+3FfyiwjGukF1hvj0jErgs8=
|
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42 h1:F3zS5fT9xzD1OFLfcdAE+3FfyiwjGukF1hvj0jErgs8=
|
||||||
@@ -707,8 +711,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||||
@@ -725,8 +729,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
@@ -745,8 +749,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
@@ -797,8 +801,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -811,8 +815,8 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -824,8 +828,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -839,8 +843,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -913,3 +917,5 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
|||||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
gvisor.dev/gvisor v0.0.0-20260219192049-0f2374377e89 h1:mGJaeA61P8dEHTqdvAgc70ZIV3QoUoJcXCRyyjO26OA=
|
gvisor.dev/gvisor v0.0.0-20260219192049-0f2374377e89 h1:mGJaeA61P8dEHTqdvAgc70ZIV3QoUoJcXCRyyjO26OA=
|
||||||
gvisor.dev/gvisor v0.0.0-20260219192049-0f2374377e89/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
gvisor.dev/gvisor v0.0.0-20260219192049-0f2374377e89/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||||
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func (c *Connector) ToStorageConnector() (storage.Connector, error) {
|
|||||||
// are stored with types that Dex can open.
|
// are stored with types that Dex can open.
|
||||||
func mapConnectorToDex(connType string, config map[string]interface{}) (string, map[string]interface{}) {
|
func mapConnectorToDex(connType string, config map[string]interface{}) (string, map[string]interface{}) {
|
||||||
switch connType {
|
switch connType {
|
||||||
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
|
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak", "adfs":
|
||||||
return "oidc", applyOIDCDefaults(connType, config)
|
return "oidc", applyOIDCDefaults(connType, config)
|
||||||
default:
|
default:
|
||||||
return connType, config
|
return connType, config
|
||||||
@@ -218,6 +218,8 @@ func applyOIDCDefaults(connType string, config map[string]interface{}) map[strin
|
|||||||
setDefault(augmented, "claimMapping", map[string]string{"email": "preferred_username"})
|
setDefault(augmented, "claimMapping", map[string]string{"email": "preferred_username"})
|
||||||
case "okta", "pocketid":
|
case "okta", "pocketid":
|
||||||
augmented["scopes"] = []string{"openid", "profile", "email", "groups"}
|
augmented["scopes"] = []string{"openid", "profile", "email", "groups"}
|
||||||
|
case "adfs":
|
||||||
|
augmented["scopes"] = []string{"openid", "profile", "email", "allatclaims"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return augmented
|
return augmented
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user