Compare commits
69 Commits
ci/freebsd
...
ui-refacto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5cc8da054 | ||
|
|
a4fd5a78b4 | ||
|
|
5b70989e3e | ||
|
|
d324a5ff48 | ||
|
|
cce80f8276 | ||
|
|
05ee4e52b8 | ||
|
|
bb2bf673a0 | ||
|
|
91c745e5e8 | ||
|
|
68c38247f1 | ||
|
|
8b8f38de1b | ||
|
|
2b272e74c8 | ||
|
|
e6cbf30415 | ||
|
|
490b60ad0e | ||
|
|
a8812d5fb1 | ||
|
|
6f93cf6ac3 | ||
|
|
18909390c2 | ||
|
|
cd8e71002f | ||
|
|
b3eb5f2453 | ||
|
|
dc02542a9e | ||
|
|
0c136fffb9 | ||
|
|
fffb9dd219 | ||
|
|
93275f9052 | ||
|
|
dd9c15072f | ||
|
|
4c743bc03d | ||
|
|
2e61b42e92 | ||
|
|
3f8de2a149 | ||
|
|
bc609c3ae7 | ||
|
|
97db824929 | ||
|
|
77a0992dc2 | ||
|
|
104990dfdd | ||
|
|
bde632c3b2 | ||
|
|
4268a5cfb7 | ||
|
|
e3994d0c99 | ||
|
|
ba6e10cef3 | ||
|
|
ce53981b55 | ||
|
|
a69037630b | ||
|
|
df58935cc0 | ||
|
|
a1743dbf9b | ||
|
|
f9771de3f5 | ||
|
|
a547fc74ed | ||
|
|
a21f6ecb0a | ||
|
|
6262b0d841 | ||
|
|
50b58a6828 | ||
|
|
057d651d2e | ||
|
|
c4b2da4c92 | ||
|
|
dcd1db42ef | ||
|
|
f29f5a0978 | ||
|
|
88a2bf582d | ||
|
|
3fc5a8d4a1 | ||
|
|
0148d926d5 | ||
|
|
57945fc328 | ||
|
|
8f16a19b8f | ||
|
|
ed828b7af4 | ||
|
|
11ac2af2f5 | ||
|
|
df197d5001 | ||
|
|
ad93dcf980 | ||
|
|
7eba5dafd8 | ||
|
|
504dceedf3 | ||
|
|
28fe26637b | ||
|
|
407e9d304b | ||
|
|
e5474e199f | ||
|
|
db44848e2d | ||
|
|
9417ce3b3a | ||
|
|
8fc4265995 | ||
|
|
9c50819f20 | ||
|
|
6f0eff3ba0 | ||
|
|
f8745723fc | ||
|
|
154b81645a | ||
|
|
34167c8a16 |
10
.github/workflows/golang-test-darwin.yml
vendored
@@ -43,5 +43,13 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined)
|
# Exclude client/ui-wails: its main.go uses //go:embed all:frontend/dist,
|
||||||
|
# which fails to compile until the frontend has been built. The Wails UI
|
||||||
|
# has no Go-side unit tests, and its release pipeline runs `pnpm build`
|
||||||
|
# before goreleaser.
|
||||||
|
# `go list -e` lets the listing succeed even though the embed fails to
|
||||||
|
# resolve; the grep then drops the broken package by path. Without -e,
|
||||||
|
# go list aborts with empty stdout and `go test` falls back to the repo
|
||||||
|
# root, which has no Go files.
|
||||||
|
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list -e ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui-wails)
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/golang-test-linux.yml
vendored
@@ -154,7 +154,15 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined)
|
# Exclude client/ui-wails: its main.go uses //go:embed all:frontend/dist,
|
||||||
|
# which fails to compile until the frontend has been built. The Wails UI
|
||||||
|
# has no Go-side unit tests, and its release pipeline runs `pnpm build`
|
||||||
|
# before goreleaser.
|
||||||
|
# `go list -e` lets the listing succeed even though the embed fails to
|
||||||
|
# resolve; the grep then drops the broken package by path. Without -e,
|
||||||
|
# go list aborts with empty stdout and `go test` falls back to the repo
|
||||||
|
# root, which has no Go files.
|
||||||
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list -e ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui-wails)
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
name: "Client (Docker) / Unit"
|
name: "Client (Docker) / Unit"
|
||||||
@@ -214,7 +222,7 @@ jobs:
|
|||||||
sh -c ' \
|
sh -c ' \
|
||||||
apk update; apk add --no-cache \
|
apk update; apk add --no-cache \
|
||||||
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
||||||
go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server)
|
go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -e -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server)
|
||||||
'
|
'
|
||||||
|
|
||||||
test_relay:
|
test_relay:
|
||||||
|
|||||||
9
.github/workflows/golang-test-windows.yml
vendored
@@ -64,8 +64,15 @@ jobs:
|
|||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
||||||
- name: Generate test script
|
- name: Generate test script
|
||||||
|
# Exclude client/ui-wails: its main.go uses //go:embed all:frontend/dist,
|
||||||
|
# which fails to compile until the frontend has been built. The Wails UI
|
||||||
|
# has no Go-side unit tests, and its release pipeline runs `pnpm build`
|
||||||
|
# before goreleaser.
|
||||||
|
# `go list -e` lets the listing succeed even though the embed fails to
|
||||||
|
# resolve; the Where-Object pipeline then drops the broken package by
|
||||||
|
# path. Without -e, go list aborts with empty stdout.
|
||||||
run: |
|
run: |
|
||||||
$packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' }
|
$packages = go list -e ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' } | Where-Object { $_ -notmatch '/client/ui-wails' }
|
||||||
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
|
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
|
||||||
$cmd = "$goExe test -tags=devcert -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
|
$cmd = "$goExe test -tags=devcert -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
|
||||||
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
|
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
|
||||||
|
|||||||
11
.github/workflows/golangci-lint.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA
|
||||||
skip: go.mod,go.sum,**/proxy/web/**
|
skip: go.mod,go.sum,**/proxy/web/**,**/pnpm-lock.yaml,**/package-lock.json
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -51,6 +51,15 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
|
- name: Stub Wails frontend bundle
|
||||||
|
# client/ui-wails/main.go has //go:embed all:frontend/dist. The
|
||||||
|
# directory is produced by `pnpm run build` and is gitignored, so
|
||||||
|
# lint-only runs (no frontend toolchain) need a placeholder file
|
||||||
|
# for the embed pattern to match.
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p client/ui-wails/frontend/dist
|
||||||
|
touch client/ui-wails/frontend/dist/.embed-placeholder
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
331
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.3"
|
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
|
||||||
@@ -306,8 +349,18 @@ jobs:
|
|||||||
- name: check git status
|
- name: check git status
|
||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libappindicator3-dev gir1.2-appindicator3-0.1 libxxf86vm-dev gcc-mingw-w64-x86-64
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libwebkit2gtk-4.1-dev libsoup-3.0-dev libayatana-appindicator3-dev gcc-mingw-w64-x86-64
|
||||||
|
|
||||||
- name: Decode GPG signing key
|
- name: Decode GPG signing key
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
@@ -327,9 +380,9 @@ jobs:
|
|||||||
- name: Install goversioninfo
|
- name: Install goversioninfo
|
||||||
run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e
|
run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e
|
||||||
- name: Generate windows syso amd64
|
- name: Generate windows syso amd64
|
||||||
run: goversioninfo -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso
|
run: goversioninfo -64 -icon client/ui-wails/build/windows/icon.ico -manifest client/ui-wails/build/windows/wails.exe.manifest -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui-wails/resources_windows_amd64.syso
|
||||||
- name: Generate windows syso arm64
|
- name: Generate windows syso arm64
|
||||||
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso
|
run: goversioninfo -arm -64 -icon client/ui-wails/build/windows/icon.ico -manifest client/ui-wails/build/windows/wails.exe.manifest -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui-wails/resources_windows_arm64.syso
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v4
|
||||||
@@ -360,6 +413,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 +422,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
|
||||||
@@ -393,6 +449,14 @@ jobs:
|
|||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
- name: check git status
|
- name: check git status
|
||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v4
|
||||||
@@ -402,15 +466,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
@@ -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:
|
||||||
@@ -109,6 +114,16 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- staticcheck
|
- staticcheck
|
||||||
text: "QF1012"
|
text: "QF1012"
|
||||||
|
# client/ui-wails/main.go uses //go:embed all:frontend/dist; the
|
||||||
|
# directory is populated by `pnpm build` in the release pipeline
|
||||||
|
# and missing at lint time, so the embed parses to "no matching
|
||||||
|
# files found" — surfaced by golangci-lint's typecheck pre-pass.
|
||||||
|
# Suppress just that one diagnostic; the rest of the package
|
||||||
|
# (services/, tray.go, grpc.go, ...) still gets linted normally.
|
||||||
|
- linters:
|
||||||
|
- typecheck
|
||||||
|
path: client/ui-wails/main\.go
|
||||||
|
text: "pattern all:frontend/dist"
|
||||||
paths:
|
paths:
|
||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
project_name: netbird-ui
|
project_name: netbird-ui
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- sh -c 'cd client/ui-wails/frontend && pnpm install --frozen-lockfile && pnpm build'
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-ui
|
- id: netbird-ui
|
||||||
dir: client/ui
|
dir: client/ui-wails
|
||||||
binary: netbird-ui
|
binary: netbird-ui
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
@@ -16,7 +21,7 @@ builds:
|
|||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
- id: netbird-ui-windows-amd64
|
- id: netbird-ui-windows-amd64
|
||||||
dir: client/ui
|
dir: client/ui-wails
|
||||||
binary: netbird-ui
|
binary: netbird-ui
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
@@ -31,7 +36,7 @@ builds:
|
|||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
- id: netbird-ui-windows-arm64
|
- id: netbird-ui-windows-arm64
|
||||||
dir: client/ui
|
dir: client/ui-wails
|
||||||
binary: netbird-ui
|
binary: netbird-ui
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
@@ -70,12 +75,15 @@ nfpms:
|
|||||||
scripts:
|
scripts:
|
||||||
postinstall: "release_files/ui-post-install.sh"
|
postinstall: "release_files/ui-post-install.sh"
|
||||||
contents:
|
contents:
|
||||||
- src: client/ui/build/netbird.desktop
|
- src: client/ui-wails/build/linux/netbird.desktop
|
||||||
dst: /usr/share/applications/netbird.desktop
|
dst: /usr/share/applications/netbird.desktop
|
||||||
- src: client/ui/assets/netbird.png
|
- src: client/ui-wails/build/appicon.png
|
||||||
dst: /usr/share/pixmaps/netbird.png
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
dependencies:
|
dependencies:
|
||||||
- netbird
|
- netbird
|
||||||
|
- libgtk-3-0
|
||||||
|
- libwebkit2gtk-4.1-0
|
||||||
|
- libayatana-appindicator3-1
|
||||||
|
|
||||||
- maintainer: Netbird <dev@netbird.io>
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
description: Netbird client UI.
|
description: Netbird client UI.
|
||||||
@@ -89,12 +97,15 @@ nfpms:
|
|||||||
scripts:
|
scripts:
|
||||||
postinstall: "release_files/ui-post-install.sh"
|
postinstall: "release_files/ui-post-install.sh"
|
||||||
contents:
|
contents:
|
||||||
- src: client/ui/build/netbird.desktop
|
- src: client/ui-wails/build/linux/netbird.desktop
|
||||||
dst: /usr/share/applications/netbird.desktop
|
dst: /usr/share/applications/netbird.desktop
|
||||||
- src: client/ui/assets/netbird.png
|
- src: client/ui-wails/build/appicon.png
|
||||||
dst: /usr/share/pixmaps/netbird.png
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
dependencies:
|
dependencies:
|
||||||
- netbird
|
- netbird
|
||||||
|
- gtk3
|
||||||
|
- webkit2gtk4.1
|
||||||
|
- libayatana-appindicator-gtk3
|
||||||
rpm:
|
rpm:
|
||||||
signature:
|
signature:
|
||||||
key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}'
|
key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}'
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
project_name: netbird-ui
|
project_name: netbird-ui
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- sh -c 'cd client/ui-wails/frontend && pnpm install --frozen-lockfile && pnpm build'
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-ui-darwin
|
- id: netbird-ui-darwin
|
||||||
dir: client/ui
|
dir: client/ui-wails
|
||||||
binary: netbird-ui
|
binary: netbird-ui
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
@@ -20,8 +25,6 @@ builds:
|
|||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
tags:
|
|
||||||
- load_wgnt_from_rsrc
|
|
||||||
|
|
||||||
universal_binaries:
|
universal_binaries:
|
||||||
- id: netbird-ui-darwin
|
- id: netbird-ui-darwin
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||||
@@ -262,6 +280,43 @@ CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
|||||||
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
|
# Install the Microsoft Edge WebView2 runtime if it isn't already present.
|
||||||
|
# Macro adapted from Wails3's NSIS template (wails_tools.nsh): a registry
|
||||||
|
# probe followed by a silent install of the embedded evergreen bootstrapper.
|
||||||
|
# The MicrosoftEdgeWebview2Setup.exe payload is staged next to this script
|
||||||
|
# by the sign-pipelines build step (`wails3 generate webview2bootstrapper`).
|
||||||
|
!macro nb.webview2runtime
|
||||||
|
SetRegView 64
|
||||||
|
# Per-machine install marker — populated when the runtime ships with
|
||||||
|
# Edge or has been installed by an admin previously.
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto webview2_ok
|
||||||
|
${EndIf}
|
||||||
|
# Per-user fallback for HKCU installs.
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto webview2_ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "Installing: WebView2 Runtime"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
webview2_ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Section -WebView2
|
||||||
|
!insertmacro nb.webview2runtime
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
Section -Post
|
Section -Post
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
||||||
@@ -283,6 +338,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
|
||||||
@@ -306,9 +363,9 @@ DetailPrint "Deleting application files..."
|
|||||||
Delete "$INSTDIR\${UI_APP_EXE}"
|
Delete "$INSTDIR\${UI_APP_EXE}"
|
||||||
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
Delete "$INSTDIR\wintun.dll"
|
Delete "$INSTDIR\wintun.dll"
|
||||||
!if ${ARCH} == "amd64"
|
# Legacy: pre-Wails installs shipped opengl32.dll (Mesa3D for Fyne); remove
|
||||||
|
# any leftover copy on uninstall so old upgrades don't leave it behind.
|
||||||
Delete "$INSTDIR\opengl32.dll"
|
Delete "$INSTDIR\opengl32.dll"
|
||||||
!endif
|
|
||||||
DetailPrint "Removing application directory..."
|
DetailPrint "Removing application directory..."
|
||||||
RmDir /r "$INSTDIR"
|
RmDir /r "$INSTDIR"
|
||||||
|
|
||||||
@@ -321,6 +378,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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,14 @@ type Status struct {
|
|||||||
eventStreams map[string]chan *proto.SystemEvent
|
eventStreams map[string]chan *proto.SystemEvent
|
||||||
eventQueue *EventQueue
|
eventQueue *EventQueue
|
||||||
|
|
||||||
|
// stateChangeStreams fan-out connection-state changes (connected /
|
||||||
|
// disconnected / connecting / address change / peers list change) to
|
||||||
|
// every active SubscribeStatus gRPC stream. Each subscriber gets a
|
||||||
|
// buffered chan; the notifier non-blockingly pings them so a slow
|
||||||
|
// consumer can never stall the daemon.
|
||||||
|
stateChangeMux sync.Mutex
|
||||||
|
stateChangeStreams map[string]chan struct{}
|
||||||
|
|
||||||
ingressGwMgr *ingressgw.Manager
|
ingressGwMgr *ingressgw.Manager
|
||||||
|
|
||||||
routeIDLookup routeIDLookup
|
routeIDLookup routeIDLookup
|
||||||
@@ -228,6 +236,7 @@ func NewRecorder(mgmAddress string) *Status {
|
|||||||
changeNotify: make(map[string]map[string]*StatusChangeSubscription),
|
changeNotify: make(map[string]map[string]*StatusChangeSubscription),
|
||||||
eventStreams: make(map[string]chan *proto.SystemEvent),
|
eventStreams: make(map[string]chan *proto.SystemEvent),
|
||||||
eventQueue: NewEventQueue(eventQueueSize),
|
eventQueue: NewEventQueue(eventQueueSize),
|
||||||
|
stateChangeStreams: make(map[string]chan struct{}),
|
||||||
offlinePeers: make([]State, 0),
|
offlinePeers: make([]State, 0),
|
||||||
notifier: newNotifier(),
|
notifier: newNotifier(),
|
||||||
mgmAddress: mgmAddress,
|
mgmAddress: mgmAddress,
|
||||||
@@ -320,10 +329,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 +352,30 @@ 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)
|
||||||
|
}
|
||||||
|
d.notifyStateChange()
|
||||||
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 +387,21 @@ 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)
|
||||||
|
d.notifyStateChange()
|
||||||
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 +413,12 @@ 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)
|
||||||
|
d.notifyStateChange()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,10 +434,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 +455,29 @@ 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)
|
||||||
|
}
|
||||||
|
d.notifyStateChange()
|
||||||
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 +492,29 @@ 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)
|
||||||
|
}
|
||||||
|
d.notifyStateChange()
|
||||||
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 +528,29 @@ 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)
|
||||||
|
}
|
||||||
|
d.notifyStateChange()
|
||||||
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,13 +567,20 @@ 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)
|
||||||
|
}
|
||||||
|
d.notifyStateChange()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,18 +646,35 @@ 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()
|
||||||
|
|
||||||
for key := range d.peers {
|
// snapshot per-peer router state to deliver after the lock is released
|
||||||
d.notifyPeerStateChangeListeners(key)
|
type routerDispatch struct {
|
||||||
|
peerID string
|
||||||
|
snapshot map[string]RouterState
|
||||||
}
|
}
|
||||||
|
dispatches := make([]routerDispatch, 0, len(d.peers))
|
||||||
|
for key := range d.peers {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) SubscribeToPeerStateChanges(ctx context.Context, peerID string) *StatusChangeSubscription {
|
func (d *Status) SubscribeToPeerStateChanges(ctx context.Context, peerID string) *StatusChangeSubscription {
|
||||||
@@ -655,10 +724,13 @@ 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)
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddLocalPeerStateRoute adds a route to the local peer state
|
// AddLocalPeerStateRoute adds a route to the local peer state
|
||||||
@@ -721,30 +793,39 @@ 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)
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSignalAddress update the address of the signal server
|
// UpdateSignalAddress update the address of the signal server
|
||||||
@@ -778,21 +859,27 @@ 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)
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) {
|
func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) {
|
||||||
@@ -919,7 +1006,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() {
|
||||||
@@ -990,16 +1077,19 @@ func (d *Status) GetFullStatus() FullStatus {
|
|||||||
// ClientStart will notify all listeners about the new service state
|
// ClientStart will notify all listeners about the new service state
|
||||||
func (d *Status) ClientStart() {
|
func (d *Status) ClientStart() {
|
||||||
d.notifier.clientStart()
|
d.notifier.clientStart()
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientStop will notify all listeners about the new service state
|
// ClientStop will notify all listeners about the new service state
|
||||||
func (d *Status) ClientStop() {
|
func (d *Status) ClientStop() {
|
||||||
d.notifier.clientStop()
|
d.notifier.clientStop()
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientTeardown will notify all listeners about the service is under teardown
|
// ClientTeardown will notify all listeners about the service is under teardown
|
||||||
func (d *Status) ClientTeardown() {
|
func (d *Status) ClientTeardown() {
|
||||||
d.notifier.clientTearDown()
|
d.notifier.clientTearDown()
|
||||||
|
d.notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConnectionListener set a listener to the notifier
|
// SetConnectionListener set a listener to the notifier
|
||||||
@@ -1012,18 +1102,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 +1120,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 +1158,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)
|
||||||
}
|
}
|
||||||
@@ -1128,6 +1231,50 @@ func (d *Status) GetEventHistory() []*proto.SystemEvent {
|
|||||||
return d.eventQueue.GetAll()
|
return d.eventQueue.GetAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscribeToStateChanges hands back a channel that receives a tick on
|
||||||
|
// every connection-state change (connected / disconnected / connecting /
|
||||||
|
// address change / peers-list change). The channel is buffered to one
|
||||||
|
// pending tick so a coalesced burst still wakes the consumer exactly
|
||||||
|
// once. Pass the returned id to UnsubscribeFromStateChanges to detach.
|
||||||
|
func (d *Status) SubscribeToStateChanges() (string, <-chan struct{}) {
|
||||||
|
d.stateChangeMux.Lock()
|
||||||
|
defer d.stateChangeMux.Unlock()
|
||||||
|
|
||||||
|
id := uuid.New().String()
|
||||||
|
ch := make(chan struct{}, 1)
|
||||||
|
d.stateChangeStreams[id] = ch
|
||||||
|
return id, ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsubscribeFromStateChanges releases a SubscribeToStateChanges channel
|
||||||
|
// and closes it so any consumer goroutine selecting on the channel
|
||||||
|
// unblocks cleanly.
|
||||||
|
func (d *Status) UnsubscribeFromStateChanges(id string) {
|
||||||
|
d.stateChangeMux.Lock()
|
||||||
|
defer d.stateChangeMux.Unlock()
|
||||||
|
|
||||||
|
if ch, ok := d.stateChangeStreams[id]; ok {
|
||||||
|
close(ch)
|
||||||
|
delete(d.stateChangeStreams, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyStateChange wakes every SubscribeToStateChanges subscriber. Drops
|
||||||
|
// the tick if a subscriber's buffer is full — by definition the consumer
|
||||||
|
// is already going to fetch the latest snapshot, so multiple pending ticks
|
||||||
|
// would be redundant.
|
||||||
|
func (d *Status) notifyStateChange() {
|
||||||
|
d.stateChangeMux.Lock()
|
||||||
|
defer d.stateChangeMux.Unlock()
|
||||||
|
|
||||||
|
for _, ch := range d.stateChangeStreams {
|
||||||
|
select {
|
||||||
|
case ch <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
|
func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,18 +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" />
|
||||||
<?if $(var.ArchSuffix) = "amd64" ?>
|
<File Id="NetbirdToastIcon" Name="netbird.png" Source=".\client\ui\assets\netbird.png" />
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
|
||||||
<?endif ?>
|
|
||||||
|
|
||||||
<ServiceInstall
|
<ServiceInstall
|
||||||
Id="NetBirdService"
|
Id="NetBirdService"
|
||||||
@@ -46,14 +53,74 @@
|
|||||||
</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" />
|
||||||
|
<!-- Pre-seed the CLSID the Wails notifications service reads on
|
||||||
|
first startup (notifications_windows.go:getGUID looks for
|
||||||
|
the CustomActivator value under this key). Without this
|
||||||
|
the service generates a fresh per-install UUID, which
|
||||||
|
diverges from the ToastActivatorCLSID set on the Start
|
||||||
|
Menu / Desktop shortcuts above and the COM activator
|
||||||
|
never fires when a toast is clicked. -->
|
||||||
|
<RegistryValue Name="CustomActivator" Type="string" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</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" />
|
||||||
<util:CloseApplication Id="CloseNetBirdUI" CloseMessage="no" Target="netbird-ui.exe" RebootPrompt="no" TerminateProcess="0" />
|
<util:CloseApplication Id="CloseNetBirdUI" CloseMessage="no" Target="netbird-ui.exe" RebootPrompt="no" TerminateProcess="0" />
|
||||||
|
|
||||||
|
<!-- WebView2 evergreen runtime detection.
|
||||||
|
Probe both the per-machine and per-user EdgeUpdate keys; if either
|
||||||
|
reports a non-empty `pv` value the runtime is already installed
|
||||||
|
and we skip the bootstrapper. -->
|
||||||
|
<Property Id="WEBVIEW2_VERSION_HKLM">
|
||||||
|
<RegistrySearch Id="WV2HKLM" Root="HKLM"
|
||||||
|
Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
|
||||||
|
Name="pv" Type="raw" Bitness="always64" />
|
||||||
|
</Property>
|
||||||
|
<Property Id="WEBVIEW2_VERSION_HKCU">
|
||||||
|
<RegistrySearch Id="WV2HKCU" Root="HKCU"
|
||||||
|
Key="Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
|
||||||
|
Name="pv" Type="raw" />
|
||||||
|
</Property>
|
||||||
|
|
||||||
|
<!-- Embed the bootstrapper payload. Path is relative to the WiX
|
||||||
|
working directory; sign-pipelines stages it next to client/
|
||||||
|
via `wails3 generate webview2bootstrapper`. -->
|
||||||
|
<Binary Id="WebView2Bootstrapper" SourceFile=".\client\MicrosoftEdgeWebview2Setup.exe" />
|
||||||
|
|
||||||
|
<CustomAction Id="InstallWebView2"
|
||||||
|
BinaryRef="WebView2Bootstrapper"
|
||||||
|
ExeCommand="/silent /install"
|
||||||
|
Execute="deferred"
|
||||||
|
Impersonate="no"
|
||||||
|
Return="check" />
|
||||||
|
|
||||||
|
<InstallExecuteSequence>
|
||||||
|
<Custom Action="InstallWebView2" Before="InstallFinalize"
|
||||||
|
Condition="NOT WEBVIEW2_VERSION_HKLM AND NOT WEBVIEW2_VERSION_HKCU AND NOT REMOVE" />
|
||||||
|
</InstallExecuteSequence>
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Icons -->
|
||||||
<Icon Id="NetbirdIcon" SourceFile=".\client\ui\assets\netbird.ico" />
|
<Icon Id="NetbirdIcon" SourceFile=".\client\ui\assets\netbird.ico" />
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ service DaemonService {
|
|||||||
// Status of the service.
|
// Status of the service.
|
||||||
rpc Status(StatusRequest) returns (StatusResponse) {}
|
rpc Status(StatusRequest) returns (StatusResponse) {}
|
||||||
|
|
||||||
|
// SubscribeStatus pushes a fresh StatusResponse on connection state
|
||||||
|
// changes (Connected / Disconnected / Connecting / address change /
|
||||||
|
// peers list change). The first message on the stream is the current
|
||||||
|
// snapshot, so a freshly-subscribed UI doesn't need to also call Status.
|
||||||
|
rpc SubscribeStatus(StatusRequest) returns (stream StatusResponse) {}
|
||||||
|
|
||||||
// Down stops engine work in the daemon.
|
// Down stops engine work in the daemon.
|
||||||
rpc Down(DownRequest) returns (DownResponse) {}
|
rpc Down(DownRequest) returns (DownResponse) {}
|
||||||
|
|
||||||
@@ -64,6 +70,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 +121,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 +129,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 +849,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 {}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ type DaemonServiceClient interface {
|
|||||||
Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error)
|
Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error)
|
||||||
// Status of the service.
|
// Status of the service.
|
||||||
Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
|
Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
|
||||||
|
// SubscribeStatus pushes a fresh StatusResponse on connection state
|
||||||
|
// changes (Connected / Disconnected / Connecting / address change /
|
||||||
|
// peers list change). The first message on the stream is the current
|
||||||
|
// snapshot, so a freshly-subscribed UI doesn't need to also call Status.
|
||||||
|
SubscribeStatus(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (DaemonService_SubscribeStatusClient, error)
|
||||||
// Down stops engine work in the daemon.
|
// Down stops engine work in the daemon.
|
||||||
Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error)
|
Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error)
|
||||||
// GetConfig of the daemon.
|
// GetConfig of the daemon.
|
||||||
@@ -53,6 +58,14 @@ type DaemonServiceClient interface {
|
|||||||
// SetSyncResponsePersistence enables or disables sync response persistence
|
// SetSyncResponsePersistence enables or disables sync response persistence
|
||||||
SetSyncResponsePersistence(ctx context.Context, in *SetSyncResponsePersistenceRequest, opts ...grpc.CallOption) (*SetSyncResponsePersistenceResponse, error)
|
SetSyncResponsePersistence(ctx context.Context, in *SetSyncResponsePersistenceRequest, opts ...grpc.CallOption) (*SetSyncResponsePersistenceResponse, error)
|
||||||
TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error)
|
TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error)
|
||||||
|
// StartCapture begins streaming packet capture on the WireGuard interface.
|
||||||
|
// Requires --enable-capture set at service install/reconfigure time.
|
||||||
|
StartCapture(ctx context.Context, in *StartCaptureRequest, opts ...grpc.CallOption) (DaemonService_StartCaptureClient, error)
|
||||||
|
// StartBundleCapture begins capturing packets to a server-side temp file
|
||||||
|
// for inclusion in the next debug bundle. Auto-stops after the given timeout.
|
||||||
|
StartBundleCapture(ctx context.Context, in *StartBundleCaptureRequest, opts ...grpc.CallOption) (*StartBundleCaptureResponse, error)
|
||||||
|
// StopBundleCapture stops the running bundle capture. Idempotent.
|
||||||
|
StopBundleCapture(ctx context.Context, in *StopBundleCaptureRequest, opts ...grpc.CallOption) (*StopBundleCaptureResponse, error)
|
||||||
SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error)
|
SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error)
|
||||||
GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error)
|
GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error)
|
||||||
SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error)
|
SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error)
|
||||||
@@ -77,7 +90,6 @@ type DaemonServiceClient interface {
|
|||||||
StartCPUProfile(ctx context.Context, in *StartCPUProfileRequest, opts ...grpc.CallOption) (*StartCPUProfileResponse, error)
|
StartCPUProfile(ctx context.Context, in *StartCPUProfileRequest, opts ...grpc.CallOption) (*StartCPUProfileResponse, error)
|
||||||
// StopCPUProfile stops CPU profiling in the daemon
|
// StopCPUProfile stops CPU profiling in the daemon
|
||||||
StopCPUProfile(ctx context.Context, in *StopCPUProfileRequest, opts ...grpc.CallOption) (*StopCPUProfileResponse, error)
|
StopCPUProfile(ctx context.Context, in *StopCPUProfileRequest, opts ...grpc.CallOption) (*StopCPUProfileResponse, error)
|
||||||
NotifyOSLifecycle(ctx context.Context, in *OSLifecycleRequest, opts ...grpc.CallOption) (*OSLifecycleResponse, error)
|
|
||||||
GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error)
|
GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error)
|
||||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error)
|
ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error)
|
||||||
@@ -127,6 +139,38 @@ func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) SubscribeStatus(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (DaemonService_SubscribeStatusClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeStatus", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &daemonServiceSubscribeStatusClient{stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonService_SubscribeStatusClient interface {
|
||||||
|
Recv() (*StatusResponse, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceSubscribeStatusClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *daemonServiceSubscribeStatusClient) Recv() (*StatusResponse, error) {
|
||||||
|
m := new(StatusResponse)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) {
|
func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) {
|
||||||
out := new(DownResponse)
|
out := new(DownResponse)
|
||||||
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Down", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Down", in, out, opts...)
|
||||||
@@ -253,8 +297,58 @@ func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRe
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) StartCapture(ctx context.Context, in *StartCaptureRequest, opts ...grpc.CallOption) (DaemonService_StartCaptureClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[1], "/daemon.DaemonService/StartCapture", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &daemonServiceStartCaptureClient{stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonService_StartCaptureClient interface {
|
||||||
|
Recv() (*CapturePacket, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceStartCaptureClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *daemonServiceStartCaptureClient) Recv() (*CapturePacket, error) {
|
||||||
|
m := new(CapturePacket)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) StartBundleCapture(ctx context.Context, in *StartBundleCaptureRequest, opts ...grpc.CallOption) (*StartBundleCaptureResponse, error) {
|
||||||
|
out := new(StartBundleCaptureResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/StartBundleCapture", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) StopBundleCapture(ctx context.Context, in *StopBundleCaptureRequest, opts ...grpc.CallOption) (*StopBundleCaptureResponse, error) {
|
||||||
|
out := new(StopBundleCaptureResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/StopBundleCapture", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) {
|
func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) {
|
||||||
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeEvents", opts...)
|
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[2], "/daemon.DaemonService/SubscribeEvents", opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -420,15 +514,6 @@ func (c *daemonServiceClient) StopCPUProfile(ctx context.Context, in *StopCPUPro
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) NotifyOSLifecycle(ctx context.Context, in *OSLifecycleRequest, opts ...grpc.CallOption) (*OSLifecycleResponse, error) {
|
|
||||||
out := new(OSLifecycleResponse)
|
|
||||||
err := c.cc.Invoke(ctx, "/daemon.DaemonService/NotifyOSLifecycle", in, out, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *daemonServiceClient) GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error) {
|
func (c *daemonServiceClient) GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error) {
|
||||||
out := new(InstallerResultResponse)
|
out := new(InstallerResultResponse)
|
||||||
err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetInstallerResult", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetInstallerResult", in, out, opts...)
|
||||||
@@ -439,7 +524,7 @@ func (c *daemonServiceClient) GetInstallerResult(ctx context.Context, in *Instal
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error) {
|
func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error) {
|
||||||
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[1], "/daemon.DaemonService/ExposeService", opts...)
|
stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[3], "/daemon.DaemonService/ExposeService", opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -483,6 +568,11 @@ type DaemonServiceServer interface {
|
|||||||
Up(context.Context, *UpRequest) (*UpResponse, error)
|
Up(context.Context, *UpRequest) (*UpResponse, error)
|
||||||
// Status of the service.
|
// Status of the service.
|
||||||
Status(context.Context, *StatusRequest) (*StatusResponse, error)
|
Status(context.Context, *StatusRequest) (*StatusResponse, error)
|
||||||
|
// SubscribeStatus pushes a fresh StatusResponse on connection state
|
||||||
|
// changes (Connected / Disconnected / Connecting / address change /
|
||||||
|
// peers list change). The first message on the stream is the current
|
||||||
|
// snapshot, so a freshly-subscribed UI doesn't need to also call Status.
|
||||||
|
SubscribeStatus(*StatusRequest, DaemonService_SubscribeStatusServer) error
|
||||||
// Down stops engine work in the daemon.
|
// Down stops engine work in the daemon.
|
||||||
Down(context.Context, *DownRequest) (*DownResponse, error)
|
Down(context.Context, *DownRequest) (*DownResponse, error)
|
||||||
// GetConfig of the daemon.
|
// GetConfig of the daemon.
|
||||||
@@ -509,6 +599,14 @@ type DaemonServiceServer interface {
|
|||||||
// SetSyncResponsePersistence enables or disables sync response persistence
|
// SetSyncResponsePersistence enables or disables sync response persistence
|
||||||
SetSyncResponsePersistence(context.Context, *SetSyncResponsePersistenceRequest) (*SetSyncResponsePersistenceResponse, error)
|
SetSyncResponsePersistence(context.Context, *SetSyncResponsePersistenceRequest) (*SetSyncResponsePersistenceResponse, error)
|
||||||
TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error)
|
TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error)
|
||||||
|
// StartCapture begins streaming packet capture on the WireGuard interface.
|
||||||
|
// Requires --enable-capture set at service install/reconfigure time.
|
||||||
|
StartCapture(*StartCaptureRequest, DaemonService_StartCaptureServer) error
|
||||||
|
// StartBundleCapture begins capturing packets to a server-side temp file
|
||||||
|
// for inclusion in the next debug bundle. Auto-stops after the given timeout.
|
||||||
|
StartBundleCapture(context.Context, *StartBundleCaptureRequest) (*StartBundleCaptureResponse, error)
|
||||||
|
// StopBundleCapture stops the running bundle capture. Idempotent.
|
||||||
|
StopBundleCapture(context.Context, *StopBundleCaptureRequest) (*StopBundleCaptureResponse, error)
|
||||||
SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error
|
SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error
|
||||||
GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error)
|
GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error)
|
||||||
SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error)
|
SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error)
|
||||||
@@ -533,7 +631,6 @@ type DaemonServiceServer interface {
|
|||||||
StartCPUProfile(context.Context, *StartCPUProfileRequest) (*StartCPUProfileResponse, error)
|
StartCPUProfile(context.Context, *StartCPUProfileRequest) (*StartCPUProfileResponse, error)
|
||||||
// StopCPUProfile stops CPU profiling in the daemon
|
// StopCPUProfile stops CPU profiling in the daemon
|
||||||
StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error)
|
StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error)
|
||||||
NotifyOSLifecycle(context.Context, *OSLifecycleRequest) (*OSLifecycleResponse, error)
|
|
||||||
GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error)
|
GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error)
|
||||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error
|
ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error
|
||||||
@@ -556,6 +653,9 @@ func (UnimplementedDaemonServiceServer) Up(context.Context, *UpRequest) (*UpResp
|
|||||||
func (UnimplementedDaemonServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) {
|
func (UnimplementedDaemonServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Status not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Status not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) SubscribeStatus(*StatusRequest, DaemonService_SubscribeStatusServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*DownResponse, error) {
|
func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*DownResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Down not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Down not implemented")
|
||||||
}
|
}
|
||||||
@@ -598,6 +698,15 @@ func (UnimplementedDaemonServiceServer) SetSyncResponsePersistence(context.Conte
|
|||||||
func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) {
|
func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) StartCapture(*StartCaptureRequest, DaemonService_StartCaptureServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method StartCapture not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) StartBundleCapture(context.Context, *StartBundleCaptureRequest) (*StartBundleCaptureResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method StartBundleCapture not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) StopBundleCapture(context.Context, *StopBundleCaptureRequest) (*StopBundleCaptureResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method StopBundleCapture not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error {
|
func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error {
|
||||||
return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented")
|
return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented")
|
||||||
}
|
}
|
||||||
@@ -646,9 +755,6 @@ func (UnimplementedDaemonServiceServer) StartCPUProfile(context.Context, *StartC
|
|||||||
func (UnimplementedDaemonServiceServer) StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error) {
|
func (UnimplementedDaemonServiceServer) StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StopCPUProfile not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method StopCPUProfile not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedDaemonServiceServer) NotifyOSLifecycle(context.Context, *OSLifecycleRequest) (*OSLifecycleResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method NotifyOSLifecycle not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) {
|
func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetInstallerResult not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetInstallerResult not implemented")
|
||||||
}
|
}
|
||||||
@@ -740,6 +846,27 @@ func _DaemonService_Status_Handler(srv interface{}, ctx context.Context, dec fun
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _DaemonService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(StatusRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(DaemonServiceServer).SubscribeStatus(m, &daemonServiceSubscribeStatusServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonService_SubscribeStatusServer interface {
|
||||||
|
Send(*StatusResponse) error
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceSubscribeStatusServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *daemonServiceSubscribeStatusServer) Send(m *StatusResponse) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(DownRequest)
|
in := new(DownRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -992,6 +1119,63 @@ func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, de
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _DaemonService_StartCapture_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(StartCaptureRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(DaemonServiceServer).StartCapture(m, &daemonServiceStartCaptureServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonService_StartCaptureServer interface {
|
||||||
|
Send(*CapturePacket) error
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceStartCaptureServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *daemonServiceStartCaptureServer) Send(m *CapturePacket) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_StartBundleCapture_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(StartBundleCaptureRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).StartBundleCapture(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/StartBundleCapture",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).StartBundleCapture(ctx, req.(*StartBundleCaptureRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_StopBundleCapture_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(StopBundleCaptureRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).StopBundleCapture(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/StopBundleCapture",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).StopBundleCapture(ctx, req.(*StopBundleCaptureRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
|
func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
m := new(SubscribeRequest)
|
m := new(SubscribeRequest)
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
@@ -1283,24 +1467,6 @@ func _DaemonService_StopCPUProfile_Handler(srv interface{}, ctx context.Context,
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _DaemonService_NotifyOSLifecycle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(OSLifecycleRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(DaemonServiceServer).NotifyOSLifecycle(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: "/daemon.DaemonService/NotifyOSLifecycle",
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(DaemonServiceServer).NotifyOSLifecycle(ctx, req.(*OSLifecycleRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _DaemonService_GetInstallerResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _DaemonService_GetInstallerResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(InstallerResultRequest)
|
in := new(InstallerResultRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -1419,6 +1585,14 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "TracePacket",
|
MethodName: "TracePacket",
|
||||||
Handler: _DaemonService_TracePacket_Handler,
|
Handler: _DaemonService_TracePacket_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StartBundleCapture",
|
||||||
|
Handler: _DaemonService_StartBundleCapture_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "StopBundleCapture",
|
||||||
|
Handler: _DaemonService_StopBundleCapture_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "GetEvents",
|
MethodName: "GetEvents",
|
||||||
Handler: _DaemonService_GetEvents_Handler,
|
Handler: _DaemonService_GetEvents_Handler,
|
||||||
@@ -1479,16 +1653,22 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "StopCPUProfile",
|
MethodName: "StopCPUProfile",
|
||||||
Handler: _DaemonService_StopCPUProfile_Handler,
|
Handler: _DaemonService_StopCPUProfile_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "NotifyOSLifecycle",
|
|
||||||
Handler: _DaemonService_NotifyOSLifecycle_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "GetInstallerResult",
|
MethodName: "GetInstallerResult",
|
||||||
Handler: _DaemonService_GetInstallerResult_Handler,
|
Handler: _DaemonService_GetInstallerResult_Handler,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "SubscribeStatus",
|
||||||
|
Handler: _DaemonService_SubscribeStatus_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StreamName: "StartCapture",
|
||||||
|
Handler: _DaemonService_StartCapture_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
StreamName: "SubscribeEvents",
|
StreamName: "SubscribeEvents",
|
||||||
Handler: _DaemonService_SubscribeEvents_Handler,
|
Handler: _DaemonService_SubscribeEvents_Handler,
|
||||||
|
|||||||
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
|
||||||
}
|
}
|
||||||
@@ -1101,6 +1108,13 @@ func (s *Server) Status(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s.buildStatusResponse(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildStatusResponse composes a StatusResponse from the current daemon
|
||||||
|
// state. Shared between the unary Status RPC and the SubscribeStatus
|
||||||
|
// stream so both paths return identical snapshots.
|
||||||
|
func (s *Server) buildStatusResponse(msg *proto.StatusRequest) (*proto.StatusResponse, error) {
|
||||||
status, err := internal.CtxGetState(s.rootCtx).Status()
|
status, err := internal.CtxGetState(s.rootCtx).Status()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
57
client/server/status_stream.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubscribeStatus pushes a fresh StatusResponse on every connection state
|
||||||
|
// change. The first message is the current snapshot, so a re-subscribing
|
||||||
|
// client doesn't need to also call Status. Subsequent messages fire when
|
||||||
|
// the peer recorder reports any of: connected/disconnected/connecting,
|
||||||
|
// management or signal flip, address change, or peers list change.
|
||||||
|
//
|
||||||
|
// The change channel coalesces bursts to a single tick. If the consumer
|
||||||
|
// is slow the daemon drops extras (not blocks), and the next snapshot
|
||||||
|
// the consumer pulls already reflects everything.
|
||||||
|
func (s *Server) SubscribeStatus(req *proto.StatusRequest, stream proto.DaemonService_SubscribeStatusServer) error {
|
||||||
|
subID, ch := s.statusRecorder.SubscribeToStateChanges()
|
||||||
|
defer func() {
|
||||||
|
s.statusRecorder.UnsubscribeFromStateChanges(subID)
|
||||||
|
log.Debug("client unsubscribed from status updates")
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Debug("client subscribed to status updates")
|
||||||
|
|
||||||
|
if err := s.sendStatusSnapshot(req, stream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.sendStatusSnapshot(req, stream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-stream.Context().Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendStatusSnapshot(req *proto.StatusRequest, stream proto.DaemonService_SubscribeStatusServer) error {
|
||||||
|
resp, err := s.buildStatusResponse(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("build status snapshot for stream: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := stream.Send(resp); err != nil {
|
||||||
|
log.Warnf("send status snapshot to stream: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
6
client/ui-wails/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.task
|
||||||
|
bin
|
||||||
|
frontend/dist
|
||||||
|
frontend/node_modules
|
||||||
|
build/linux/appimage/build
|
||||||
|
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||||
100
client/ui-wails/README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# NetBird desktop UI (Wails3 + React)
|
||||||
|
|
||||||
|
Replaces `client/ui` (Fyne). One binary on Windows / macOS / Linux,
|
||||||
|
talks to the NetBird daemon over gRPC, renders a React frontend in a
|
||||||
|
WebView.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go ≥ 1.25, Node ≥ 20, **pnpm** (`corepack enable && corepack prepare pnpm@latest --activate`)
|
||||||
|
- `wails3` CLI: `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`
|
||||||
|
- `task`: `go install github.com/go-task/task/v3/cmd/task@latest`
|
||||||
|
- A running NetBird daemon (default: `unix:///var/run/netbird.sock`,
|
||||||
|
Windows `tcp://127.0.0.1:41731`)
|
||||||
|
- Linux only: `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`,
|
||||||
|
`libayatana-appindicator3-dev`
|
||||||
|
|
||||||
|
## Develop without rebuilding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client/ui-wails
|
||||||
|
task dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`task dev` runs Vite (port 9245) + the Go binary + a `*.go` watcher.
|
||||||
|
Frontend edits hot-reload instantly. Go edits trigger a rebuild and
|
||||||
|
relaunch. Pass daemon flags after `--`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task dev -- --daemon-addr=tcp://127.0.0.1:41731
|
||||||
|
```
|
||||||
|
|
||||||
|
For pure UI work (no native window, fastest loop):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task build
|
||||||
|
```
|
||||||
|
|
||||||
|
Output in `bin/`. Frontend assets are embedded into the binary.
|
||||||
|
|
||||||
|
### Cross-compile Windows from Linux
|
||||||
|
|
||||||
|
Install the mingw-w64 toolchain once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install gcc-mingw-w64-x86-64 # Debian/Ubuntu
|
||||||
|
sudo dnf install mingw64-gcc # Fedora
|
||||||
|
sudo pacman -S mingw-w64-gcc # Arch
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=1 task windows:build
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `bin/netbird-ui.exe`. macOS cross-compile from Linux is not
|
||||||
|
supported (signing and notarization need a real Mac).
|
||||||
|
|
||||||
|
### Windows console build (logs in the terminal)
|
||||||
|
|
||||||
|
Default `windows:build` links the binary as a Windows GUI app, which
|
||||||
|
detaches from the launching console — `logrus` output, `fmt.Println`,
|
||||||
|
and panics go nowhere visible. To debug tray/event/daemon issues:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=1 task windows:build:console
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `bin/netbird-ui-console.exe`. Run it from `cmd.exe` /
|
||||||
|
PowerShell / Windows Terminal and stdout/stderr land in that
|
||||||
|
terminal. Same flag works on a native Windows build (drop the
|
||||||
|
`CGO_ENABLED=1` if your toolchain already has it set).
|
||||||
|
|
||||||
|
## Regenerating bindings
|
||||||
|
|
||||||
|
When a Go service signature changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wails3 generate bindings
|
||||||
|
```
|
||||||
|
|
||||||
|
`task dev` does this automatically on `*.go` save.
|
||||||
|
|
||||||
|
## Tray icons
|
||||||
|
|
||||||
|
Source SVGs live in `assets/svg/` (state.svg + state-macos.svg). After editing
|
||||||
|
any SVG, rasterize to the PNGs the Go side embeds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task common:generate:tray:icons
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Inkscape. Commit the resulting `assets/*.png` files alongside the
|
||||||
|
SVG change so CI doesn't need Inkscape installed.
|
||||||
58
client/ui-wails/Taskfile.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ./build/Taskfile.yml
|
||||||
|
windows: ./build/windows/Taskfile.yml
|
||||||
|
darwin: ./build/darwin/Taskfile.yml
|
||||||
|
linux: ./build/linux/Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
APP_NAME: "netbird-ui"
|
||||||
|
BIN_DIR: "bin"
|
||||||
|
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:build"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:package"
|
||||||
|
|
||||||
|
run:
|
||||||
|
summary: Runs the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:run"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
summary: Runs the application in development mode
|
||||||
|
cmds:
|
||||||
|
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||||
|
|
||||||
|
setup:docker:
|
||||||
|
summary: Builds Docker image for cross-compilation (~800MB download)
|
||||||
|
cmds:
|
||||||
|
- task: common:setup:docker
|
||||||
|
|
||||||
|
build:server:
|
||||||
|
summary: Builds the application in server mode (no GUI, HTTP server only)
|
||||||
|
cmds:
|
||||||
|
- task: common:build:server
|
||||||
|
|
||||||
|
run:server:
|
||||||
|
summary: Runs the application in server mode
|
||||||
|
cmds:
|
||||||
|
- task: common:run:server
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Builds a Docker image for server mode deployment
|
||||||
|
cmds:
|
||||||
|
- task: common:build:docker
|
||||||
|
|
||||||
|
run:docker:
|
||||||
|
summary: Builds and runs the Docker image
|
||||||
|
cmds:
|
||||||
|
- task: common:run:docker
|
||||||
BIN
client/ui-wails/assets/netbird-systemtray-connected-dark.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connected-macos.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connected.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connecting-dark.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connecting-macos.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connecting.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-disconnected-macos.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-disconnected.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-error-dark.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-error-macos.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-error.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-update-connected.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/ui-wails/assets/netbird.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
14
client/ui-wails/assets/svg/_base.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
NetBird base mark, centered in a 32×32 viewBox with badge-friendly margins.
|
||||||
|
Preserved across every state icon as required by the design plan; state
|
||||||
|
badges sit on top in the bottom-right 12×12 area (x=18..30, y=18..30).
|
||||||
|
The mark itself is taken verbatim from dashboard/src/assets/netbird.svg
|
||||||
|
(three orange/red paths) and translated into the 32×32 grid.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g id="netbird-mark" transform="translate(2 5) scale(0.8)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 932 B |
17
client/ui-wails/assets/svg/appicon.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
App icon source. Rasterized to build/appicon.png by
|
||||||
|
`task common:generate:icons`, which then drives `wails3 generate icons`
|
||||||
|
to produce the per-platform .ico / .icns artifacts.
|
||||||
|
|
||||||
|
The mark fills ~90% of the canvas width (with vertical centering) so
|
||||||
|
Windows Explorer and macOS Finder render a recognisable bird at small
|
||||||
|
sizes. The mark's native aspect (31:23) is wider than tall, so width is
|
||||||
|
the binding dimension.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<g transform="translate(37 170) scale(29.7)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 997 B |
10
client/ui-wails/assets/svg/connected-macos.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="black"/>
|
||||||
|
<path d="M22 25 L24 27 L28 23" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 723 B |
14
client/ui-wails/assets/svg/connected.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<!-- Mark fills the canvas. Badge overlaps the bottom-right corner so most
|
||||||
|
of the mark is still visible at 16 px tray sizes. -->
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<!-- connected badge: green check, ~25% canvas, with a thin white halo so
|
||||||
|
the green disc reads cleanly on top of the orange mark. -->
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="#0E9F6E"/>
|
||||||
|
<path d="M22 25 L24 27 L28 23" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
9
client/ui-wails/assets/svg/connecting-macos.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="black" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 678 B |
9
client/ui-wails/assets/svg/connecting.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="#F68330" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 723 B |
10
client/ui-wails/assets/svg/disconnected-macos.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black" opacity="0.5">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="black" stroke-width="1.6"/>
|
||||||
|
<line x1="21.5" y1="25" x2="28.5" y2="25" stroke="black" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 745 B |
10
client/ui-wails/assets/svg/disconnected.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.45">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="#7c8994" stroke-width="1.6"/>
|
||||||
|
<line x1="21.5" y1="25" x2="28.5" y2="25" stroke="#7c8994" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 793 B |