mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-24 16:41:30 +02:00
Compare commits
89 Commits
v0.71.0
...
fix/ipv6-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8b0310a4 | ||
|
|
ee360963f9 | ||
|
|
8d9580e491 | ||
|
|
5bd7c6c7ea | ||
|
|
8ae2cd0a08 | ||
|
|
e4397d4d46 | ||
|
|
6fbc90b4d3 | ||
|
|
5095e17cc5 | ||
|
|
6df0175607 | ||
|
|
3c23700e56 | ||
|
|
38ad2b67e8 | ||
|
|
01aa49433e | ||
|
|
08a2b63675 | ||
|
|
b3f9e6588a | ||
|
|
967e2d6864 | ||
|
|
e7c1d364c3 | ||
|
|
a44198fd77 | ||
|
|
b57f714350 | ||
|
|
f893abc41d | ||
|
|
60067619a1 | ||
|
|
cd777395f2 | ||
|
|
b19467e3af | ||
|
|
2bcea9d582 | ||
|
|
8ff3b06cf1 | ||
|
|
d7703767d5 | ||
|
|
7feda907ca | ||
|
|
62da482133 | ||
|
|
079bce3c2f | ||
|
|
1a09aa6715 | ||
|
|
61abf5b9ea | ||
|
|
e229050ba3 | ||
|
|
e919b2d55d | ||
|
|
a40028092d | ||
|
|
13200265d8 | ||
|
|
ed7a9363aa | ||
|
|
d56859dc5d | ||
|
|
367d37050b | ||
|
|
106527182f | ||
|
|
8e1d5b78c2 | ||
|
|
d3b63c6be9 | ||
|
|
60d2fa08b0 | ||
|
|
1e7b16db0a | ||
|
|
b377d99933 | ||
|
|
512899d82d | ||
|
|
5993ec6e43 | ||
|
|
eac6d501c3 | ||
|
|
deeae30612 | ||
|
|
f3cdf163e1 | ||
|
|
3e61ccb162 | ||
|
|
a48c20d8d8 | ||
|
|
2b57a7d43b | ||
|
|
fa1e241aea | ||
|
|
e7c9182ff9 | ||
|
|
9189625487 | ||
|
|
e9dbf9db6f | ||
|
|
5a9e9e7bc9 | ||
|
|
43e041cf9f | ||
|
|
77e5693200 | ||
|
|
174dc24867 | ||
|
|
7ea5e37dd4 | ||
|
|
9d7ef9b255 | ||
|
|
944a258459 | ||
|
|
1f9a829f2c | ||
|
|
14af179556 | ||
|
|
1fbb5e6d5d | ||
|
|
6771e35d57 | ||
|
|
e89b1e0596 | ||
|
|
d542c60e21 | ||
|
|
4983b5cf17 | ||
|
|
b3b0feb3b8 | ||
|
|
7aebdd69dd | ||
|
|
0358be2313 | ||
|
|
37052fd5bc | ||
|
|
454ff66518 | ||
|
|
6137a1fcc5 | ||
|
|
4955c345d5 | ||
|
|
9192b4f029 | ||
|
|
c784b02550 | ||
|
|
d250f92c43 | ||
|
|
80966ab1b0 | ||
|
|
af24fd7796 | ||
|
|
13d32d274f | ||
|
|
705f87fc20 | ||
|
|
3f91f49277 | ||
|
|
347c5bf317 | ||
|
|
22e2519d71 | ||
|
|
e916f12cca | ||
|
|
9ed2e2a5b4 | ||
|
|
2ccae7ec47 |
45
.github/dependabot.yml
vendored
Normal file
45
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
ignore:
|
||||||
|
# git-town/action v1.3.x crashes on cyclic PR graphs (self-loop main->main
|
||||||
|
# fork PRs) via its topological-sort visualization. Pinned to v1.2.1 in
|
||||||
|
# git-town.yml; block v1.3.x until upstream tolerates cyclic edges.
|
||||||
|
- dependency-name: "git-town/action"
|
||||||
|
update-types:
|
||||||
|
- "version-update:semver-minor"
|
||||||
|
- "version-update:semver-major"
|
||||||
|
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directories:
|
||||||
|
- "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
groups:
|
||||||
|
aws-sdk:
|
||||||
|
patterns:
|
||||||
|
- "github.com/aws/aws-sdk-go-v2/*"
|
||||||
|
pion:
|
||||||
|
patterns:
|
||||||
|
- "github.com/pion/*"
|
||||||
|
gorm:
|
||||||
|
patterns:
|
||||||
|
- "gorm.io/*"
|
||||||
|
otel:
|
||||||
|
patterns:
|
||||||
|
- "go.opentelemetry.io/*"
|
||||||
|
testcontainers:
|
||||||
|
patterns:
|
||||||
|
- "github.com/testcontainers/testcontainers-go/*"
|
||||||
|
wireguard:
|
||||||
|
patterns:
|
||||||
|
- "golang.zx2c4.com/wireguard*"
|
||||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -12,6 +12,7 @@
|
|||||||
- [ ] Is a feature enhancement
|
- [ ] Is a feature enhancement
|
||||||
- [ ] It is a refactor
|
- [ ] It is a refactor
|
||||||
- [ ] Created tests that fail without the change (if possible)
|
- [ ] Created tests that fail without the change (if possible)
|
||||||
|
- [ ] This change does **not** modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — **OR** I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See [CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).
|
||||||
|
|
||||||
> By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).
|
> By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).
|
||||||
|
|
||||||
|
|||||||
109
.github/workflows/check-license-dependencies.yml
vendored
109
.github/workflows/check-license-dependencies.yml
vendored
@@ -2,16 +2,16 @@ name: Check License Dependencies
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'go.mod'
|
- "go.mod"
|
||||||
- 'go.sum'
|
- "go.sum"
|
||||||
- '.github/workflows/check-license-dependencies.yml'
|
- ".github/workflows/check-license-dependencies.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'go.mod'
|
- "go.mod"
|
||||||
- 'go.sum'
|
- "go.sum"
|
||||||
- '.github/workflows/check-license-dependencies.yml'
|
- ".github/workflows/check-license-dependencies.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-internal-dependencies:
|
check-internal-dependencies:
|
||||||
@@ -19,7 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for problematic license dependencies
|
- name: Check for problematic license dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -56,55 +59,57 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: "go.mod"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install go-licenses
|
- name: Install go-licenses
|
||||||
run: go install github.com/google/go-licenses@v1.6.0
|
run: go install github.com/google/go-licenses@v1.6.0
|
||||||
|
|
||||||
- name: Check for GPL/AGPL licensed dependencies
|
- name: Check for GPL/AGPL licensed dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
|
||||||
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
|
||||||
|
|
||||||
if [ -n "$COPYLEFT_DEPS" ]; then
|
|
||||||
echo "Found copyleft licensed dependencies:"
|
|
||||||
echo "$COPYLEFT_DEPS"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Filter out dependencies that are only pulled in by internal AGPL packages
|
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
||||||
INCOMPATIBLE=""
|
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
||||||
while IFS=',' read -r package url license; do
|
|
||||||
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
|
||||||
# Find ALL packages that import this GPL package using go list
|
|
||||||
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
|
||||||
|
|
||||||
# Check if any importer is NOT in management/signal/relay
|
if [ -n "$COPYLEFT_DEPS" ]; then
|
||||||
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
echo "Found copyleft licensed dependencies:"
|
||||||
|
echo "$COPYLEFT_DEPS"
|
||||||
if [ -n "$BSD_IMPORTER" ]; then
|
|
||||||
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
|
||||||
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
|
||||||
else
|
|
||||||
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$COPYLEFT_DEPS"
|
|
||||||
|
|
||||||
if [ -n "$INCOMPATIBLE" ]; then
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
|
||||||
echo -e "$INCOMPATIBLE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
# Filter out dependencies that are only pulled in by internal AGPL packages
|
||||||
|
INCOMPATIBLE=""
|
||||||
|
while IFS=',' read -r package url license; do
|
||||||
|
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
||||||
|
# Find ALL packages that import this GPL package using go list
|
||||||
|
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
||||||
|
|
||||||
|
# Check if any importer is NOT in management/signal/relay
|
||||||
|
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
||||||
|
|
||||||
|
if [ -n "$BSD_IMPORTER" ]; then
|
||||||
|
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
||||||
|
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
||||||
|
else
|
||||||
|
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$COPYLEFT_DEPS"
|
||||||
|
|
||||||
|
if [ -n "$INCOMPATIBLE" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
||||||
|
echo -e "$INCOMPATIBLE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
||||||
|
|||||||
2
.github/workflows/docs-ack.yml
vendored
2
.github/workflows/docs-ack.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify docs PR exists (and is open or merged)
|
- name: Verify docs PR exists (and is open or merged)
|
||||||
if: steps.validate.outputs.mode == 'added'
|
if: steps.validate.outputs.mode == 'added'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
id: verify
|
id: verify
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.extract.outputs.pr_number }}
|
pr_number: ${{ steps.extract.outputs.pr_number }}
|
||||||
|
|||||||
5
.github/workflows/forum.yml
vendored
5
.github/workflows/forum.yml
vendored
@@ -8,11 +8,10 @@ jobs:
|
|||||||
post:
|
post:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: roots/discourse-topic-github-release-action@main
|
- uses: roots/discourse-topic-github-release-action@557d74ea05b6cc0c47f555c1d5d28a89d904005b # v1.1.0
|
||||||
with:
|
with:
|
||||||
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
|
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
|
||||||
discourse-base-url: https://forum.netbird.io
|
discourse-base-url: https://forum.netbird.io
|
||||||
discourse-author-username: NetBird
|
discourse-author-username: NetBird
|
||||||
discourse-category: 17
|
discourse-category: 17
|
||||||
discourse-tags:
|
discourse-tags: releases
|
||||||
releases
|
|
||||||
|
|||||||
8
.github/workflows/git-town.yml
vendored
8
.github/workflows/git-town.yml
vendored
@@ -3,7 +3,7 @@ name: Git Town
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
git-town:
|
git-town:
|
||||||
@@ -15,7 +15,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: git-town/action@v1.2.1
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: git-town/action@3d8b878379abb1ee393fb49865a28b4a6c2cd3b0 # v1.2.1
|
||||||
with:
|
with:
|
||||||
skip-single-stacks: true
|
skip-single-stacks: true
|
||||||
|
|||||||
16
.github/workflows/golang-test-darwin.yml
vendored
16
.github/workflows/golang-test-darwin.yml
vendored
@@ -16,16 +16,18 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -43,5 +45,11 @@ 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)
|
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -coverprofile=coverage.txt -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)
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,client
|
||||||
|
|||||||
21
.github/workflows/golang-test-freebsd.yml
vendored
21
.github/workflows/golang-test-freebsd.yml
vendored
@@ -15,20 +15,31 @@ jobs:
|
|||||||
name: "Client / Unit"
|
name: "Client / Unit"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Read Go version from go.mod
|
||||||
|
id: goversion
|
||||||
|
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Test in FreeBSD
|
- name: Test in FreeBSD
|
||||||
id: test
|
id: test
|
||||||
uses: vmactions/freebsd-vm@v1
|
env:
|
||||||
|
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
||||||
|
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "14.2"
|
release: "15.0"
|
||||||
|
envs: "GO_VERSION"
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y curl pkgconf xorg
|
pkg install -y curl pkgconf xorg
|
||||||
GO_TARBALL="go1.25.3.freebsd-amd64.tar.gz"
|
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -vLO "$GO_URL"
|
curl -vLO "$GO_URL"
|
||||||
tar -C /usr/local -vxzf "$GO_TARBALL"
|
tar -C /usr/local -vxzf "$GO_TARBALL"
|
||||||
|
|
||||||
# -x - to print all executed commands
|
# -x - to print all executed commands
|
||||||
# -e - to faile on first error
|
# -e - to faile on first error
|
||||||
|
|||||||
199
.github/workflows/golang-test-linux.yml
vendored
199
.github/workflows/golang-test-linux.yml
vendored
@@ -18,9 +18,11 @@ jobs:
|
|||||||
management: ${{ steps.filter.outputs.management }}
|
management: ${{ steps.filter.outputs.management }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
@@ -28,7 +30,7 @@ jobs:
|
|||||||
- 'management/**'
|
- 'management/**'
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -36,10 +38,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -113,14 +115,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: ["386", "amd64"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -128,10 +132,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -154,18 +158,29 @@ 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)
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -coverprofile=coverage.txt -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined)
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,client
|
||||||
|
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
name: "Client (Docker) / Unit"
|
name: "Client (Docker) / Unit"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -177,7 +192,7 @@ jobs:
|
|||||||
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
id: cache-restore
|
id: cache-restore
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -231,10 +246,12 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -246,10 +263,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -268,23 +285,33 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
go test ${{ matrix.raceFlag }} \
|
go test ${{ matrix.raceFlag }} \
|
||||||
-exec 'sudo' \
|
-exec 'sudo' -coverprofile=coverage.txt \
|
||||||
-timeout 10m -p 1 ./relay/... ./shared/relay/...
|
-timeout 10m -p 1 ./relay/... ./shared/relay/...
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,relay
|
||||||
|
|
||||||
test_proxy:
|
test_proxy:
|
||||||
name: "Proxy / Unit"
|
name: "Proxy / Unit"
|
||||||
needs: [build-cache]
|
needs: [build-cache]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: ["386", "amd64"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -298,7 +325,7 @@ jobs:
|
|||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -316,7 +343,15 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
go test -timeout 10m -p 1 ./proxy/...
|
go test -timeout 10m -p 1 -coverprofile=coverage.txt ./proxy/...
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,proxy
|
||||||
|
|
||||||
test_signal:
|
test_signal:
|
||||||
name: "Signal / Unit"
|
name: "Signal / Unit"
|
||||||
@@ -324,14 +359,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: ["386", "amd64"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -343,10 +380,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -365,24 +402,34 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
go test \
|
go test \
|
||||||
-exec 'sudo' \
|
-exec 'sudo' -coverprofile=coverage.txt \
|
||||||
-timeout 10m ./signal/... ./shared/signal/...
|
-timeout 10m ./signal/... ./shared/signal/...
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,signal
|
||||||
|
|
||||||
test_management:
|
test_management:
|
||||||
name: "Management / Unit"
|
name: "Management / Unit"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'amd64' ]
|
arch: ["amd64"]
|
||||||
store: [ 'sqlite', 'postgres', 'mysql' ]
|
store: ["sqlite", "postgres", "mysql"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -390,10 +437,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -410,7 +457,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -427,23 +474,31 @@ jobs:
|
|||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
run: docker pull mlsmaycon/warmed-mysql:8
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
go test -tags=devcert \
|
go test -tags=devcert -coverprofile=coverage.txt \
|
||||||
-exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \
|
-exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/... ./shared/management/...
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,management
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
name: "Management / Benchmark"
|
name: "Management / Benchmark"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'amd64' ]
|
arch: ["amd64"]
|
||||||
store: [ 'sqlite', 'postgres' ]
|
store: ["sqlite", "postgres"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -474,10 +529,12 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -485,10 +542,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -505,7 +562,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -529,13 +586,13 @@ jobs:
|
|||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'amd64' ]
|
arch: ["amd64"]
|
||||||
store: [ 'sqlite', 'postgres' ]
|
store: ["sqlite", "postgres"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -566,10 +623,12 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -577,10 +636,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -597,7 +656,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -623,20 +682,22 @@ jobs:
|
|||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'amd64' ]
|
arch: ["amd64"]
|
||||||
store: [ 'sqlite', 'postgres']
|
store: ["sqlite", "postgres"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -644,10 +705,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -667,6 +728,14 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
go test -tags=integration \
|
go test -tags=integration -coverprofile=coverage.txt \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
||||||
-timeout 20m ./management/server/http/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: integration,management
|
||||||
|
|||||||
19
.github/workflows/golang-test-windows.yml
vendored
19
.github/workflows/golang-test-windows.yml
vendored
@@ -18,10 +18,12 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
@@ -33,7 +35,7 @@ jobs:
|
|||||||
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -44,16 +46,15 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
file-name: wintun.zip
|
destination: ${{ env.downloadPath }}\wintun.zip
|
||||||
location: ${{ env.downloadPath }}
|
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
||||||
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
|
||||||
|
|
||||||
- name: Decompressing wintun files
|
- name: Decompressing wintun files
|
||||||
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
run: tar -xvf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
||||||
|
|
||||||
|
|||||||
14
.github/workflows/golangci-lint.yml
vendored
14
.github/workflows/golangci-lint.yml
vendored
@@ -15,9 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals
|
||||||
skip: go.mod,go.sum,**/proxy/web/**
|
skip: go.mod,go.sum,**/proxy/web/**
|
||||||
@@ -38,13 +40,15 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Check for duplicate constants
|
- name: Check for duplicate constants
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -52,7 +56,7 @@ jobs:
|
|||||||
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: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
skip-cache: true
|
skip-cache: true
|
||||||
|
|||||||
4
.github/workflows/install-script-test.yml
vendored
4
.github/workflows/install-script-test.yml
vendored
@@ -22,7 +22,9 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: run install script
|
- name: run install script
|
||||||
env:
|
env:
|
||||||
|
|||||||
18
.github/workflows/mobile-build-validation.yml
vendored
18
.github/workflows/mobile-build-validation.yml
vendored
@@ -16,23 +16,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
||||||
with:
|
with:
|
||||||
cmdline-tools-version: 8512546
|
cmdline-tools-version: 8512546
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
|
||||||
with:
|
with:
|
||||||
java-version: "11"
|
java-version: "11"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
- name: NDK Cache
|
- name: NDK Cache
|
||||||
id: ndk-cache
|
id: ndk-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: /usr/local/lib/android/sdk/ndk
|
path: /usr/local/lib/android/sdk/ndk
|
||||||
key: ndk-cache-23.1.7779620
|
key: ndk-cache-23.1.7779620
|
||||||
@@ -52,9 +54,11 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
|
|||||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Validate PR title prefix
|
- name: Validate PR title prefix
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const title = context.payload.pull_request.title;
|
const title = context.payload.pull_request.title;
|
||||||
|
|||||||
87
.github/workflows/proto-version-check.yml
vendored
87
.github/workflows/proto-version-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check for proto tool version changes
|
- name: Check for proto tool version changes
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||||
@@ -20,34 +20,83 @@ jobs:
|
|||||||
per_page: 100,
|
per_page: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pbFiles = files.filter(f => f.filename.endsWith('.pb.go'));
|
// Cover renamed .pb.go files in addition to plain edits.
|
||||||
const missingPatch = pbFiles.filter(f => !f.patch).map(f => f.filename);
|
// Renamed entries land under the new path with previous_filename
|
||||||
if (missingPatch.length > 0) {
|
// pointing at the base-side name, so we read the base content
|
||||||
core.setFailed(
|
// from the old path when present.
|
||||||
`Cannot inspect patch data for:\n` +
|
const changedPbFiles = files
|
||||||
missingPatch.map(f => `- ${f}`).join('\n') +
|
.filter(f => (f.status === 'modified' || f.status === 'renamed')
|
||||||
`\nThis can happen with very large PRs. Verify proto versions manually.`
|
&& f.filename.endsWith('.pb.go'))
|
||||||
);
|
.map(f => ({
|
||||||
|
headPath: f.filename,
|
||||||
|
basePath: f.previous_filename || f.filename,
|
||||||
|
}));
|
||||||
|
if (changedPbFiles.length === 0) {
|
||||||
|
console.log('No modified or renamed .pb.go files to check');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const versionPattern = /^[+-]\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
|
|
||||||
const violations = [];
|
|
||||||
|
|
||||||
for (const file of pbFiles) {
|
// Matches the generator version headers protoc writes at the top
|
||||||
const changed = file.patch
|
// of generated files:
|
||||||
.split('\n')
|
// // protoc v3.21.12
|
||||||
.filter(line => versionPattern.test(line));
|
// // protoc-gen-go v1.26.0
|
||||||
if (changed.length > 0) {
|
// // - protoc-gen-go-grpc v1.6.1 (grpc files prefix with "- ")
|
||||||
|
// The optional "- " prefix and the optional -gen-go / -gen-go-grpc
|
||||||
|
// suffixes keep the *_grpc.pb.go headers in scope.
|
||||||
|
const versionPattern = /^\s*\/\/\s+(?:-\s+)?protoc(?:-gen-go(?:-grpc)?)?\s+v[\d.]+/;
|
||||||
|
const baseSha = context.payload.pull_request.base.sha;
|
||||||
|
const headSha = context.payload.pull_request.head.sha;
|
||||||
|
|
||||||
|
async function getVersionHeader(path, ref) {
|
||||||
|
try {
|
||||||
|
const res = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path,
|
||||||
|
ref,
|
||||||
|
});
|
||||||
|
if (!res.data.content) {
|
||||||
|
return { ok: false, reason: 'no inline content (file too large)' };
|
||||||
|
}
|
||||||
|
const content = Buffer.from(res.data.content, 'base64').toString('utf8');
|
||||||
|
const lines = content
|
||||||
|
.split('\n')
|
||||||
|
.slice(0, 20)
|
||||||
|
.filter(line => versionPattern.test(line));
|
||||||
|
return { ok: true, lines };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, reason: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
for (const file of changedPbFiles) {
|
||||||
|
const [base, head] = await Promise.all([
|
||||||
|
getVersionHeader(file.basePath, baseSha),
|
||||||
|
getVersionHeader(file.headPath, headSha),
|
||||||
|
]);
|
||||||
|
if (!base.ok || !head.ok) {
|
||||||
|
core.warning(
|
||||||
|
`Skipping ${file.headPath}: base=${base.ok ? 'ok' : base.reason}, head=${head.ok ? 'ok' : head.reason}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (base.lines.join('\n') !== head.lines.join('\n')) {
|
||||||
violations.push({
|
violations.push({
|
||||||
file: file.filename,
|
file: file.basePath === file.headPath
|
||||||
lines: changed,
|
? file.headPath
|
||||||
|
: `${file.basePath} → ${file.headPath}`,
|
||||||
|
base: base.lines,
|
||||||
|
head: head.lines,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
const details = violations.map(v =>
|
const details = violations.map(v =>
|
||||||
`${v.file}:\n${v.lines.map(l => ' ' + l).join('\n')}`
|
`${v.file}:\n` +
|
||||||
|
` base:\n${v.base.map(l => ' ' + l).join('\n') || ' (none)'}\n` +
|
||||||
|
` head:\n${v.head.map(l => ' ' + l).join('\n') || ' (none)'}`
|
||||||
).join('\n\n');
|
).join('\n\n');
|
||||||
|
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
|
|||||||
225
.github/workflows/release.yml
vendored
225
.github/workflows/release.yml
vendored
@@ -9,10 +9,13 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.4"
|
SIGN_PIPE_VER: "v0.1.6"
|
||||||
GORELEASER_VER: "v2.14.3"
|
GORELEASER_VER: "v2.16.0"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
|
flags: ""
|
||||||
|
SKIP_PUBLISH: "true"
|
||||||
|
SKIP_DOCKER_PUSH: "false"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -24,13 +27,15 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Generate FreeBSD port diff
|
- name: Generate FreeBSD port diff
|
||||||
run: bash release_files/freebsd-port-diff.sh
|
run: bash -x release_files/freebsd-port-diff.sh
|
||||||
|
|
||||||
- name: Generate FreeBSD port issue body
|
- name: Generate FreeBSD port issue body
|
||||||
run: bash release_files/freebsd-port-issue-body.sh
|
run: bash -x release_files/freebsd-port-issue-body.sh
|
||||||
|
|
||||||
- name: Check if diff was generated
|
- name: Check if diff was generated
|
||||||
id: check_diff
|
id: check_diff
|
||||||
@@ -51,19 +56,26 @@ jobs:
|
|||||||
echo "Generated files for version: $VERSION"
|
echo "Generated files for version: $VERSION"
|
||||||
cat netbird-*.diff
|
cat netbird-*.diff
|
||||||
|
|
||||||
|
- name: Read Go version from go.mod
|
||||||
|
id: goversion
|
||||||
|
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Test FreeBSD port
|
- name: Test FreeBSD port
|
||||||
if: steps.check_diff.outputs.diff_exists == 'true'
|
if: steps.check_diff.outputs.diff_exists == 'true'
|
||||||
uses: vmactions/freebsd-vm@v1
|
env:
|
||||||
|
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
||||||
|
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "15.0"
|
release: "15.0"
|
||||||
|
envs: "GO_VERSION"
|
||||||
prepare: |
|
prepare: |
|
||||||
# Install required packages
|
# Install required packages
|
||||||
pkg install -y git curl portlint go
|
pkg install -y git curl portlint
|
||||||
|
|
||||||
# Install Go for building
|
# Install Go for building
|
||||||
GO_TARBALL="go1.25.5.freebsd-amd64.tar.gz"
|
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -LO "$GO_URL"
|
curl -LO "$GO_URL"
|
||||||
tar -C /usr/local -xzf "$GO_TARBALL"
|
tar -C /usr/local -xzf "$GO_TARBALL"
|
||||||
@@ -93,19 +105,19 @@ jobs:
|
|||||||
|
|
||||||
# Show patched Makefile
|
# Show patched Makefile
|
||||||
version=$(cat security/netbird/Makefile | grep -E '^DISTVERSION=' | awk '{print $NF}')
|
version=$(cat security/netbird/Makefile | grep -E '^DISTVERSION=' | awk '{print $NF}')
|
||||||
|
|
||||||
cd /usr/ports/security/netbird
|
cd /usr/ports/security/netbird
|
||||||
export BATCH=yes
|
export BATCH=yes
|
||||||
make package
|
make package
|
||||||
pkg add ./work/pkg/netbird-*.pkg
|
pkg add ./work/pkg/netbird-*.pkg
|
||||||
|
|
||||||
netbird version | grep "$version"
|
netbird version | grep "$version"
|
||||||
|
|
||||||
echo "FreeBSD port test completed successfully!"
|
echo "FreeBSD port test completed successfully!"
|
||||||
|
|
||||||
- name: Upload FreeBSD port files
|
- name: Upload FreeBSD port files
|
||||||
if: steps.check_diff.outputs.diff_exists == 'true'
|
if: steps.check_diff.outputs.diff_exists == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: freebsd-port-files
|
name: freebsd-port-files
|
||||||
path: |
|
path: |
|
||||||
@@ -121,29 +133,45 @@ jobs:
|
|||||||
windows_packages_artifact_url: ${{ steps.upload_windows_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 }}
|
macos_packages_artifact_url: ${{ steps.upload_macos_packages.outputs.artifact-url }}
|
||||||
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
||||||
env:
|
|
||||||
flags: ""
|
|
||||||
steps:
|
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(.*)$'
|
|
||||||
|
|
||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Parse semver string
|
||||||
|
id: semver_parser
|
||||||
|
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
|
|
||||||
|
- name: Set snapshot flag
|
||||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set build vars
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
if [[ "x-${{ steps.semver_parser.outputs.prerelease }}" == "x-" && "x-${{ github.repository }}" == "x-netbirdio/netbird" ]]; then
|
||||||
|
echo "x-${{ github.repository }}"
|
||||||
|
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||||
|
echo "SKIP_PUBLISH=false" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "x-${{ github.repository }}"
|
||||||
|
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "x-${{ github.repository }}" != "x-netbirdio/netbird" ]]; then
|
||||||
|
echo "SKIP_DOCKER_PUSH=true" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -153,21 +181,23 @@ jobs:
|
|||||||
${{ runner.os }}-go-releaser-
|
${{ runner.os }}-go-releaser-
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
- name: run openapi generator
|
||||||
|
run: bash shared/management/http/api/generate.sh
|
||||||
- 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 QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a #v4.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
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
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -191,7 +221,7 @@ jobs:
|
|||||||
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -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/resources_windows_arm64.syso
|
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -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/resources_windows_arm64.syso
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --clean ${{ env.flags }}
|
args: release --clean ${{ env.flags }}
|
||||||
@@ -202,6 +232,8 @@ jobs:
|
|||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||||
NFPM_NETBIRD_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
NFPM_NETBIRD_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||||
|
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
||||||
|
SKIP_DOCKER_PUSH: ${{ env.SKIP_DOCKER_PUSH }}
|
||||||
- name: Verify RPM signatures
|
- name: Verify RPM signatures
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
||||||
@@ -282,28 +314,28 @@ jobs:
|
|||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release
|
id: upload_release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
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
|
id: upload_linux_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
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
|
id: upload_windows_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
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
|
id: upload_macos_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: macos-packages
|
name: macos-packages
|
||||||
path: dist/netbird_darwin**
|
path: dist/netbird_darwin**
|
||||||
@@ -314,27 +346,40 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
||||||
steps:
|
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(.*)$'
|
|
||||||
|
|
||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Parse semver string
|
||||||
|
id: semver_parser
|
||||||
|
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
|
|
||||||
|
- name: Set snapshot flag
|
||||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set build vars
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
if [[ "x-${{ steps.semver_parser.outputs.prerelease }}" == "x-" && "x-${{ github.repository }}" == "x-netbirdio/netbird" ]]; then
|
||||||
|
echo "x-${{ github.repository }}"
|
||||||
|
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||||
|
echo "SKIP_PUBLISH=false" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "x-${{ github.repository }}"
|
||||||
|
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -375,7 +420,7 @@ jobs:
|
|||||||
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/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
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
||||||
@@ -386,6 +431,7 @@ jobs:
|
|||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||||
NFPM_NETBIRD_UI_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
NFPM_NETBIRD_UI_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||||
|
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
||||||
- name: Verify RPM signatures
|
- name: Verify RPM signatures
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
||||||
@@ -404,7 +450,7 @@ jobs:
|
|||||||
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
|
id: upload_release_ui
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -418,16 +464,17 @@ jobs:
|
|||||||
- 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
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
persist-credentials: false
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -441,7 +488,7 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
||||||
@@ -449,7 +496,7 @@ jobs:
|
|||||||
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
|
id: upload_release_ui_darwin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -474,27 +521,26 @@ jobs:
|
|||||||
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
||||||
downloadPath: '${{ github.workspace }}\temp'
|
downloadPath: '${{ github.workspace }}\temp'
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: booxmedialtd/ws-action-parse-semver@v1
|
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
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
|
- name: Add 7-Zip to PATH
|
||||||
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: Download release artifacts
|
- name: Download release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Download UI release artifacts
|
- name: Download UI release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: release-ui
|
path: release-ui
|
||||||
@@ -514,29 +560,27 @@ jobs:
|
|||||||
Get-ChildItem $workdir
|
Get-ChildItem $workdir
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
file-name: wintun.zip
|
destination: ${{ env.downloadPath }}\wintun.zip
|
||||||
location: ${{ env.downloadPath }}
|
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
||||||
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
|
||||||
|
|
||||||
- name: Decompress wintun files
|
- name: Decompress wintun files
|
||||||
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
run: tar -xvf "${{ env.downloadPath }}\wintun.zip" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- name: Move wintun.dll into dist
|
- name: Move wintun.dll into dist
|
||||||
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
- name: Download Mesa3D (amd64 only)
|
- name: Download Mesa3D (amd64 only)
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
id: download-mesa3d
|
id: download-mesa3d
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
|
url: https://pkgs.netbird.io/mesa3d/MesaForWindows-x64-20.1.8.7z
|
||||||
file-name: mesa3d.7z
|
destination: ${{ env.downloadPath }}\mesa3d.7z
|
||||||
location: ${{ env.downloadPath }}
|
sha256: 71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9
|
||||||
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
|
|
||||||
|
|
||||||
- name: Extract Mesa3D driver (amd64 only)
|
- name: Extract Mesa3D driver (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
@@ -547,35 +591,38 @@ jobs:
|
|||||||
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
- name: Download EnVar plugin for NSIS
|
- name: Download EnVar plugin for NSIS
|
||||||
uses: carlosperate/download-file-action@v2
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
|
url: https://pkgs.netbird.io/nsis/EnVar_plugin.zip
|
||||||
file-name: envar_plugin.zip
|
destination: ${{ github.workspace }}\envar_plugin.zip
|
||||||
location: ${{ github.workspace }}
|
sha256: e9aa92de351345ed82795251d838f1ae9041ba35af9d381a5780c7843b01f56a
|
||||||
|
|
||||||
- name: Extract EnVar plugin
|
- name: Extract EnVar plugin
|
||||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
||||||
|
|
||||||
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
|
url: https://pkgs.netbird.io/nsis/ShellExecAsUser_amd64-Unicode.7z
|
||||||
file-name: ShellExecAsUser_amd64-Unicode.7z
|
destination: ${{ github.workspace }}\ShellExecAsUser_amd64-Unicode.7z
|
||||||
location: ${{ github.workspace }}
|
sha256: 0a55ea25c7330a92cec028eda8afcaf1b1a7092e0dfb77c21c8f654564b4ff9d
|
||||||
|
|
||||||
- name: Extract ShellExecAsUser plugin (amd64 only)
|
- name: Extract ShellExecAsUser plugin (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||||
|
|
||||||
- name: Build NSIS installer
|
- name: Build NSIS installer
|
||||||
uses: joncloud/makensis-action@v3.3
|
shell: pwsh
|
||||||
with:
|
|
||||||
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
|
|
||||||
script-file: client/installer.nsis
|
|
||||||
arguments: "/V4 /DARCH=${{ matrix.arch }}"
|
|
||||||
env:
|
env:
|
||||||
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||||
|
$srcPlugins = "${{ github.workspace }}\NSIS_Plugins\Plugins"
|
||||||
|
Get-ChildItem -Path $srcPlugins -Recurse -Filter *.dll |
|
||||||
|
Copy-Item -Destination $nsisPluginDir -Force
|
||||||
|
& "C:\Program Files (x86)\NSIS\makensis.exe" /V4 "/DARCH=${{ matrix.arch }}" client\installer.nsis
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" }
|
||||||
|
|
||||||
- name: Rename NSIS installer
|
- name: Rename NSIS installer
|
||||||
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||||
@@ -592,7 +639,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload installer artifacts
|
- name: Upload installer artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: windows-installer-test-${{ matrix.arch }}
|
name: windows-installer-test-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -611,7 +658,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Create or update PR comment
|
- name: Create or update PR comment
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
env:
|
env:
|
||||||
RELEASE_RESULT: ${{ needs.release.result }}
|
RELEASE_RESULT: ${{ needs.release.result }}
|
||||||
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||||
@@ -703,7 +750,7 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger binaries sign pipelines
|
- name: Trigger binaries sign pipelines
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: Sign bin and installer
|
workflow: Sign bin and installer
|
||||||
repo: netbirdio/sign-pipelines
|
repo: netbirdio/sign-pipelines
|
||||||
|
|||||||
4
.github/workflows/sync-main.yml
vendored
4
.github/workflows/sync-main.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger main branch sync
|
- name: Trigger main branch sync
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: sync-main.yml
|
workflow: sync-main.yml
|
||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "sha": "${{ github.sha }}" }'
|
inputs: '{ "sha": "${{ github.sha }}" }'
|
||||||
|
|||||||
10
.github/workflows/sync-tag.yml
vendored
10
.github/workflows/sync-tag.yml
vendored
@@ -3,7 +3,7 @@ name: sync tag
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger release tag sync
|
- name: Trigger release tag sync
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: sync-tag.yml
|
workflow: sync-tag.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger android-client submodule bump
|
- name: Trigger android-client submodule bump
|
||||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -42,10 +42,10 @@ jobs:
|
|||||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger ios-client submodule bump
|
- name: Trigger ios-client submodule bump
|
||||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
repo: netbirdio/ios-client
|
repo: netbirdio/ios-client
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
|
|||||||
26
.github/workflows/test-infrastructure-files.yml
vendored
26
.github/workflows/test-infrastructure-files.yml
vendored
@@ -6,10 +6,10 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'infrastructure_files/**'
|
- "infrastructure_files/**"
|
||||||
- '.github/workflows/test-infrastructure-files.yml'
|
- ".github/workflows/test-infrastructure-files.yml"
|
||||||
- 'management/cmd/**'
|
- "management/cmd/**"
|
||||||
- 'signal/cmd/**'
|
- "signal/cmd/**"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
store: [ 'sqlite', 'postgres', 'mysql' ]
|
store: ["sqlite", "postgres", "mysql"]
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
||||||
@@ -68,15 +68,17 @@ jobs:
|
|||||||
run: sudo apt-get install -y curl
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -139,8 +141,8 @@ jobs:
|
|||||||
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
||||||
CI_NETBIRD_SIGNAL_PORT: 12345
|
CI_NETBIRD_SIGNAL_PORT: 12345
|
||||||
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
|
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
|
||||||
NETBIRD_STORE_ENGINE_POSTGRES_DSN: '${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$'
|
NETBIRD_STORE_ENGINE_POSTGRES_DSN: "${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$"
|
||||||
NETBIRD_STORE_ENGINE_MYSQL_DSN: '${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$'
|
NETBIRD_STORE_ENGINE_MYSQL_DSN: "${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$"
|
||||||
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
||||||
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
||||||
CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false
|
CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false
|
||||||
@@ -254,7 +256,9 @@ jobs:
|
|||||||
run: sudo apt-get install -y jq
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: run script with Zitadel PostgreSQL
|
- name: run script with Zitadel PostgreSQL
|
||||||
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|||||||
8
.github/workflows/update-docs.yml
vendored
8
.github/workflows/update-docs.yml
vendored
@@ -3,9 +3,9 @@ name: update docs
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
paths:
|
paths:
|
||||||
- 'shared/management/http/api/openapi.yml'
|
- "shared/management/http/api/openapi.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger_docs_api_update:
|
trigger_docs_api_update:
|
||||||
@@ -13,10 +13,10 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger API pages generation
|
- name: Trigger API pages generation
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: generate api pages
|
workflow: generate api pages
|
||||||
repo: netbirdio/docs
|
repo: netbirdio/docs
|
||||||
ref: "refs/heads/main"
|
ref: "refs/heads/main"
|
||||||
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref }}" }'
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
|
|||||||
19
.github/workflows/wasm-build-validation.yml
vendored
19
.github/workflows/wasm-build-validation.yml
vendored
@@ -19,15 +19,17 @@ jobs:
|
|||||||
GOARCH: wasm
|
GOARCH: wasm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
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: Install golangci-lint
|
- name: Install golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
@@ -42,9 +44,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Build Wasm client
|
- name: Build Wasm client
|
||||||
@@ -61,8 +65,7 @@ jobs:
|
|||||||
|
|
||||||
echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"
|
echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"
|
||||||
|
|
||||||
if [ ${SIZE} -gt 58720256 ]; then
|
if [ ${SIZE} -gt 62914560 ]; then
|
||||||
echo "Wasm binary size (${SIZE_MB}MB) exceeds 56MB limit!"
|
echo "Wasm binary size (${SIZE_MB}MB) exceeds 60MB limit!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
862
.goreleaser.yaml
862
.goreleaser.yaml
@@ -1,5 +1,7 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
env:
|
||||||
|
- SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }}
|
||||||
|
- SKIP_DOCKER_PUSH={{ if index .Env "SKIP_DOCKER_PUSH" }}{{ .Env.SKIP_DOCKER_PUSH }}{{ else }}false{{ end }}
|
||||||
project_name: netbird
|
project_name: netbird
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-wasm
|
- id: netbird-wasm
|
||||||
@@ -74,6 +76,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
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 }}"
|
||||||
@@ -88,6 +92,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
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 }}"
|
||||||
@@ -102,6 +108,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
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 }}"
|
||||||
@@ -122,6 +130,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
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 }}"
|
||||||
@@ -136,6 +146,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
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 }}"
|
||||||
@@ -150,6 +162,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.CommitDate}}
|
- -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.CommitDate}}
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -170,6 +184,8 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
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 }}"
|
||||||
@@ -222,670 +238,192 @@ nfpms:
|
|||||||
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 }}'
|
||||||
dockers:
|
dockers_v2:
|
||||||
- image_templates:
|
- id: netbird
|
||||||
- netbirdio/netbird:{{ .Version }}-amd64
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-amd64
|
ids:
|
||||||
ids:
|
- netbird
|
||||||
- netbird
|
images:
|
||||||
goarch: amd64
|
- netbirdio/netbird
|
||||||
use: buildx
|
- ghcr.io/netbirdio/netbird
|
||||||
dockerfile: client/Dockerfile
|
tags:
|
||||||
extra_files:
|
- "v{{ .Version }}"
|
||||||
- client/netbird-entrypoint.sh
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
build_flag_templates:
|
dockerfile: client/Dockerfile
|
||||||
- "--platform=linux/amd64"
|
extra_files:
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- client/netbird-entrypoint.sh
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
platforms:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- linux/amd64
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- linux/arm64
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
- linux/arm/6
|
||||||
- "--label=maintainer=dev@netbird.io"
|
annotations:
|
||||||
- image_templates:
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
- netbirdio/netbird:{{ .Version }}-arm64v8
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
ids:
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- netbird
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
goarch: arm64
|
"maintainer": "dev@netbird.io"
|
||||||
use: buildx
|
- id: netbird-rootless
|
||||||
dockerfile: client/Dockerfile
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
extra_files:
|
ids:
|
||||||
- client/netbird-entrypoint.sh
|
- netbird
|
||||||
build_flag_templates:
|
images:
|
||||||
- "--platform=linux/arm64"
|
- netbirdio/netbird
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- ghcr.io/netbirdio/netbird
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
tags:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "v{{ .Version }}-rootless"
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
dockerfile: client/Dockerfile-rootless
|
||||||
- "--label=maintainer=dev@netbird.io"
|
extra_files:
|
||||||
- image_templates:
|
- client/netbird-entrypoint.sh
|
||||||
- netbirdio/netbird:{{ .Version }}-arm
|
platforms:
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm
|
- linux/amd64
|
||||||
ids:
|
- linux/arm64
|
||||||
- netbird
|
- linux/arm/6
|
||||||
goarch: arm
|
annotations:
|
||||||
goarm: 6
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
use: buildx
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
dockerfile: client/Dockerfile
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
extra_files:
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- client/netbird-entrypoint.sh
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
build_flag_templates:
|
"maintainer": "dev@netbird.io"
|
||||||
- "--platform=linux/arm"
|
- id: relay
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
ids:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- netbird-relay
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
images:
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
- netbirdio/relay
|
||||||
- "--label=maintainer=dev@netbird.io"
|
- ghcr.io/netbirdio/relay
|
||||||
|
tags:
|
||||||
- image_templates:
|
- "v{{ .Version }}"
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-amd64
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64
|
dockerfile: relay/Dockerfile
|
||||||
ids:
|
platforms:
|
||||||
- netbird
|
- linux/amd64
|
||||||
goarch: amd64
|
- linux/arm64
|
||||||
use: buildx
|
- linux/arm
|
||||||
dockerfile: client/Dockerfile-rootless
|
annotations:
|
||||||
extra_files:
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
- client/netbird-entrypoint.sh
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
build_flag_templates:
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
- "--platform=linux/amd64"
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
"maintainer": "dev@netbird.io"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- id: signal
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
ids:
|
||||||
- "--label=maintainer=dev@netbird.io"
|
- netbird-signal
|
||||||
- image_templates:
|
images:
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
- netbirdio/signal
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
- ghcr.io/netbirdio/signal
|
||||||
ids:
|
tags:
|
||||||
- netbird
|
- "v{{ .Version }}"
|
||||||
goarch: arm64
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
use: buildx
|
dockerfile: signal/Dockerfile
|
||||||
dockerfile: client/Dockerfile-rootless
|
platforms:
|
||||||
extra_files:
|
- linux/amd64
|
||||||
- client/netbird-entrypoint.sh
|
- linux/arm64
|
||||||
build_flag_templates:
|
- linux/arm
|
||||||
- "--platform=linux/arm64"
|
annotations:
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
"maintainer": "dev@netbird.io"
|
||||||
- image_templates:
|
- id: management
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-arm
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm
|
ids:
|
||||||
ids:
|
- netbird-mgmt
|
||||||
- netbird
|
images:
|
||||||
goarch: arm
|
- netbirdio/management
|
||||||
goarm: 6
|
- ghcr.io/netbirdio/management
|
||||||
use: buildx
|
tags:
|
||||||
dockerfile: client/Dockerfile-rootless
|
- "v{{ .Version }}"
|
||||||
extra_files:
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
- client/netbird-entrypoint.sh
|
dockerfile: management/Dockerfile
|
||||||
build_flag_templates:
|
platforms:
|
||||||
- "--platform=linux/arm"
|
- linux/amd64
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- linux/arm64
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
- linux/arm
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
annotations:
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- image_templates:
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
- netbirdio/relay:{{ .Version }}-amd64
|
"maintainer": "dev@netbird.io"
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-amd64
|
- id: upload
|
||||||
ids:
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- netbird-relay
|
ids:
|
||||||
goarch: amd64
|
- netbird-upload
|
||||||
use: buildx
|
images:
|
||||||
dockerfile: relay/Dockerfile
|
- netbirdio/upload
|
||||||
build_flag_templates:
|
- ghcr.io/netbirdio/upload
|
||||||
- "--platform=linux/amd64"
|
tags:
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- "v{{ .Version }}"
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
dockerfile: upload-server/Dockerfile
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
platforms:
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
- linux/amd64
|
||||||
- "--label=maintainer=dev@netbird.io"
|
- linux/arm64
|
||||||
- image_templates:
|
- linux/arm
|
||||||
- netbirdio/relay:{{ .Version }}-arm64v8
|
annotations:
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
ids:
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
- netbird-relay
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
goarch: arm64
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
use: buildx
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
dockerfile: relay/Dockerfile
|
"maintainer": "dev@netbird.io"
|
||||||
build_flag_templates:
|
- id: netbird-server
|
||||||
- "--platform=linux/arm64"
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
ids:
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
- netbird-server
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
images:
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- netbirdio/netbird-server
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
- ghcr.io/netbirdio/netbird-server
|
||||||
- "--label=maintainer=dev@netbird.io"
|
tags:
|
||||||
- image_templates:
|
- "v{{ .Version }}"
|
||||||
- netbirdio/relay:{{ .Version }}-arm
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-arm
|
dockerfile: combined/Dockerfile
|
||||||
ids:
|
platforms:
|
||||||
- netbird-relay
|
- linux/amd64
|
||||||
goarch: arm
|
- linux/arm64
|
||||||
goarm: 6
|
- linux/arm
|
||||||
use: buildx
|
annotations:
|
||||||
dockerfile: relay/Dockerfile
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
build_flag_templates:
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
- "--platform=linux/arm"
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
"maintainer": "dev@netbird.io"
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- id: netbird-proxy
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
ids:
|
||||||
- image_templates:
|
- netbird-proxy
|
||||||
- netbirdio/signal:{{ .Version }}-amd64
|
images:
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-amd64
|
- netbirdio/reverse-proxy
|
||||||
ids:
|
- ghcr.io/netbirdio/reverse-proxy
|
||||||
- netbird-signal
|
tags:
|
||||||
goarch: amd64
|
- "v{{ .Version }}"
|
||||||
use: buildx
|
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
||||||
dockerfile: signal/Dockerfile
|
dockerfile: proxy/Dockerfile
|
||||||
build_flag_templates:
|
platforms:
|
||||||
- "--platform=linux/amd64"
|
- linux/amd64
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- linux/arm64
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
- linux/arm
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
annotations:
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
"org.opencontainers.image.title": "{{.ProjectName}}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
- image_templates:
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- netbirdio/signal:{{ .Version }}-arm64v8
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8
|
"maintainer": "dev@netbird.io"
|
||||||
ids:
|
|
||||||
- netbird-signal
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: signal/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/signal:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-arm
|
|
||||||
ids:
|
|
||||||
- netbird-signal
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
use: buildx
|
|
||||||
dockerfile: signal/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-amd64
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-amd64
|
|
||||||
ids:
|
|
||||||
- netbird-mgmt
|
|
||||||
goarch: amd64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: management/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-arm64v8
|
|
||||||
ids:
|
|
||||||
- netbird-mgmt
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: management/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-arm
|
|
||||||
ids:
|
|
||||||
- netbird-mgmt
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
use: buildx
|
|
||||||
dockerfile: management/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-debug-amd64
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-debug-amd64
|
|
||||||
ids:
|
|
||||||
- netbird-mgmt
|
|
||||||
goarch: amd64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: management/Dockerfile.debug
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm64v8
|
|
||||||
ids:
|
|
||||||
- netbird-mgmt
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: management/Dockerfile.debug
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-debug-arm
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm
|
|
||||||
ids:
|
|
||||||
- netbird-mgmt
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
use: buildx
|
|
||||||
dockerfile: management/Dockerfile.debug
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/upload:{{ .Version }}-amd64
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
|
||||||
ids:
|
|
||||||
- netbird-upload
|
|
||||||
goarch: amd64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: upload-server/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/upload:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
|
||||||
ids:
|
|
||||||
- netbird-upload
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: upload-server/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/upload:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
|
||||||
ids:
|
|
||||||
- netbird-upload
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
use: buildx
|
|
||||||
dockerfile: upload-server/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-amd64
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
|
||||||
ids:
|
|
||||||
- netbird-server
|
|
||||||
goarch: amd64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: combined/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
|
||||||
ids:
|
|
||||||
- netbird-server
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: combined/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
|
||||||
ids:
|
|
||||||
- netbird-server
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
use: buildx
|
|
||||||
dockerfile: combined/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-amd64
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-amd64
|
|
||||||
ids:
|
|
||||||
- netbird-proxy
|
|
||||||
goarch: amd64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: proxy/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
|
||||||
ids:
|
|
||||||
- netbird-proxy
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: proxy/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
- image_templates:
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm
|
|
||||||
ids:
|
|
||||||
- netbird-proxy
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
use: buildx
|
|
||||||
dockerfile: proxy/Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
|
||||||
- "--label=maintainer=dev@netbird.io"
|
|
||||||
docker_manifests:
|
|
||||||
- name_template: netbirdio/netbird:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/netbird:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/netbird:{{ .Version }}-arm
|
|
||||||
- netbirdio/netbird:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/netbird:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/netbird:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/netbird:{{ .Version }}-arm
|
|
||||||
- netbirdio/netbird:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/netbird:{{ .Version }}-rootless
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-arm
|
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/netbird:rootless-latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-arm
|
|
||||||
- netbirdio/netbird:{{ .Version }}-rootless-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/relay:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/relay:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/relay:{{ .Version }}-arm
|
|
||||||
- netbirdio/relay:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/relay:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/relay:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/relay:{{ .Version }}-arm
|
|
||||||
- netbirdio/relay:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/signal:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/signal:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/signal:{{ .Version }}-arm
|
|
||||||
- netbirdio/signal:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/signal:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/signal:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/signal:{{ .Version }}-arm
|
|
||||||
- netbirdio/signal:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/management:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/management:{{ .Version }}-arm
|
|
||||||
- netbirdio/management:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/management:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/management:{{ .Version }}-arm
|
|
||||||
- netbirdio/management:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/management:debug-latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
|
||||||
- netbirdio/management:{{ .Version }}-debug-arm
|
|
||||||
- netbirdio/management:{{ .Version }}-debug-amd64
|
|
||||||
- name_template: netbirdio/upload:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/upload:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/upload:{{ .Version }}-arm
|
|
||||||
- netbirdio/upload:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/upload:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/upload:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/upload:{{ .Version }}-arm
|
|
||||||
- netbirdio/upload:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/netbird-server:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-arm
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/netbird-server:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-arm
|
|
||||||
- netbirdio/netbird-server:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird:{{ .Version }}-rootless
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird:rootless-latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm
|
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/relay:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/relay:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/relay:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/signal:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/signal:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/signal:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/management:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/management:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/management:debug-latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm64v8
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm
|
|
||||||
- ghcr.io/netbirdio/management:{{ .Version }}-debug-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/upload:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/upload:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird-server:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird-server:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/reverse-proxy:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-arm
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: netbirdio/reverse-proxy:latest
|
|
||||||
image_templates:
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-arm
|
|
||||||
- netbirdio/reverse-proxy:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/reverse-proxy:{{ .Version }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/reverse-proxy:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm
|
|
||||||
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-amd64
|
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- ids:
|
- ids:
|
||||||
- default
|
- default
|
||||||
|
skip_upload: "{{ .Env.SKIP_PUBLISH }}"
|
||||||
repository:
|
repository:
|
||||||
owner: netbirdio
|
owner: netbirdio
|
||||||
name: homebrew-tap
|
name: homebrew-tap
|
||||||
@@ -902,6 +440,7 @@ brews:
|
|||||||
|
|
||||||
uploads:
|
uploads:
|
||||||
- name: debian
|
- name: debian
|
||||||
|
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||||
ids:
|
ids:
|
||||||
- netbird_deb
|
- netbird_deb
|
||||||
mode: archive
|
mode: archive
|
||||||
@@ -910,6 +449,7 @@ uploads:
|
|||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
- name: yum
|
- name: yum
|
||||||
|
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||||
ids:
|
ids:
|
||||||
- netbird_rpm
|
- netbird_rpm
|
||||||
mode: archive
|
mode: archive
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
env:
|
||||||
|
- SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }}
|
||||||
project_name: netbird-ui
|
project_name: netbird-ui
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-ui
|
- id: netbird-ui
|
||||||
@@ -101,6 +102,7 @@ nfpms:
|
|||||||
|
|
||||||
uploads:
|
uploads:
|
||||||
- name: debian
|
- name: debian
|
||||||
|
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||||
ids:
|
ids:
|
||||||
- netbird_ui_deb
|
- netbird_ui_deb
|
||||||
mode: archive
|
mode: archive
|
||||||
@@ -109,6 +111,7 @@ uploads:
|
|||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
- name: yum
|
- name: yum
|
||||||
|
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||||
ids:
|
ids:
|
||||||
- netbird_ui_rpm
|
- netbird_ui_rpm
|
||||||
mode: archive
|
mode: archive
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ If you haven't already, join our slack workspace [here](https://docs.netbird.io/
|
|||||||
- [Contributing to NetBird](#contributing-to-netbird)
|
- [Contributing to NetBird](#contributing-to-netbird)
|
||||||
- [Contents](#contents)
|
- [Contents](#contents)
|
||||||
- [Code of conduct](#code-of-conduct)
|
- [Code of conduct](#code-of-conduct)
|
||||||
|
- [Discuss changes with the NetBird team first](#discuss-changes-with-the-netbird-team-first)
|
||||||
- [Directory structure](#directory-structure)
|
- [Directory structure](#directory-structure)
|
||||||
- [Development setup](#development-setup)
|
- [Development setup](#development-setup)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
@@ -33,6 +34,14 @@ Conduct which can be found in the file [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|||||||
By participating, you are expected to uphold this code. Please report
|
By participating, you are expected to uphold this code. Please report
|
||||||
unacceptable behavior to community@netbird.io.
|
unacceptable behavior to community@netbird.io.
|
||||||
|
|
||||||
|
## Discuss changes with the NetBird team first
|
||||||
|
|
||||||
|
Changes to the **public API**, **gRPC protocols**, **functionality behavior**, **CLI / service flags**, or **new features** should be discussed with the NetBird team before you start the work. These surfaces are part of NetBird's contract with operators, self-hosters, and downstream integrators, and changes to them have compatibility, security, and release-planning implications that benefit from an early conversation.
|
||||||
|
|
||||||
|
Open an issue or reach out on [Slack](https://docs.netbird.io/slack-url) to talk through what you have in mind. We'll help shape the change, flag any constraints we know about, and confirm the direction so the PR review can focus on implementation rather than design.
|
||||||
|
|
||||||
|
Typical bug fixes, internal refactors, documentation updates, and tests do not need pre-discussion — open the PR directly.
|
||||||
|
|
||||||
## Directory structure
|
## Directory structure
|
||||||
|
|
||||||
The NetBird project monorepo is organized to maintain most of its individual dependencies code within their directories, except for a few auxiliary or shared packages.
|
The NetBird project monorepo is organized to maintain most of its individual dependencies code within their directories, except for a few auxiliary or shared packages.
|
||||||
|
|||||||
153
README.md
153
README.md
@@ -1,147 +1,134 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<br/>
|
<p align="center">
|
||||||
<br/>
|
<img width="234" src="docs/media/logo-full.png" alt="NetBird logo"/>
|
||||||
<p align="center">
|
</p>
|
||||||
<img width="234" src="docs/media/logo-full.png"/>
|
<p align="center">
|
||||||
</p>
|
<a href="https://sonarcloud.io/dashboard?id=netbirdio_netbird">
|
||||||
<p>
|
<img src="https://sonarcloud.io/api/project_badges/measure?project=netbirdio_netbird&metric=alert_status" alt="SonarCloud alert status"/>
|
||||||
<a href="https://img.shields.io/badge/license-BSD--3-blue)">
|
</a>
|
||||||
<img src="https://sonarcloud.io/api/project_badges/measure?project=netbirdio_netbird&metric=alert_status" />
|
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
|
||||||
</a>
|
<img src="https://img.shields.io/badge/license-BSD--3-blue" alt="BSD-3 License"/>
|
||||||
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
|
</a>
|
||||||
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<a href="https://docs.netbird.io/slack-url">
|
<a href="https://docs.netbird.io/slack-url">
|
||||||
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack" alt="NetBird Slack"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://forum.netbird.io">
|
<a href="https://forum.netbird.io">
|
||||||
<img src="https://img.shields.io/badge/community forum-@netbird-red.svg?logo=discourse"/>
|
<img src="https://img.shields.io/badge/community%20forum-@netbird-red.svg?logo=discourse" alt="Community forum"/>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
|
||||||
<a href="https://gurubase.io/g/netbird">
|
<a href="https://gurubase.io/g/netbird">
|
||||||
<img src="https://img.shields.io/badge/Gurubase-Ask%20NetBird%20Guru-006BFF"/>
|
<img src="https://img.shields.io/badge/Gurubase-Ask%20NetBird%20Guru-006BFF" alt="Gurubase: Ask NetBird Guru"/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start using NetBird at <a href="https://netbird.io/pricing">netbird.io</a>
|
Start using NetBird at <a href="https://netbird.io/pricing">netbird.io</a>
|
||||||
|
<br/>
|
||||||
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
|
<br/>
|
||||||
|
Join our <a href="https://docs.netbird.io/slack-url">Slack channel</a> or our <a href="https://forum.netbird.io">Community forum</a>
|
||||||
|
</strong>
|
||||||
<br/>
|
<br/>
|
||||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
|
||||||
<br/>
|
<br/>
|
||||||
Join our <a href="https://docs.netbird.io/slack-url">Slack channel</a> or our <a href="https://forum.netbird.io">Community forum</a>
|
<strong>
|
||||||
<br/>
|
🚀 <a href="https://careers.netbird.io">We are hiring! Join us at careers.netbird.io</a>
|
||||||
|
</strong>
|
||||||
</strong>
|
|
||||||
<br>
|
|
||||||
<strong>
|
|
||||||
🚀 <a href="https://careers.netbird.io">We are hiring! Join us at careers.netbird.io</a>
|
|
||||||
</strong>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a href="https://registry.terraform.io/providers/netbirdio/netbird/latest">
|
|
||||||
New: NetBird terraform provider
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
||||||
|
|
||||||
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|
||||||
**Secure.** NetBird enables secure remote access by applying granular access policies while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
|
**Secure.** NetBird enables secure remote access by applying granular access policies while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
|
||||||
|
|
||||||
### Open Source Network Security in a Single Platform
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
||||||
|
|
||||||
### Self-Host NetBird (Video)
|
### Self-host NetBird (video)
|
||||||
|
|
||||||
[](https://youtu.be/bZAgpT6nzaQ)
|
[](https://youtu.be/bZAgpT6nzaQ)
|
||||||
|
|
||||||
### Key features
|
### Key features
|
||||||
|
|
||||||
| Connectivity | Management | Security | Automation| Platforms |
|
| Connectivity | Management | Security | Automation | Platforms |
|
||||||
|----|----|----|----|----|
|
|---|---|---|---|---|
|
||||||
| <ul><li>- \[x] Kernel WireGuard</ul></li> | <ul><li>- \[x] [Admin Web UI](https://github.com/netbirdio/dashboard)</ul></li> | <ul><li>- \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login)</ul></li> | <ul><li>- \[x] [Public API](https://docs.netbird.io/api)</ul></li> | <ul><li>- \[x] Linux</ul></li> |
|
| ✓ [Kernel WireGuard](https://docs.netbird.io/about-netbird/why-wireguard-with-netbird) | ✓ [Admin Web UI](https://github.com/netbirdio/dashboard) | ✓ [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) | ✓ [Public API](https://docs.netbird.io/api) | ✓ [Linux](https://docs.netbird.io/get-started/install/linux) |
|
||||||
| <ul><li>- \[x] Peer-to-peer connections</ul></li> | <ul><li>- \[x] Auto peer discovery and configuration</ui></li> | <ul><li>- \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access)</ui></li> | <ul><li>- \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys)</ui></li> | <ul><li>- \[x] Mac</ui></li> |
|
| ✓ [Peer-to-peer connections](https://docs.netbird.io/about-netbird/how-netbird-works) | ✓ Auto peer discovery and configuration | ✓ [Access control: groups & rules](https://docs.netbird.io/how-to/manage-network-access) | ✓ [Setup keys for bulk provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) | ✓ [macOS](https://docs.netbird.io/get-started/install/macos) |
|
||||||
| <ul><li>- \[x] Connection relay fallback</ui></li> | <ul><li>- \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers)</ui></li> | <ul><li>- \[x] [Activity logging](https://docs.netbird.io/how-to/audit-events-logging)</ui></li> | <ul><li>- \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart)</ui></li> | <ul><li>- \[x] Windows</ui></li> |
|
| ✓ Connection relay fallback | ✓ [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) | ✓ [Activity logging](https://docs.netbird.io/how-to/audit-events-logging) | ✓ [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) | ✓ [Windows](https://docs.netbird.io/get-started/install/windows) |
|
||||||
| <ul><li>- \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks)</ui></li> | <ul><li>- \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network)</ui></li> | <ul><li>- \[x] [Device posture checks](https://docs.netbird.io/how-to/manage-posture-checks)</ui></li> | <ul><li>- \[x] IdP groups sync with JWT</ui></li> | <ul><li>- \[x] Android</ui></li> |
|
| ✓ [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) | ✓ [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) | ✓ [Traffic events](https://docs.netbird.io/manage/activity/traffic-events-logging) | ✓ [IdP groups sync with JWT](https://docs.netbird.io/manage/team/idp-sync) | ✓ [Android](https://docs.netbird.io/get-started/install/android) |
|
||||||
| <ul><li>- \[x] NAT traversal with BPF</ui></li> | <ul><li>- \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network)</ui></li> | <ul><li>- \[x] Peer-to-peer encryption</ui></li> || <ul><li>- \[x] iOS</ui></li> |
|
| ✓ [Domain-based DNS routes](https://docs.netbird.io/manage/dns/dns-aliases-for-routed-networks) | ✓ [Custom DNS zones](https://docs.netbird.io/manage/dns/custom-zones) | ✓ [Device posture checks](https://docs.netbird.io/how-to/manage-posture-checks) | ✓ [Terraform provider](https://registry.terraform.io/providers/netbirdio/netbird/latest) | ✓ [Android TV](https://docs.netbird.io/get-started/install/android-tv) |
|
||||||
||| <ul><li>- \[x] [Quantum-resistance with Rosenpass](https://netbird.io/knowledge-hub/the-first-quantum-resistant-mesh-vpn)</ui></li> || <ul><li>- \[x] OpenWRT</ui></li> |
|
| ✓ [Exit nodes](https://docs.netbird.io/manage/network-routes/use-cases/exit-nodes) | ✓ [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) | ✓ Peer-to-peer encryption | ✓ [Ansible collection](https://github.com/netbirdio/ansible-netbird) | ✓ [iOS](https://docs.netbird.io/get-started/install/ios) |
|
||||||
||| <ul><li>- \[x] [Periodic re-authentication](https://docs.netbird.io/how-to/enforce-periodic-user-authentication)</ui></li> || <ul><li>- \[x] [Serverless](https://docs.netbird.io/how-to/netbird-on-faas)</ui></li> |
|
| ✓ [IPv6 dual-stack overlay](https://docs.netbird.io/manage/settings/ipv6) | ✓ [Multi-account profile switching](https://docs.netbird.io/client/profiles) | ✓ [SSH with central access policies](https://docs.netbird.io/manage/peers/ssh) | | ✓ [Apple TV](https://docs.netbird.io/get-started/install/tvos) |
|
||||||
||||| <ul><li>- \[x] Docker</ui></li> |
|
| ✓ [Browser SSH & RDP](https://docs.netbird.io/manage/peers/browser-client) | | ✓ [Quantum-resistance with Rosenpass](https://netbird.io/knowledge-hub/the-first-quantum-resistant-mesh-vpn) | | ✓ FreeBSD |
|
||||||
|
| ✓ [Reverse proxy with auto-TLS](https://docs.netbird.io/manage/reverse-proxy) | | ✓ [Periodic re-authentication](https://docs.netbird.io/how-to/enforce-periodic-user-authentication) | | ✓ [pfSense](https://docs.netbird.io/get-started/install/pfsense) |
|
||||||
|
| | | | | ✓ [OPNsense](https://docs.netbird.io/get-started/install/opnsense) |
|
||||||
|
| | | | | ✓ [MikroTik RouterOS](https://docs.netbird.io/use-cases/homelab/client-on-mikrotik-router) |
|
||||||
|
| | | | | ✓ OpenWRT |
|
||||||
|
| | | | | ✓ [Synology](https://docs.netbird.io/get-started/install/synology) |
|
||||||
|
| | | | | ✓ [TrueNAS](https://docs.netbird.io/get-started/install/truenas) |
|
||||||
|
| | | | | ✓ [Proxmox](https://docs.netbird.io/get-started/install/proxmox-ve) |
|
||||||
|
| | | | | ✓ [Raspberry Pi](https://docs.netbird.io/get-started/install/raspberrypi) |
|
||||||
|
| | | | | ✓ [Serverless](https://docs.netbird.io/how-to/netbird-on-faas) |
|
||||||
|
| | | | | ✓ [Container](https://docs.netbird.io/get-started/install/docker) |
|
||||||
|
|
||||||
### Quickstart with NetBird Cloud
|
### Quickstart with NetBird Cloud
|
||||||
|
|
||||||
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
|
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install).
|
||||||
- Follow the steps to sign-up with Google, Microsoft, GitHub or your email address.
|
- Follow the steps to sign up with Google, Microsoft, GitHub or your email address.
|
||||||
- Check NetBird [admin UI](https://app.netbird.io/).
|
- Check the NetBird [admin UI](https://app.netbird.io/).
|
||||||
- Add more machines.
|
|
||||||
|
|
||||||
### Quickstart with self-hosted NetBird
|
### Quickstart with self-hosted NetBird
|
||||||
|
|
||||||
> This is the quickest way to try self-hosted NetBird. It should take around 5 minutes to get started if you already have a public domain and a VM.
|
This is the quickest way to try self-hosted NetBird. It should take around 5 minutes to get started if you already have a public domain and a VM. Follow the [Advanced guide with a custom identity provider](https://docs.netbird.io/selfhosted/selfhosted-guide#advanced-guide-with-a-custom-identity-provider) for installations with different IdPs.
|
||||||
Follow the [Advanced guide with a custom identity provider](https://docs.netbird.io/selfhosted/selfhosted-guide#advanced-guide-with-a-custom-identity-provider) for installations with different IDPs.
|
|
||||||
|
|
||||||
**Infrastructure requirements:**
|
**Infrastructure requirements:**
|
||||||
- A Linux VM with at least **1CPU** and **2GB** of memory.
|
- A Linux VM with at least **1 CPU** and **2 GB** of memory.
|
||||||
- The VM should be publicly accessible on TCP ports **80** and **443** and UDP port: **3478**.
|
- The VM should be publicly accessible on TCP ports **80** and **443** and UDP port **3478**.
|
||||||
- **Public domain** name pointing to the VM.
|
- A **public domain** name pointing to the VM.
|
||||||
|
|
||||||
**Software requirements:**
|
**Software requirements:**
|
||||||
- Docker installed on the VM with the docker-compose plugin ([Docker installation guide](https://docs.docker.com/engine/install/)) or docker with docker-compose in version 2 or higher.
|
- Docker with the Compose plugin (Compose v2 or higher). See the [Docker installation guide](https://docs.docker.com/engine/install/).
|
||||||
- [jq](https://jqlang.github.io/jq/) installed. In most distributions
|
|
||||||
Usually available in the official repositories and can be installed with `sudo apt install jq` or `sudo yum install jq`
|
|
||||||
- [curl](https://curl.se/) installed.
|
|
||||||
Usually available in the official repositories and can be installed with `sudo apt install curl` or `sudo yum install curl`
|
|
||||||
|
|
||||||
**Steps**
|
**Steps**
|
||||||
- Download and run the installation script:
|
- Download and run the installation script:
|
||||||
```bash
|
```bash
|
||||||
export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash
|
export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash
|
||||||
```
|
```
|
||||||
- Once finished, you can manage the resources via `docker-compose`
|
|
||||||
|
|
||||||
### A bit on NetBird internals
|
### A bit on NetBird internals
|
||||||
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
- Every machine in the network runs the [NetBird agent](client/), which manages WireGuard.
|
||||||
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
|
- Every agent connects to the [Management Service](management/), which holds network state, manages peer IPs, and distributes updates to agents.
|
||||||
- NetBird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
|
- Agents use ICE (via [pion/ice](https://github.com/pion/ice)) to discover connection candidates for peer-to-peer connections.
|
||||||
- Connection candidates are discovered with the help of [STUN](https://en.wikipedia.org/wiki/STUN) servers.
|
- Candidates are discovered with the help of [STUN](https://en.wikipedia.org/wiki/STUN) servers.
|
||||||
- Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages with candidates.
|
- Agents negotiate a connection through the [Signal Service](signal/), exchanging end-to-end encrypted messages with candidates.
|
||||||
- Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and a p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure WireGuard tunnel is established via the TURN server.
|
- When NAT traversal fails (e.g. mobile carrier-grade NAT) and a direct p2p connection isn't possible, the system falls back to a [Relay Service](relay/) and a secure WireGuard tunnel is established through it.
|
||||||
|
|
||||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
|
||||||
|
|
||||||
<p float="left" align="middle">
|
<p float="left" align="middle">
|
||||||
<img src="https://docs.netbird.io/docs-static/img/about-netbird/high-level-dia.png" width="700"/>
|
<img src="https://docs.netbird.io/docs-static/img/about-netbird/high-level-dia.png" width="700" alt="NetBird high-level architecture diagram"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
||||||
|
|
||||||
### Community projects
|
### Community projects
|
||||||
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
||||||
- [NetBird ansible collection by Dominion Solutions](https://galaxy.ansible.com/ui/repo/published/dominion_solutions/netbird/)
|
- [netbird-tui](https://github.com/n0pashkov/netbird-tui) - terminal UI for managing NetBird peers, routes, and settings
|
||||||
- [netbird-tui](https://github.com/n0pashkov/netbird-tui) — terminal UI for managing NetBird peers, routes, and settings
|
- [caddy-netbird](https://github.com/lixmal/caddy-netbird) - Caddy plugin that embeds a NetBird client for proxying HTTP and TCP/UDP traffic through NetBird networks
|
||||||
|
|
||||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||||
|
|
||||||
### Support acknowledgement
|
### Support acknowledgement
|
||||||
|
|
||||||
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
|
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by the Federal Ministry of Education and Research of the Federal Republic of Germany. Together with the [CISPA Helmholtz Center for Information Security](https://cispa.de/en), NetBird brings security best practices and simplicity to private networking.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Testimonials
|
### Acknowledgements
|
||||||
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn), and [Rosenpass](https://rosenpass.eu). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g., by giving a star or a contribution).
|
We build on open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE](https://github.com/pion/ice), and [Rosenpass](https://rosenpass.eu). We greatly appreciate the work these projects are doing, and we'd love it if you could support them too (e.g., by starring or contributing).
|
||||||
|
|
||||||
### Legal
|
### Legal
|
||||||
This repository is licensed under BSD-3-Clause license that applies to all parts of the repository except for the directories management/, signal/ and relay/.
|
This repository is licensed under the BSD-3-Clause license, which applies to all parts of the repository except for the directories management/, signal/ and relay/.
|
||||||
Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory.
|
Those directories are licensed under the GNU Affero General Public License version 3.0 (AGPLv3). See the respective LICENSE files inside each directory.
|
||||||
|
|
||||||
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
||||||
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
||||||
|
|
||||||
FROM alpine:3.23.3
|
FROM alpine:3.24
|
||||||
# iproute2: busybox doesn't display ip rules properly
|
# iproute2: busybox doesn't display ip rules properly
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
bash \
|
bash \
|
||||||
@@ -21,7 +21,7 @@ ENV \
|
|||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
ARG TARGETPLATFORM
|
||||||
ARG NETBIRD_BINARY=netbird
|
ARG NETBIRD_BINARY=$TARGETPLATFORM/netbird
|
||||||
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
||||||
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
||||||
# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
||||||
|
|
||||||
FROM alpine:3.22.0
|
FROM alpine:3.24
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
bash \
|
bash \
|
||||||
@@ -27,7 +27,7 @@ ENV \
|
|||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
ARG TARGETPLATFORM
|
||||||
ARG NETBIRD_BINARY=netbird
|
ARG NETBIRD_BINARY=$TARGETPLATFORM/netbird
|
||||||
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
||||||
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -24,6 +23,7 @@ const (
|
|||||||
|
|
||||||
// Profile represents a profile for gomobile
|
// Profile represents a profile for gomobile
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
|
ID string
|
||||||
Name string
|
Name string
|
||||||
IsActive bool
|
IsActive bool
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,10 @@ func (p *ProfileArray) Get(i int) *Profile {
|
|||||||
├── state.json ← Default profile state
|
├── state.json ← Default profile state
|
||||||
├── active_profile.json ← Active profile tracker (JSON with Name + Username)
|
├── active_profile.json ← Active profile tracker (JSON with Name + Username)
|
||||||
└── profiles/ ← Subdirectory for non-default profiles
|
└── profiles/ ← Subdirectory for non-default profiles
|
||||||
├── work.json ← Work profile config
|
├── work.json ← Legacy work profile config
|
||||||
├── work.state.json ← Work profile state
|
├── work.state.json ← Legacy work profile state
|
||||||
├── personal.json ← Personal profile config
|
├── 4c5f5c8198c3989cffb5b5394f5a7ae0.json ← ID profile config
|
||||||
└── personal.state.json ← Personal profile state
|
├── 4c5f5c8198c3989cffb5b5394f5a7ae0.state.json ← ID profile state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ProfileManager manages profiles for Android
|
// ProfileManager manages profiles for Android
|
||||||
@@ -99,6 +99,7 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
|
|||||||
var profiles []*Profile
|
var profiles []*Profile
|
||||||
for _, p := range internalProfiles {
|
for _, p := range internalProfiles {
|
||||||
profiles = append(profiles, &Profile{
|
profiles = append(profiles, &Profile{
|
||||||
|
ID: p.ID.String(),
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsActive: p.IsActive,
|
IsActive: p.IsActive,
|
||||||
})
|
})
|
||||||
@@ -108,55 +109,65 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveProfile returns the currently active profile name
|
// GetActiveProfile returns the currently active profile name
|
||||||
func (pm *ProfileManager) GetActiveProfile() (string, error) {
|
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
|
||||||
// Use ServiceManager to stay consistent with ListProfiles
|
// Use ServiceManager to stay consistent with ListProfiles
|
||||||
// ServiceManager uses active_profile.json
|
// ServiceManager uses active_profile.json
|
||||||
activeState, err := pm.serviceMgr.GetActiveProfileState()
|
activeState, err := pm.serviceMgr.GetActiveProfileState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
return nil, fmt.Errorf("failed to get active profile: %w", err)
|
||||||
}
|
}
|
||||||
return activeState.Name, nil
|
|
||||||
|
// ActiveProfileState only stores the ID (and username), not the display
|
||||||
|
// name. Resolve the ID to the full profile so callers get the real Name.
|
||||||
|
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID.String(), androidUsername)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve active profile %q: %w", activeState.ID, err)
|
||||||
|
}
|
||||||
|
return &Profile{ID: prof.ID.String(), Name: prof.Name, IsActive: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwitchProfile switches to a different profile
|
// SwitchProfile switches to a different profile
|
||||||
func (pm *ProfileManager) SwitchProfile(profileName string) error {
|
func (pm *ProfileManager) SwitchProfile(id string) error {
|
||||||
// Use ServiceManager to stay consistent with ListProfiles
|
// Use ServiceManager to stay consistent with ListProfiles
|
||||||
// ServiceManager uses active_profile.json
|
// ServiceManager uses active_profile.json
|
||||||
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||||
Name: profileName,
|
ID: profilemanager.ID(id),
|
||||||
Username: androidUsername,
|
Username: androidUsername,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to switch profile: %w", err)
|
return fmt.Errorf("failed to switch profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("switched to profile: %s", profileName)
|
log.Infof("switched to profile: %s", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddProfile creates a new profile
|
// AddProfile creates a new profile
|
||||||
func (pm *ProfileManager) AddProfile(profileName string) error {
|
func (pm *ProfileManager) AddProfile(profileName string) error {
|
||||||
// Use ServiceManager (creates profile in profiles/ directory)
|
// Use ServiceManager (creates profile in profiles/ directory)
|
||||||
if err := pm.serviceMgr.AddProfile(profileName, androidUsername); err != nil {
|
profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add profile: %w", err)
|
return fmt.Errorf("failed to add profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("created new profile: %s", profileName)
|
log.Infof("created new profile: %s", profile.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutProfile logs out from a profile (clears authentication)
|
// LogoutProfile logs out from a profile (clears authentication)
|
||||||
func (pm *ProfileManager) LogoutProfile(profileName string) error {
|
func (pm *ProfileManager) LogoutProfile(id string) error {
|
||||||
profileName = sanitizeProfileName(profileName)
|
configPath, err := pm.getProfileConfigPath(id)
|
||||||
|
|
||||||
configPath, err := pm.getProfileConfigPath(profileName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
||||||
|
return fmt.Errorf("id '%s' is not valid", id)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if profile exists
|
// Check if profile exists
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("profile '%s' does not exist", profileName)
|
return fmt.Errorf("profile '%s' does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read current config using internal profilemanager
|
// Read current config using internal profilemanager
|
||||||
@@ -174,53 +185,57 @@ func (pm *ProfileManager) LogoutProfile(profileName string) error {
|
|||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("logged out from profile: %s", profileName)
|
log.Infof("logged out from profile: %s", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveProfile deletes a profile
|
// RemoveProfile deletes a profile
|
||||||
func (pm *ProfileManager) RemoveProfile(profileName string) error {
|
func (pm *ProfileManager) RemoveProfile(id string) error {
|
||||||
// Use ServiceManager (removes profile from profiles/ directory)
|
// Use ServiceManager (removes profile from profiles/ directory)
|
||||||
if err := pm.serviceMgr.RemoveProfile(profileName, androidUsername); err != nil {
|
if err := pm.serviceMgr.RemoveProfile(profilemanager.ID(id), androidUsername); err != nil {
|
||||||
return fmt.Errorf("failed to remove profile: %w", err)
|
return fmt.Errorf("failed to remove profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("removed profile: %s", profileName)
|
log.Infof("removed profile: %s", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getProfileConfigPath returns the config file path for a profile
|
// getProfileConfigPath returns the config file path for a profile
|
||||||
// This is needed for Android-specific path handling (netbird.cfg for default profile)
|
// This is needed for Android-specific path handling (netbird.cfg for default profile)
|
||||||
func (pm *ProfileManager) getProfileConfigPath(profileName string) (string, error) {
|
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {
|
||||||
if profileName == "" || profileName == profilemanager.DefaultProfileName {
|
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
||||||
|
return "", fmt.Errorf("id %q is not valid", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == profilemanager.DefaultProfileName {
|
||||||
// Android uses netbird.cfg for default profile instead of default.json
|
// Android uses netbird.cfg for default profile instead of default.json
|
||||||
// Default profile is stored in root configDir, not in profiles/
|
// Default profile is stored in root configDir, not in profiles/
|
||||||
return filepath.Join(pm.configDir, defaultConfigFilename), nil
|
return filepath.Join(pm.configDir, defaultConfigFilename), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-default profiles are stored in profiles subdirectory
|
|
||||||
// This matches the Java Preferences.java expectation
|
|
||||||
profileName = sanitizeProfileName(profileName)
|
|
||||||
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
||||||
return filepath.Join(profilesDir, profileName+".json"), nil
|
return filepath.Join(profilesDir, id+".json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigPath returns the config file path for a given profile
|
// GetConfigPath returns the config file path for a given profile id
|
||||||
// Java should call this instead of constructing paths with Preferences.configFile()
|
// Java should call this instead of constructing paths with Preferences.configFile()
|
||||||
func (pm *ProfileManager) GetConfigPath(profileName string) (string, error) {
|
func (pm *ProfileManager) GetConfigPath(id string) (string, error) {
|
||||||
return pm.getProfileConfigPath(profileName)
|
return pm.getProfileConfigPath(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStateFilePath returns the state file path for a given profile
|
// GetStateFilePath returns the state file path for a given profile
|
||||||
// Java should call this instead of constructing paths with Preferences.stateFile()
|
// Java should call this instead of constructing paths with Preferences.stateFile()
|
||||||
func (pm *ProfileManager) GetStateFilePath(profileName string) (string, error) {
|
func (pm *ProfileManager) GetStateFilePath(id string) (string, error) {
|
||||||
if profileName == "" || profileName == profilemanager.DefaultProfileName {
|
if id == "" || id == profilemanager.DefaultProfileName {
|
||||||
return filepath.Join(pm.configDir, "state.json"), nil
|
return filepath.Join(pm.configDir, "state.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
profileName = sanitizeProfileName(profileName)
|
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
||||||
|
return "", fmt.Errorf("id %q is not valid", id)
|
||||||
|
}
|
||||||
|
|
||||||
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
||||||
return filepath.Join(profilesDir, profileName+".state.json"), nil
|
return filepath.Join(profilesDir, id+".state.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveConfigPath returns the config file path for the currently active profile
|
// GetActiveConfigPath returns the config file path for the currently active profile
|
||||||
@@ -230,7 +245,7 @@ func (pm *ProfileManager) GetActiveConfigPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||||
}
|
}
|
||||||
return pm.GetConfigPath(activeProfile)
|
return pm.GetConfigPath(activeProfile.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveStateFilePath returns the state file path for the currently active profile
|
// GetActiveStateFilePath returns the state file path for the currently active profile
|
||||||
@@ -240,18 +255,5 @@ func (pm *ProfileManager) GetActiveStateFilePath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||||
}
|
}
|
||||||
return pm.GetStateFilePath(activeProfile)
|
return pm.GetStateFilePath(activeProfile.ID)
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeProfileName removes invalid characters from profile name
|
|
||||||
func sanitizeProfileName(name string) string {
|
|
||||||
// Keep only alphanumeric, underscore, and hyphen
|
|
||||||
var result strings.Builder
|
|
||||||
for _, r := range name {
|
|
||||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
|
||||||
(r >= '0' && r <= '9') || r == '_' || r == '-' {
|
|
||||||
result.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.String()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
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/encoding/protojson"
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
@@ -19,6 +21,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/upload-server/types"
|
"github.com/netbirdio/netbird/upload-server/types"
|
||||||
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const errCloseConnection = "Failed to close connection: %v"
|
const errCloseConnection = "Failed to close connection: %v"
|
||||||
@@ -84,6 +87,73 @@ var persistenceCmd = &cobra.Command{
|
|||||||
RunE: setSyncResponsePersistence,
|
RunE: setSyncResponsePersistence,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var debugConfigCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Example: " netbird debug config",
|
||||||
|
Short: "Dump the effective configuration",
|
||||||
|
Long: "Prints the daemon's resolved configuration (after applying defaults, file, env, CLI input, and MDM policy overrides) as JSON. Includes the list of MDM-managed fields.",
|
||||||
|
RunE: debugConfigDump,
|
||||||
|
}
|
||||||
|
|
||||||
|
// debugConfigDump implements `netbird debug config`. It resolves the
|
||||||
|
// active profile, queries the daemon for the effective configuration
|
||||||
|
// via GetConfig, and prints the resulting GetConfigResponse as JSON
|
||||||
|
// (via protojson with EmitUnpopulated=true so the output is stable
|
||||||
|
// across runs and includes zero-valued fields).
|
||||||
|
//
|
||||||
|
// Useful for verifying MDM enforcement end-to-end: the response's
|
||||||
|
// mDMManagedFields array is the single source of truth for "which
|
||||||
|
// fields is the daemon currently enforcing from the MDM source", and
|
||||||
|
// every config field side-by-side with that list confirms the merge
|
||||||
|
// result. Secrets in the response (e.g. PreSharedKey) are already
|
||||||
|
// redacted by the daemon-side handler.
|
||||||
|
func debugConfigDump(cmd *cobra.Command, _ []string) error {
|
||||||
|
pm := profilemanager.NewProfileManager()
|
||||||
|
activeProf, err := pm.GetActiveProfile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get active profile: %v", err)
|
||||||
|
}
|
||||||
|
currUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get current user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Errorf(errCloseConnection, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.GetConfig(cmd.Context(), &proto.GetConfigRequest{
|
||||||
|
ProfileName: activeProf.Name,
|
||||||
|
Username: currUser.Username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get config: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use protojson so well-known fields render correctly; emit defaults so
|
||||||
|
// the operator sees every field even when zero/empty.
|
||||||
|
m := protojson.MarshalOptions{Multiline: true, Indent: " ", EmitUnpopulated: true}
|
||||||
|
out, err := m.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal config: %w", err)
|
||||||
|
}
|
||||||
|
cmd.Println(string(out))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// debugBundle requests the daemon to create a debug bundle and prints
|
||||||
|
// the resulting local file path and, if uploaded, the uploaded file
|
||||||
|
// key. It uses the package flags (anonymize, system info, log file
|
||||||
|
// count, CLI version, optional upload URL) to configure the bundle
|
||||||
|
// request. Returns an error if the RPC fails or if the daemon reports
|
||||||
|
// an upload failure reason.
|
||||||
func debugBundle(cmd *cobra.Command, _ []string) error {
|
func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||||
conn, err := getClient(cmd)
|
conn, err := getClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,6 +170,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
|||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
SystemInfo: systemInfoFlag,
|
SystemInfo: systemInfoFlag,
|
||||||
LogFileCount: logFileCount,
|
LogFileCount: logFileCount,
|
||||||
|
CliVersion: version.NetbirdVersion(),
|
||||||
}
|
}
|
||||||
if uploadBundleFlag {
|
if uploadBundleFlag {
|
||||||
request.UploadURL = uploadBundleURLFlag
|
request.UploadURL = uploadBundleURLFlag
|
||||||
@@ -298,6 +369,7 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
SystemInfo: systemInfoFlag,
|
SystemInfo: systemInfoFlag,
|
||||||
LogFileCount: logFileCount,
|
LogFileCount: logFileCount,
|
||||||
|
CliVersion: version.NetbirdVersion(),
|
||||||
}
|
}
|
||||||
if uploadBundleFlag {
|
if uploadBundleFlag {
|
||||||
request.UploadURL = uploadBundleURLFlag
|
request.UploadURL = uploadBundleURLFlag
|
||||||
@@ -432,6 +504,7 @@ func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, c
|
|||||||
SyncResponse: syncResponse,
|
SyncResponse: syncResponse,
|
||||||
LogPath: logFilePath,
|
LogPath: logFilePath,
|
||||||
CPUProfile: nil,
|
CPUProfile: nil,
|
||||||
|
DaemonVersion: version.NetbirdVersion(), // acting as daemon
|
||||||
},
|
},
|
||||||
debug.BundleConfig{
|
debug.BundleConfig{
|
||||||
IncludeSystemInfo: true,
|
IncludeSystemInfo: true,
|
||||||
|
|||||||
301
client/cmd/kubernetes.go
Normal file
301
client/cmd/kubernetes.go
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KubernetesDNSSuffix = "netbird-kubeapi-proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var kubernetesCmd = &cobra.Command{
|
||||||
|
Use: "kubernetes",
|
||||||
|
Short: "Kubernetes cluster commands.",
|
||||||
|
Long: "Kubernetes cluster commands.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var kubernetesListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
RunE: kubernetesList,
|
||||||
|
Short: "List Kubernetes clusters.",
|
||||||
|
Long: "List Kubernetes clusters by discovering NetBird peers running netbird-kubeapi-proxy.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var kubernetesWriteKubeconfigCmd = &cobra.Command{
|
||||||
|
Use: "write-kubeconfig",
|
||||||
|
RunE: kubernetesWriteKubeconfig,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Write kubeconfig for a Kubernetes cluster.",
|
||||||
|
Long: "Updates kubeconfig in place to allow token-less access to the Kubernetes cluster through NetBird.",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
kubernetesWriteKubeconfigCmd.Flags().String("kubeconfig", "", "path to kubeconfig file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func kubernetesList(cmd *cobra.Command, _ []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
statusResp, err := client.Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kcs, err := getKubernetesClusters(cmd.Context(), statusResp.FullStatus.Peers, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(kcs) == 0 {
|
||||||
|
cmd.Println("No Kubernetes clusters available.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmd.Println("Available Kubernetes clusters:")
|
||||||
|
for _, k := range kcs {
|
||||||
|
cmd.Printf("\n - Name: %s\n FQDN: %s\n Version: %s\n", k.name, k.url.Host, k.version)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func kubernetesWriteKubeconfig(cmd *cobra.Command, args []string) error {
|
||||||
|
kubeconfigPath, err := resolveKubeconfigPath(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
statusResp, err := client.Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterName := args[0]
|
||||||
|
kcs, err := getKubernetesClusters(cmd.Context(), statusResp.FullStatus.Peers, clusterName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(kcs) == 0 {
|
||||||
|
return fmt.Errorf("kubernetes cluster named %s not found", clusterName)
|
||||||
|
}
|
||||||
|
if len(kcs) > 1 {
|
||||||
|
return fmt.Errorf("too many Kubernetes clusters returned")
|
||||||
|
}
|
||||||
|
err = writeKubeconfig(kubeconfigPath, kcs[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type kubernetesCluster struct {
|
||||||
|
name string
|
||||||
|
url *url.URL
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKubernetesClusters(ctx context.Context, peers []*proto.PeerState, nameFilter string) ([]kubernetesCluster, error) {
|
||||||
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
transport.TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
resolver := net.Resolver{
|
||||||
|
// Required so both DNS records are returned.
|
||||||
|
// https://github.com/golang/go/issues/17093
|
||||||
|
PreferGo: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
kcs := []kubernetesCluster{}
|
||||||
|
attempted := map[string]struct{}{}
|
||||||
|
for _, peer := range peers {
|
||||||
|
fqdns, err := resolver.LookupAddr(ctx, peer.IP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, fqdn := range fqdns {
|
||||||
|
if _, ok := attempted[fqdn]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attempted[fqdn] = struct{}{}
|
||||||
|
comps := strings.Split(fqdn, ".")
|
||||||
|
if len(comps) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if comps[1] != KubernetesDNSSuffix {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nameFilter != "" && nameFilter != comps[0] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clusterURL, clusterVersion, err := fingerprintClusters(ctx, httpClient, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("could not fingerprint Kubernetes cluster %s %q", fqdn, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kc := kubernetesCluster{
|
||||||
|
name: comps[0],
|
||||||
|
url: clusterURL,
|
||||||
|
version: clusterVersion,
|
||||||
|
}
|
||||||
|
if nameFilter != "" {
|
||||||
|
return []kubernetesCluster{kc}, nil
|
||||||
|
}
|
||||||
|
kcs = append(kcs, kc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kcs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fingerprintClusters(ctx context.Context, httpClient *http.Client, fqdn string) (*url.URL, string, error) {
|
||||||
|
clusterURL, err := url.Parse("https://" + fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
versionURL, err := clusterURL.Parse("/version")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, "", fmt.Errorf("expected %d response but got %s", http.StatusOK, resp.Status)
|
||||||
|
}
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
versionData := map[string]string{}
|
||||||
|
err = json.Unmarshal(b, &versionData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
version, ok := versionData["gitVersion"]
|
||||||
|
if !ok {
|
||||||
|
return nil, "", errors.New("no version found in response")
|
||||||
|
}
|
||||||
|
return clusterURL, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveKubeconfigPath(cmd *cobra.Command) (string, error) {
|
||||||
|
if cmd.Flags().Changed("kubeconfig") {
|
||||||
|
path, err := cmd.Flags().GetString("kubeconfig")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
if env := os.Getenv("KUBECONFIG"); env != "" {
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not determine home directory: %w", err)
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".kube", "config"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeKubeconfig(kubeconfigPath string, kc kubernetesCluster) error {
|
||||||
|
b, err := os.ReadFile(kubeconfigPath)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var cfg map[string]any
|
||||||
|
if err := yaml.Unmarshal(b, &cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = map[string]any{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg["clusters"] = appendWithName(cfg["clusters"], map[string]any{
|
||||||
|
"name": kc.name,
|
||||||
|
"cluster": map[string]any{
|
||||||
|
"server": kc.url.String(),
|
||||||
|
"insecure-skip-tls-verify": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
cfg["users"] = appendWithName(cfg["users"], map[string]any{
|
||||||
|
"name": "netbird",
|
||||||
|
"user": map[string]any{
|
||||||
|
"token": "none",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
cfg["contexts"] = appendWithName(cfg["contexts"], map[string]any{
|
||||||
|
"name": kc.name,
|
||||||
|
"context": map[string]any{
|
||||||
|
"cluster": kc.name,
|
||||||
|
"user": "netbird",
|
||||||
|
"namespace": "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
cfg["current-context"] = kc.name
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(kubeconfigPath, out, 0o600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendWithName(data any, add map[string]any) any {
|
||||||
|
if data == nil {
|
||||||
|
return []any{add}
|
||||||
|
}
|
||||||
|
v, ok := data.([]any)
|
||||||
|
if !ok {
|
||||||
|
return []any{add}
|
||||||
|
}
|
||||||
|
i := slices.IndexFunc(v, func(item any) bool {
|
||||||
|
m, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m["name"] == add["name"]
|
||||||
|
})
|
||||||
|
if i == -1 {
|
||||||
|
return append(v, add)
|
||||||
|
}
|
||||||
|
v[i] = add
|
||||||
|
return v
|
||||||
|
}
|
||||||
120
client/cmd/kubernetes_test.go
Normal file
120
client/cmd/kubernetes_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFingerprintClusters(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//nolint: errcheck
|
||||||
|
w.Write([]byte(`{"gitVersion": "foobar"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
clusterURL, clusterVersion, err := fingerprintClusters(t.Context(), srv.Client(), srv.Listener.Addr().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, srv.URL, clusterURL.String())
|
||||||
|
require.Equal(t, "foobar", clusterVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveKubeconfigPath(t *testing.T) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not determine home directory: %v", err)
|
||||||
|
}
|
||||||
|
defaultPath := filepath.Join(home, ".kube", "config")
|
||||||
|
path, err := resolveKubeconfigPath(&cobra.Command{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, defaultPath, path)
|
||||||
|
|
||||||
|
flagPath := "flag-path"
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.Flags().String("kubeconfig", "", "")
|
||||||
|
err = cmd.Flags().Set("kubeconfig", flagPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
path, err = resolveKubeconfigPath(cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, flagPath, path)
|
||||||
|
|
||||||
|
envPath := "env-path"
|
||||||
|
t.Setenv("KUBECONFIG", envPath)
|
||||||
|
path, err = resolveKubeconfigPath(&cobra.Command{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, envPath, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteKubeconfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
existing string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "existing content",
|
||||||
|
existing: `apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
insecure-skip-tls-verify: true
|
||||||
|
server: https://foobar.com
|
||||||
|
name: foo
|
||||||
|
current-context: test
|
||||||
|
kind: Config
|
||||||
|
users: []
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
kubeconfigPath := filepath.Join(t.TempDir(), "config")
|
||||||
|
err := os.WriteFile(kubeconfigPath, []byte(tt.existing), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
kc := kubernetesCluster{
|
||||||
|
name: "foo",
|
||||||
|
url: &url.URL{Scheme: "https", Host: "example.com"},
|
||||||
|
}
|
||||||
|
err = writeKubeconfig(kubeconfigPath, kc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
b, err := os.ReadFile(kubeconfigPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := `apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
insecure-skip-tls-verify: true
|
||||||
|
server: https://example.com
|
||||||
|
name: foo
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: foo
|
||||||
|
namespace: default
|
||||||
|
user: netbird
|
||||||
|
name: foo
|
||||||
|
current-context: foo
|
||||||
|
kind: Config
|
||||||
|
users:
|
||||||
|
- name: netbird
|
||||||
|
user:
|
||||||
|
token: none
|
||||||
|
`
|
||||||
|
require.Equal(t, expected, string(b))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -96,17 +96,19 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str
|
|||||||
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
|
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handle := activeProf.ID.String()
|
||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: providedSetupKey,
|
SetupKey: providedSetupKey,
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
IsUnixDesktopClient: isUnixRunningDesktop(),
|
IsUnixDesktopClient: isUnixRunningDesktop(),
|
||||||
Hostname: hostName,
|
Hostname: hostName,
|
||||||
DnsLabels: dnsLabelsReq,
|
DnsLabels: dnsLabelsReq,
|
||||||
ProfileName: &activeProf.Name,
|
ProfileName: &handle,
|
||||||
Username: &username,
|
Username: &username,
|
||||||
}
|
}
|
||||||
|
|
||||||
profileState, err := pm.GetProfileState(activeProf.Name)
|
profileState, err := pm.GetProfileState(activeProf.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
||||||
} else if profileState.Email != "" {
|
} else if profileState.Email != "" {
|
||||||
@@ -170,14 +172,13 @@ func getActiveProfile(ctx context.Context, pm *profilemanager.ProfileManager, pr
|
|||||||
return activeProf, nil
|
return activeProf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) error {
|
func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, handle string, username string) error {
|
||||||
err := switchProfile(context.Background(), profileName, username)
|
resolvedID, err := switchProfile(ctx, handle, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("switch profile on daemon: %v", err)
|
return fmt.Errorf("switch profile on daemon: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pm.SwitchProfile(profileName)
|
if err := pm.SwitchProfile(resolvedID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("switch profile: %v", err)
|
return fmt.Errorf("switch profile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,11 +206,15 @@ func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManage
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchProfile(ctx context.Context, profileName string, username string) error {
|
// switchProfile asks the daemon to switch to the profile identified by
|
||||||
|
// handle (a name, ID, or unique ID prefix). Returns the resolved profile
|
||||||
|
// ID so the caller can update the local active-profile state without
|
||||||
|
// re-resolving the handle.
|
||||||
|
func switchProfile(ctx context.Context, handle string, username string) (profilemanager.ID, error) {
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint
|
//nolint
|
||||||
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
return "", fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
"If the daemon is not running please run: "+
|
"If the daemon is not running please run: "+
|
||||||
"\nnetbird service install \nnetbird service start\n", err)
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
}
|
}
|
||||||
@@ -217,15 +222,15 @@ func switchProfile(ctx context.Context, profileName string, username string) err
|
|||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
_, err = client.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
resp, err := client.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
||||||
ProfileName: &profileName,
|
ProfileName: &handle,
|
||||||
Username: &username,
|
Username: &username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("switch profile failed: %v", err)
|
return "", fmt.Errorf("switch profile failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return profilemanager.ID(resp.Id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error {
|
func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error {
|
||||||
@@ -249,7 +254,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
|||||||
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.Name)
|
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -277,7 +282,7 @@ func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.Lo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey, profileName string) error {
|
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey string, profileID profilemanager.ID) error {
|
||||||
authClient, err := auth.NewAuth(ctx, config.PrivateKey, config.ManagementURL, config)
|
authClient, err := auth.NewAuth(ctx, config.PrivateKey, config.ManagementURL, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create auth client: %v", err)
|
return fmt.Errorf("failed to create auth client: %v", err)
|
||||||
@@ -291,7 +296,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
|
|||||||
|
|
||||||
jwtToken := ""
|
jwtToken := ""
|
||||||
if setupKey == "" && needsLogin {
|
if setupKey == "" && needsLogin {
|
||||||
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config, profileName)
|
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config, profileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("interactive sso login failed: %v", err)
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -306,10 +311,10 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, profileName string) (*auth.TokenInfo, error) {
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, profileID profilemanager.ID) (*auth.TokenInfo, error) {
|
||||||
hint := ""
|
hint := ""
|
||||||
pm := profilemanager.NewProfileManager()
|
pm := profilemanager.NewProfileManager()
|
||||||
profileState, err := pm.GetProfileState(profileName)
|
profileState, err := pm.GetProfileState(profileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
||||||
} else if profileState.Email != "" {
|
} else if profileState.Email != "" {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestLogin(t *testing.T) {
|
|||||||
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
|
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
|
||||||
sm := profilemanager.ServiceManager{}
|
sm := profilemanager.ServiceManager{}
|
||||||
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||||
Name: "default",
|
ID: "default",
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
@@ -14,6 +19,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var profileListShowID bool
|
||||||
|
|
||||||
var profileCmd = &cobra.Command{
|
var profileCmd = &cobra.Command{
|
||||||
Use: "profile",
|
Use: "profile",
|
||||||
Short: "Manage NetBird client profiles",
|
Short: "Manage NetBird client profiles",
|
||||||
@@ -31,27 +38,40 @@ var profileListCmd = &cobra.Command{
|
|||||||
var profileAddCmd = &cobra.Command{
|
var profileAddCmd = &cobra.Command{
|
||||||
Use: "add <profile_name>",
|
Use: "add <profile_name>",
|
||||||
Short: "Add a new profile",
|
Short: "Add a new profile",
|
||||||
Long: `Add a new profile to the NetBird client. The profile name must be unique.`,
|
Long: `Add a new profile. Profile name is free-form, a unique ID is generated for the on-disk config file.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: addProfileFunc,
|
RunE: addProfileFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var profileRenameCmd = &cobra.Command{
|
||||||
|
Use: "rename <profile> <new_profile_name>",
|
||||||
|
Short: "Renames an existing profile",
|
||||||
|
Long: `Renames an existing profile (by a name, ID, or unique ID prefix). Profile name is free-form.`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: renameProfileFunc,
|
||||||
|
}
|
||||||
|
|
||||||
var profileRemoveCmd = &cobra.Command{
|
var profileRemoveCmd = &cobra.Command{
|
||||||
Use: "remove <profile_name>",
|
Use: "remove <profile>",
|
||||||
Short: "Remove a profile",
|
Short: "Remove a profile",
|
||||||
Long: `Remove a profile from the NetBird client. The profile must not be inactive.`,
|
Long: `Remove a profile by name, ID, or unique ID prefix.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Aliases: []string{"rm"},
|
||||||
RunE: removeProfileFunc,
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: removeProfileFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileSelectCmd = &cobra.Command{
|
var profileSelectCmd = &cobra.Command{
|
||||||
Use: "select <profile_name>",
|
Use: "select <profile>",
|
||||||
Short: "Select a profile",
|
Short: "Select a profile",
|
||||||
Long: `Make the specified profile active. This will switch the client to use the selected profile's configuration.`,
|
Long: `Make the specified profile active. Accepts a name, ID, or unique ID prefix.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: selectProfileFunc,
|
RunE: selectProfileFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
profileListCmd.Flags().BoolVar(&profileListShowID, "show-id", false, "show the profile ID column")
|
||||||
|
}
|
||||||
|
|
||||||
func setupCmd(cmd *cobra.Command) error {
|
func setupCmd(cmd *cobra.Command) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
SetFlagsFromEnvVars(cmd)
|
SetFlagsFromEnvVars(cmd)
|
||||||
@@ -65,6 +85,7 @@ func setupCmd(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listProfilesFunc(cmd *cobra.Command, _ []string) error {
|
func listProfilesFunc(cmd *cobra.Command, _ []string) error {
|
||||||
if err := setupCmd(cmd); err != nil {
|
if err := setupCmd(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -83,25 +104,33 @@ func listProfilesFunc(cmd *cobra.Command, _ []string) error {
|
|||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
profiles, err := daemonClient.ListProfiles(cmd.Context(), &proto.ListProfilesRequest{
|
resp, err := daemonClient.ListProfiles(cmd.Context(), &proto.ListProfilesRequest{
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// list profiles, add a tick if the profile is active
|
tw := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
|
||||||
cmd.Println("Found", len(profiles.Profiles), "profiles:")
|
if profileListShowID {
|
||||||
for _, profile := range profiles.Profiles {
|
fmt.Fprintln(tw, "ID\tNAME\tACTIVE")
|
||||||
// use a cross to indicate the passive profiles
|
} else {
|
||||||
activeMarker := "✗"
|
fmt.Fprintln(tw, "NAME\tACTIVE")
|
||||||
if profile.IsActive {
|
|
||||||
activeMarker = "✓"
|
|
||||||
}
|
|
||||||
cmd.Println(activeMarker, profile.Name)
|
|
||||||
}
|
}
|
||||||
|
for _, profile := range resp.Profiles {
|
||||||
return nil
|
marker := ""
|
||||||
|
if profile.IsActive {
|
||||||
|
marker = "✓"
|
||||||
|
}
|
||||||
|
name := profilemanager.StripCtrlChars(profile.Name)
|
||||||
|
id := profilemanager.ID(profile.Id)
|
||||||
|
if profileListShowID {
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t%s\n", id.ShortID(), name, marker)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\n", name, marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addProfileFunc(cmd *cobra.Command, args []string) error {
|
func addProfileFunc(cmd *cobra.Command, args []string) error {
|
||||||
@@ -121,21 +150,82 @@ func addProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
profileName := args[0]
|
profileName := args[0]
|
||||||
|
|
||||||
_, err = daemonClient.AddProfile(cmd.Context(), &proto.AddProfileRequest{
|
resp, err := daemonClient.AddProfile(cmd.Context(), &proto.AddProfileRequest{
|
||||||
ProfileName: profileName,
|
ProfileName: profileName,
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return fmt.Errorf("add profile request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dupCount, _ := countProfilesWithName(cmd.Context(), daemonClient, currUser.Username, profileName)
|
||||||
|
if dupCount > 1 {
|
||||||
|
cmd.Printf("Warning: %d other profile(s) already use the name %q.\n", dupCount-1, profileName)
|
||||||
|
cmd.Println("Use `netbird profile list --show-id` to disambiguate later.")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := profilemanager.ID(resp.Id)
|
||||||
|
cmd.Printf("Profile added: %s %s\n", id.ShortID(), profilemanager.StripCtrlChars(profileName))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func renameProfileFunc(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := setupCmd(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("Profile added successfully:", profileName)
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to service CLI interface: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
currUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
handle := args[0]
|
||||||
|
newProfilename := args[1]
|
||||||
|
|
||||||
|
resp, err := daemonClient.RenameProfile(cmd.Context(), &proto.RenameProfileRequest{
|
||||||
|
Handle: handle,
|
||||||
|
Username: currUser.Username,
|
||||||
|
NewProfileName: newProfilename,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return wrapAmbiguityError(err, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
dupCount, _ := countProfilesWithName(cmd.Context(), daemonClient, currUser.Username, newProfilename)
|
||||||
|
if dupCount > 1 {
|
||||||
|
cmd.Printf("Warning: %d other profile(s) already use the name %q.\n", dupCount-1, newProfilename)
|
||||||
|
cmd.Println("Use `netbird profile list --show-id` to disambiguate later.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Profile renamed from %s to %s\n", profilemanager.StripCtrlChars(resp.OldProfileName), profilemanager.StripCtrlChars(newProfilename))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func countProfilesWithName(ctx context.Context, c proto.DaemonServiceClient, username, name string) (int, error) {
|
||||||
|
resp, err := c.ListProfiles(ctx, &proto.ListProfilesRequest{Username: username})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for _, p := range resp.Profiles {
|
||||||
|
if p.Name == name {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
func removeProfileFunc(cmd *cobra.Command, args []string) error {
|
func removeProfileFunc(cmd *cobra.Command, args []string) error {
|
||||||
if err := setupCmd(cmd); err != nil {
|
if err := setupCmd(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -153,18 +243,17 @@ func removeProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
handle := args[0]
|
||||||
|
|
||||||
profileName := args[0]
|
resp, err := daemonClient.RemoveProfile(cmd.Context(), &proto.RemoveProfileRequest{
|
||||||
|
ProfileName: handle,
|
||||||
_, err = daemonClient.RemoveProfile(cmd.Context(), &proto.RemoveProfileRequest{
|
|
||||||
ProfileName: profileName,
|
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return wrapAmbiguityError(err, handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("Profile removed successfully:", profileName)
|
cmd.Printf("Profile removed: %s\n", resp.Id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +263,7 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
profileManager := profilemanager.NewProfileManager()
|
profileManager := profilemanager.NewProfileManager()
|
||||||
profileName := args[0]
|
handle := args[0]
|
||||||
|
|
||||||
currUser, err := user.Current()
|
currUser, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -191,32 +280,15 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
profiles, err := daemonClient.ListProfiles(ctx, &proto.ListProfilesRequest{
|
switchResp, err := daemonClient.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
||||||
Username: currUser.Username,
|
ProfileName: &handle,
|
||||||
|
Username: &currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list profiles: %w", err)
|
return wrapAmbiguityError(err, handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileExists bool
|
if err := profileManager.SwitchProfile(profilemanager.ID(switchResp.Id)); err != nil {
|
||||||
|
|
||||||
for _, profile := range profiles.Profiles {
|
|
||||||
if profile.Name == profileName {
|
|
||||||
profileExists = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !profileExists {
|
|
||||||
return fmt.Errorf("profile %s does not exist", profileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := switchProfile(cmd.Context(), profileName, currUser.Username); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = profileManager.SwitchProfile(profileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +303,30 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("Profile switched successfully to:", profileName)
|
id := profilemanager.ID(switchResp.Id)
|
||||||
|
cmd.Printf("Profile switched to: %s\n", id.ShortID())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapAmbiguityError turns the daemon's gRPC InvalidArgument errors
|
||||||
|
// (which carry the resolver's message verbatim) into CLI-friendly text
|
||||||
|
// that points the user at --show-id.
|
||||||
|
func wrapAmbiguityError(err error, handle string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
st, ok := gstatus.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch st.Code() {
|
||||||
|
case codes.InvalidArgument:
|
||||||
|
msg := st.Message()
|
||||||
|
if strings.Contains(msg, "ambiguous") {
|
||||||
|
return errors.New(msg + "\nRun `netbird profile list --show-id` to see IDs, then select by ID prefix:\n netbird profile select|remove <id-prefix>")
|
||||||
|
}
|
||||||
|
case codes.NotFound:
|
||||||
|
return fmt.Errorf("profile %q not found", handle)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Execute executes the root command.
|
// Execute runs the appropriate Cobra command for the CLI.
|
||||||
|
// If the process is the update binary it delegates to updateCmd; otherwise it runs the root command.
|
||||||
|
// It returns any error produced during command execution.
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
if isUpdateBinary() {
|
if isUpdateBinary() {
|
||||||
return updateCmd.Execute()
|
return updateCmd.Execute()
|
||||||
@@ -103,6 +105,16 @@ func Execute() error {
|
|||||||
return rootCmd.Execute()
|
return rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init initialises package-level defaults and configures the root
|
||||||
|
// Cobra command tree. Sets platform-specific config / log directory
|
||||||
|
// paths (including legacy Wiretrustee fallbacks) and a default daemon
|
||||||
|
// address; registers persistent CLI flags (daemon address,
|
||||||
|
// management / admin URLs, logging, setup key (file and inline,
|
||||||
|
// mutually exclusive), preshared key, hostname, anonymise, config
|
||||||
|
// path); attaches top-level and nested subcommands to the root
|
||||||
|
// command; and registers `up`-specific persistent flags (external IP
|
||||||
|
// maps, custom DNS resolver address, Rosenpass options, auto-connect
|
||||||
|
// disabling, lazy connection).
|
||||||
func init() {
|
func init() {
|
||||||
defaultConfigPathDir = "/etc/netbird/"
|
defaultConfigPathDir = "/etc/netbird/"
|
||||||
defaultLogFileDir = "/var/log/netbird/"
|
defaultLogFileDir = "/var/log/netbird/"
|
||||||
@@ -168,10 +180,17 @@ func init() {
|
|||||||
logCmd.AddCommand(logLevelCmd)
|
logCmd.AddCommand(logLevelCmd)
|
||||||
debugCmd.AddCommand(forCmd)
|
debugCmd.AddCommand(forCmd)
|
||||||
debugCmd.AddCommand(persistenceCmd)
|
debugCmd.AddCommand(persistenceCmd)
|
||||||
|
debugCmd.AddCommand(debugConfigCmd)
|
||||||
|
|
||||||
|
// kubernetes commands
|
||||||
|
rootCmd.AddCommand(kubernetesCmd)
|
||||||
|
kubernetesCmd.AddCommand(kubernetesListCmd)
|
||||||
|
kubernetesCmd.AddCommand(kubernetesWriteKubeconfigCmd)
|
||||||
|
|
||||||
// profile commands
|
// profile commands
|
||||||
profileCmd.AddCommand(profileListCmd)
|
profileCmd.AddCommand(profileListCmd)
|
||||||
profileCmd.AddCommand(profileAddCmd)
|
profileCmd.AddCommand(profileAddCmd)
|
||||||
|
profileCmd.AddCommand(profileRenameCmd)
|
||||||
profileCmd.AddCommand(profileRemoveCmd)
|
profileCmd.AddCommand(profileRemoveCmd)
|
||||||
profileCmd.AddCommand(profileSelectCmd)
|
profileCmd.AddCommand(profileSelectCmd)
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (p *program) Stop(srv service.Service) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Common setup for service control commands
|
// Common setup for service control commands
|
||||||
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc) (service.Service, error) {
|
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc, consoleLog bool) (service.Service, error) {
|
||||||
// rootCmd env vars are already applied by PersistentPreRunE.
|
// rootCmd env vars are already applied by PersistentPreRunE.
|
||||||
SetFlagsFromEnvVars(serviceCmd)
|
SetFlagsFromEnvVars(serviceCmd)
|
||||||
|
|
||||||
@@ -112,8 +112,14 @@ func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := util.InitLog(logLevel, logFiles...); err != nil {
|
if consoleLog {
|
||||||
return nil, fmt.Errorf("init log: %w", err)
|
if err := util.InitLog(logLevel, util.LogConsole); err != nil {
|
||||||
|
return nil, fmt.Errorf("init log: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := util.InitLog(logLevel, logFiles...); err != nil {
|
||||||
|
return nil, fmt.Errorf("init log: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := newSVCConfig()
|
cfg, err := newSVCConfig()
|
||||||
@@ -138,7 +144,7 @@ var runCmd = &cobra.Command{
|
|||||||
SetupCloseHandler(ctx, cancel)
|
SetupCloseHandler(ctx, cancel)
|
||||||
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
|
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
|
||||||
|
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -152,7 +158,7 @@ var startCmd = &cobra.Command{
|
|||||||
Short: "starts NetBird service",
|
Short: "starts NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -170,7 +176,7 @@ var stopCmd = &cobra.Command{
|
|||||||
Short: "stops NetBird service",
|
Short: "stops NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -188,7 +194,7 @@ var restartCmd = &cobra.Command{
|
|||||||
Short: "restarts NetBird service",
|
Short: "restarts NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -206,7 +212,7 @@ var svcStatusCmd = &cobra.Command{
|
|||||||
Short: "shows NetBird service status",
|
Short: "shows NetBird service status",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||||
|
|
||||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
iv, _ := integrations.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
iv, _ := validator.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -128,13 +128,12 @@ func upFunc(cmd *cobra.Command, args []string) error {
|
|||||||
var profileSwitched bool
|
var profileSwitched bool
|
||||||
// switch profile if provided
|
// switch profile if provided
|
||||||
if profileName != "" {
|
if profileName != "" {
|
||||||
err = switchProfile(cmd.Context(), profileName, username.Username)
|
resolvedID, err := switchProfile(cmd.Context(), profileName, username.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("switch profile: %v", err)
|
return fmt.Errorf("switch profile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pm.SwitchProfile(profileName)
|
if err := pm.SwitchProfile(resolvedID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("switch profile: %v", err)
|
return fmt.Errorf("switch profile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +189,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
|
|||||||
|
|
||||||
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, providedSetupKey, activeProf.Name)
|
err = foregroundLogin(ctx, cmd, config, providedSetupKey, activeProf.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -261,10 +260,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the new config
|
// set the new config
|
||||||
req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.Name, username.Username)
|
req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.ID.String(), username.Username)
|
||||||
if _, err := client.SetConfig(ctx, req); err != nil {
|
if _, err := client.SetConfig(ctx, req); err != nil {
|
||||||
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable {
|
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable {
|
||||||
log.Warnf("setConfig method is not available in the daemon")
|
log.Warnf("setConfig method is not available in the daemon: %s", st.Message())
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("call service setConfig method: %v", err)
|
return fmt.Errorf("call service setConfig method: %v", err)
|
||||||
}
|
}
|
||||||
@@ -289,10 +288,11 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
|
|||||||
return fmt.Errorf("setup login request: %v", err)
|
return fmt.Errorf("setup login request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loginRequest.ProfileName = &activeProf.Name
|
profileID := activeProf.ID.String()
|
||||||
|
loginRequest.ProfileName = &profileID
|
||||||
loginRequest.Username = &username
|
loginRequest.Username = &username
|
||||||
|
|
||||||
profileState, err := pm.GetProfileState(activeProf.Name)
|
profileState, err := pm.GetProfileState(activeProf.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
||||||
} else if profileState.Email != "" {
|
} else if profileState.Email != "" {
|
||||||
@@ -329,7 +329,7 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := client.Up(ctx, &proto.UpRequest{
|
if _, err := client.Up(ctx, &proto.UpRequest{
|
||||||
ProfileName: &activeProf.Name,
|
ProfileName: &profileID,
|
||||||
Username: &username,
|
Username: &username,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("call service up method: %v", err)
|
return fmt.Errorf("call service up method: %v", err)
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ func TestUpDaemon(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sm := profilemanager.ServiceManager{}
|
sm := profilemanager.ServiceManager{}
|
||||||
err = sm.AddProfile("test1", currUser.Username)
|
created, err := sm.AddProfile("test1", currUser.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to add profile: %v", err)
|
t.Fatalf("failed to add profile: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||||
Name: "test1",
|
ID: created.ID,
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ var (
|
|||||||
Short: "Print the NetBird's client application version",
|
Short: "Print the NetBird's client application version",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
cmd.Println(version.NetbirdVersion())
|
out := version.NetbirdVersion()
|
||||||
|
if version.IsDevelopmentVersion(out) {
|
||||||
|
if commit := version.NetbirdCommit(); commit != "" {
|
||||||
|
out += "-" + commit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.Println(out)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
@@ -84,6 +85,12 @@ type Options struct {
|
|||||||
DisableIPv6 bool
|
DisableIPv6 bool
|
||||||
// BlockInbound blocks all inbound connections from peers
|
// BlockInbound blocks all inbound connections from peers
|
||||||
BlockInbound bool
|
BlockInbound bool
|
||||||
|
// BlockLANAccess blocks the embedded peer from reaching the host's
|
||||||
|
// LAN (RFC 1918, link-local, loopback) when it's used as a routing
|
||||||
|
// peer. Mirrors profilemanager.ConfigInput.BlockLANAccess. Useful
|
||||||
|
// when the embedded client must never act as a stepping stone into
|
||||||
|
// the host's local network (e.g. the proxy's overlay peer).
|
||||||
|
BlockLANAccess bool
|
||||||
// WireguardPort is the port for the tunnel 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 tunnel interface.
|
// MTU is the MTU for the tunnel interface.
|
||||||
@@ -94,6 +101,26 @@ type Options struct {
|
|||||||
MTU *uint16
|
MTU *uint16
|
||||||
// DNSLabels defines additional DNS labels configured in the peer.
|
// DNSLabels defines additional DNS labels configured in the peer.
|
||||||
DNSLabels []string
|
DNSLabels []string
|
||||||
|
// Performance configures the tunnel's buffer pool cap and batch size.
|
||||||
|
Performance Performance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance configures the embedded client's tunnel memory/throughput knobs.
|
||||||
|
//
|
||||||
|
// These settings are process-global: any non-nil field also becomes the
|
||||||
|
// default for Clients constructed by later embed.New calls in the same
|
||||||
|
// process. Nil fields are ignored.
|
||||||
|
type Performance struct {
|
||||||
|
// PreallocatedBuffersPerPool caps the per-tunnel buffer pool. Zero
|
||||||
|
// leaves the pool unbounded. Lower values trade throughput for a
|
||||||
|
// tighter memory ceiling. May also be changed on a running Client via
|
||||||
|
// Client.SetPerformance, provided this field was nonzero at construction.
|
||||||
|
PreallocatedBuffersPerPool *uint32
|
||||||
|
// MaxBatchSize overrides the number of packets the tunnel reads or
|
||||||
|
// writes per syscall, which also bounds eager buffer allocation per
|
||||||
|
// worker. Zero uses the platform default. Applied at construction
|
||||||
|
// only; ignored by Client.SetPerformance.
|
||||||
|
MaxBatchSize *uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCredentials checks that exactly one credential type is provided
|
// validateCredentials checks that exactly one credential type is provided
|
||||||
@@ -175,6 +202,7 @@ func New(opts Options) (*Client, error) {
|
|||||||
DisableClientRoutes: &opts.DisableClientRoutes,
|
DisableClientRoutes: &opts.DisableClientRoutes,
|
||||||
DisableIPv6: &opts.DisableIPv6,
|
DisableIPv6: &opts.DisableIPv6,
|
||||||
BlockInbound: &opts.BlockInbound,
|
BlockInbound: &opts.BlockInbound,
|
||||||
|
BlockLANAccess: &opts.BlockLANAccess,
|
||||||
WireguardPort: opts.WireguardPort,
|
WireguardPort: opts.WireguardPort,
|
||||||
MTU: opts.MTU,
|
MTU: opts.MTU,
|
||||||
DNSLabels: parsedLabels,
|
DNSLabels: parsedLabels,
|
||||||
@@ -192,6 +220,13 @@ func New(opts Options) (*Client, error) {
|
|||||||
config.PrivateKey = opts.PrivateKey
|
config.PrivateKey = opts.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Performance.PreallocatedBuffersPerPool != nil {
|
||||||
|
wgdevice.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool)
|
||||||
|
}
|
||||||
|
if opts.Performance.MaxBatchSize != nil {
|
||||||
|
wgdevice.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
deviceName: opts.DeviceName,
|
deviceName: opts.DeviceName,
|
||||||
setupKey: opts.SetupKey,
|
setupKey: opts.SetupKey,
|
||||||
@@ -244,6 +279,10 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-startCtx.Done():
|
case <-startCtx.Done():
|
||||||
|
// Cancel the client context before stopping: Engine.Start blocks on the
|
||||||
|
// signal stream while holding the engine mutex and only unblocks on
|
||||||
|
// cancellation. Stopping first would deadlock on that mutex.
|
||||||
|
cancel()
|
||||||
if stopErr := client.Stop(); stopErr != nil {
|
if stopErr := client.Stop(); stopErr != nil {
|
||||||
return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
|
return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
|
||||||
}
|
}
|
||||||
@@ -405,6 +444,21 @@ func (c *Client) Expose(ctx context.Context, req ExposeRequest) (*ExposeSession,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IdentityForIP looks up a remote peer by its tunnel IP using the
|
||||||
|
// embedded client's status recorder. Returns the peer's WireGuard public
|
||||||
|
// key and FQDN. ok=false means the IP doesn't belong to an active peer
|
||||||
|
// — offline roster peers are treated as unknown, same as foreign IPs.
|
||||||
|
func (c *Client) IdentityForIP(ip netip.Addr) (pubKey, fqdn string, ok bool) {
|
||||||
|
if !ip.IsValid() || c.recorder == nil {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
state, found := c.recorder.PeerStateByIP(ip.String())
|
||||||
|
if !found {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return state.PubKey, state.FQDN, true
|
||||||
|
}
|
||||||
|
|
||||||
// Status returns the current status of the client.
|
// Status returns the current status of the client.
|
||||||
func (c *Client) Status() (peer.FullStatus, error) {
|
func (c *Client) Status() (peer.FullStatus, error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -473,6 +527,25 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
|||||||
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPerformance retunes a running Client. Only PreallocatedBuffersPerPool
|
||||||
|
// takes effect, and only when it was nonzero at construction;
|
||||||
|
// MaxBatchSize is construction-only and returns an error if set here.
|
||||||
|
//
|
||||||
|
// Returns ErrClientNotStarted / ErrEngineNotStarted if the Client is not
|
||||||
|
// running yet.
|
||||||
|
func (c *Client) SetPerformance(t Performance) error {
|
||||||
|
if t.MaxBatchSize != nil {
|
||||||
|
return errors.New("MaxBatchSize is construction-only and cannot be changed at runtime")
|
||||||
|
}
|
||||||
|
engine, err := c.getEngine()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return engine.SetPerformance(internal.Performance{
|
||||||
|
PreallocatedBuffersPerPool: t.PreallocatedBuffersPerPool,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// StartCapture begins capturing packets on this client's tunnel device.
|
// 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.
|
// Only one capture can be active at a time; starting a new one stops the previous.
|
||||||
// Call StopCapture (or CaptureSession.Stop) to end it.
|
// Call StopCapture (or CaptureSession.Stop) to end it.
|
||||||
|
|||||||
168
client/embed/embed_test.go
Normal file
168
client/embed/embed_test.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package embed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/modules/peers"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||||
|
mgmt "github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||||
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/job"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
||||||
|
|
||||||
|
// TestClientStartTimeoutRollback reproduces a deadlock between Engine.Start and
|
||||||
|
// Engine.Stop. The signal endpoint accepts gRPC connections but never serves the
|
||||||
|
// SignalExchange service, so Engine.Start parks in WaitStreamConnected while
|
||||||
|
// holding the engine mutex. When the Start context expires, the rollback path
|
||||||
|
// calls ConnectClient.Stop, which must not block forever acquiring that mutex.
|
||||||
|
func TestClientStartTimeoutRollback(t *testing.T) {
|
||||||
|
signalAddr := startBlackholeSignal(t)
|
||||||
|
mgmAddr := startManagement(t, signalAddr)
|
||||||
|
|
||||||
|
wgPort := 0
|
||||||
|
client, err := New(Options{
|
||||||
|
DeviceName: "embed-rollback-test",
|
||||||
|
SetupKey: testSetupKey,
|
||||||
|
ManagementURL: "http://" + mgmAddr,
|
||||||
|
WireguardPort: &wgPort,
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "embed client creation must succeed")
|
||||||
|
|
||||||
|
startCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
startErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
startErr <- client.Start(startCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-startErr:
|
||||||
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
t.Fatal("client.Start did not return after its context expired: Engine.Stop deadlocked against Engine.Start waiting for the signal stream")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startBlackholeSignal starts a gRPC server without the SignalExchange service
|
||||||
|
// registered. Connections succeed, but the signal stream can never be
|
||||||
|
// established, which keeps Engine.Start parked in WaitStreamConnected.
|
||||||
|
func startBlackholeSignal(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "localhost:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s := grpc.NewServer()
|
||||||
|
go func() {
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
t.Cleanup(s.Stop)
|
||||||
|
|
||||||
|
return lis.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startManagement(t *testing.T, signalAddr string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Stuns: []*config.Host{},
|
||||||
|
TURNConfig: &config.TURNConfig{},
|
||||||
|
Relay: &config.Relay{
|
||||||
|
Addresses: []string{"127.0.0.1:1234"},
|
||||||
|
CredentialsTTL: util.Duration{Duration: time.Hour},
|
||||||
|
Secret: "222222222222222222",
|
||||||
|
},
|
||||||
|
Signal: &config.Host{
|
||||||
|
Proto: "http",
|
||||||
|
URI: signalAddr,
|
||||||
|
},
|
||||||
|
Datadir: t.TempDir(),
|
||||||
|
HttpConfig: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "localhost:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s := grpc.NewServer()
|
||||||
|
|
||||||
|
testStore, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", cfg.Datadir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
|
|
||||||
|
permissionsManager := permissions.NewManager(testStore)
|
||||||
|
peersManager := peers.NewManager(testStore, permissionsManager)
|
||||||
|
jobManager := job.NewJobManager(nil, testStore, peersManager)
|
||||||
|
|
||||||
|
cacheStore, err := nbcache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
iv, err := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
||||||
|
require.NoError(t, err)
|
||||||
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
settingsMockManager := settings.NewMockManager(ctrl)
|
||||||
|
settingsMockManager.EXPECT().
|
||||||
|
GetSettings(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
Return(&types.Settings{}, nil).
|
||||||
|
AnyTimes()
|
||||||
|
settingsMockManager.EXPECT().
|
||||||
|
GetExtraSettings(gomock.Any(), gomock.Any()).
|
||||||
|
Return(&types.ExtraSettings{}, nil).
|
||||||
|
AnyTimes()
|
||||||
|
|
||||||
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
|
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||||
|
requestBuffer := mgmt.NewAccountRequestBuffer(context.Background(), testStore)
|
||||||
|
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg)
|
||||||
|
accountManager, err := mgmt.BuildManager(context.Background(), cfg, testStore, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
secretsManager, err := nbgrpc.NewTimeBasedAuthSecretsManager(updateManager, cfg.TURNConfig, cfg.Relay, settingsMockManager, groupsManager)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mgmtServer, err := nbgrpc.NewServer(cfg, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
t.Cleanup(s.Stop)
|
||||||
|
|
||||||
|
return lis.Addr().String()
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package iptables
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
@@ -421,12 +422,17 @@ func (m *aclManager) updateState() {
|
|||||||
currentState.Lock()
|
currentState.Lock()
|
||||||
defer currentState.Unlock()
|
defer currentState.Unlock()
|
||||||
|
|
||||||
|
// Clone the maps so the persisted state holds a private snapshot. The
|
||||||
|
// live maps keep being mutated by subsequent rule operations while the
|
||||||
|
// state manager marshals the state from its periodic-save goroutine.
|
||||||
|
// Sharing them by reference races the two and aborts the process with a
|
||||||
|
// concurrent map iteration and write.
|
||||||
if m.v6 {
|
if m.v6 {
|
||||||
currentState.ACLEntries6 = m.entries
|
currentState.ACLEntries6 = maps.Clone(m.entries)
|
||||||
currentState.ACLIPsetStore6 = m.ipsetStore
|
currentState.ACLIPsetStore6 = m.ipsetStore.clone()
|
||||||
} else {
|
} else {
|
||||||
currentState.ACLEntries = m.entries
|
currentState.ACLEntries = maps.Clone(m.entries)
|
||||||
currentState.ACLIPsetStore = m.ipsetStore
|
currentState.ACLIPsetStore = m.ipsetStore.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.stateManager.UpdateState(currentState); err != nil {
|
if err := m.stateManager.UpdateState(currentState); err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package iptables
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -749,11 +750,17 @@ func (r *router) updateState() {
|
|||||||
currentState.Lock()
|
currentState.Lock()
|
||||||
defer currentState.Unlock()
|
defer currentState.Unlock()
|
||||||
|
|
||||||
|
// Clone the rule map so the persisted state holds a private snapshot. The
|
||||||
|
// live map keeps being mutated by subsequent rule operations while the
|
||||||
|
// state manager marshals the state from its periodic-save goroutine.
|
||||||
|
// Sharing it by reference races the two and aborts the process with a
|
||||||
|
// concurrent map iteration and write. The ipset counter guards itself
|
||||||
|
// during marshaling, so it can be shared directly.
|
||||||
if r.v6 {
|
if r.v6 {
|
||||||
currentState.RouteRules6 = r.rules
|
currentState.RouteRules6 = maps.Clone(r.rules)
|
||||||
currentState.RouteIPsetCounter6 = r.ipsetCounter
|
currentState.RouteIPsetCounter6 = r.ipsetCounter
|
||||||
} else {
|
} else {
|
||||||
currentState.RouteRules = r.rules
|
currentState.RouteRules = maps.Clone(r.rules)
|
||||||
currentState.RouteIPsetCounter = r.ipsetCounter
|
currentState.RouteIPsetCounter = r.ipsetCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"maps"
|
||||||
|
)
|
||||||
|
|
||||||
type ipList struct {
|
type ipList struct {
|
||||||
ips map[string]struct{}
|
ips map[string]struct{}
|
||||||
@@ -19,6 +22,14 @@ func (s *ipList) addIP(ip string) {
|
|||||||
s.ips[ip] = struct{}{}
|
s.ips[ip] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clone returns a deep copy of the ipList with its own ips map.
|
||||||
|
func (s *ipList) clone() *ipList {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ipList{ips: maps.Clone(s.ips)}
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler
|
// MarshalJSON implements json.Marshaler
|
||||||
func (s *ipList) MarshalJSON() ([]byte, error) {
|
func (s *ipList) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(struct {
|
return json.Marshal(struct {
|
||||||
@@ -55,6 +66,19 @@ func newIpsetStore() *ipsetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clone returns a deep copy of the ipsetStore with its own ipsets map and
|
||||||
|
// independent ipList entries.
|
||||||
|
func (s *ipsetStore) clone() *ipsetStore {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cloned := &ipsetStore{ipsets: make(map[string]*ipList, len(s.ipsets))}
|
||||||
|
for name, list := range s.ipsets {
|
||||||
|
cloned.ipsets[name] = list.clone()
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ipsetStore) ipset(ipsetName string) (*ipList, bool) {
|
func (s *ipsetStore) ipset(ipsetName string) (*ipList, bool) {
|
||||||
r, ok := s.ipsets[ipsetName]
|
r, ok := s.ipsets[ipsetName]
|
||||||
return r, ok
|
return r, ok
|
||||||
|
|||||||
@@ -52,9 +52,10 @@ func (m *externalChainMonitor) start() {
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
m.cancel = cancel
|
m.cancel = cancel
|
||||||
m.done = make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
m.done = done
|
||||||
|
|
||||||
go m.run(ctx)
|
go m.run(ctx, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *externalChainMonitor) stop() {
|
func (m *externalChainMonitor) stop() {
|
||||||
@@ -72,8 +73,8 @@ func (m *externalChainMonitor) stop() {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *externalChainMonitor) run(ctx context.Context) {
|
func (m *externalChainMonitor) run(ctx context.Context, done chan struct{}) {
|
||||||
defer close(m.done)
|
defer close(done)
|
||||||
|
|
||||||
bo := &backoff.ExponentialBackOff{
|
bo := &backoff.ExponentialBackOff{
|
||||||
InitialInterval: externalMonitorInitInterval,
|
InitialInterval: externalMonitorInitInterval,
|
||||||
|
|||||||
@@ -362,6 +362,10 @@ func (f *Forwarder) injectICMPv6Reply(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type ICEBind struct {
|
|||||||
*wgConn.StdNetBind
|
*wgConn.StdNetBind
|
||||||
|
|
||||||
transportNet transport.Net
|
transportNet transport.Net
|
||||||
filterFn udpmux.FilterFn
|
|
||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
mtu uint16
|
mtu uint16
|
||||||
|
|
||||||
@@ -61,12 +60,11 @@ type ICEBind struct {
|
|||||||
ipv6Conn *net.UDPConn
|
ipv6Conn *net.UDPConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wgaddr.Address, mtu uint16) *ICEBind {
|
func NewICEBind(transportNet transport.Net, address wgaddr.Address, mtu uint16) *ICEBind {
|
||||||
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
||||||
ib := &ICEBind{
|
ib := &ICEBind{
|
||||||
StdNetBind: b,
|
StdNetBind: b,
|
||||||
transportNet: transportNet,
|
transportNet: transportNet,
|
||||||
filterFn: filterFn,
|
|
||||||
address: address,
|
address: address,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
endpoints: make(map[netip.Addr]net.Conn),
|
endpoints: make(map[netip.Addr]net.Conn),
|
||||||
@@ -265,7 +263,6 @@ func (s *ICEBind) createOrUpdateMux() {
|
|||||||
udpmux.UniversalUDPMuxParams{
|
udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: muxConn,
|
UDPConn: muxConn,
|
||||||
Net: s.transportNet,
|
Net: s.transportNet,
|
||||||
FilterFn: s.filterFn,
|
|
||||||
WGAddress: s.address,
|
WGAddress: s.address,
|
||||||
MTU: s.mtu,
|
MTU: s.mtu,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ func setupICEBind(t *testing.T) *ICEBind {
|
|||||||
IP: netip.MustParseAddr("100.64.0.1"),
|
IP: netip.MustParseAddr("100.64.0.1"),
|
||||||
Network: netip.MustParsePrefix("100.64.0.0/10"),
|
Network: netip.MustParsePrefix("100.64.0.0/10"),
|
||||||
}
|
}
|
||||||
return NewICEBind(transportNet, nil, address, 1280)
|
return NewICEBind(transportNet, address, 1280)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDualStackConns(t *testing.T) (*net.UDPConn, *net.UDPConn) {
|
func createDualStackConns(t *testing.T) (*net.UDPConn, *net.UDPConn) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/tun"
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,10 +44,13 @@ type PacketCapture interface {
|
|||||||
type FilteredDevice struct {
|
type FilteredDevice struct {
|
||||||
tun.Device
|
tun.Device
|
||||||
|
|
||||||
filter PacketFilter
|
filter PacketFilter
|
||||||
capture atomic.Pointer[PacketCapture]
|
capture atomic.Pointer[PacketCapture]
|
||||||
mutex sync.RWMutex
|
// panicHandler is invoked after a panic in the underlying device is
|
||||||
closeOnce sync.Once
|
// recovered in Read or Write.
|
||||||
|
panicHandler atomic.Pointer[func()]
|
||||||
|
mutex sync.RWMutex
|
||||||
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDeviceFilter constructor function
|
// newDeviceFilter constructor function
|
||||||
@@ -70,7 +76,7 @@ func (d *FilteredDevice) Close() error {
|
|||||||
|
|
||||||
// Read wraps read method with filtering feature
|
// Read wraps read method with filtering feature
|
||||||
func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
||||||
if n, err = d.Device.Read(bufs, sizes, offset); err != nil {
|
if n, err = d.deviceRead(bufs, sizes, offset); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +118,7 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
|||||||
d.mutex.RUnlock()
|
d.mutex.RUnlock()
|
||||||
|
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
return d.Device.Write(bufs, offset)
|
return d.deviceWrite(bufs, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredBufs := make([][]byte, 0, len(bufs))
|
filteredBufs := make([][]byte, 0, len(bufs))
|
||||||
@@ -125,9 +131,44 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := d.Device.Write(filteredBufs, offset)
|
n, err := d.deviceWrite(filteredBufs, offset)
|
||||||
n += dropped
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
|
}
|
||||||
|
return n + dropped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deviceRead calls the underlying device Read, recovering from panics in the
|
||||||
|
// wintun read path and converting them into errors.
|
||||||
|
func (d *FilteredDevice) deviceRead(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
||||||
|
defer d.recoverFromPanic("read", &n, &err)
|
||||||
|
return d.Device.Read(bufs, sizes, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deviceWrite calls the underlying device Write, recovering from panics in the
|
||||||
|
// wintun write path and converting them into errors.
|
||||||
|
func (d *FilteredDevice) deviceWrite(bufs [][]byte, offset int) (n int, err error) {
|
||||||
|
defer d.recoverFromPanic("write", &n, &err)
|
||||||
|
return d.Device.Write(bufs, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recoverFromPanic converts a panic in the underlying device into a regular
|
||||||
|
// error and invokes the registered panic handler. The wintun read path is
|
||||||
|
// known to panic on zero-length packets that third-party filter drivers can
|
||||||
|
// place in the ring.
|
||||||
|
func (d *FilteredDevice) recoverFromPanic(op string, n *int, err *error) {
|
||||||
|
r := recover()
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("recovered panic in tun device %s: %v\n%s", op, r, debug.Stack())
|
||||||
|
*n = 0
|
||||||
|
*err = fmt.Errorf("tun device %s panic: %v", op, r)
|
||||||
|
|
||||||
|
if handler := d.panicHandler.Load(); handler != nil {
|
||||||
|
(*handler)()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFilter sets packet filter to device
|
// SetFilter sets packet filter to device
|
||||||
@@ -137,6 +178,17 @@ func (d *FilteredDevice) SetFilter(filter PacketFilter) {
|
|||||||
d.mutex.Unlock()
|
d.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPanicHandler registers a handler invoked after a recovered panic in Read
|
||||||
|
// or Write. The device is unusable after such a panic; the handler should
|
||||||
|
// trigger recreation of the interface. Pass nil to remove.
|
||||||
|
func (d *FilteredDevice) SetPanicHandler(handler func()) {
|
||||||
|
if handler == nil {
|
||||||
|
d.panicHandler.Store(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.panicHandler.Store(&handler)
|
||||||
|
}
|
||||||
|
|
||||||
// SetCapture sets or clears the packet capture sink. Pass nil to disable.
|
// 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
|
// Uses atomic store so the hot path (Read/Write) is a single pointer load
|
||||||
// with no locking overhead when capture is off.
|
// with no locking overhead when capture is off.
|
||||||
|
|||||||
@@ -221,3 +221,60 @@ func TestDeviceWrapperRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeviceWrapperReadPanic(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
tun := mocks.NewMockDevice(ctrl)
|
||||||
|
tun.EXPECT().Read(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
DoAndReturn(func(bufs [][]byte, sizes []int, offset int) (int, error) {
|
||||||
|
// Reproduce the wintun zero-length packet panic (index out of range).
|
||||||
|
packet := make([]byte, 0)
|
||||||
|
return int(packet[0]), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapped := newDeviceFilter(tun)
|
||||||
|
|
||||||
|
handlerCalled := false
|
||||||
|
wrapped.SetPanicHandler(func() { handlerCalled = true })
|
||||||
|
|
||||||
|
n, err := wrapped.Read([][]byte{{}}, []int{0}, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error from recovered panic, got nil")
|
||||||
|
}
|
||||||
|
if n != 0 {
|
||||||
|
t.Errorf("expected n=0, got %d", n)
|
||||||
|
}
|
||||||
|
if !handlerCalled {
|
||||||
|
t.Errorf("expected panic handler to be called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeviceWrapperWritePanic(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
tun := mocks.NewMockDevice(ctrl)
|
||||||
|
tun.EXPECT().Write(gomock.Any(), gomock.Any()).
|
||||||
|
DoAndReturn(func(bufs [][]byte, offset int) (int, error) {
|
||||||
|
packet := make([]byte, 0)
|
||||||
|
return int(packet[0]), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapped := newDeviceFilter(tun)
|
||||||
|
|
||||||
|
handlerCalled := false
|
||||||
|
wrapped.SetPanicHandler(func() { handlerCalled = true })
|
||||||
|
|
||||||
|
n, err := wrapped.Write([][]byte{{0x45, 0x00}}, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error from recovered panic, got nil")
|
||||||
|
}
|
||||||
|
if n != 0 {
|
||||||
|
t.Errorf("expected n=0, got %d", n)
|
||||||
|
}
|
||||||
|
if !handlerCalled {
|
||||||
|
t.Errorf("expected panic handler to be called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ type TunKernelDevice struct {
|
|||||||
link *wgLink
|
link *wgLink
|
||||||
udpMuxConn net.PacketConn
|
udpMuxConn net.PacketConn
|
||||||
udpMux *udpmux.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
|
|
||||||
filterFn udpmux.FilterFn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
||||||
@@ -104,7 +102,6 @@ func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|||||||
bindParams := udpmux.UniversalUDPMuxParams{
|
bindParams := udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: nbnet.WrapPacketConn(rawSock),
|
UDPConn: nbnet.WrapPacketConn(rawSock),
|
||||||
Net: t.transportNet,
|
Net: t.transportNet,
|
||||||
FilterFn: t.filterFn,
|
|
||||||
WGAddress: t.address,
|
WGAddress: t.address,
|
||||||
MTU: t.mtu,
|
MTU: t.mtu,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ type WGIFaceOpts struct {
|
|||||||
MTU uint16
|
MTU uint16
|
||||||
MobileArgs *device.MobileIFaceArguments
|
MobileArgs *device.MobileIFaceArguments
|
||||||
TransportNet transport.Net
|
TransportNet transport.Net
|
||||||
FilterFn udpmux.FilterFn
|
|
||||||
DisableDNS bool
|
DisableDNS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
||||||
|
|
||||||
var tun WGTunDevice
|
var tun WGTunDevice
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
||||||
|
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
||||||
|
|
||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunFd),
|
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunFd),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
||||||
return &WGIface{
|
return &WGIface{
|
||||||
tun: device.NewNetstackDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr()),
|
tun: device.NewNetstackDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr()),
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
@@ -30,7 +30,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if device.ModuleTunIsLoaded() {
|
if device.ModuleTunIsLoaded() {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
||||||
return &WGIface{
|
return &WGIface{
|
||||||
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind),
|
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind),
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -22,10 +20,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FilterFn is a function that filters out candidates based on the address.
|
|
||||||
// If it returns true, the address is to be filtered. It also returns the prefix of matching route.
|
|
||||||
type FilterFn func(address netip.Addr) (bool, netip.Prefix, error)
|
|
||||||
|
|
||||||
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn
|
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn
|
||||||
// It then passes packets to the UDPMux that does the actual connection muxing.
|
// It then passes packets to the UDPMux that does the actual connection muxing.
|
||||||
type UniversalUDPMuxDefault struct {
|
type UniversalUDPMuxDefault struct {
|
||||||
@@ -43,7 +37,6 @@ type UniversalUDPMuxParams struct {
|
|||||||
UDPConn net.PacketConn
|
UDPConn net.PacketConn
|
||||||
XORMappedAddrCacheTTL time.Duration
|
XORMappedAddrCacheTTL time.Duration
|
||||||
Net transport.Net
|
Net transport.Net
|
||||||
FilterFn FilterFn
|
|
||||||
WGAddress wgaddr.Address
|
WGAddress wgaddr.Address
|
||||||
MTU uint16
|
MTU uint16
|
||||||
}
|
}
|
||||||
@@ -68,7 +61,6 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef
|
|||||||
PacketConn: params.UDPConn,
|
PacketConn: params.UDPConn,
|
||||||
mux: m,
|
mux: m,
|
||||||
logger: params.Logger,
|
logger: params.Logger,
|
||||||
filterFn: params.FilterFn,
|
|
||||||
address: params.WGAddress,
|
address: params.WGAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,15 +107,12 @@ func (m *UniversalUDPMuxDefault) ReadFromConn(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDPConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets
|
// UDPConn is a wrapper around UDPMux conn that overrides WriteTo to drop packets destined for the overlay subnet.
|
||||||
type UDPConn struct {
|
type UDPConn struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
mux *UniversalUDPMuxDefault
|
mux *UniversalUDPMuxDefault
|
||||||
logger logging.LeveledLogger
|
logger logging.LeveledLogger
|
||||||
filterFn FilterFn
|
address wgaddr.Address
|
||||||
// TODO: reset cache on route changes
|
|
||||||
addrCache sync.Map
|
|
||||||
address wgaddr.Address
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPacketConn returns the underlying PacketConn
|
// GetPacketConn returns the underlying PacketConn
|
||||||
@@ -132,65 +121,16 @@ func (u *UDPConn) GetPacketConn() net.PacketConn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
if u.filterFn == nil {
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
return u.PacketConn.WriteTo(b, addr)
|
return u.PacketConn.WriteTo(b, addr)
|
||||||
}
|
}
|
||||||
|
dst := udpAddr.AddrPort().Addr().Unmap()
|
||||||
if isRouted, found := u.addrCache.Load(addr.String()); found {
|
if (u.address.Network.IsValid() && u.address.Network.Contains(dst)) || (u.address.IPv6Net.IsValid() && u.address.IPv6Net.Contains(dst)) {
|
||||||
return u.handleCachedAddress(isRouted.(bool), b, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.handleUncachedAddress(b, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UDPConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) {
|
|
||||||
if isRouted {
|
|
||||||
return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr)
|
|
||||||
}
|
|
||||||
return u.PacketConn.WriteTo(b, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UDPConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) {
|
|
||||||
if err := u.performFilterCheck(addr); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return u.PacketConn.WriteTo(b, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UDPConn) performFilterCheck(addr net.Addr) error {
|
|
||||||
host, err := getHostFromAddr(addr)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to get host from address %s: %v", addr, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
a, err := netip.ParseAddr(host)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to parse address %s: %v", addr, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.address.Network.Contains(a) {
|
|
||||||
log.Warnf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
log.Warnf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
return 0, fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
}
|
}
|
||||||
|
return u.PacketConn.WriteTo(b, addr)
|
||||||
if isRouted, prefix, err := u.filterFn(a); err != nil {
|
|
||||||
log.Errorf("Failed to check if address %s is routed: %v", addr, err)
|
|
||||||
} else {
|
|
||||||
u.addrCache.Store(addr.String(), isRouted)
|
|
||||||
if isRouted {
|
|
||||||
// Extra log, as the error only shows up with ICE logging enabled
|
|
||||||
log.Infof("address %s is part of routed network %s, refusing to write", addr, prefix)
|
|
||||||
return fmt.Errorf("address %s is part of routed network %s, refusing to write", addr, prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHostFromAddr(addr net.Addr) (string, error) {
|
|
||||||
host, _, err := net.SplitHostPort(addr.String())
|
|
||||||
return host, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSharedConn returns the shared udp conn
|
// GetSharedConn returns the shared udp conn
|
||||||
@@ -225,6 +165,13 @@ func (m *UniversalUDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.A
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
src := udpAddr.AddrPort().Addr().Unmap()
|
||||||
|
wg := m.params.WGAddress
|
||||||
|
if (wg.Network.IsValid() && wg.Network.Contains(src)) || (wg.IPv6Net.IsValid() && wg.IPv6Net.Contains(src)) {
|
||||||
|
log.Debugf("dropping STUN message from overlay source %s", udpAddr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if m.isXORMappedResponse(msg, udpAddr.String()) {
|
if m.isXORMappedResponse(msg, udpAddr.String()) {
|
||||||
err := m.handleXORMappedResponse(udpAddr, msg)
|
err := m.handleXORMappedResponse(udpAddr, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func seedProxyForProxyCloseByRemoteConn() ([]proxyInstance, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
iceBind := bind.NewICEBind(nil, nil, wgAddress, 1280)
|
iceBind := bind.NewICEBind(nil, wgAddress, 1280)
|
||||||
endpointAddress := &net.UDPAddr{
|
endpointAddress := &net.UDPAddr{
|
||||||
IP: net.IPv4(10, 0, 0, 1),
|
IP: net.IPv4(10, 0, 0, 1),
|
||||||
Port: 1234,
|
Port: 1234,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func seedProxyForProxyCloseByRemoteConn() ([]proxyInstance, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
iceBind := bind.NewICEBind(nil, nil, wgAddress, 1280)
|
iceBind := bind.NewICEBind(nil, wgAddress, 1280)
|
||||||
endpointAddress := &net.UDPAddr{
|
endpointAddress := &net.UDPAddr{
|
||||||
IP: net.IPv4(10, 0, 0, 1),
|
IP: net.IPv4(10, 0, 0, 1),
|
||||||
Port: 1234,
|
Port: 1234,
|
||||||
|
|||||||
@@ -360,7 +360,13 @@ func isRedirectURLPortUsed(redirectURL string, excludedRanges []excludedPortRang
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%s", port)
|
// FreeBSD 15 disables connecting to INADDR_ANY (0.0.0.0) as a localhost
|
||||||
|
// alias by default, ensure explicit ip for localhost.
|
||||||
|
host := parsedURL.Hostname()
|
||||||
|
if host == "" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(host, port)
|
||||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -116,8 +117,9 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
fileDescriptor int32,
|
fileDescriptor int32,
|
||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
dnsAddresses []netip.AddrPort,
|
|
||||||
stateFilePath string,
|
stateFilePath string,
|
||||||
|
cacheDir string,
|
||||||
|
logFilePath string,
|
||||||
) error {
|
) error {
|
||||||
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
debug.SetGCPercent(5)
|
debug.SetGCPercent(5)
|
||||||
@@ -126,10 +128,10 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
FileDescriptor: fileDescriptor,
|
FileDescriptor: fileDescriptor,
|
||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
HostDNSAddresses: dnsAddresses,
|
|
||||||
StateFilePath: stateFilePath,
|
StateFilePath: stateFilePath,
|
||||||
|
TempDir: cacheDir,
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, "")
|
return c.run(mobileDependency, nil, logFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan struct{}, logPath string) error {
|
func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan struct{}, logPath string) error {
|
||||||
@@ -348,6 +350,11 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
engineConfig.TempDir = mobileDependency.TempDir
|
engineConfig.TempDir = mobileDependency.TempDir
|
||||||
|
// Leave StateDir empty when there is no state path so a disk-backed
|
||||||
|
// syncstore falls back to os.TempDir() instead of filepath.Dir("") == ".".
|
||||||
|
if path != "" {
|
||||||
|
engineConfig.StateDir = filepath.Dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), engineConfig.MTU)
|
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), engineConfig.MTU)
|
||||||
c.statusRecorder.SetRelayMgr(relayManager)
|
c.statusRecorder.SetRelayMgr(relayManager)
|
||||||
|
|||||||
@@ -250,10 +250,13 @@ type BundleGenerator struct {
|
|||||||
syncResponse *mgmProto.SyncResponse
|
syncResponse *mgmProto.SyncResponse
|
||||||
logPath string
|
logPath string
|
||||||
tempDir string
|
tempDir string
|
||||||
|
statePath string
|
||||||
cpuProfile []byte
|
cpuProfile []byte
|
||||||
capturePath string
|
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
|
||||||
|
daemonVersion string
|
||||||
|
cliVersion string
|
||||||
|
|
||||||
anonymize bool
|
anonymize bool
|
||||||
includeSystemInfo bool
|
includeSystemInfo bool
|
||||||
@@ -274,10 +277,13 @@ type GeneratorDependencies struct {
|
|||||||
SyncResponse *mgmProto.SyncResponse
|
SyncResponse *mgmProto.SyncResponse
|
||||||
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.
|
||||||
|
StatePath string // Path to the state file. If empty, the ServiceManager default path is used.
|
||||||
CPUProfile []byte
|
CPUProfile []byte
|
||||||
CapturePath string
|
CapturePath string
|
||||||
RefreshStatus func()
|
RefreshStatus func()
|
||||||
ClientMetrics MetricsExporter
|
ClientMetrics MetricsExporter
|
||||||
|
DaemonVersion string
|
||||||
|
CliVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
|
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
|
||||||
@@ -295,10 +301,13 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
|
|||||||
syncResponse: deps.SyncResponse,
|
syncResponse: deps.SyncResponse,
|
||||||
logPath: deps.LogPath,
|
logPath: deps.LogPath,
|
||||||
tempDir: deps.TempDir,
|
tempDir: deps.TempDir,
|
||||||
|
statePath: deps.StatePath,
|
||||||
cpuProfile: deps.CPUProfile,
|
cpuProfile: deps.CPUProfile,
|
||||||
capturePath: deps.CapturePath,
|
capturePath: deps.CapturePath,
|
||||||
refreshStatus: deps.RefreshStatus,
|
refreshStatus: deps.RefreshStatus,
|
||||||
clientMetrics: deps.ClientMetrics,
|
clientMetrics: deps.ClientMetrics,
|
||||||
|
daemonVersion: deps.DaemonVersion,
|
||||||
|
cliVersion: deps.CliVersion,
|
||||||
|
|
||||||
anonymize: cfg.Anonymize,
|
anonymize: cfg.Anonymize,
|
||||||
includeSystemInfo: cfg.IncludeSystemInfo,
|
includeSystemInfo: cfg.IncludeSystemInfo,
|
||||||
@@ -459,9 +468,11 @@ func (g *BundleGenerator) addStatus() error {
|
|||||||
protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus)
|
protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus)
|
||||||
protoFullStatus.Events = g.statusRecorder.GetEventHistory()
|
protoFullStatus.Events = g.statusRecorder.GetEventHistory()
|
||||||
overview := nbstatus.ConvertToStatusOutputOverview(protoFullStatus, nbstatus.ConvertOptions{
|
overview := nbstatus.ConvertToStatusOutputOverview(protoFullStatus, nbstatus.ConvertOptions{
|
||||||
Anonymize: g.anonymize,
|
Anonymize: g.anonymize,
|
||||||
ProfileName: profName,
|
ProfileName: profName,
|
||||||
|
DaemonVersion: g.daemonVersion,
|
||||||
})
|
})
|
||||||
|
overview.CliVersion = g.cliVersion
|
||||||
statusOutput := overview.FullDetailSummary()
|
statusOutput := overview.FullDetailSummary()
|
||||||
|
|
||||||
statusReader := strings.NewReader(statusOutput)
|
statusReader := strings.NewReader(statusOutput)
|
||||||
@@ -508,6 +519,14 @@ func (g *BundleGenerator) addConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Surface the set of MDM-enforced keys so a support engineer reading
|
||||||
|
// the bundle can tell which field values are user-set vs MDM-overridden.
|
||||||
|
// Same semantics as the mDMManagedFields list returned by the
|
||||||
|
// GetConfig RPC consumed by `netbird debug config`.
|
||||||
|
if managed := g.internalConfig.Policy().ManagedKeys(); len(managed) > 0 {
|
||||||
|
configContent.WriteString(fmt.Sprintf("MDMManagedFields: %v\n", managed))
|
||||||
|
}
|
||||||
|
|
||||||
configReader := strings.NewReader(configContent.String())
|
configReader := strings.NewReader(configContent.String())
|
||||||
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
|
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
|
||||||
return fmt.Errorf("add config file to zip: %w", err)
|
return fmt.Errorf("add config file to zip: %w", err)
|
||||||
@@ -798,6 +817,8 @@ func (g *BundleGenerator) addSyncResponse() error {
|
|||||||
AllowPartial: true,
|
AllowPartial: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.maskSecrets()
|
||||||
|
|
||||||
jsonBytes, err := options.Marshal(g.syncResponse)
|
jsonBytes, err := options.Marshal(g.syncResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generate json: %w", err)
|
return fmt.Errorf("generate json: %w", err)
|
||||||
@@ -810,9 +831,33 @@ func (g *BundleGenerator) addSyncResponse() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *BundleGenerator) maskSecrets() {
|
||||||
|
if g.syncResponse == nil || g.syncResponse.NetbirdConfig == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.syncResponse.NetbirdConfig.Flow != nil {
|
||||||
|
g.syncResponse.NetbirdConfig.Flow.TokenPayload = maskedValue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.syncResponse.NetbirdConfig.Relay != nil {
|
||||||
|
g.syncResponse.NetbirdConfig.Relay.TokenPayload = maskedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range g.syncResponse.NetbirdConfig.Turns {
|
||||||
|
if g.syncResponse.NetbirdConfig.Turns[i] != nil {
|
||||||
|
g.syncResponse.NetbirdConfig.Turns[i].Password = maskedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) addStateFile() error {
|
func (g *BundleGenerator) addStateFile() error {
|
||||||
sm := profilemanager.NewServiceManager("")
|
path := g.statePath
|
||||||
path := sm.GetStatePath()
|
if path == "" {
|
||||||
|
sm := profilemanager.NewServiceManager("")
|
||||||
|
path = sm.GetStatePath()
|
||||||
|
}
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1039,7 +1084,8 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pattern := filepath.Join(logDir, "client-*.log.gz")
|
// This regex will match both logs rotated by us and logrotate on linux
|
||||||
|
pattern := filepath.Join(logDir, "client*.log.*")
|
||||||
files, err := filepath.Glob(pattern)
|
files, err := filepath.Glob(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to glob rotated logs: %v", err)
|
log.Warnf("failed to glob rotated logs: %v", err)
|
||||||
@@ -1072,7 +1118,12 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
|
|||||||
|
|
||||||
for i := 0; i < maxFiles; i++ {
|
for i := 0; i < maxFiles; i++ {
|
||||||
name := filepath.Base(files[i])
|
name := filepath.Base(files[i])
|
||||||
if err := g.addSingleLogFileGz(files[i], name); err != nil {
|
if strings.HasSuffix(name, ".gz") {
|
||||||
|
err = g.addSingleLogFileGz(files[i], name)
|
||||||
|
} else {
|
||||||
|
err = g.addSingleLogfile(files[i], name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
log.Warnf("failed to add rotated log %s: %v", name, err)
|
log.Warnf("failed to add rotated log %s: %v", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
client/internal/debug/debug_ios.go
Normal file
36
client/internal/debug/debug_ios.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swiftLogFile is the Swift app log written by the iOS app into the same log
|
||||||
|
// directory as the Go client log, so it can be collected into the bundle.
|
||||||
|
const swiftLogFile = "swift-log.log"
|
||||||
|
|
||||||
|
// addPlatformLog collects logs for the iOS debug bundle. iOS has no logcat or
|
||||||
|
// systemd journal, so we rely on file-based logs. addLogfile handles the Go
|
||||||
|
// client log (logPath) with rotation, the stderr/stdout companions and
|
||||||
|
// anonymization. The iOS app writes its own Swift log into the same directory,
|
||||||
|
// so we add it alongside the Go log.
|
||||||
|
func (g *BundleGenerator) addPlatformLog() error {
|
||||||
|
if err := g.addLogfile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.logPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
swiftLogPath := filepath.Join(filepath.Dir(g.logPath), swiftLogFile)
|
||||||
|
if err := g.addSingleLogfile(swiftLogPath, swiftLogFile); err != nil {
|
||||||
|
// The Swift log is best-effort: the app may not have written it yet.
|
||||||
|
log.Warnf("failed to add %s to debug bundle: %v", swiftLogFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
103
client/internal/debug/debug_logfiles_test.go
Normal file
103
client/internal/debug/debug_logfiles_test.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAddRotatedLogFiles_PicksUpAllVariants asserts that the rotated-log
|
||||||
|
// glob picks up logs rotated by timberjack (gzipped) and by logrotate (plain
|
||||||
|
// and gzipped), and skips unrelated files.
|
||||||
|
func TestAddRotatedLogFiles_PicksUpAllVariants(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
writeFile(t, filepath.Join(dir, "client.log"), "active log\n")
|
||||||
|
writeFile(t, filepath.Join(dir, "other.log"), "unrelated\n")
|
||||||
|
|
||||||
|
timberjackRotated := "client-2026-05-21T10-30-45.000.log.gz"
|
||||||
|
writeGzFile(t, filepath.Join(dir, timberjackRotated), "timberjack rotated content\n")
|
||||||
|
|
||||||
|
logrotatePlain := "client.log.1"
|
||||||
|
writeFile(t, filepath.Join(dir, logrotatePlain), "logrotate plain content\n")
|
||||||
|
|
||||||
|
logrotateGz := "client.log.2.gz"
|
||||||
|
writeGzFile(t, filepath.Join(dir, logrotateGz), "logrotate gz content\n")
|
||||||
|
|
||||||
|
names := runAddRotatedLogFiles(t, dir, 10)
|
||||||
|
|
||||||
|
require.Contains(t, names, timberjackRotated, "timberjack rotated file should be in bundle")
|
||||||
|
require.Contains(t, names, logrotatePlain, "logrotate plain rotated file should be in bundle")
|
||||||
|
require.Contains(t, names, logrotateGz, "logrotate gzipped rotated file should be in bundle")
|
||||||
|
require.NotContains(t, names, "client.log", "active log should not be added by addRotatedLogFiles")
|
||||||
|
require.NotContains(t, names, "other.log", "unrelated files should not be in bundle")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddRotatedLogFiles_RespectsLogFileCount asserts that only the newest
|
||||||
|
// logFileCount rotated files are bundled, ordered by mtime.
|
||||||
|
func TestAddRotatedLogFiles_RespectsLogFileCount(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
oldest := filepath.Join(dir, "client.log.3")
|
||||||
|
middle := filepath.Join(dir, "client.log.2")
|
||||||
|
newest := filepath.Join(dir, "client.log.1")
|
||||||
|
writeFile(t, oldest, "old\n")
|
||||||
|
writeFile(t, middle, "mid\n")
|
||||||
|
writeFile(t, newest, "new\n")
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
require.NoError(t, os.Chtimes(oldest, now.Add(-2*time.Hour), now.Add(-2*time.Hour)))
|
||||||
|
require.NoError(t, os.Chtimes(middle, now.Add(-1*time.Hour), now.Add(-1*time.Hour)))
|
||||||
|
require.NoError(t, os.Chtimes(newest, now, now))
|
||||||
|
|
||||||
|
names := runAddRotatedLogFiles(t, dir, 2)
|
||||||
|
|
||||||
|
require.Contains(t, names, "client.log.1")
|
||||||
|
require.Contains(t, names, "client.log.2")
|
||||||
|
require.NotContains(t, names, "client.log.3", "oldest file should be dropped when logFileCount=2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runAddRotatedLogFiles calls addRotatedLogFiles against a fresh in-memory
|
||||||
|
// zip writer and returns the set of entry names that ended up in the archive.
|
||||||
|
func runAddRotatedLogFiles(t *testing.T, dir string, logFileCount uint32) map[string]struct{} {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
g := &BundleGenerator{
|
||||||
|
archive: zip.NewWriter(&buf),
|
||||||
|
logFileCount: logFileCount,
|
||||||
|
}
|
||||||
|
g.addRotatedLogFiles(dir)
|
||||||
|
require.NoError(t, g.archive.Close())
|
||||||
|
|
||||||
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
names := make(map[string]struct{}, len(zr.File))
|
||||||
|
for _, f := range zr.File {
|
||||||
|
names[f.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(t *testing.T, path, content string) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeGzFile(t *testing.T, path, content string) {
|
||||||
|
t.Helper()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gw := gzip.NewWriter(&buf)
|
||||||
|
_, err := io.WriteString(gw, content)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, gw.Close())
|
||||||
|
require.NoError(t, os.WriteFile(path, buf.Bytes(), 0o644))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build !android && !ios
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
|
|||||||
@@ -843,6 +843,8 @@ func TestAddConfig_AllFieldsCovered(t *testing.T) {
|
|||||||
"PreSharedKey": "sensitive: WireGuard pre-shared key",
|
"PreSharedKey": "sensitive: WireGuard pre-shared key",
|
||||||
"SSHKey": "sensitive: SSH private key",
|
"SSHKey": "sensitive: SSH private key",
|
||||||
"ClientCertKeyPair": "non-config: parsed cert pair, not serialized",
|
"ClientCertKeyPair": "non-config: parsed cert pair, not serialized",
|
||||||
|
"Name": "non-config: profile name is not needed for debug purposes",
|
||||||
|
"policy": "non-config: in-memory MDM policy snapshot, surfaced via Config.Policy() / GetConfigResponse.MDMManagedFields",
|
||||||
}
|
}
|
||||||
|
|
||||||
mURL, _ := url.Parse("https://api.example.com:443")
|
mURL, _ := url.Parse("https://api.example.com:443")
|
||||||
|
|||||||
@@ -339,8 +339,7 @@ func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
|
|||||||
case entry.Pattern == ".":
|
case entry.Pattern == ".":
|
||||||
return true
|
return true
|
||||||
case entry.IsWildcard:
|
case entry.IsWildcard:
|
||||||
parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
|
return strings.HasSuffix(qname, "."+entry.Pattern)
|
||||||
return len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
|
|
||||||
default:
|
default:
|
||||||
// For non-wildcard patterns:
|
// For non-wildcard patterns:
|
||||||
// If handler wants subdomain matching, allow suffix match
|
// If handler wants subdomain matching, allow suffix match
|
||||||
|
|||||||
@@ -164,6 +164,54 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
|||||||
matchSubdomains: true,
|
matchSubdomains: true,
|
||||||
shouldMatch: true,
|
shouldMatch: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard label-boundary mismatch (suffix overlap)",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "x.ab.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard label-boundary match",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "x.b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard multi-label match",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "x.y.b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard no match on multi-label apex",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard no match on unrelated suffix containment",
|
||||||
|
handlerDomain: "*.example.com.",
|
||||||
|
queryDomain: "notexample.com.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard accepts pattern registered without trailing dot",
|
||||||
|
handlerDomain: "*.b.test",
|
||||||
|
queryDomain: "x.b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -273,6 +321,19 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
expectedCalls: 1,
|
expectedCalls: 1,
|
||||||
expectedHandler: 2, // highest priority matching handler should be called
|
expectedHandler: 2, // highest priority matching handler should be called
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "overlapping wildcard suffixes route to correct handler",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: "*.b.test.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
{pattern: "*.ab.test.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
queryDomain: "app.ab.test.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "root zone with specific domain",
|
name: "root zone with specific domain",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ type hostManager interface {
|
|||||||
restoreHostDNS() error
|
restoreHostDNS() error
|
||||||
supportCustomPort() bool
|
supportCustomPort() bool
|
||||||
string() string
|
string() string
|
||||||
|
// getOriginalNameservers returns the OS-side resolvers used as PriorityFallback
|
||||||
|
// upstreams: pre-takeover snapshots on desktop, the OS-pushed list on Android,
|
||||||
|
// hardcoded Quad9 on iOS, nil for noop / mock.
|
||||||
|
getOriginalNameservers() []netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemDNSSettings struct {
|
type SystemDNSSettings struct {
|
||||||
@@ -131,3 +135,11 @@ func (n noopHostConfigurator) supportCustomPort() bool {
|
|||||||
func (n noopHostConfigurator) string() string {
|
func (n noopHostConfigurator) string() string {
|
||||||
return "noop"
|
return "noop"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n noopHostConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHostConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// androidHostManager is a noop on the OS side (Android's VPN service handles
|
||||||
|
// DNS for us) but tracks the OS-reported resolver list pushed via
|
||||||
|
// OnUpdatedHostDNSServer so it can serve as the fallback nameserver source.
|
||||||
type androidHostManager struct {
|
type androidHostManager struct {
|
||||||
|
holder *hostsDNSHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHostManager() (*androidHostManager, error) {
|
func newHostManager(holder *hostsDNSHolder) (*androidHostManager, error) {
|
||||||
return &androidHostManager{}, nil
|
return &androidHostManager{holder: holder}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a androidHostManager) applyDNSConfig(HostDNSConfig, *statemanager.Manager) error {
|
func (a androidHostManager) applyDNSConfig(HostDNSConfig, *statemanager.Manager) error {
|
||||||
@@ -26,3 +32,12 @@ func (a androidHostManager) supportCustomPort() bool {
|
|||||||
func (a androidHostManager) string() string {
|
func (a androidHostManager) string() string {
|
||||||
return "none"
|
return "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a androidHostManager) getOriginalNameservers() []netip.Addr {
|
||||||
|
hosts := a.holder.get()
|
||||||
|
out := make([]netip.Addr, 0, len(hosts))
|
||||||
|
for ap := range hosts {
|
||||||
|
out = append(out, ap.Addr())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -20,6 +21,14 @@ func newHostManager(dnsManager IosDnsManager) (*iosHostManager, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a iosHostManager) getOriginalNameservers() []netip.Addr {
|
||||||
|
// Quad9 v4+v6: 9.9.9.9, 2620:fe::fe.
|
||||||
|
return []netip.Addr{
|
||||||
|
netip.AddrFrom4([4]byte{9, 9, 9, 9}),
|
||||||
|
netip.AddrFrom16([16]byte{0x26, 0x20, 0x00, 0xfe, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xfe}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a iosHostManager) applyDNSConfig(config HostDNSConfig, _ *statemanager.Manager) error {
|
func (a iosHostManager) applyDNSConfig(config HostDNSConfig, _ *statemanager.Manager) error {
|
||||||
jsonData, err := json.Marshal(config)
|
jsonData, err := json.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -44,9 +45,11 @@ const (
|
|||||||
|
|
||||||
nrptMaxDomainsPerRule = 50
|
nrptMaxDomainsPerRule = 50
|
||||||
|
|
||||||
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
|
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
|
||||||
interfaceConfigNameServerKey = "NameServer"
|
interfaceConfigPathV6 = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces`
|
||||||
interfaceConfigSearchListKey = "SearchList"
|
interfaceConfigNameServerKey = "NameServer"
|
||||||
|
interfaceConfigDhcpNameSrvKey = "DhcpNameServer"
|
||||||
|
interfaceConfigSearchListKey = "SearchList"
|
||||||
|
|
||||||
// Network interface DNS registration settings
|
// Network interface DNS registration settings
|
||||||
disableDynamicUpdateKey = "DisableDynamicUpdate"
|
disableDynamicUpdateKey = "DisableDynamicUpdate"
|
||||||
@@ -67,10 +70,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type registryConfigurator struct {
|
type registryConfigurator struct {
|
||||||
guid string
|
guid string
|
||||||
routingAll bool
|
routingAll bool
|
||||||
gpo bool
|
gpo bool
|
||||||
nrptEntryCount int
|
nrptEntryCount int
|
||||||
|
origNameservers []netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
||||||
@@ -94,6 +98,17 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
|||||||
gpo: useGPO,
|
gpo: useGPO,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
origNameservers, err := configurator.captureOriginalNameservers()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
log.Warnf("capture original nameservers from non-WG adapters: %v", err)
|
||||||
|
case len(origNameservers) == 0:
|
||||||
|
log.Warnf("no original nameservers captured from non-WG adapters; DNS fallback will be empty")
|
||||||
|
default:
|
||||||
|
log.Debugf("captured %d original nameservers from non-WG adapters: %v", len(origNameservers), origNameservers)
|
||||||
|
}
|
||||||
|
configurator.origNameservers = origNameservers
|
||||||
|
|
||||||
if err := configurator.configureInterface(); err != nil {
|
if err := configurator.configureInterface(); err != nil {
|
||||||
log.Errorf("failed to configure interface settings: %v", err)
|
log.Errorf("failed to configure interface settings: %v", err)
|
||||||
}
|
}
|
||||||
@@ -101,6 +116,98 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
|||||||
return configurator, nil
|
return configurator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// captureOriginalNameservers reads DNS addresses from every Tcpip(6) interface
|
||||||
|
// registry key except the WG adapter. v4 and v6 servers live in separate
|
||||||
|
// hives (Tcpip vs Tcpip6) keyed by the same interface GUID.
|
||||||
|
func (r *registryConfigurator) captureOriginalNameservers() ([]netip.Addr, error) {
|
||||||
|
seen := make(map[netip.Addr]struct{})
|
||||||
|
var out []netip.Addr
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, root := range []string{interfaceConfigPath, interfaceConfigPathV6} {
|
||||||
|
addrs, err := r.captureFromTcpipRoot(root)
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("%s: %w", root, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if _, dup := seen[addr]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[addr] = struct{}{}
|
||||||
|
out = append(out, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) captureFromTcpipRoot(rootPath string) ([]netip.Addr, error) {
|
||||||
|
root, err := registry.OpenKey(registry.LOCAL_MACHINE, rootPath, registry.READ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open key: %w", err)
|
||||||
|
}
|
||||||
|
defer closer(root)
|
||||||
|
|
||||||
|
guids, err := root.ReadSubKeyNames(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read subkeys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []netip.Addr
|
||||||
|
for _, guid := range guids {
|
||||||
|
if strings.EqualFold(guid, r.guid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, readInterfaceNameservers(rootPath, guid)...)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInterfaceNameservers(rootPath, guid string) []netip.Addr {
|
||||||
|
keyPath := rootPath + "\\" + guid
|
||||||
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer closer(k)
|
||||||
|
|
||||||
|
// Static NameServer wins over DhcpNameServer for actual resolution.
|
||||||
|
for _, name := range []string{interfaceConfigNameServerKey, interfaceConfigDhcpNameSrvKey} {
|
||||||
|
raw, _, err := k.GetStringValue(name)
|
||||||
|
if err != nil || raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out := parseRegistryNameservers(raw); len(out) > 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRegistryNameservers(raw string) []netip.Addr {
|
||||||
|
var out []netip.Addr
|
||||||
|
for _, field := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == ' ' || r == '\t' }) {
|
||||||
|
addr, err := netip.ParseAddr(strings.TrimSpace(field))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr = addr.Unmap()
|
||||||
|
if !addr.IsValid() || addr.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Drop unzoned link-local: not routable without a scope id. If
|
||||||
|
// the user wrote "fe80::1%eth0" ParseAddr preserves the zone.
|
||||||
|
if addr.IsLinkLocalUnicast() && addr.Zone() == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, addr)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
|
return slices.Clone(r.origNameservers)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) supportCustomPort() bool {
|
func (r *registryConfigurator) supportCustomPort() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func (h *hostsDNSHolder) set(list []netip.AddrPort) {
|
|||||||
h.mutex.Unlock()
|
h.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:unused
|
||||||
func (h *hostsDNSHolder) get() map[netip.AddrPort]struct{} {
|
func (h *hostsDNSHolder) get() map[netip.AddrPort]struct{} {
|
||||||
h.mutex.RLock()
|
h.mutex.RLock()
|
||||||
l := h.unprotectedDNSList
|
l := h.unprotectedDNSList
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ type resolver interface {
|
|||||||
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerConnectivity reports whether a tunnel IP belongs to a peer the
|
||||||
|
// client knows about and whether that peer is currently connected. The
|
||||||
|
// local resolver uses this to suppress A/AAAA answers whose RDATA points
|
||||||
|
// at a disconnected peer (typical case: a synthesized private-service
|
||||||
|
// record pointing at an embedded proxy peer that just went offline).
|
||||||
|
//
|
||||||
|
// known=false means the IP isn't in the local peerstore at all — the
|
||||||
|
// record is left alone (it points at something outside our mesh, e.g.
|
||||||
|
// a non-peer upstream).
|
||||||
|
type PeerConnectivity interface {
|
||||||
|
IsConnectedByIP(ip string) (known, connected bool)
|
||||||
|
}
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
records map[dns.Question][]dns.RR
|
records map[dns.Question][]dns.RR
|
||||||
@@ -33,6 +46,11 @@ type Resolver struct {
|
|||||||
// zones maps zone domain -> NonAuthoritative (true = non-authoritative, user-created zone)
|
// zones maps zone domain -> NonAuthoritative (true = non-authoritative, user-created zone)
|
||||||
zones map[domain.Domain]bool
|
zones map[domain.Domain]bool
|
||||||
resolver resolver
|
resolver resolver
|
||||||
|
// peerConn, when non-nil, is consulted on every A/AAAA answer to
|
||||||
|
// drop records pointing at disconnected peers. nil disables the
|
||||||
|
// filter and preserves the legacy "return whatever is registered"
|
||||||
|
// behaviour for callers that never wire a status source.
|
||||||
|
peerConn PeerConnectivity
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -49,6 +67,15 @@ func NewResolver() *Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPeerConnectivity wires the per-IP connectivity check used to filter
|
||||||
|
// out A/AAAA answers pointing at disconnected peers. Pass nil to disable.
|
||||||
|
// Safe to call multiple times; the latest value wins.
|
||||||
|
func (d *Resolver) SetPeerConnectivity(p PeerConnectivity) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.peerConn = p
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Resolver) MatchSubdomains() bool {
|
func (d *Resolver) MatchSubdomains() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -76,8 +103,6 @@ func (d *Resolver) ID() types.HandlerID {
|
|||||||
return "local-resolver"
|
return "local-resolver"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Resolver) ProbeAvailability(context.Context) {}
|
|
||||||
|
|
||||||
// ServeDNS handles a DNS request
|
// ServeDNS handles a DNS request
|
||||||
func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
logger := log.WithFields(log.Fields{
|
logger := log.WithFields(log.Fields{
|
||||||
@@ -97,6 +122,7 @@ func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
replyMessage.RecursionAvailable = true
|
replyMessage.RecursionAvailable = true
|
||||||
|
|
||||||
result := d.lookupRecords(logger, question)
|
result := d.lookupRecords(logger, question)
|
||||||
|
result.records = d.filterDisconnectedPeerAnswers(logger, question, result.records)
|
||||||
replyMessage.Authoritative = !result.hasExternalData
|
replyMessage.Authoritative = !result.hasExternalData
|
||||||
replyMessage.Answer = result.records
|
replyMessage.Answer = result.records
|
||||||
replyMessage.Rcode = d.determineRcode(question, result)
|
replyMessage.Rcode = d.determineRcode(question, result)
|
||||||
@@ -438,6 +464,78 @@ func (d *Resolver) logDNSError(logger *log.Entry, hostname string, qtype uint16,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterDisconnectedPeerAnswers drops A/AAAA records whose RDATA matches
|
||||||
|
// a known but disconnected peer. The synthesized private-service zones
|
||||||
|
// emit one A record per connected proxy peer in a cluster; when a peer
|
||||||
|
// goes offline, the server-side refresh removes the record from the
|
||||||
|
// next netmap, but the client may still hold the previous netmap for a
|
||||||
|
// short window. This filter is the local belt to that braces — even on
|
||||||
|
// the stale netmap, the resolver hides the offline target.
|
||||||
|
//
|
||||||
|
// Records pointing at unknown IPs (outside the local peerstore, e.g.
|
||||||
|
// non-mesh upstreams) are never dropped. Non-A/AAAA records pass
|
||||||
|
// through untouched.
|
||||||
|
//
|
||||||
|
// Escape hatch: if filtering would leave the answer empty AND at least
|
||||||
|
// one record was filtered, the original list is returned. Better to
|
||||||
|
// hand the client a record that may not respond than NXDOMAIN it
|
||||||
|
// completely when every proxy peer is offline (the upstream may still
|
||||||
|
// be reachable some other way, or the peerstore may be stale).
|
||||||
|
func (d *Resolver) filterDisconnectedPeerAnswers(logger *log.Entry, question dns.Question, records []dns.RR) []dns.RR {
|
||||||
|
if len(records) < 2 {
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
d.mu.RLock()
|
||||||
|
checker := d.peerConn
|
||||||
|
d.mu.RUnlock()
|
||||||
|
if checker == nil {
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
kept := make([]dns.RR, 0, len(records))
|
||||||
|
var dropped int
|
||||||
|
for _, rr := range records {
|
||||||
|
ip := extractRecordIP(rr)
|
||||||
|
if ip == "" {
|
||||||
|
kept = append(kept, rr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
known, connected := checker.IsConnectedByIP(ip)
|
||||||
|
if known && !connected {
|
||||||
|
dropped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, rr)
|
||||||
|
}
|
||||||
|
if dropped == 0 {
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
if len(kept) == 0 {
|
||||||
|
logger.Debugf("all %d answers for %s point at disconnected peers; returning the original list", dropped, question.Name)
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
logger.Tracef("dropped %d disconnected-peer answer(s) for %s, returning %d", dropped, question.Name, len(kept))
|
||||||
|
return kept
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractRecordIP returns the dotted-decimal / colon-hex IP carried by
|
||||||
|
// an A or AAAA record, or "" for any other record type.
|
||||||
|
func extractRecordIP(rr dns.RR) string {
|
||||||
|
switch r := rr.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
if r.A == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return r.A.String()
|
||||||
|
case *dns.AAAA:
|
||||||
|
if r.AAAA == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return r.AAAA.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Update replaces all zones and their records
|
// Update replaces all zones and their records
|
||||||
func (d *Resolver) Update(customZones []nbdns.CustomZone) {
|
func (d *Resolver) Update(customZones []nbdns.CustomZone) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ func (m *mockResolver) LookupNetIP(ctx context.Context, network, host string) ([
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockPeerConnectivity returns canned (known, connected) results per IP.
|
||||||
|
// Used by the disconnected-peer filter tests below. IPs not in the map
|
||||||
|
// are reported as unknown so the filter leaves them alone.
|
||||||
|
type mockPeerConnectivity struct {
|
||||||
|
byIP map[string]struct{ known, connected bool }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockPeerConnectivity) IsConnectedByIP(ip string) (known, connected bool) {
|
||||||
|
v, ok := m.byIP[ip]
|
||||||
|
if !ok {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
return v.known, v.connected
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocalResolver_ServeDNS(t *testing.T) {
|
func TestLocalResolver_ServeDNS(t *testing.T) {
|
||||||
recordA := nbdns.SimpleRecord{
|
recordA := nbdns.SimpleRecord{
|
||||||
Name: "peera.netbird.cloud.",
|
Name: "peera.netbird.cloud.",
|
||||||
@@ -2652,3 +2667,125 @@ func BenchmarkIsInManagedZone_ManyZones(b *testing.B) {
|
|||||||
resolver.isInManagedZone(qname)
|
resolver.isInManagedZone(qname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLocalResolver_FilterDisconnectedPeerAnswers verifies the
|
||||||
|
// connectivity-aware filtering layered on top of lookupRecords:
|
||||||
|
// when an A record's IP belongs to a known peer that's disconnected,
|
||||||
|
// the record is dropped from the answer. Records for unknown IPs pass
|
||||||
|
// through. If filtering would empty the answer entirely and at least
|
||||||
|
// one record was dropped, the original list is restored (escape hatch
|
||||||
|
// for the "all proxies offline" case).
|
||||||
|
func TestLocalResolver_FilterDisconnectedPeerAnswers(t *testing.T) {
|
||||||
|
zone := "svc.cluster.netbird."
|
||||||
|
connectedRec := nbdns.SimpleRecord{
|
||||||
|
Name: zone,
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 5,
|
||||||
|
RData: "100.64.0.10",
|
||||||
|
}
|
||||||
|
disconnectedRec := nbdns.SimpleRecord{
|
||||||
|
Name: zone,
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 5,
|
||||||
|
RData: "100.64.0.11",
|
||||||
|
}
|
||||||
|
unknownRec := nbdns.SimpleRecord{
|
||||||
|
Name: zone,
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 5,
|
||||||
|
RData: "203.0.113.5",
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipState struct{ known, connected bool }
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
records []nbdns.SimpleRecord
|
||||||
|
connByIP map[string]ipState
|
||||||
|
wantInOrder []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "drops disconnected peer, keeps connected",
|
||||||
|
records: []nbdns.SimpleRecord{connectedRec, disconnectedRec},
|
||||||
|
connByIP: map[string]ipState{
|
||||||
|
"100.64.0.10": {known: true, connected: true},
|
||||||
|
"100.64.0.11": {known: true, connected: false},
|
||||||
|
},
|
||||||
|
wantInOrder: []string{"100.64.0.10"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown IPs pass through untouched",
|
||||||
|
records: []nbdns.SimpleRecord{unknownRec, disconnectedRec},
|
||||||
|
connByIP: map[string]ipState{
|
||||||
|
"100.64.0.11": {known: true, connected: false},
|
||||||
|
},
|
||||||
|
wantInOrder: []string{"203.0.113.5"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all disconnected falls back to original list",
|
||||||
|
records: []nbdns.SimpleRecord{disconnectedRec, connectedRec},
|
||||||
|
connByIP: map[string]ipState{
|
||||||
|
"100.64.0.10": {known: true, connected: false},
|
||||||
|
"100.64.0.11": {known: true, connected: false},
|
||||||
|
},
|
||||||
|
wantInOrder: []string{"100.64.0.11", "100.64.0.10"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no checker wired returns all records",
|
||||||
|
records: []nbdns.SimpleRecord{connectedRec, disconnectedRec},
|
||||||
|
connByIP: nil,
|
||||||
|
wantInOrder: []string{"100.64.0.10", "100.64.0.11"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A single answer is never filtered: dropping it would only
|
||||||
|
// trigger the empty-answer escape hatch, so the fast path
|
||||||
|
// returns it untouched.
|
||||||
|
name: "single disconnected answer passes through",
|
||||||
|
records: []nbdns.SimpleRecord{disconnectedRec},
|
||||||
|
connByIP: map[string]ipState{
|
||||||
|
"100.64.0.11": {known: true, connected: false},
|
||||||
|
},
|
||||||
|
wantInOrder: []string{"100.64.0.11"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
if tc.connByIP != nil {
|
||||||
|
cm := mockPeerConnectivity{byIP: make(map[string]struct{ known, connected bool }, len(tc.connByIP))}
|
||||||
|
for ip, st := range tc.connByIP {
|
||||||
|
cm.byIP[ip] = struct{ known, connected bool }{st.known, st.connected}
|
||||||
|
}
|
||||||
|
resolver.SetPeerConnectivity(cm)
|
||||||
|
}
|
||||||
|
resolver.Update([]nbdns.CustomZone{{
|
||||||
|
Domain: strings.TrimSuffix(zone, "."),
|
||||||
|
Records: tc.records,
|
||||||
|
NonAuthoritative: true,
|
||||||
|
}})
|
||||||
|
|
||||||
|
var got *dns.Msg
|
||||||
|
writer := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
got = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := new(dns.Msg).SetQuestion(zone, dns.TypeA)
|
||||||
|
resolver.ServeDNS(writer, req)
|
||||||
|
|
||||||
|
require.NotNil(t, got, "resolver must produce a response")
|
||||||
|
require.Len(t, got.Answer, len(tc.wantInOrder),
|
||||||
|
"answer count must match expected: %v", tc.wantInOrder)
|
||||||
|
for i, want := range tc.wantInOrder {
|
||||||
|
a, ok := got.Answer[i].(*dns.A)
|
||||||
|
require.True(t, ok, "answer[%d] must be an A record", i)
|
||||||
|
assert.Equal(t, want, a.A.String(),
|
||||||
|
"answer[%d] expected %s got %s", i, want, a.A.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,10 +71,6 @@ func (m *MockServer) SearchDomains() []string {
|
|||||||
return make([]string, 0)
|
return make([]string, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProbeAvailability mocks implementation of ProbeAvailability from the Server interface
|
|
||||||
func (m *MockServer) ProbeAvailability() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockServer) UpdateServerConfig(domains dnsconfig.ServerDomains) error {
|
func (m *MockServer) UpdateServerConfig(domains dnsconfig.ServerDomains) error {
|
||||||
if m.UpdateServerConfigFunc != nil {
|
if m.UpdateServerConfigFunc != nil {
|
||||||
return m.UpdateServerConfigFunc(domains)
|
return m.UpdateServerConfigFunc(domains)
|
||||||
@@ -85,8 +82,8 @@ func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRouteChecker mock implementation of SetRouteChecker from Server interface
|
// SetRouteSources mock implementation of SetRouteSources from Server interface
|
||||||
func (m *MockServer) SetRouteChecker(func(netip.Addr) bool) {
|
func (m *MockServer) SetRouteSources(selected, active func() route.HAMap) {
|
||||||
// Mock implementation - no-op
|
// Mock implementation - no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,6 +33,15 @@ const (
|
|||||||
networkManagerDbusDeviceGetAppliedConnectionMethod = networkManagerDbusDeviceInterface + ".GetAppliedConnection"
|
networkManagerDbusDeviceGetAppliedConnectionMethod = networkManagerDbusDeviceInterface + ".GetAppliedConnection"
|
||||||
networkManagerDbusDeviceReapplyMethod = networkManagerDbusDeviceInterface + ".Reapply"
|
networkManagerDbusDeviceReapplyMethod = networkManagerDbusDeviceInterface + ".Reapply"
|
||||||
networkManagerDbusDeviceDeleteMethod = networkManagerDbusDeviceInterface + ".Delete"
|
networkManagerDbusDeviceDeleteMethod = networkManagerDbusDeviceInterface + ".Delete"
|
||||||
|
networkManagerDbusDeviceIp4ConfigProperty = networkManagerDbusDeviceInterface + ".Ip4Config"
|
||||||
|
networkManagerDbusDeviceIp6ConfigProperty = networkManagerDbusDeviceInterface + ".Ip6Config"
|
||||||
|
networkManagerDbusDeviceIfaceProperty = networkManagerDbusDeviceInterface + ".Interface"
|
||||||
|
networkManagerDbusGetDevicesMethod = networkManagerDest + ".GetDevices"
|
||||||
|
networkManagerDbusIp4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config"
|
||||||
|
networkManagerDbusIp6ConfigInterface = "org.freedesktop.NetworkManager.IP6Config"
|
||||||
|
networkManagerDbusIp4ConfigNameserverDataProperty = networkManagerDbusIp4ConfigInterface + ".NameserverData"
|
||||||
|
networkManagerDbusIp4ConfigNameserversProperty = networkManagerDbusIp4ConfigInterface + ".Nameservers"
|
||||||
|
networkManagerDbusIp6ConfigNameserversProperty = networkManagerDbusIp6ConfigInterface + ".Nameservers"
|
||||||
networkManagerDbusDefaultBehaviorFlag networkManagerConfigBehavior = 0
|
networkManagerDbusDefaultBehaviorFlag networkManagerConfigBehavior = 0
|
||||||
networkManagerDbusIPv4Key = "ipv4"
|
networkManagerDbusIPv4Key = "ipv4"
|
||||||
networkManagerDbusIPv6Key = "ipv6"
|
networkManagerDbusIPv6Key = "ipv6"
|
||||||
@@ -51,9 +61,10 @@ var supportedNetworkManagerVersionConstraints = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type networkManagerDbusConfigurator struct {
|
type networkManagerDbusConfigurator struct {
|
||||||
dbusLinkObject dbus.ObjectPath
|
dbusLinkObject dbus.ObjectPath
|
||||||
routingAll bool
|
routingAll bool
|
||||||
ifaceName string
|
ifaceName string
|
||||||
|
origNameservers []netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
// the types below are based on dbus specification, each field is mapped to a dbus type
|
// the types below are based on dbus specification, each field is mapped to a dbus type
|
||||||
@@ -92,10 +103,200 @@ func newNetworkManagerDbusConfigurator(wgInterface string) (*networkManagerDbusC
|
|||||||
|
|
||||||
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface)
|
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface)
|
||||||
|
|
||||||
return &networkManagerDbusConfigurator{
|
c := &networkManagerDbusConfigurator{
|
||||||
dbusLinkObject: dbus.ObjectPath(s),
|
dbusLinkObject: dbus.ObjectPath(s),
|
||||||
ifaceName: wgInterface,
|
ifaceName: wgInterface,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
origNameservers, err := c.captureOriginalNameservers()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
log.Warnf("capture original nameservers from NetworkManager: %v", err)
|
||||||
|
case len(origNameservers) == 0:
|
||||||
|
log.Warnf("no original nameservers captured from non-WG NetworkManager devices; DNS fallback will be empty")
|
||||||
|
default:
|
||||||
|
log.Debugf("captured %d original nameservers from non-WG NetworkManager devices: %v", len(origNameservers), origNameservers)
|
||||||
|
}
|
||||||
|
c.origNameservers = origNameservers
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureOriginalNameservers reads DNS servers from every NM device's
|
||||||
|
// IP4Config / IP6Config except our WG device.
|
||||||
|
func (n *networkManagerDbusConfigurator) captureOriginalNameservers() ([]netip.Addr, error) {
|
||||||
|
devices, err := networkManagerListDevices()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list devices: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[netip.Addr]struct{})
|
||||||
|
var out []netip.Addr
|
||||||
|
for _, dev := range devices {
|
||||||
|
if dev == n.dbusLinkObject {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ifaceName := readNetworkManagerDeviceInterface(dev)
|
||||||
|
for _, addr := range readNetworkManagerDeviceDNS(dev) {
|
||||||
|
addr = addr.Unmap()
|
||||||
|
if !addr.IsValid() || addr.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// IP6Config.Nameservers is a byte slice without zone info;
|
||||||
|
// reattach the device's interface name so a captured fe80::…
|
||||||
|
// stays routable.
|
||||||
|
if addr.IsLinkLocalUnicast() && ifaceName != "" {
|
||||||
|
addr = addr.WithZone(ifaceName)
|
||||||
|
}
|
||||||
|
if _, dup := seen[addr]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[addr] = struct{}{}
|
||||||
|
out = append(out, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNetworkManagerDeviceInterface(devicePath dbus.ObjectPath) string {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, devicePath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
v, err := obj.GetProperty(networkManagerDbusDeviceIfaceProperty)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s, _ := v.Value().(string)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkManagerListDevices() ([]dbus.ObjectPath, error) {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dbus NetworkManager: %w", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
var devs []dbus.ObjectPath
|
||||||
|
if err := obj.Call(networkManagerDbusGetDevicesMethod, dbusDefaultFlag).Store(&devs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return devs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNetworkManagerDeviceDNS(devicePath dbus.ObjectPath) []netip.Addr {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, devicePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
var out []netip.Addr
|
||||||
|
if path := readNetworkManagerConfigPath(obj, networkManagerDbusDeviceIp4ConfigProperty); path != "" {
|
||||||
|
out = append(out, readIPv4ConfigDNS(path)...)
|
||||||
|
}
|
||||||
|
if path := readNetworkManagerConfigPath(obj, networkManagerDbusDeviceIp6ConfigProperty); path != "" {
|
||||||
|
out = append(out, readIPv6ConfigDNS(path)...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNetworkManagerConfigPath(obj dbus.BusObject, property string) dbus.ObjectPath {
|
||||||
|
v, err := obj.GetProperty(property)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
path, ok := v.Value().(dbus.ObjectPath)
|
||||||
|
if !ok || path == "/" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIPv4ConfigDNS(path dbus.ObjectPath) []netip.Addr {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
|
||||||
|
// NameserverData (NM 1.13+) carries strings; older NMs only expose the
|
||||||
|
// legacy uint32 Nameservers property.
|
||||||
|
if out := readIPv4NameserverData(obj); len(out) > 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
return readIPv4LegacyNameservers(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIPv4NameserverData(obj dbus.BusObject) []netip.Addr {
|
||||||
|
v, err := obj.GetProperty(networkManagerDbusIp4ConfigNameserverDataProperty)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, ok := v.Value().([]map[string]dbus.Variant)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []netip.Addr
|
||||||
|
for _, entry := range entries {
|
||||||
|
addrVar, ok := entry["address"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, ok := addrVar.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if a, err := netip.ParseAddr(s); err == nil {
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIPv4LegacyNameservers(obj dbus.BusObject) []netip.Addr {
|
||||||
|
v, err := obj.GetProperty(networkManagerDbusIp4ConfigNameserversProperty)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, ok := v.Value().([]uint32)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]netip.Addr, 0, len(raw))
|
||||||
|
for _, n := range raw {
|
||||||
|
var b [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(b[:], n)
|
||||||
|
out = append(out, netip.AddrFrom4(b))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIPv6ConfigDNS(path dbus.ObjectPath) []netip.Addr {
|
||||||
|
obj, closeConn, err := getDbusObject(networkManagerDest, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
v, err := obj.GetProperty(networkManagerDbusIp6ConfigNameserversProperty)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, ok := v.Value().([][]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]netip.Addr, 0, len(raw))
|
||||||
|
for _, b := range raw {
|
||||||
|
if a, ok := netip.AddrFromSlice(b); ok {
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *networkManagerDbusConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
|
return slices.Clone(n.origNameservers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *networkManagerDbusConfigurator) supportCustomPort() bool {
|
func (n *networkManagerDbusConfigurator) supportCustomPort() bool {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errNoSuitableAddress mirrors the unexported error string the net package
|
||||||
|
// uses when a resolved host has no addresses of the requested family.
|
||||||
|
const errNoSuitableAddress = "no suitable address found"
|
||||||
|
|
||||||
// GenerateRequestID creates a random 8-character hex string for request tracing.
|
// GenerateRequestID creates a random 8-character hex string for request tracing.
|
||||||
func GenerateRequestID() string {
|
func GenerateRequestID() string {
|
||||||
bytes := make([]byte, 4)
|
bytes := make([]byte, 4)
|
||||||
@@ -126,6 +130,14 @@ func LookupIP(ctx context.Context, r resolver, network, host string, qtype uint1
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getRcodeForError(ctx context.Context, r resolver, host string, qtype uint16, err error) int {
|
func getRcodeForError(ctx context.Context, r resolver, host string, qtype uint16, err error) int {
|
||||||
|
// The net package returns this AddrError when the host resolves but has
|
||||||
|
// no addresses of the requested family. The domain exists, so answer
|
||||||
|
// NODATA instead of SERVFAIL.
|
||||||
|
var addrErr *net.AddrError
|
||||||
|
if errors.As(err, &addrErr) && addrErr.Err == errNoSuitableAddress {
|
||||||
|
return dns.RcodeSuccess
|
||||||
|
}
|
||||||
|
|
||||||
var dnsErr *net.DNSError
|
var dnsErr *net.DNSError
|
||||||
if !errors.As(err, &dnsErr) {
|
if !errors.As(err, &dnsErr) {
|
||||||
return dns.RcodeServerFailure
|
return dns.RcodeServerFailure
|
||||||
|
|||||||
122
client/internal/dns/resutil/resolve_test.go
Normal file
122
client/internal/dns/resutil/resolve_test.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package resutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockResolver struct {
|
||||||
|
// results maps network ("ip4"/"ip6") to the lookup outcome.
|
||||||
|
results map[string]mockLookup
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockLookup struct {
|
||||||
|
ips []netip.Addr
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockResolver) LookupNetIP(_ context.Context, network, _ string) ([]netip.Addr, error) {
|
||||||
|
res, ok := m.results[network]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("unexpected network: " + network)
|
||||||
|
}
|
||||||
|
return res.ips, res.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_Success(t *testing.T) {
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip4": {ips: []netip.Addr{netip.MustParseAddr("::ffff:192.0.2.1")}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip4", "example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, result.Rcode, "successful lookup should return NOERROR")
|
||||||
|
require.Len(t, result.IPs, 1, "should return the resolved address")
|
||||||
|
assert.Equal(t, netip.MustParseAddr("192.0.2.1"), result.IPs[0], "v4-mapped address should be unmapped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_NoSuitableAddress(t *testing.T) {
|
||||||
|
// The net package returns this AddrError when the host resolves but has
|
||||||
|
// no addresses of the requested family (e.g. AAAA query for a v4-only
|
||||||
|
// hosts file entry). The domain exists, so this is NODATA, not SERVFAIL.
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip6": {err: &net.AddrError{Err: "no suitable address found", Addr: "example.com."}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip6", "example.com.", dns.TypeAAAA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, result.Rcode, "no suitable address should map to NODATA")
|
||||||
|
assert.Empty(t, result.IPs, "NODATA response should carry no addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestErrNoSuitableAddressMatchesNetPackage pins our copy of the error string
|
||||||
|
// to what the net package actually emits. A literal IP of the wrong family
|
||||||
|
// takes the same filterAddrList path as a resolved hostname, without network
|
||||||
|
// access.
|
||||||
|
func TestErrNoSuitableAddressMatchesNetPackage(t *testing.T) {
|
||||||
|
_, err := (&net.Resolver{}).LookupNetIP(context.Background(), "ip6", "192.0.2.1")
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
var addrErr *net.AddrError
|
||||||
|
require.ErrorAs(t, err, &addrErr, "wrong-family lookup should return AddrError")
|
||||||
|
assert.Equal(t, errNoSuitableAddress, addrErr.Err, "net package error string should match our constant")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_OtherAddrError(t *testing.T) {
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip4": {err: &net.AddrError{Err: "some other address problem", Addr: "example.com."}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip4", "example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeServerFailure, result.Rcode, "unrecognized AddrError should map to SERVFAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_NotFoundNXDomain(t *testing.T) {
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip4": {err: &net.DNSError{Err: "no such host", Name: "example.com.", IsNotFound: true}},
|
||||||
|
"ip6": {err: &net.DNSError{Err: "no such host", Name: "example.com.", IsNotFound: true}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip4", "example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeNameError, result.Rcode, "not found for both families should map to NXDOMAIN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_NotFoundNoData(t *testing.T) {
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip6": {err: &net.DNSError{Err: "no such host", Name: "example.com.", IsNotFound: true}},
|
||||||
|
"ip4": {ips: []netip.Addr{netip.MustParseAddr("192.0.2.1")}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip6", "example.com.", dns.TypeAAAA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, result.Rcode, "not found with the other family present should map to NODATA")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_GenericError(t *testing.T) {
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip4": {err: errors.New("connection refused")},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip4", "example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeServerFailure, result.Rcode, "generic error should map to SERVFAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupIP_DNSErrorNotIsNotFound(t *testing.T) {
|
||||||
|
r := &mockResolver{results: map[string]mockLookup{
|
||||||
|
"ip4": {err: &net.DNSError{Err: "server misbehaving", Name: "example.com.", IsTemporary: true}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
result := LookupIP(context.Background(), r, "ip4", "example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
assert.Equal(t, dns.RcodeServerFailure, result.Rcode, "upstream failure should map to SERVFAIL")
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
func (s *DefaultServer) initialize() (manager hostManager, err error) {
|
func (s *DefaultServer) initialize() (manager hostManager, err error) {
|
||||||
return newHostManager()
|
return newHostManager(s.hostsDNSHolder)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
@@ -31,8 +32,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,16 +104,17 @@ func init() {
|
|||||||
formatter.SetTextFormatter(log.StandardLogger())
|
formatter.SetTextFormatter(log.StandardLogger())
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase {
|
func generateDummyHandler(d string, servers []nbdns.NameServer) *upstreamResolverBase {
|
||||||
var srvs []netip.AddrPort
|
var srvs []netip.AddrPort
|
||||||
for _, srv := range servers {
|
for _, srv := range servers {
|
||||||
srvs = append(srvs, srv.AddrPort())
|
srvs = append(srvs, srv.AddrPort())
|
||||||
}
|
}
|
||||||
return &upstreamResolverBase{
|
u := &upstreamResolverBase{
|
||||||
domain: domain,
|
domain: domain.Domain(d),
|
||||||
upstreamServers: srvs,
|
cancel: func() {},
|
||||||
cancel: func() {},
|
|
||||||
}
|
}
|
||||||
|
u.addRace(srvs)
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateDNSServer(t *testing.T) {
|
func TestUpdateDNSServer(t *testing.T) {
|
||||||
@@ -653,74 +657,8 @@ func TestDNSServerStartStop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
|
||||||
hostManager := &mockHostConfigurator{}
|
|
||||||
server := DefaultServer{
|
|
||||||
ctx: context.Background(),
|
|
||||||
service: NewServiceViaMemory(&mocWGIface{}),
|
|
||||||
localResolver: local.NewResolver(),
|
|
||||||
handlerChain: NewHandlerChain(),
|
|
||||||
hostManager: hostManager,
|
|
||||||
currentConfig: HostDNSConfig{
|
|
||||||
Domains: []DomainConfig{
|
|
||||||
{false, "domain0", false},
|
|
||||||
{false, "domain1", false},
|
|
||||||
{false, "domain2", false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
statusRecorder: peer.NewRecorder("mgm"),
|
|
||||||
}
|
|
||||||
|
|
||||||
var domainsUpdate string
|
|
||||||
hostManager.applyDNSConfigFunc = func(config HostDNSConfig, statemanager *statemanager.Manager) error {
|
|
||||||
domains := []string{}
|
|
||||||
for _, item := range config.Domains {
|
|
||||||
if item.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
domains = append(domains, item.Domain)
|
|
||||||
}
|
|
||||||
domainsUpdate = strings.Join(domains, ",")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivate, reactivate := server.upstreamCallbacks(&nbdns.NameServerGroup{
|
|
||||||
Domains: []string{"domain1"},
|
|
||||||
NameServers: []nbdns.NameServer{
|
|
||||||
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
|
|
||||||
},
|
|
||||||
}, nil, 0)
|
|
||||||
|
|
||||||
deactivate(nil)
|
|
||||||
expected := "domain0,domain2"
|
|
||||||
domains := []string{}
|
|
||||||
for _, item := range server.currentConfig.Domains {
|
|
||||||
if item.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
domains = append(domains, item.Domain)
|
|
||||||
}
|
|
||||||
got := strings.Join(domains, ",")
|
|
||||||
if expected != got {
|
|
||||||
t.Errorf("expected domains list: %q, got %q", expected, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
reactivate()
|
|
||||||
expected = "domain0,domain1,domain2"
|
|
||||||
domains = []string{}
|
|
||||||
for _, item := range server.currentConfig.Domains {
|
|
||||||
if item.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
domains = append(domains, item.Domain)
|
|
||||||
}
|
|
||||||
got = strings.Join(domains, ",")
|
|
||||||
if expected != got {
|
|
||||||
t.Errorf("expected domains list: %q, got %q", expected, domainsUpdate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
|
func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
|
||||||
|
skipUnlessAndroid(t)
|
||||||
wgIFace, err := createWgInterfaceWithBind(t)
|
wgIFace, err := createWgInterfaceWithBind(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("failed to initialize wg interface")
|
t.Fatal("failed to initialize wg interface")
|
||||||
@@ -748,6 +686,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDNSPermanent_updateUpstream(t *testing.T) {
|
func TestDNSPermanent_updateUpstream(t *testing.T) {
|
||||||
|
skipUnlessAndroid(t)
|
||||||
wgIFace, err := createWgInterfaceWithBind(t)
|
wgIFace, err := createWgInterfaceWithBind(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("failed to initialize wg interface")
|
t.Fatal("failed to initialize wg interface")
|
||||||
@@ -841,6 +780,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDNSPermanent_matchOnly(t *testing.T) {
|
func TestDNSPermanent_matchOnly(t *testing.T) {
|
||||||
|
skipUnlessAndroid(t)
|
||||||
wgIFace, err := createWgInterfaceWithBind(t)
|
wgIFace, err := createWgInterfaceWithBind(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("failed to initialize wg interface")
|
t.Fatal("failed to initialize wg interface")
|
||||||
@@ -913,6 +853,18 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skipUnlessAndroid marks tests that exercise the mobile-permanent DNS path,
|
||||||
|
// which only matches a real production setup on android (NewDefaultServerPermanentUpstream
|
||||||
|
// + androidHostManager). On non-android the desktop host manager replaces it
|
||||||
|
// during Initialize and the assertion stops making sense. Skipped here until we
|
||||||
|
// have an android CI runner.
|
||||||
|
func skipUnlessAndroid(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if runtime.GOOS != "android" {
|
||||||
|
t.Skip("requires android runner; mobile-permanent path doesn't match production on this OS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
|
func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ov := os.Getenv("NB_WG_KERNEL_DISABLED")
|
ov := os.Getenv("NB_WG_KERNEL_DISABLED")
|
||||||
@@ -1065,7 +1017,6 @@ type mockHandler struct {
|
|||||||
|
|
||||||
func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
|
func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
|
||||||
func (m *mockHandler) Stop() {}
|
func (m *mockHandler) Stop() {}
|
||||||
func (m *mockHandler) ProbeAvailability(context.Context) {}
|
|
||||||
func (m *mockHandler) ID() types.HandlerID { return types.HandlerID(m.Id) }
|
func (m *mockHandler) ID() types.HandlerID { return types.HandlerID(m.Id) }
|
||||||
|
|
||||||
type mockService struct{}
|
type mockService struct{}
|
||||||
@@ -2085,6 +2036,598 @@ func TestLocalResolverPriorityConstants(t *testing.T) {
|
|||||||
assert.Equal(t, "local.example.com", localMuxUpdates[0].domain)
|
assert.Equal(t, "local.example.com", localMuxUpdates[0].domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildUpstreamHandler_MergesGroupsPerDomain verifies that multiple
|
||||||
|
// admin-defined nameserver groups targeting the same domain collapse into a
|
||||||
|
// single handler with each group preserved as a sequential inner list.
|
||||||
|
func TestBuildUpstreamHandler_MergesGroupsPerDomain(t *testing.T) {
|
||||||
|
wgInterface := &mocWGIface{}
|
||||||
|
service := NewServiceViaMemory(wgInterface)
|
||||||
|
server := &DefaultServer{
|
||||||
|
ctx: context.Background(),
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
service: service,
|
||||||
|
localResolver: local.NewResolver(),
|
||||||
|
handlerChain: NewHandlerChain(),
|
||||||
|
hostManager: &noopHostConfigurator{},
|
||||||
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{IP: netip.MustParseAddr("192.0.2.1"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||||
|
},
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{IP: netip.MustParseAddr("192.0.2.2"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||||
|
{IP: netip.MustParseAddr("192.0.2.3"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||||
|
},
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
muxUpdates, err := server.buildUpstreamHandlerUpdate(groups)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, muxUpdates, 1, "same-domain groups should merge into one handler")
|
||||||
|
assert.Equal(t, "example.com", muxUpdates[0].domain)
|
||||||
|
assert.Equal(t, PriorityUpstream, muxUpdates[0].priority)
|
||||||
|
|
||||||
|
handler := muxUpdates[0].handler.(*upstreamResolver)
|
||||||
|
require.Len(t, handler.upstreamServers, 2, "handler should have two groups")
|
||||||
|
assert.Equal(t, upstreamRace{netip.MustParseAddrPort("192.0.2.1:53")}, handler.upstreamServers[0])
|
||||||
|
assert.Equal(t, upstreamRace{
|
||||||
|
netip.MustParseAddrPort("192.0.2.2:53"),
|
||||||
|
netip.MustParseAddrPort("192.0.2.3:53"),
|
||||||
|
}, handler.upstreamServers[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEvaluateNSGroupHealth covers the records-only verdict. The gate
|
||||||
|
// (overlay route selected-but-no-active-peer) is intentionally NOT an
|
||||||
|
// input to the evaluator anymore: the verdict drives the Enabled flag,
|
||||||
|
// which must always reflect what we actually observed. Gate-aware event
|
||||||
|
// suppression is tested separately in the projection test.
|
||||||
|
//
|
||||||
|
// Matrix per upstream: {no record, fresh Ok, fresh Fail, stale Fail,
|
||||||
|
// stale Ok, Ok newer than Fail, Fail newer than Ok}.
|
||||||
|
// Group verdict: any fresh-working → Healthy; any fresh-broken with no
|
||||||
|
// fresh-working → Unhealthy; otherwise Undecided.
|
||||||
|
func TestEvaluateNSGroupHealth(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
a := netip.MustParseAddrPort("192.0.2.1:53")
|
||||||
|
b := netip.MustParseAddrPort("192.0.2.2:53")
|
||||||
|
|
||||||
|
recentOk := UpstreamHealth{LastOk: now.Add(-2 * time.Second)}
|
||||||
|
recentFail := UpstreamHealth{LastFail: now.Add(-1 * time.Second), LastErr: "timeout"}
|
||||||
|
staleOk := UpstreamHealth{LastOk: now.Add(-10 * time.Minute)}
|
||||||
|
staleFail := UpstreamHealth{LastFail: now.Add(-10 * time.Minute), LastErr: "timeout"}
|
||||||
|
okThenFail := UpstreamHealth{
|
||||||
|
LastOk: now.Add(-10 * time.Second),
|
||||||
|
LastFail: now.Add(-1 * time.Second),
|
||||||
|
LastErr: "timeout",
|
||||||
|
}
|
||||||
|
failThenOk := UpstreamHealth{
|
||||||
|
LastOk: now.Add(-1 * time.Second),
|
||||||
|
LastFail: now.Add(-10 * time.Second),
|
||||||
|
LastErr: "timeout",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
health map[netip.AddrPort]UpstreamHealth
|
||||||
|
servers []netip.AddrPort
|
||||||
|
wantVerdict nsGroupVerdict
|
||||||
|
wantErrSubst string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no record, undecided",
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictUndecided,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fresh success, healthy",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{a: recentOk},
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictHealthy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fresh failure, unhealthy",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{a: recentFail},
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictUnhealthy,
|
||||||
|
wantErrSubst: "timeout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only stale success, undecided",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{a: staleOk},
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictUndecided,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only stale failure, undecided",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{a: staleFail},
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictUndecided,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both fresh, fail newer, unhealthy",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{a: okThenFail},
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictUnhealthy,
|
||||||
|
wantErrSubst: "timeout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both fresh, ok newer, healthy",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{a: failThenOk},
|
||||||
|
servers: []netip.AddrPort{a},
|
||||||
|
wantVerdict: nsVerdictHealthy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two upstreams, one success wins",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{
|
||||||
|
a: recentFail,
|
||||||
|
b: recentOk,
|
||||||
|
},
|
||||||
|
servers: []netip.AddrPort{a, b},
|
||||||
|
wantVerdict: nsVerdictHealthy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two upstreams, one fail one unseen, unhealthy",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{
|
||||||
|
a: recentFail,
|
||||||
|
},
|
||||||
|
servers: []netip.AddrPort{a, b},
|
||||||
|
wantVerdict: nsVerdictUnhealthy,
|
||||||
|
wantErrSubst: "timeout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two upstreams, all recent failures, unhealthy",
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{
|
||||||
|
a: {LastFail: now.Add(-5 * time.Second), LastErr: "timeout"},
|
||||||
|
b: {LastFail: now.Add(-1 * time.Second), LastErr: "SERVFAIL"},
|
||||||
|
},
|
||||||
|
servers: []netip.AddrPort{a, b},
|
||||||
|
wantVerdict: nsVerdictUnhealthy,
|
||||||
|
wantErrSubst: "SERVFAIL",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
verdict, err := evaluateNSGroupHealth(tc.health, tc.servers, now)
|
||||||
|
assert.Equal(t, tc.wantVerdict, verdict, "verdict mismatch")
|
||||||
|
if tc.wantErrSubst != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tc.wantErrSubst)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// healthStubHandler is a minimal dnsMuxMap entry that exposes a fixed
|
||||||
|
// UpstreamHealth snapshot, letting tests drive recomputeNSGroupStates
|
||||||
|
// without spinning up real handlers.
|
||||||
|
type healthStubHandler struct {
|
||||||
|
health map[netip.AddrPort]UpstreamHealth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *healthStubHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
|
||||||
|
func (h *healthStubHandler) Stop() {}
|
||||||
|
func (h *healthStubHandler) ID() types.HandlerID { return "health-stub" }
|
||||||
|
func (h *healthStubHandler) UpstreamHealth() map[netip.AddrPort]UpstreamHealth {
|
||||||
|
return h.health
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_SteadyStateIsSilent guards against duplicate events:
|
||||||
|
// while a group stays Unhealthy tick after tick, only the first
|
||||||
|
// Unhealthy transition may emit. Same for staying Healthy.
|
||||||
|
func TestProjection_SteadyStateIsSilent(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("unreachable", "first fail emits warning")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.tick()
|
||||||
|
fx.expectNoEvent("staying unhealthy must not re-emit")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("recovered", "recovery on transition")
|
||||||
|
|
||||||
|
fx.tick()
|
||||||
|
fx.tick()
|
||||||
|
fx.expectNoEvent("staying healthy must not re-emit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// projTestFixture is the common setup for the projection tests: a
|
||||||
|
// single-upstream group whose route classification the test can flip by
|
||||||
|
// assigning to selected/active. Callers drive failures/successes by
|
||||||
|
// mutating stub.health and calling refreshHealth.
|
||||||
|
type projTestFixture struct {
|
||||||
|
t *testing.T
|
||||||
|
recorder *peer.Status
|
||||||
|
events <-chan *proto.SystemEvent
|
||||||
|
server *DefaultServer
|
||||||
|
stub *healthStubHandler
|
||||||
|
group *nbdns.NameServerGroup
|
||||||
|
srv netip.AddrPort
|
||||||
|
selected route.HAMap
|
||||||
|
active route.HAMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProjTestFixture(t *testing.T) *projTestFixture {
|
||||||
|
t.Helper()
|
||||||
|
recorder := peer.NewRecorder("mgm")
|
||||||
|
sub := recorder.SubscribeToEvents()
|
||||||
|
t.Cleanup(func() { recorder.UnsubscribeFromEvents(sub) })
|
||||||
|
|
||||||
|
srv := netip.MustParseAddrPort("100.64.0.1:53")
|
||||||
|
fx := &projTestFixture{
|
||||||
|
t: t,
|
||||||
|
recorder: recorder,
|
||||||
|
events: sub.Events(),
|
||||||
|
stub: &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{}},
|
||||||
|
srv: srv,
|
||||||
|
group: &nbdns.NameServerGroup{
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
NameServers: []nbdns.NameServer{{IP: srv.Addr(), NSType: nbdns.UDPNameServerType, Port: int(srv.Port())}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fx.server = &DefaultServer{
|
||||||
|
ctx: context.Background(),
|
||||||
|
wgInterface: &mocWGIface{},
|
||||||
|
statusRecorder: recorder,
|
||||||
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
|
selectedRoutes: func() route.HAMap { return fx.selected },
|
||||||
|
activeRoutes: func() route.HAMap { return fx.active },
|
||||||
|
warningDelayBase: defaultWarningDelayBase,
|
||||||
|
}
|
||||||
|
fx.server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: fx.stub, priority: PriorityUpstream}
|
||||||
|
|
||||||
|
fx.server.mux.Lock()
|
||||||
|
fx.server.updateNSGroupStates([]*nbdns.NameServerGroup{fx.group})
|
||||||
|
fx.server.mux.Unlock()
|
||||||
|
return fx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *projTestFixture) setHealth(h UpstreamHealth) {
|
||||||
|
f.stub.health = map[netip.AddrPort]UpstreamHealth{f.srv: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *projTestFixture) tick() []peer.NSGroupState {
|
||||||
|
f.server.refreshHealth()
|
||||||
|
return f.recorder.GetDNSStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *projTestFixture) expectNoEvent(why string) {
|
||||||
|
f.t.Helper()
|
||||||
|
select {
|
||||||
|
case evt := <-f.events:
|
||||||
|
f.t.Fatalf("unexpected event (%s): %+v", why, evt)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *projTestFixture) expectEvent(substr, why string) *proto.SystemEvent {
|
||||||
|
f.t.Helper()
|
||||||
|
select {
|
||||||
|
case evt := <-f.events:
|
||||||
|
assert.Contains(f.t, evt.Message, substr, why)
|
||||||
|
return evt
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
f.t.Fatalf("expected event (%s) with %q", why, substr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var overlayNetForTest = netip.MustParsePrefix("100.64.0.0/16")
|
||||||
|
var overlayMapForTest = route.HAMap{"overlay": {{Network: overlayNetForTest}}}
|
||||||
|
|
||||||
|
// TestProjection_PublicFailEmitsImmediately covers rule 1: an upstream
|
||||||
|
// that is not inside any selected route (public DNS) fires the warning
|
||||||
|
// on the first Unhealthy tick, no grace period.
|
||||||
|
func TestProjection_PublicFailEmitsImmediately(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
states := fx.tick()
|
||||||
|
require.Len(t, states, 1)
|
||||||
|
assert.False(t, states[0].Enabled)
|
||||||
|
fx.expectEvent("unreachable", "public DNS failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_OverlayConnectedFailEmitsImmediately covers rule 2:
|
||||||
|
// the upstream is inside a selected route AND the route has a Connected
|
||||||
|
// peer. Tunnel is up, failure is real, emit immediately.
|
||||||
|
func TestProjection_OverlayConnectedFailEmitsImmediately(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
fx.selected = overlayMapForTest
|
||||||
|
fx.active = overlayMapForTest
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
states := fx.tick()
|
||||||
|
require.Len(t, states, 1)
|
||||||
|
assert.False(t, states[0].Enabled)
|
||||||
|
fx.expectEvent("unreachable", "overlay + connected failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_OverlayNotConnectedDelaysWarning covers rule 3: the
|
||||||
|
// upstream is routed but no peer is Connected (Connecting/Idle/missing).
|
||||||
|
// First tick: Unhealthy display, no warning. After the grace window
|
||||||
|
// elapses with no recovery, the warning fires.
|
||||||
|
func TestProjection_OverlayNotConnectedDelaysWarning(t *testing.T) {
|
||||||
|
grace := 50 * time.Millisecond
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
fx.server.warningDelayBase = grace
|
||||||
|
fx.selected = overlayMapForTest
|
||||||
|
// active stays nil: routed but not connected.
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
states := fx.tick()
|
||||||
|
require.Len(t, states, 1)
|
||||||
|
assert.False(t, states[0].Enabled, "display must reflect failure even during grace window")
|
||||||
|
fx.expectNoEvent("first fail tick within grace window")
|
||||||
|
|
||||||
|
time.Sleep(grace + 10*time.Millisecond)
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("unreachable", "warning after grace window")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_OverlayAddrNoRouteDelaysWarning covers an upstream
|
||||||
|
// whose address is inside the WireGuard overlay range but is not
|
||||||
|
// covered by any selected route (peer-to-peer DNS without an explicit
|
||||||
|
// route). Until a peer reports Connected for that address, startup
|
||||||
|
// failures must be held just like the routed case.
|
||||||
|
func TestProjection_OverlayAddrNoRouteDelaysWarning(t *testing.T) {
|
||||||
|
recorder := peer.NewRecorder("mgm")
|
||||||
|
sub := recorder.SubscribeToEvents()
|
||||||
|
t.Cleanup(func() { recorder.UnsubscribeFromEvents(sub) })
|
||||||
|
|
||||||
|
overlayPeer := netip.MustParseAddrPort("100.66.100.5:53")
|
||||||
|
server := &DefaultServer{
|
||||||
|
ctx: context.Background(),
|
||||||
|
wgInterface: &mocWGIface{},
|
||||||
|
statusRecorder: recorder,
|
||||||
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
|
selectedRoutes: func() route.HAMap { return nil },
|
||||||
|
activeRoutes: func() route.HAMap { return nil },
|
||||||
|
warningDelayBase: 50 * time.Millisecond,
|
||||||
|
}
|
||||||
|
group := &nbdns.NameServerGroup{
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
NameServers: []nbdns.NameServer{{IP: overlayPeer.Addr(), NSType: nbdns.UDPNameServerType, Port: int(overlayPeer.Port())}},
|
||||||
|
}
|
||||||
|
stub := &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{
|
||||||
|
overlayPeer: {LastFail: time.Now(), LastErr: "timeout"},
|
||||||
|
}}
|
||||||
|
server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: stub, priority: PriorityUpstream}
|
||||||
|
|
||||||
|
server.mux.Lock()
|
||||||
|
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
||||||
|
server.mux.Unlock()
|
||||||
|
server.refreshHealth()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case evt := <-sub.Events():
|
||||||
|
t.Fatalf("unexpected event during grace window: %+v", evt)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(60 * time.Millisecond)
|
||||||
|
stub.health = map[netip.AddrPort]UpstreamHealth{overlayPeer: {LastFail: time.Now(), LastErr: "timeout"}}
|
||||||
|
server.refreshHealth()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case evt := <-sub.Events():
|
||||||
|
assert.Contains(t, evt.Message, "unreachable")
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("expected warning after grace window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_StopClearsHealthState verifies that Stop wipes the
|
||||||
|
// per-group projection state so a subsequent Start doesn't inherit
|
||||||
|
// sticky flags (notably everHealthy) that would bypass the grace
|
||||||
|
// window during the next peer handshake.
|
||||||
|
func TestProjection_StopClearsHealthState(t *testing.T) {
|
||||||
|
wgIface := &mocWGIface{}
|
||||||
|
server := &DefaultServer{
|
||||||
|
ctx: context.Background(),
|
||||||
|
wgInterface: wgIface,
|
||||||
|
service: NewServiceViaMemory(wgIface),
|
||||||
|
hostManager: &noopHostConfigurator{},
|
||||||
|
extraDomains: map[domain.Domain]int{},
|
||||||
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
|
statusRecorder: peer.NewRecorder("mgm"),
|
||||||
|
selectedRoutes: func() route.HAMap { return nil },
|
||||||
|
activeRoutes: func() route.HAMap { return nil },
|
||||||
|
warningDelayBase: defaultWarningDelayBase,
|
||||||
|
currentConfigHash: ^uint64(0),
|
||||||
|
}
|
||||||
|
server.ctx, server.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
srv := netip.MustParseAddrPort("8.8.8.8:53")
|
||||||
|
group := &nbdns.NameServerGroup{
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
NameServers: []nbdns.NameServer{{IP: srv.Addr(), NSType: nbdns.UDPNameServerType, Port: int(srv.Port())}},
|
||||||
|
}
|
||||||
|
stub := &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{srv: {LastOk: time.Now()}}}
|
||||||
|
server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: stub, priority: PriorityUpstream}
|
||||||
|
|
||||||
|
server.mux.Lock()
|
||||||
|
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
||||||
|
server.mux.Unlock()
|
||||||
|
server.refreshHealth()
|
||||||
|
|
||||||
|
server.healthProjectMu.Lock()
|
||||||
|
p, ok := server.nsGroupProj[generateGroupKey(group)]
|
||||||
|
server.healthProjectMu.Unlock()
|
||||||
|
require.True(t, ok, "projection state should exist after tick")
|
||||||
|
require.True(t, p.everHealthy, "tick with success must set everHealthy")
|
||||||
|
|
||||||
|
server.Stop()
|
||||||
|
|
||||||
|
server.healthProjectMu.Lock()
|
||||||
|
cleared := server.nsGroupProj == nil
|
||||||
|
server.healthProjectMu.Unlock()
|
||||||
|
assert.True(t, cleared, "Stop must clear nsGroupProj")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_OverlayRecoversDuringGrace covers the happy path of
|
||||||
|
// rule 3: startup failures while the peer is handshaking, then the peer
|
||||||
|
// comes up and a query succeeds before the grace window elapses. No
|
||||||
|
// warning should ever have fired, and no recovery either.
|
||||||
|
func TestProjection_OverlayRecoversDuringGrace(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
fx.server.warningDelayBase = 200 * time.Millisecond
|
||||||
|
fx.selected = overlayMapForTest
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectNoEvent("fail within grace, warning suppressed")
|
||||||
|
|
||||||
|
fx.active = overlayMapForTest
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
states := fx.tick()
|
||||||
|
require.Len(t, states, 1)
|
||||||
|
assert.True(t, states[0].Enabled)
|
||||||
|
fx.expectNoEvent("recovery without prior warning must not emit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_RecoveryOnlyAfterWarning enforces the invariant the
|
||||||
|
// whole design leans on: recovery events only appear when a warning
|
||||||
|
// event was actually emitted for the current streak. A Healthy verdict
|
||||||
|
// without a prior warning is silent, so the user never sees "recovered"
|
||||||
|
// out of thin air.
|
||||||
|
func TestProjection_RecoveryOnlyAfterWarning(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
states := fx.tick()
|
||||||
|
require.Len(t, states, 1)
|
||||||
|
assert.True(t, states[0].Enabled)
|
||||||
|
fx.expectNoEvent("first healthy tick should not recover anything")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("unreachable", "public fail emits immediately")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("recovered", "recovery follows real warning")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("unreachable", "second cycle warning")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("recovered", "second cycle recovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_EverHealthyOverridesDelay covers rule 4: once a group
|
||||||
|
// has ever been Healthy, subsequent failures skip the grace window even
|
||||||
|
// if classification says "routed + not connected". The system has
|
||||||
|
// proved it can work, so any new failure is real.
|
||||||
|
func TestProjection_EverHealthyOverridesDelay(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
// Large base so any emission must come from the everHealthy bypass, not elapsed time.
|
||||||
|
fx.server.warningDelayBase = time.Hour
|
||||||
|
fx.selected = overlayMapForTest
|
||||||
|
fx.active = overlayMapForTest
|
||||||
|
|
||||||
|
// Establish "ever healthy".
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectNoEvent("first healthy tick")
|
||||||
|
|
||||||
|
// Peer drops. Query fails. Routed + not connected → normally grace,
|
||||||
|
// but everHealthy flag bypasses it.
|
||||||
|
fx.active = nil
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("unreachable", "failure after ever-healthy must be immediate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_ReconnectBlipEmitsPair covers the explicit tradeoff
|
||||||
|
// from the design discussion: once a group has been healthy, a brief
|
||||||
|
// reconnect that produces a failing tick will fire warning + recovery.
|
||||||
|
// This is by design: user-visible blips are accurate signal, not noise.
|
||||||
|
func TestProjection_ReconnectBlipEmitsPair(t *testing.T) {
|
||||||
|
fx := newProjTestFixture(t)
|
||||||
|
fx.selected = overlayMapForTest
|
||||||
|
fx.active = overlayMapForTest
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
fx.tick()
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastFail: time.Now(), LastErr: "timeout"})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("unreachable", "blip warning")
|
||||||
|
|
||||||
|
fx.setHealth(UpstreamHealth{LastOk: time.Now()})
|
||||||
|
fx.tick()
|
||||||
|
fx.expectEvent("recovered", "blip recovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProjection_MixedGroupEmitsImmediately covers the multi-upstream
|
||||||
|
// rule: a group with at least one public upstream is in the "immediate"
|
||||||
|
// category regardless of the other upstreams' routing, because the
|
||||||
|
// public one has no peer-startup excuse. Prevents public-DNS failures
|
||||||
|
// from being hidden behind a routed sibling.
|
||||||
|
func TestProjection_MixedGroupEmitsImmediately(t *testing.T) {
|
||||||
|
recorder := peer.NewRecorder("mgm")
|
||||||
|
sub := recorder.SubscribeToEvents()
|
||||||
|
t.Cleanup(func() { recorder.UnsubscribeFromEvents(sub) })
|
||||||
|
events := sub.Events()
|
||||||
|
|
||||||
|
public := netip.MustParseAddrPort("8.8.8.8:53")
|
||||||
|
overlay := netip.MustParseAddrPort("100.64.0.1:53")
|
||||||
|
overlayMap := route.HAMap{"overlay": {{Network: netip.MustParsePrefix("100.64.0.0/16")}}}
|
||||||
|
|
||||||
|
server := &DefaultServer{
|
||||||
|
ctx: context.Background(),
|
||||||
|
statusRecorder: recorder,
|
||||||
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
|
selectedRoutes: func() route.HAMap { return overlayMap },
|
||||||
|
activeRoutes: func() route.HAMap { return nil },
|
||||||
|
warningDelayBase: time.Hour,
|
||||||
|
}
|
||||||
|
group := &nbdns.NameServerGroup{
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{IP: public.Addr(), NSType: nbdns.UDPNameServerType, Port: int(public.Port())},
|
||||||
|
{IP: overlay.Addr(), NSType: nbdns.UDPNameServerType, Port: int(overlay.Port())},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
stub := &healthStubHandler{
|
||||||
|
health: map[netip.AddrPort]UpstreamHealth{
|
||||||
|
public: {LastFail: time.Now(), LastErr: "servfail"},
|
||||||
|
overlay: {LastFail: time.Now(), LastErr: "timeout"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: stub, priority: PriorityUpstream}
|
||||||
|
|
||||||
|
server.mux.Lock()
|
||||||
|
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
||||||
|
server.mux.Unlock()
|
||||||
|
server.refreshHealth()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case evt := <-events:
|
||||||
|
assert.Contains(t, evt.Message, "unreachable")
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("expected immediate warning because group contains a public upstream")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDNSLoopPrevention(t *testing.T) {
|
func TestDNSLoopPrevention(t *testing.T) {
|
||||||
wgInterface := &mocWGIface{}
|
wgInterface := &mocWGIface{}
|
||||||
service := NewServiceViaMemory(wgInterface)
|
service := NewServiceViaMemory(wgInterface)
|
||||||
@@ -2183,17 +2726,18 @@ func TestDNSLoopPrevention(t *testing.T) {
|
|||||||
|
|
||||||
if tt.expectedHandlers > 0 {
|
if tt.expectedHandlers > 0 {
|
||||||
handler := muxUpdates[0].handler.(*upstreamResolver)
|
handler := muxUpdates[0].handler.(*upstreamResolver)
|
||||||
assert.Len(t, handler.upstreamServers, len(tt.expectedServers))
|
flat := handler.flatUpstreams()
|
||||||
|
assert.Len(t, flat, len(tt.expectedServers))
|
||||||
|
|
||||||
if tt.shouldFilterOwnIP {
|
if tt.shouldFilterOwnIP {
|
||||||
for _, upstream := range handler.upstreamServers {
|
for _, upstream := range flat {
|
||||||
assert.NotEqual(t, dnsServerIP, upstream.Addr())
|
assert.NotEqual(t, dnsServerIP, upstream.Addr())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, expected := range tt.expectedServers {
|
for _, expected := range tt.expectedServers {
|
||||||
found := false
|
found := false
|
||||||
for _, upstream := range handler.upstreamServers {
|
for _, upstream := range flat {
|
||||||
if upstream.Addr() == expected {
|
if upstream.Addr() == expected {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -40,10 +41,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type systemdDbusConfigurator struct {
|
type systemdDbusConfigurator struct {
|
||||||
dbusLinkObject dbus.ObjectPath
|
dbusLinkObject dbus.ObjectPath
|
||||||
ifaceName string
|
ifaceName string
|
||||||
|
wgIndex int
|
||||||
|
origNameservers []netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
systemdDbusLinkDNSProperty = systemdDbusLinkInterface + ".DNS"
|
||||||
|
systemdDbusLinkDefaultRouteProperty = systemdDbusLinkInterface + ".DefaultRoute"
|
||||||
|
)
|
||||||
|
|
||||||
// the types below are based on dbus specification, each field is mapped to a dbus type
|
// the types below are based on dbus specification, each field is mapped to a dbus type
|
||||||
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
|
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
|
||||||
// see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html on resolve1 input types
|
// see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html on resolve1 input types
|
||||||
@@ -79,10 +87,145 @@ func newSystemdDbusConfigurator(wgInterface string) (*systemdDbusConfigurator, e
|
|||||||
|
|
||||||
log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index)
|
log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index)
|
||||||
|
|
||||||
return &systemdDbusConfigurator{
|
c := &systemdDbusConfigurator{
|
||||||
dbusLinkObject: dbus.ObjectPath(s),
|
dbusLinkObject: dbus.ObjectPath(s),
|
||||||
ifaceName: wgInterface,
|
ifaceName: wgInterface,
|
||||||
}, nil
|
wgIndex: iface.Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
origNameservers, err := c.captureOriginalNameservers()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
log.Warnf("capture original nameservers from systemd-resolved: %v", err)
|
||||||
|
case len(origNameservers) == 0:
|
||||||
|
log.Warnf("no original nameservers captured from systemd-resolved default-route links; DNS fallback will be empty")
|
||||||
|
default:
|
||||||
|
log.Debugf("captured %d original nameservers from systemd-resolved default-route links: %v", len(origNameservers), origNameservers)
|
||||||
|
}
|
||||||
|
c.origNameservers = origNameservers
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureOriginalNameservers reads per-link DNS from systemd-resolved for
|
||||||
|
// every default-route link except our own WG link. Non-default-route links
|
||||||
|
// (VPNs, docker bridges) are skipped because their upstreams wouldn't
|
||||||
|
// actually serve host queries.
|
||||||
|
func (s *systemdDbusConfigurator) captureOriginalNameservers() ([]netip.Addr, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list interfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[netip.Addr]struct{})
|
||||||
|
var out []netip.Addr
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if !s.isCandidateLink(iface) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
linkPath, err := getSystemdLinkPath(iface.Index)
|
||||||
|
if err != nil || !isSystemdLinkDefaultRoute(linkPath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, addr := range readSystemdLinkDNS(linkPath) {
|
||||||
|
addr = normalizeSystemdAddr(addr, iface.Name)
|
||||||
|
if !addr.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, dup := seen[addr]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[addr] = struct{}{}
|
||||||
|
out = append(out, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) isCandidateLink(iface net.Interface) bool {
|
||||||
|
if iface.Index == s.wgIndex {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeSystemdAddr unmaps v4-mapped-v6, drops unspecified, and reattaches
|
||||||
|
// the link's iface name as zone for link-local v6 (Link.DNS strips it).
|
||||||
|
// Returns the zero Addr to signal "skip this entry".
|
||||||
|
func normalizeSystemdAddr(addr netip.Addr, ifaceName string) netip.Addr {
|
||||||
|
addr = addr.Unmap()
|
||||||
|
if !addr.IsValid() || addr.IsUnspecified() {
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
if addr.IsLinkLocalUnicast() {
|
||||||
|
return addr.WithZone(ifaceName)
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemdLinkPath(ifIndex int) (dbus.ObjectPath, error) {
|
||||||
|
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("dbus resolve1: %w", err)
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
var p string
|
||||||
|
if err := obj.Call(systemdDbusGetLinkMethod, dbusDefaultFlag, int32(ifIndex)).Store(&p); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dbus.ObjectPath(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSystemdLinkDefaultRoute(linkPath dbus.ObjectPath) bool {
|
||||||
|
obj, closeConn, err := getDbusObject(systemdResolvedDest, linkPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
v, err := obj.GetProperty(systemdDbusLinkDefaultRouteProperty)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b, ok := v.Value().(bool)
|
||||||
|
return ok && b
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSystemdLinkDNS(linkPath dbus.ObjectPath) []netip.Addr {
|
||||||
|
obj, closeConn, err := getDbusObject(systemdResolvedDest, linkPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer closeConn()
|
||||||
|
v, err := obj.GetProperty(systemdDbusLinkDNSProperty)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, ok := v.Value().([][]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []netip.Addr
|
||||||
|
for _, entry := range entries {
|
||||||
|
if len(entry) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw, ok := entry[1].([]byte)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, ok := netip.AddrFromSlice(raw)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, addr)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemdDbusConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
|
return slices.Clone(s.origNameservers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemdDbusConfigurator) supportCustomPort() bool {
|
func (s *systemdDbusConfigurator) supportCustomPort() bool {
|
||||||
|
|||||||
@@ -1,3 +1,32 @@
|
|||||||
|
// Package dns implements the client-side DNS stack: listener/service on the
|
||||||
|
// peer's tunnel address, handler chain that routes questions by domain and
|
||||||
|
// priority, and upstream resolvers that forward what remains to configured
|
||||||
|
// nameservers.
|
||||||
|
//
|
||||||
|
// # Upstream resolution and the race model
|
||||||
|
//
|
||||||
|
// When two or more nameserver groups target the same domain, DefaultServer
|
||||||
|
// merges them into one upstream handler whose state is:
|
||||||
|
//
|
||||||
|
// upstreamResolverBase
|
||||||
|
// └── upstreamServers []upstreamRace // one entry per source NS group
|
||||||
|
// └── []netip.AddrPort // primary, fallback, ...
|
||||||
|
//
|
||||||
|
// Each source nameserver group contributes one upstreamRace. Within a race
|
||||||
|
// upstreams are tried in order: the next is used only on failure (timeout,
|
||||||
|
// SERVFAIL, REFUSED, no response). NXDOMAIN is a valid answer and stops
|
||||||
|
// the walk. When more than one race exists, ServeDNS fans out one
|
||||||
|
// goroutine per race and returns the first valid answer, cancelling the
|
||||||
|
// rest. A handler with a single race skips the fan-out.
|
||||||
|
//
|
||||||
|
// # Health projection
|
||||||
|
//
|
||||||
|
// Query outcomes are recorded per-upstream in UpstreamHealth. The server
|
||||||
|
// periodically merges these snapshots across handlers and projects them
|
||||||
|
// into peer.NSGroupState. There is no active probing: a group is marked
|
||||||
|
// unhealthy only when every seen upstream has a recent failure and none
|
||||||
|
// has a recent success. Healthy→unhealthy fires a single
|
||||||
|
// SystemEvent_WARNING; steady-state refreshes do not duplicate it.
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,11 +40,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
@@ -25,7 +51,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/dns/resutil"
|
"github.com/netbirdio/netbird/client/internal/dns/resutil"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns/types"
|
"github.com/netbirdio/netbird/client/internal/dns/types"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentMTU uint16 = iface.DefaultMTU
|
var currentMTU uint16 = iface.DefaultMTU
|
||||||
@@ -67,15 +94,17 @@ const (
|
|||||||
// Set longer than UpstreamTimeout to ensure context timeout takes precedence
|
// Set longer than UpstreamTimeout to ensure context timeout takes precedence
|
||||||
ClientTimeout = 5 * time.Second
|
ClientTimeout = 5 * time.Second
|
||||||
|
|
||||||
reactivatePeriod = 30 * time.Second
|
|
||||||
probeTimeout = 2 * time.Second
|
|
||||||
|
|
||||||
// ipv6HeaderSize + udpHeaderSize, used to derive the maximum DNS UDP
|
// ipv6HeaderSize + udpHeaderSize, used to derive the maximum DNS UDP
|
||||||
// payload from the tunnel MTU.
|
// payload from the tunnel MTU.
|
||||||
ipUDPHeaderSize = 60 + 8
|
ipUDPHeaderSize = 60 + 8
|
||||||
)
|
|
||||||
|
|
||||||
const testRecord = "com."
|
// raceMaxTotalTimeout caps the combined time spent walking all upstreams
|
||||||
|
// within one race, so a slow primary can't eat the whole race budget.
|
||||||
|
raceMaxTotalTimeout = 5 * time.Second
|
||||||
|
// raceMinPerUpstreamTimeout is the floor applied when dividing
|
||||||
|
// raceMaxTotalTimeout across upstreams within a race.
|
||||||
|
raceMinPerUpstreamTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
protoUDP = "udp"
|
protoUDP = "udp"
|
||||||
@@ -84,6 +113,69 @@ const (
|
|||||||
|
|
||||||
type dnsProtocolKey struct{}
|
type dnsProtocolKey struct{}
|
||||||
|
|
||||||
|
type upstreamProtocolKey struct{}
|
||||||
|
|
||||||
|
// upstreamProtocolResult holds the protocol used for the upstream exchange.
|
||||||
|
// Stored as a pointer in context so the exchange function can set it.
|
||||||
|
type upstreamProtocolResult struct {
|
||||||
|
protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
type upstreamClient interface {
|
||||||
|
exchange(ctx context.Context, upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpstreamResolver interface {
|
||||||
|
serveDNS(r *dns.Msg) (*dns.Msg, time.Duration, error)
|
||||||
|
upstreamExchange(upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstreamRace is an ordered list of upstreams derived from one configured
|
||||||
|
// nameserver group. Order matters: the first upstream is tried first, the
|
||||||
|
// second only on failure, and so on. Multiple upstreamRace values coexist
|
||||||
|
// inside one resolver when overlapping nameserver groups target the same
|
||||||
|
// domain; those races run in parallel and the first valid answer wins.
|
||||||
|
type upstreamRace []netip.AddrPort
|
||||||
|
|
||||||
|
// UpstreamHealth is the last query-path outcome for a single upstream,
|
||||||
|
// consumed by nameserver-group status projection.
|
||||||
|
type UpstreamHealth struct {
|
||||||
|
LastOk time.Time
|
||||||
|
LastFail time.Time
|
||||||
|
LastErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type upstreamResolverBase struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
upstreamClient upstreamClient
|
||||||
|
upstreamServers []upstreamRace
|
||||||
|
domain domain.Domain
|
||||||
|
upstreamTimeout time.Duration
|
||||||
|
|
||||||
|
healthMu sync.RWMutex
|
||||||
|
health map[netip.AddrPort]*UpstreamHealth
|
||||||
|
|
||||||
|
statusRecorder *peer.Status
|
||||||
|
// selectedRoutes returns the current set of client routes the admin
|
||||||
|
// has enabled. Called lazily from the query hot path when an upstream
|
||||||
|
// might need a tunnel-bound client (iOS) and from health projection.
|
||||||
|
selectedRoutes func() route.HAMap
|
||||||
|
}
|
||||||
|
|
||||||
|
type upstreamFailure struct {
|
||||||
|
upstream netip.AddrPort
|
||||||
|
reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
type raceResult struct {
|
||||||
|
msg *dns.Msg
|
||||||
|
upstream netip.AddrPort
|
||||||
|
protocol string
|
||||||
|
ede string
|
||||||
|
failures []upstreamFailure
|
||||||
|
}
|
||||||
|
|
||||||
// contextWithDNSProtocol stores the inbound DNS protocol ("udp" or "tcp") in context.
|
// contextWithDNSProtocol stores the inbound DNS protocol ("udp" or "tcp") in context.
|
||||||
func contextWithDNSProtocol(ctx context.Context, network string) context.Context {
|
func contextWithDNSProtocol(ctx context.Context, network string) context.Context {
|
||||||
return context.WithValue(ctx, dnsProtocolKey{}, network)
|
return context.WithValue(ctx, dnsProtocolKey{}, network)
|
||||||
@@ -100,16 +192,8 @@ func dnsProtocolFromContext(ctx context.Context) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type upstreamProtocolKey struct{}
|
// contextWithUpstreamProtocolResult stores a mutable result holder in the context.
|
||||||
|
func contextWithUpstreamProtocolResult(ctx context.Context) (context.Context, *upstreamProtocolResult) {
|
||||||
// upstreamProtocolResult holds the protocol used for the upstream exchange.
|
|
||||||
// Stored as a pointer in context so the exchange function can set it.
|
|
||||||
type upstreamProtocolResult struct {
|
|
||||||
protocol string
|
|
||||||
}
|
|
||||||
|
|
||||||
// contextWithupstreamProtocolResult stores a mutable result holder in the context.
|
|
||||||
func contextWithupstreamProtocolResult(ctx context.Context) (context.Context, *upstreamProtocolResult) {
|
|
||||||
r := &upstreamProtocolResult{}
|
r := &upstreamProtocolResult{}
|
||||||
return context.WithValue(ctx, upstreamProtocolKey{}, r), r
|
return context.WithValue(ctx, upstreamProtocolKey{}, r), r
|
||||||
}
|
}
|
||||||
@@ -124,67 +208,37 @@ func setUpstreamProtocol(ctx context.Context, protocol string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type upstreamClient interface {
|
func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, d domain.Domain) *upstreamResolverBase {
|
||||||
exchange(ctx context.Context, upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpstreamResolver interface {
|
|
||||||
serveDNS(r *dns.Msg) (*dns.Msg, time.Duration, error)
|
|
||||||
upstreamExchange(upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type upstreamResolverBase struct {
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
upstreamClient upstreamClient
|
|
||||||
upstreamServers []netip.AddrPort
|
|
||||||
domain string
|
|
||||||
disabled bool
|
|
||||||
successCount atomic.Int32
|
|
||||||
mutex sync.Mutex
|
|
||||||
reactivatePeriod time.Duration
|
|
||||||
upstreamTimeout time.Duration
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
deactivate func(error)
|
|
||||||
reactivate func()
|
|
||||||
statusRecorder *peer.Status
|
|
||||||
routeMatch func(netip.Addr) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type upstreamFailure struct {
|
|
||||||
upstream netip.AddrPort
|
|
||||||
reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, domain string) *upstreamResolverBase {
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
return &upstreamResolverBase{
|
return &upstreamResolverBase{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
domain: domain,
|
domain: d,
|
||||||
upstreamTimeout: UpstreamTimeout,
|
upstreamTimeout: UpstreamTimeout,
|
||||||
reactivatePeriod: reactivatePeriod,
|
statusRecorder: statusRecorder,
|
||||||
statusRecorder: statusRecorder,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a string representation of the upstream resolver
|
// String returns a string representation of the upstream resolver
|
||||||
func (u *upstreamResolverBase) String() string {
|
func (u *upstreamResolverBase) String() string {
|
||||||
return fmt.Sprintf("Upstream %s", u.upstreamServers)
|
return fmt.Sprintf("Upstream %s", u.flatUpstreams())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the unique handler ID
|
// ID returns the unique handler ID. Race groupings and within-race
|
||||||
|
// ordering are both part of the identity: [[A,B]] and [[A],[B]] query
|
||||||
|
// the same servers but with different semantics (serial fallback vs
|
||||||
|
// parallel race), so their handlers must not collide.
|
||||||
func (u *upstreamResolverBase) ID() types.HandlerID {
|
func (u *upstreamResolverBase) ID() types.HandlerID {
|
||||||
servers := slices.Clone(u.upstreamServers)
|
|
||||||
slices.SortFunc(servers, func(a, b netip.AddrPort) int { return a.Compare(b) })
|
|
||||||
|
|
||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
hash.Write([]byte(u.domain + ":"))
|
hash.Write([]byte(u.domain.PunycodeString() + ":"))
|
||||||
for _, s := range servers {
|
for _, race := range u.upstreamServers {
|
||||||
hash.Write([]byte(s.String()))
|
hash.Write([]byte("["))
|
||||||
hash.Write([]byte("|"))
|
for _, s := range race {
|
||||||
|
hash.Write([]byte(s.String()))
|
||||||
|
hash.Write([]byte("|"))
|
||||||
|
}
|
||||||
|
hash.Write([]byte("]"))
|
||||||
}
|
}
|
||||||
return types.HandlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8]))
|
return types.HandlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8]))
|
||||||
}
|
}
|
||||||
@@ -194,13 +248,31 @@ func (u *upstreamResolverBase) MatchSubdomains() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) Stop() {
|
func (u *upstreamResolverBase) Stop() {
|
||||||
log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers)
|
log.Debugf("stopping serving DNS for upstreams %s", u.flatUpstreams())
|
||||||
u.cancel()
|
u.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
u.mutex.Lock()
|
// flatUpstreams is for logging and ID hashing only, not for dispatch.
|
||||||
u.wg.Wait()
|
func (u *upstreamResolverBase) flatUpstreams() []netip.AddrPort {
|
||||||
u.mutex.Unlock()
|
var out []netip.AddrPort
|
||||||
|
for _, g := range u.upstreamServers {
|
||||||
|
out = append(out, g...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSelectedRoutes swaps the accessor used to classify overlay-routed
|
||||||
|
// upstreams. Called when route sources are wired after the handler was
|
||||||
|
// built (permanent / iOS constructors).
|
||||||
|
func (u *upstreamResolverBase) setSelectedRoutes(selected func() route.HAMap) {
|
||||||
|
u.selectedRoutes = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) addRace(servers []netip.AddrPort) {
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.upstreamServers = append(u.upstreamServers, slices.Clone(servers))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeDNS handles a DNS request
|
// ServeDNS handles a DNS request
|
||||||
@@ -242,82 +314,201 @@ func (u *upstreamResolverBase) prepareRequest(r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) tryUpstreamServers(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, logger *log.Entry) (bool, []upstreamFailure) {
|
func (u *upstreamResolverBase) tryUpstreamServers(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, logger *log.Entry) (bool, []upstreamFailure) {
|
||||||
timeout := u.upstreamTimeout
|
groups := u.upstreamServers
|
||||||
if len(u.upstreamServers) > 1 {
|
switch len(groups) {
|
||||||
maxTotal := 5 * time.Second
|
case 0:
|
||||||
minPerUpstream := 2 * time.Second
|
return false, nil
|
||||||
scaledTimeout := maxTotal / time.Duration(len(u.upstreamServers))
|
case 1:
|
||||||
if scaledTimeout > minPerUpstream {
|
return u.tryOnlyRace(ctx, w, r, groups[0], logger)
|
||||||
timeout = scaledTimeout
|
default:
|
||||||
} else {
|
return u.raceAll(ctx, w, r, groups, logger)
|
||||||
timeout = minPerUpstream
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) tryOnlyRace(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, group upstreamRace, logger *log.Entry) (bool, []upstreamFailure) {
|
||||||
|
res := u.tryRace(ctx, r, group)
|
||||||
|
if res.msg == nil {
|
||||||
|
return false, res.failures
|
||||||
|
}
|
||||||
|
if res.ede != "" {
|
||||||
|
resutil.SetMeta(w, "ede", res.ede)
|
||||||
|
}
|
||||||
|
u.writeSuccessResponse(w, res.msg, res.upstream, r.Question[0].Name, res.protocol, logger)
|
||||||
|
return true, res.failures
|
||||||
|
}
|
||||||
|
|
||||||
|
// raceAll runs one worker per group in parallel, taking the first valid
|
||||||
|
// answer and cancelling the rest.
|
||||||
|
func (u *upstreamResolverBase) raceAll(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, groups []upstreamRace, logger *log.Entry) (bool, []upstreamFailure) {
|
||||||
|
raceCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Buffer sized to len(groups) so workers never block on send, even
|
||||||
|
// after the coordinator has returned.
|
||||||
|
results := make(chan raceResult, len(groups))
|
||||||
|
for _, g := range groups {
|
||||||
|
// tryRace clones the request per attempt, so workers never share
|
||||||
|
// a *dns.Msg and concurrent EDNS0 mutations can't race.
|
||||||
|
go func(g upstreamRace) {
|
||||||
|
results <- u.tryRace(raceCtx, r, g)
|
||||||
|
}(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
var failures []upstreamFailure
|
var failures []upstreamFailure
|
||||||
for _, upstream := range u.upstreamServers {
|
for range groups {
|
||||||
if failure := u.queryUpstream(ctx, w, r, upstream, timeout, logger); failure != nil {
|
select {
|
||||||
failures = append(failures, *failure)
|
case res := <-results:
|
||||||
} else {
|
failures = append(failures, res.failures...)
|
||||||
return true, failures
|
if res.msg != nil {
|
||||||
|
if res.ede != "" {
|
||||||
|
resutil.SetMeta(w, "ede", res.ede)
|
||||||
|
}
|
||||||
|
u.writeSuccessResponse(w, res.msg, res.upstream, r.Question[0].Name, res.protocol, logger)
|
||||||
|
return true, failures
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, failures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, failures
|
return false, failures
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryUpstream queries a single upstream server. Returns nil on success, or failure info to try next upstream.
|
func (u *upstreamResolverBase) tryRace(ctx context.Context, r *dns.Msg, group upstreamRace) raceResult {
|
||||||
func (u *upstreamResolverBase) queryUpstream(parentCtx context.Context, w dns.ResponseWriter, r *dns.Msg, upstream netip.AddrPort, timeout time.Duration, logger *log.Entry) *upstreamFailure {
|
timeout := u.upstreamTimeout
|
||||||
var rm *dns.Msg
|
if len(group) > 1 {
|
||||||
var t time.Duration
|
// Cap the whole walk at raceMaxTotalTimeout: per-upstream timeouts
|
||||||
var err error
|
// still honor raceMinPerUpstreamTimeout as a floor for correctness
|
||||||
|
// on slow links, but the outer context ensures the combined walk
|
||||||
|
// cannot exceed the cap regardless of group size.
|
||||||
|
timeout = max(raceMaxTotalTimeout/time.Duration(len(group)), raceMinPerUpstreamTimeout)
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, raceMaxTotalTimeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
var failures []upstreamFailure
|
||||||
|
for _, upstream := range group {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return raceResult{failures: failures}
|
||||||
|
}
|
||||||
|
// Clone the request per attempt: the exchange path mutates EDNS0
|
||||||
|
// options in-place, so reusing the same *dns.Msg across sequential
|
||||||
|
// upstreams would carry those mutations (e.g. a reduced UDP size)
|
||||||
|
// into the next attempt.
|
||||||
|
res, failure := u.queryUpstream(ctx, r.Copy(), upstream, timeout)
|
||||||
|
if failure != nil {
|
||||||
|
failures = append(failures, *failure)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res.failures = failures
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return raceResult{failures: failures}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) queryUpstream(parentCtx context.Context, r *dns.Msg, upstream netip.AddrPort, timeout time.Duration) (raceResult, *upstreamFailure) {
|
||||||
|
ctx, cancel := context.WithTimeout(parentCtx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
ctx, upstreamProto := contextWithUpstreamProtocolResult(ctx)
|
||||||
|
|
||||||
// Advertise EDNS0 so the upstream may include Extended DNS Errors
|
// Advertise EDNS0 so the upstream may include Extended DNS Errors
|
||||||
// (RFC 8914) in failure responses; we use those to short-circuit
|
// (RFC 8914) in failure responses; we use those to short-circuit
|
||||||
// failover for definitive answers like DNSSEC validation failures.
|
// failover for definitive answers like DNSSEC validation failures.
|
||||||
// Operate on a copy so the inbound request is unchanged: a client that
|
// The caller already passed a per-attempt copy, so we can mutate r
|
||||||
// did not advertise EDNS0 must not see an OPT in the response.
|
// directly; hadEdns reflects the original client request's state and
|
||||||
|
// controls whether we strip the OPT from the response.
|
||||||
hadEdns := r.IsEdns0() != nil
|
hadEdns := r.IsEdns0() != nil
|
||||||
reqUp := r
|
|
||||||
if !hadEdns {
|
if !hadEdns {
|
||||||
reqUp = r.Copy()
|
r.SetEdns0(upstreamUDPSize(), false)
|
||||||
reqUp.SetEdns0(upstreamUDPSize(), false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var startTime time.Time
|
startTime := time.Now()
|
||||||
var upstreamProto *upstreamProtocolResult
|
rm, _, err := u.upstreamClient.exchange(ctx, upstream.String(), r)
|
||||||
func() {
|
|
||||||
ctx, cancel := context.WithTimeout(parentCtx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
ctx, upstreamProto = contextWithupstreamProtocolResult(ctx)
|
|
||||||
startTime = time.Now()
|
|
||||||
rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), reqUp)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u.handleUpstreamError(err, upstream, startTime)
|
// A parent cancellation (e.g., another race won and the coordinator
|
||||||
|
// cancelled the losers) is not an upstream failure. Check both the
|
||||||
|
// error chain and the parent context: a transport may surface the
|
||||||
|
// cancellation as a read/deadline error rather than context.Canceled.
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(parentCtx.Err(), context.Canceled) {
|
||||||
|
return raceResult{}, &upstreamFailure{upstream: upstream, reason: "canceled"}
|
||||||
|
}
|
||||||
|
failure := u.handleUpstreamError(err, upstream, startTime)
|
||||||
|
u.markUpstreamFail(upstream, failure.reason)
|
||||||
|
return raceResult{}, failure
|
||||||
}
|
}
|
||||||
|
|
||||||
if rm == nil || !rm.Response {
|
if rm == nil || !rm.Response {
|
||||||
return &upstreamFailure{upstream: upstream, reason: "no response"}
|
u.markUpstreamFail(upstream, "no response")
|
||||||
|
return raceResult{}, &upstreamFailure{upstream: upstream, reason: "no response"}
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := ""
|
||||||
|
if upstreamProto != nil {
|
||||||
|
proto = upstreamProto.protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if rm.Rcode == dns.RcodeServerFailure || rm.Rcode == dns.RcodeRefused {
|
if rm.Rcode == dns.RcodeServerFailure || rm.Rcode == dns.RcodeRefused {
|
||||||
if code, ok := nonRetryableEDE(rm); ok {
|
if code, ok := nonRetryableEDE(rm); ok {
|
||||||
resutil.SetMeta(w, "ede", edeName(code))
|
|
||||||
if !hadEdns {
|
if !hadEdns {
|
||||||
stripOPT(rm)
|
stripOPT(rm)
|
||||||
}
|
}
|
||||||
u.writeSuccessResponse(w, rm, upstream, r.Question[0].Name, t, upstreamProto, logger)
|
u.markUpstreamOk(upstream)
|
||||||
return nil
|
return raceResult{msg: rm, upstream: upstream, protocol: proto, ede: edeName(code)}, nil
|
||||||
}
|
}
|
||||||
return &upstreamFailure{upstream: upstream, reason: dns.RcodeToString[rm.Rcode]}
|
reason := dns.RcodeToString[rm.Rcode]
|
||||||
|
u.markUpstreamFail(upstream, reason)
|
||||||
|
return raceResult{}, &upstreamFailure{upstream: upstream, reason: reason}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hadEdns {
|
if !hadEdns {
|
||||||
stripOPT(rm)
|
stripOPT(rm)
|
||||||
}
|
}
|
||||||
u.writeSuccessResponse(w, rm, upstream, r.Question[0].Name, t, upstreamProto, logger)
|
|
||||||
return nil
|
u.markUpstreamOk(upstream)
|
||||||
|
return raceResult{msg: rm, upstream: upstream, protocol: proto}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// healthEntry returns the mutable health record for addr, lazily creating
|
||||||
|
// the map and the entry. Caller must hold u.healthMu.
|
||||||
|
func (u *upstreamResolverBase) healthEntry(addr netip.AddrPort) *UpstreamHealth {
|
||||||
|
if u.health == nil {
|
||||||
|
u.health = make(map[netip.AddrPort]*UpstreamHealth)
|
||||||
|
}
|
||||||
|
h := u.health[addr]
|
||||||
|
if h == nil {
|
||||||
|
h = &UpstreamHealth{}
|
||||||
|
u.health[addr] = h
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) markUpstreamOk(addr netip.AddrPort) {
|
||||||
|
u.healthMu.Lock()
|
||||||
|
defer u.healthMu.Unlock()
|
||||||
|
h := u.healthEntry(addr)
|
||||||
|
h.LastOk = time.Now()
|
||||||
|
h.LastFail = time.Time{}
|
||||||
|
h.LastErr = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) markUpstreamFail(addr netip.AddrPort, reason string) {
|
||||||
|
u.healthMu.Lock()
|
||||||
|
defer u.healthMu.Unlock()
|
||||||
|
h := u.healthEntry(addr)
|
||||||
|
h.LastFail = time.Now()
|
||||||
|
h.LastErr = reason
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpstreamHealth returns a snapshot of per-upstream query outcomes.
|
||||||
|
func (u *upstreamResolverBase) UpstreamHealth() map[netip.AddrPort]UpstreamHealth {
|
||||||
|
u.healthMu.RLock()
|
||||||
|
defer u.healthMu.RUnlock()
|
||||||
|
out := make(map[netip.AddrPort]UpstreamHealth, len(u.health))
|
||||||
|
for k, v := range u.health {
|
||||||
|
out[k] = *v
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// upstreamUDPSize returns the EDNS0 UDP buffer size we advertise to upstreams,
|
// upstreamUDPSize returns the EDNS0 UDP buffer size we advertise to upstreams,
|
||||||
@@ -358,12 +549,23 @@ func (u *upstreamResolverBase) handleUpstreamError(err error, upstream netip.Add
|
|||||||
return &upstreamFailure{upstream: upstream, reason: reason}
|
return &upstreamFailure{upstream: upstream, reason: reason}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) writeSuccessResponse(w dns.ResponseWriter, rm *dns.Msg, upstream netip.AddrPort, domain string, t time.Duration, upstreamProto *upstreamProtocolResult, logger *log.Entry) bool {
|
func (u *upstreamResolverBase) debugUpstreamTimeout(upstream netip.AddrPort) string {
|
||||||
u.successCount.Add(1)
|
if u.statusRecorder == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
peerInfo := findPeerForIP(upstream.Addr(), u.statusRecorder)
|
||||||
|
if peerInfo == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("(routes through NetBird peer %s)", FormatPeerStatus(peerInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) writeSuccessResponse(w dns.ResponseWriter, rm *dns.Msg, upstream netip.AddrPort, domain string, proto string, logger *log.Entry) {
|
||||||
resutil.SetMeta(w, "upstream", upstream.String())
|
resutil.SetMeta(w, "upstream", upstream.String())
|
||||||
if upstreamProto != nil && upstreamProto.protocol != "" {
|
if proto != "" {
|
||||||
resutil.SetMeta(w, "upstream_protocol", upstreamProto.protocol)
|
resutil.SetMeta(w, "upstream_protocol", proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear Zero bit from external responses to prevent upstream servers from
|
// Clear Zero bit from external responses to prevent upstream servers from
|
||||||
@@ -372,14 +574,11 @@ func (u *upstreamResolverBase) writeSuccessResponse(w dns.ResponseWriter, rm *dn
|
|||||||
|
|
||||||
if err := w.WriteMsg(rm); err != nil {
|
if err := w.WriteMsg(rm); err != nil {
|
||||||
logger.Errorf("failed to write DNS response for question domain=%s: %s", domain, err)
|
logger.Errorf("failed to write DNS response for question domain=%s: %s", domain, err)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) logUpstreamFailures(domain string, failures []upstreamFailure, succeeded bool, logger *log.Entry) {
|
func (u *upstreamResolverBase) logUpstreamFailures(domain string, failures []upstreamFailure, succeeded bool, logger *log.Entry) {
|
||||||
totalUpstreams := len(u.upstreamServers)
|
totalUpstreams := len(u.flatUpstreams())
|
||||||
failedCount := len(failures)
|
failedCount := len(failures)
|
||||||
failureSummary := formatFailures(failures)
|
failureSummary := formatFailures(failures)
|
||||||
|
|
||||||
@@ -434,119 +633,6 @@ func edeName(code uint16) string {
|
|||||||
return fmt.Sprintf("EDE %d", code)
|
return fmt.Sprintf("EDE %d", code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProbeAvailability tests all upstream servers simultaneously and
|
|
||||||
// disables the resolver if none work
|
|
||||||
func (u *upstreamResolverBase) ProbeAvailability(ctx context.Context) {
|
|
||||||
u.mutex.Lock()
|
|
||||||
defer u.mutex.Unlock()
|
|
||||||
|
|
||||||
// avoid probe if upstreams could resolve at least one query
|
|
||||||
if u.successCount.Load() > 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var success bool
|
|
||||||
var mu sync.Mutex
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
var errs *multierror.Error
|
|
||||||
for _, upstream := range u.upstreamServers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(upstream netip.AddrPort) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := u.testNameserver(u.ctx, ctx, upstream, 500*time.Millisecond)
|
|
||||||
if err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
errs = multierror.Append(errs, err)
|
|
||||||
mu.Unlock()
|
|
||||||
log.Warnf("probing upstream nameserver %s: %s", upstream, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
success = true
|
|
||||||
mu.Unlock()
|
|
||||||
}(upstream)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-u.ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// didn't find a working upstream server, let's disable and try later
|
|
||||||
if !success {
|
|
||||||
u.disable(errs.ErrorOrNil())
|
|
||||||
|
|
||||||
if u.statusRecorder == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
u.statusRecorder.PublishEvent(
|
|
||||||
proto.SystemEvent_WARNING,
|
|
||||||
proto.SystemEvent_DNS,
|
|
||||||
"All upstream servers failed (probe failed)",
|
|
||||||
"Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
|
|
||||||
map[string]string{"upstreams": u.upstreamServersString()},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitUntilResponse retries, in an exponential interval, querying the upstream servers until it gets a positive response
|
|
||||||
func (u *upstreamResolverBase) waitUntilResponse() {
|
|
||||||
exponentialBackOff := &backoff.ExponentialBackOff{
|
|
||||||
InitialInterval: 500 * time.Millisecond,
|
|
||||||
RandomizationFactor: 0.5,
|
|
||||||
Multiplier: 1.1,
|
|
||||||
MaxInterval: u.reactivatePeriod,
|
|
||||||
MaxElapsedTime: 0,
|
|
||||||
Stop: backoff.Stop,
|
|
||||||
Clock: backoff.SystemClock,
|
|
||||||
}
|
|
||||||
|
|
||||||
operation := func() error {
|
|
||||||
select {
|
|
||||||
case <-u.ctx.Done():
|
|
||||||
return backoff.Permanent(fmt.Errorf("exiting upstream retry loop for upstreams %s: parent context has been canceled", u.upstreamServersString()))
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, upstream := range u.upstreamServers {
|
|
||||||
if err := u.testNameserver(u.ctx, nil, upstream, probeTimeout); err != nil {
|
|
||||||
log.Tracef("upstream check for %s: %s", upstream, err)
|
|
||||||
} else {
|
|
||||||
// at least one upstream server is available, stop probing
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServersString(), exponentialBackOff.NextBackOff())
|
|
||||||
return fmt.Errorf("upstream check call error")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := backoff.Retry(operation, backoff.WithContext(exponentialBackOff, u.ctx))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
log.Debugf("upstream retry loop exited for upstreams %s", u.upstreamServersString())
|
|
||||||
} else {
|
|
||||||
log.Warnf("upstream retry loop exited for upstreams %s: %v", u.upstreamServersString(), err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServersString())
|
|
||||||
u.successCount.Add(1)
|
|
||||||
u.reactivate()
|
|
||||||
u.mutex.Lock()
|
|
||||||
u.disabled = false
|
|
||||||
u.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// isTimeout returns true if the given error is a network timeout error.
|
// isTimeout returns true if the given error is a network timeout error.
|
||||||
//
|
//
|
||||||
// Copied from k8s.io/apimachinery/pkg/util/net.IsTimeout
|
// Copied from k8s.io/apimachinery/pkg/util/net.IsTimeout
|
||||||
@@ -558,45 +644,6 @@ func isTimeout(err error) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) disable(err error) {
|
|
||||||
if u.disabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
|
|
||||||
u.successCount.Store(0)
|
|
||||||
u.deactivate(err)
|
|
||||||
u.disabled = true
|
|
||||||
u.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer u.wg.Done()
|
|
||||||
u.waitUntilResponse()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *upstreamResolverBase) upstreamServersString() string {
|
|
||||||
var servers []string
|
|
||||||
for _, server := range u.upstreamServers {
|
|
||||||
servers = append(servers, server.String())
|
|
||||||
}
|
|
||||||
return strings.Join(servers, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *upstreamResolverBase) testNameserver(baseCtx context.Context, externalCtx context.Context, server netip.AddrPort, timeout time.Duration) error {
|
|
||||||
mergedCtx, cancel := context.WithTimeout(baseCtx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if externalCtx != nil {
|
|
||||||
stop2 := context.AfterFunc(externalCtx, cancel)
|
|
||||||
defer stop2()
|
|
||||||
}
|
|
||||||
|
|
||||||
r := new(dns.Msg).SetQuestion(testRecord, dns.TypeSOA)
|
|
||||||
|
|
||||||
_, _, err := u.upstreamClient.exchange(mergedCtx, server.String(), r)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// clientUDPMaxSize returns the maximum UDP response size the client accepts.
|
// clientUDPMaxSize returns the maximum UDP response size the client accepts.
|
||||||
func clientUDPMaxSize(r *dns.Msg) int {
|
func clientUDPMaxSize(r *dns.Msg) int {
|
||||||
if opt := r.IsEdns0(); opt != nil {
|
if opt := r.IsEdns0(); opt != nil {
|
||||||
@@ -608,13 +655,10 @@ func clientUDPMaxSize(r *dns.Msg) int {
|
|||||||
// ExchangeWithFallback exchanges a DNS message with the upstream server.
|
// ExchangeWithFallback exchanges a DNS message with the upstream server.
|
||||||
// It first tries to use UDP, and if it is truncated, it falls back to TCP.
|
// It first tries to use UDP, and if it is truncated, it falls back to TCP.
|
||||||
// If the inbound request came over TCP (via context), it skips the UDP attempt.
|
// If the inbound request came over TCP (via context), it skips the UDP attempt.
|
||||||
// If the passed context is nil, this will use Exchange instead of ExchangeContext.
|
|
||||||
func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, upstream string) (*dns.Msg, time.Duration, error) {
|
func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, upstream string) (*dns.Msg, time.Duration, error) {
|
||||||
// If the request came in over TCP, go straight to TCP upstream.
|
// If the request came in over TCP, go straight to TCP upstream.
|
||||||
if dnsProtocolFromContext(ctx) == protoTCP {
|
if dnsProtocolFromContext(ctx) == protoTCP {
|
||||||
tcpClient := *client
|
rm, t, err := toTCPClient(client).ExchangeContext(ctx, r, upstream)
|
||||||
tcpClient.Net = protoTCP
|
|
||||||
rm, t, err := tcpClient.ExchangeContext(ctx, r, upstream)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, t, fmt.Errorf("with tcp: %w", err)
|
return nil, t, fmt.Errorf("with tcp: %w", err)
|
||||||
}
|
}
|
||||||
@@ -634,18 +678,7 @@ func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, u
|
|||||||
opt.SetUDPSize(maxUDPPayload)
|
opt.SetUDPSize(maxUDPPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
rm, t, err := client.ExchangeContext(ctx, r, upstream)
|
||||||
rm *dns.Msg
|
|
||||||
t time.Duration
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if ctx == nil {
|
|
||||||
rm, t, err = client.Exchange(r, upstream)
|
|
||||||
} else {
|
|
||||||
rm, t, err = client.ExchangeContext(ctx, r, upstream)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, t, fmt.Errorf("with udp: %w", err)
|
return nil, t, fmt.Errorf("with udp: %w", err)
|
||||||
}
|
}
|
||||||
@@ -659,15 +692,7 @@ func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, u
|
|||||||
// data than the client's buffer, we could truncate locally and skip
|
// data than the client's buffer, we could truncate locally and skip
|
||||||
// the TCP retry.
|
// the TCP retry.
|
||||||
|
|
||||||
tcpClient := *client
|
rm, t, err = toTCPClient(client).ExchangeContext(ctx, r, upstream)
|
||||||
tcpClient.Net = protoTCP
|
|
||||||
|
|
||||||
if ctx == nil {
|
|
||||||
rm, t, err = tcpClient.Exchange(r, upstream)
|
|
||||||
} else {
|
|
||||||
rm, t, err = tcpClient.ExchangeContext(ctx, r, upstream)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, t, fmt.Errorf("with tcp: %w", err)
|
return nil, t, fmt.Errorf("with tcp: %w", err)
|
||||||
}
|
}
|
||||||
@@ -681,6 +706,25 @@ func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, u
|
|||||||
return rm, t, nil
|
return rm, t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toTCPClient returns a copy of c configured for TCP. If c's Dialer has a
|
||||||
|
// *net.UDPAddr bound as LocalAddr (iOS does this to keep the source IP on
|
||||||
|
// the tunnel interface), it is converted to the equivalent *net.TCPAddr
|
||||||
|
// so net.Dialer doesn't reject the TCP dial with "mismatched local
|
||||||
|
// address type".
|
||||||
|
func toTCPClient(c *dns.Client) *dns.Client {
|
||||||
|
tcp := *c
|
||||||
|
tcp.Net = protoTCP
|
||||||
|
if tcp.Dialer == nil {
|
||||||
|
return &tcp
|
||||||
|
}
|
||||||
|
d := *tcp.Dialer
|
||||||
|
if ua, ok := d.LocalAddr.(*net.UDPAddr); ok {
|
||||||
|
d.LocalAddr = &net.TCPAddr{IP: ua.IP, Port: ua.Port, Zone: ua.Zone}
|
||||||
|
}
|
||||||
|
tcp.Dialer = &d
|
||||||
|
return &tcp
|
||||||
|
}
|
||||||
|
|
||||||
// ExchangeWithNetstack performs a DNS exchange using netstack for dialing.
|
// ExchangeWithNetstack performs a DNS exchange using netstack for dialing.
|
||||||
// This is needed when netstack is enabled to reach peer IPs through the tunnel.
|
// This is needed when netstack is enabled to reach peer IPs through the tunnel.
|
||||||
func ExchangeWithNetstack(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upstream string) (*dns.Msg, error) {
|
func ExchangeWithNetstack(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upstream string) (*dns.Msg, error) {
|
||||||
@@ -822,15 +866,36 @@ func findPeerForIP(ip netip.Addr, statusRecorder *peer.Status) *peer.State {
|
|||||||
return bestMatch
|
return bestMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) debugUpstreamTimeout(upstream netip.AddrPort) string {
|
// haMapRouteCount returns the total number of routes across all HA
|
||||||
if u.statusRecorder == nil {
|
// groups in the map. route.HAMap is keyed by HAUniqueID with slices of
|
||||||
return ""
|
// routes per key, so len(hm) is the number of HA groups, not routes.
|
||||||
|
func haMapRouteCount(hm route.HAMap) int {
|
||||||
|
total := 0
|
||||||
|
for _, routes := range hm {
|
||||||
|
total += len(routes)
|
||||||
}
|
}
|
||||||
|
return total
|
||||||
peerInfo := findPeerForIP(upstream.Addr(), u.statusRecorder)
|
}
|
||||||
if peerInfo == nil {
|
|
||||||
return ""
|
// haMapContains checks whether ip is covered by any concrete prefix in
|
||||||
}
|
// the HA map. haveDynamic is reported separately: dynamic (domain-based)
|
||||||
|
// routes carry a placeholder Network that can't be prefix-checked, so we
|
||||||
return fmt.Sprintf("(routes through NetBird peer %s)", FormatPeerStatus(peerInfo))
|
// can't know at this point whether ip is reached through one. Callers
|
||||||
|
// decide how to interpret the unknown: health projection treats it as
|
||||||
|
// "possibly routed" to avoid emitting false-positive warnings during
|
||||||
|
// startup, while iOS dial selection requires a concrete match before
|
||||||
|
// binding to the tunnel.
|
||||||
|
func haMapContains(hm route.HAMap, ip netip.Addr) (matched, haveDynamic bool) {
|
||||||
|
for _, routes := range hm {
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.IsDynamic() {
|
||||||
|
haveDynamic = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.Network.Contains(ip) {
|
||||||
|
return true, haveDynamic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, haveDynamic
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type upstreamResolver struct {
|
type upstreamResolver struct {
|
||||||
@@ -26,9 +27,9 @@ func newUpstreamResolver(
|
|||||||
_ WGIface,
|
_ WGIface,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
hostsDNSHolder *hostsDNSHolder,
|
hostsDNSHolder *hostsDNSHolder,
|
||||||
domain string,
|
d domain.Domain,
|
||||||
) (*upstreamResolver, error) {
|
) (*upstreamResolver, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, d)
|
||||||
c := &upstreamResolver{
|
c := &upstreamResolver{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
hostsDNSHolder: hostsDNSHolder,
|
hostsDNSHolder: hostsDNSHolder,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type upstreamResolver struct {
|
type upstreamResolver struct {
|
||||||
@@ -24,9 +25,9 @@ func newUpstreamResolver(
|
|||||||
wgIface WGIface,
|
wgIface WGIface,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
_ *hostsDNSHolder,
|
_ *hostsDNSHolder,
|
||||||
domain string,
|
d domain.Domain,
|
||||||
) (*upstreamResolver, error) {
|
) (*upstreamResolver, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, d)
|
||||||
nonIOS := &upstreamResolver{
|
nonIOS := &upstreamResolver{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
nsNet: wgIface.GetNet(),
|
nsNet: wgIface.GetNet(),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type upstreamResolverIOS struct {
|
type upstreamResolverIOS struct {
|
||||||
@@ -27,9 +28,9 @@ func newUpstreamResolver(
|
|||||||
wgIface WGIface,
|
wgIface WGIface,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
_ *hostsDNSHolder,
|
_ *hostsDNSHolder,
|
||||||
domain string,
|
d domain.Domain,
|
||||||
) (*upstreamResolverIOS, error) {
|
) (*upstreamResolverIOS, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, d)
|
||||||
|
|
||||||
ios := &upstreamResolverIOS{
|
ios := &upstreamResolverIOS{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
@@ -62,9 +63,16 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
|
|||||||
upstreamIP = upstreamIP.Unmap()
|
upstreamIP = upstreamIP.Unmap()
|
||||||
}
|
}
|
||||||
addr := u.wgIface.Address()
|
addr := u.wgIface.Address()
|
||||||
|
var routed bool
|
||||||
|
if u.selectedRoutes != nil {
|
||||||
|
// Only a concrete prefix match binds to the tunnel: dialing
|
||||||
|
// through a private client for an upstream we can't prove is
|
||||||
|
// routed would break public resolvers.
|
||||||
|
routed, _ = haMapContains(u.selectedRoutes(), upstreamIP)
|
||||||
|
}
|
||||||
needsPrivate := addr.Network.Contains(upstreamIP) ||
|
needsPrivate := addr.Network.Contains(upstreamIP) ||
|
||||||
addr.IPv6Net.Contains(upstreamIP) ||
|
addr.IPv6Net.Contains(upstreamIP) ||
|
||||||
(u.routeMatch != nil && u.routeMatch(upstreamIP))
|
routed
|
||||||
if needsPrivate {
|
if needsPrivate {
|
||||||
log.Debugf("using private client to query %s via upstream %s", r.Question[0].Name, upstream)
|
log.Debugf("using private client to query %s via upstream %s", r.Question[0].Name, upstream)
|
||||||
client, err = GetClientPrivate(u.wgIface, upstreamIP, timeout)
|
client, err = GetClientPrivate(u.wgIface, upstreamIP, timeout)
|
||||||
@@ -73,8 +81,7 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cannot use client.ExchangeContext because it overwrites our Dialer
|
return ExchangeWithFallback(ctx, client, r, upstream)
|
||||||
return ExchangeWithFallback(nil, client, r, upstream)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientPrivate returns a new DNS client bound to the local IP of the Netbird interface.
|
// GetClientPrivate returns a new DNS client bound to the local IP of the Netbird interface.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
servers = append(servers, netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()))
|
servers = append(servers, netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolver.upstreamServers = servers
|
resolver.addRace(servers)
|
||||||
resolver.upstreamTimeout = testCase.timeout
|
resolver.upstreamTimeout = testCase.timeout
|
||||||
if testCase.cancelCTX {
|
if testCase.cancelCTX {
|
||||||
cancel()
|
cancel()
|
||||||
@@ -132,20 +133,10 @@ func (m *mockNetstackProvider) GetInterfaceGUIDString() (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockUpstreamResolver struct {
|
|
||||||
r *dns.Msg
|
|
||||||
rtt time.Duration
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// exchange mock implementation of exchange from upstreamResolver
|
|
||||||
func (c mockUpstreamResolver) exchange(_ context.Context, _ string, _ *dns.Msg) (*dns.Msg, time.Duration, error) {
|
|
||||||
return c.r, c.rtt, c.err
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockUpstreamResponse struct {
|
type mockUpstreamResponse struct {
|
||||||
msg *dns.Msg
|
msg *dns.Msg
|
||||||
err error
|
err error
|
||||||
|
delay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockUpstreamResolverPerServer struct {
|
type mockUpstreamResolverPerServer struct {
|
||||||
@@ -153,63 +144,19 @@ type mockUpstreamResolverPerServer struct {
|
|||||||
rtt time.Duration
|
rtt time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c mockUpstreamResolverPerServer) exchange(_ context.Context, upstream string, _ *dns.Msg) (*dns.Msg, time.Duration, error) {
|
func (c mockUpstreamResolverPerServer) exchange(ctx context.Context, upstream string, _ *dns.Msg) (*dns.Msg, time.Duration, error) {
|
||||||
if r, ok := c.responses[upstream]; ok {
|
r, ok := c.responses[upstream]
|
||||||
return r.msg, c.rtt, r.err
|
if !ok {
|
||||||
|
return nil, c.rtt, fmt.Errorf("no mock response for %s", upstream)
|
||||||
}
|
}
|
||||||
return nil, c.rtt, fmt.Errorf("no mock response for %s", upstream)
|
if r.delay > 0 {
|
||||||
}
|
select {
|
||||||
|
case <-time.After(r.delay):
|
||||||
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
|
case <-ctx.Done():
|
||||||
mockClient := &mockUpstreamResolver{
|
return nil, c.rtt, ctx.Err()
|
||||||
err: dns.ErrTime,
|
}
|
||||||
r: new(dns.Msg),
|
|
||||||
rtt: time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolver := &upstreamResolverBase{
|
|
||||||
ctx: context.TODO(),
|
|
||||||
upstreamClient: mockClient,
|
|
||||||
upstreamTimeout: UpstreamTimeout,
|
|
||||||
reactivatePeriod: time.Microsecond * 100,
|
|
||||||
}
|
|
||||||
addrPort, _ := netip.ParseAddrPort("0.0.0.0:1") // Use valid port for parsing, test will still fail on connection
|
|
||||||
resolver.upstreamServers = []netip.AddrPort{netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())}
|
|
||||||
|
|
||||||
failed := false
|
|
||||||
resolver.deactivate = func(error) {
|
|
||||||
failed = true
|
|
||||||
// After deactivation, make the mock client work again
|
|
||||||
mockClient.err = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reactivated := false
|
|
||||||
resolver.reactivate = func() {
|
|
||||||
reactivated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resolver.ProbeAvailability(context.TODO())
|
|
||||||
|
|
||||||
if !failed {
|
|
||||||
t.Errorf("expected that resolving was deactivated")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resolver.disabled {
|
|
||||||
t.Errorf("resolver should be Disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
|
|
||||||
if !reactivated {
|
|
||||||
t.Errorf("expected that resolving was reactivated")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if resolver.disabled {
|
|
||||||
t.Errorf("should be enabled")
|
|
||||||
}
|
}
|
||||||
|
return r.msg, c.rtt, r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpstreamResolver_Failover(t *testing.T) {
|
func TestUpstreamResolver_Failover(t *testing.T) {
|
||||||
@@ -339,9 +286,9 @@ func TestUpstreamResolver_Failover(t *testing.T) {
|
|||||||
resolver := &upstreamResolverBase{
|
resolver := &upstreamResolverBase{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
upstreamClient: trackingClient,
|
upstreamClient: trackingClient,
|
||||||
upstreamServers: []netip.AddrPort{upstream1, upstream2},
|
|
||||||
upstreamTimeout: UpstreamTimeout,
|
upstreamTimeout: UpstreamTimeout,
|
||||||
}
|
}
|
||||||
|
resolver.addRace([]netip.AddrPort{upstream1, upstream2})
|
||||||
|
|
||||||
var responseMSG *dns.Msg
|
var responseMSG *dns.Msg
|
||||||
responseWriter := &test.MockResponseWriter{
|
responseWriter := &test.MockResponseWriter{
|
||||||
@@ -421,9 +368,9 @@ func TestUpstreamResolver_SingleUpstreamFailure(t *testing.T) {
|
|||||||
resolver := &upstreamResolverBase{
|
resolver := &upstreamResolverBase{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
upstreamClient: mockClient,
|
upstreamClient: mockClient,
|
||||||
upstreamServers: []netip.AddrPort{upstream},
|
|
||||||
upstreamTimeout: UpstreamTimeout,
|
upstreamTimeout: UpstreamTimeout,
|
||||||
}
|
}
|
||||||
|
resolver.addRace([]netip.AddrPort{upstream})
|
||||||
|
|
||||||
var responseMSG *dns.Msg
|
var responseMSG *dns.Msg
|
||||||
responseWriter := &test.MockResponseWriter{
|
responseWriter := &test.MockResponseWriter{
|
||||||
@@ -440,6 +387,136 @@ func TestUpstreamResolver_SingleUpstreamFailure(t *testing.T) {
|
|||||||
assert.Equal(t, dns.RcodeServerFailure, responseMSG.Rcode, "single upstream SERVFAIL should return SERVFAIL")
|
assert.Equal(t, dns.RcodeServerFailure, responseMSG.Rcode, "single upstream SERVFAIL should return SERVFAIL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestUpstreamResolver_RaceAcrossGroups covers two nameserver groups
|
||||||
|
// configured for the same domain, with one broken group. The merge+race
|
||||||
|
// path should answer as fast as the working group and not pay the timeout
|
||||||
|
// of the broken one on every query.
|
||||||
|
func TestUpstreamResolver_RaceAcrossGroups(t *testing.T) {
|
||||||
|
broken := netip.MustParseAddrPort("192.0.2.1:53")
|
||||||
|
working := netip.MustParseAddrPort("192.0.2.2:53")
|
||||||
|
successAnswer := "192.0.2.100"
|
||||||
|
timeoutErr := &net.OpError{Op: "read", Err: fmt.Errorf("i/o timeout")}
|
||||||
|
|
||||||
|
mockClient := &mockUpstreamResolverPerServer{
|
||||||
|
responses: map[string]mockUpstreamResponse{
|
||||||
|
// Force the broken upstream to only unblock via timeout /
|
||||||
|
// cancellation so the assertion below can't pass if races
|
||||||
|
// were run serially.
|
||||||
|
broken.String(): {err: timeoutErr, delay: 500 * time.Millisecond},
|
||||||
|
working.String(): {msg: buildMockResponse(dns.RcodeSuccess, successAnswer)},
|
||||||
|
},
|
||||||
|
rtt: time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resolver := &upstreamResolverBase{
|
||||||
|
ctx: ctx,
|
||||||
|
upstreamClient: mockClient,
|
||||||
|
upstreamTimeout: 250 * time.Millisecond,
|
||||||
|
}
|
||||||
|
resolver.addRace([]netip.AddrPort{broken})
|
||||||
|
resolver.addRace([]netip.AddrPort{working})
|
||||||
|
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
inputMSG := new(dns.Msg).SetQuestion("example.com.", dns.TypeA)
|
||||||
|
start := time.Now()
|
||||||
|
resolver.ServeDNS(responseWriter, inputMSG)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.NotNil(t, responseMSG, "should write a response")
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, responseMSG.Rcode)
|
||||||
|
require.NotEmpty(t, responseMSG.Answer)
|
||||||
|
assert.Contains(t, responseMSG.Answer[0].String(), successAnswer)
|
||||||
|
// Working group answers in a single RTT; the broken group's
|
||||||
|
// timeout (100ms) must not block the response.
|
||||||
|
assert.Less(t, elapsed, 100*time.Millisecond, "race must not wait for broken group's timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpstreamResolver_AllGroupsFail checks that when every group fails the
|
||||||
|
// resolver returns SERVFAIL rather than leaking a partial response.
|
||||||
|
func TestUpstreamResolver_AllGroupsFail(t *testing.T) {
|
||||||
|
a := netip.MustParseAddrPort("192.0.2.1:53")
|
||||||
|
b := netip.MustParseAddrPort("192.0.2.2:53")
|
||||||
|
|
||||||
|
mockClient := &mockUpstreamResolverPerServer{
|
||||||
|
responses: map[string]mockUpstreamResponse{
|
||||||
|
a.String(): {msg: buildMockResponse(dns.RcodeServerFailure, "")},
|
||||||
|
b.String(): {msg: buildMockResponse(dns.RcodeServerFailure, "")},
|
||||||
|
},
|
||||||
|
rtt: time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resolver := &upstreamResolverBase{
|
||||||
|
ctx: ctx,
|
||||||
|
upstreamClient: mockClient,
|
||||||
|
upstreamTimeout: UpstreamTimeout,
|
||||||
|
}
|
||||||
|
resolver.addRace([]netip.AddrPort{a})
|
||||||
|
resolver.addRace([]netip.AddrPort{b})
|
||||||
|
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.ServeDNS(responseWriter, new(dns.Msg).SetQuestion("example.com.", dns.TypeA))
|
||||||
|
require.NotNil(t, responseMSG)
|
||||||
|
assert.Equal(t, dns.RcodeServerFailure, responseMSG.Rcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpstreamResolver_HealthTracking verifies that query-path results are
|
||||||
|
// recorded into per-upstream health, which is what projects back to
|
||||||
|
// NSGroupState for status reporting.
|
||||||
|
func TestUpstreamResolver_HealthTracking(t *testing.T) {
|
||||||
|
ok := netip.MustParseAddrPort("192.0.2.10:53")
|
||||||
|
bad := netip.MustParseAddrPort("192.0.2.11:53")
|
||||||
|
|
||||||
|
mockClient := &mockUpstreamResolverPerServer{
|
||||||
|
responses: map[string]mockUpstreamResponse{
|
||||||
|
ok.String(): {msg: buildMockResponse(dns.RcodeSuccess, "192.0.2.100")},
|
||||||
|
bad.String(): {msg: buildMockResponse(dns.RcodeServerFailure, "")},
|
||||||
|
},
|
||||||
|
rtt: time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resolver := &upstreamResolverBase{
|
||||||
|
ctx: ctx,
|
||||||
|
upstreamClient: mockClient,
|
||||||
|
upstreamTimeout: UpstreamTimeout,
|
||||||
|
}
|
||||||
|
resolver.addRace([]netip.AddrPort{ok, bad})
|
||||||
|
|
||||||
|
responseWriter := &test.MockResponseWriter{WriteMsgFunc: func(m *dns.Msg) error { return nil }}
|
||||||
|
resolver.ServeDNS(responseWriter, new(dns.Msg).SetQuestion("example.com.", dns.TypeA))
|
||||||
|
|
||||||
|
health := resolver.UpstreamHealth()
|
||||||
|
require.Contains(t, health, ok)
|
||||||
|
assert.False(t, health[ok].LastOk.IsZero(), "ok upstream should have LastOk set")
|
||||||
|
assert.Empty(t, health[ok].LastErr)
|
||||||
|
|
||||||
|
// bad upstream was never tried because ok answered first; its health
|
||||||
|
// should remain unset.
|
||||||
|
assert.NotContains(t, health, bad, "sibling upstream should not be queried when primary answers")
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatFailures(t *testing.T) {
|
func TestFormatFailures(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -665,10 +742,10 @@ func TestExchangeWithFallback_EDNS0Capped(t *testing.T) {
|
|||||||
// Verify that a client EDNS0 larger than our MTU-derived limit gets
|
// Verify that a client EDNS0 larger than our MTU-derived limit gets
|
||||||
// capped in the outgoing request so the upstream doesn't send a
|
// capped in the outgoing request so the upstream doesn't send a
|
||||||
// response larger than our read buffer.
|
// response larger than our read buffer.
|
||||||
var receivedUDPSize uint16
|
var receivedUDPSize atomic.Uint32
|
||||||
udpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
udpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
if opt := r.IsEdns0(); opt != nil {
|
if opt := r.IsEdns0(); opt != nil {
|
||||||
receivedUDPSize = opt.UDPSize()
|
receivedUDPSize.Store(uint32(opt.UDPSize()))
|
||||||
}
|
}
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
@@ -699,7 +776,7 @@ func TestExchangeWithFallback_EDNS0Capped(t *testing.T) {
|
|||||||
require.NotNil(t, rm)
|
require.NotNil(t, rm)
|
||||||
|
|
||||||
expectedMax := uint16(currentMTU - ipUDPHeaderSize)
|
expectedMax := uint16(currentMTU - ipUDPHeaderSize)
|
||||||
assert.Equal(t, expectedMax, receivedUDPSize,
|
assert.Equal(t, expectedMax, uint16(receivedUDPSize.Load()),
|
||||||
"upstream should see capped EDNS0, not the client's 4096")
|
"upstream should see capped EDNS0, not the client's 4096")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,7 +951,7 @@ func TestUpstreamResolver_NonRetryableEDEShortCircuits(t *testing.T) {
|
|||||||
resolver := &upstreamResolverBase{
|
resolver := &upstreamResolverBase{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
upstreamClient: tracking,
|
upstreamClient: tracking,
|
||||||
upstreamServers: []netip.AddrPort{upstream1, upstream2},
|
upstreamServers: []upstreamRace{{upstream1, upstream2}},
|
||||||
upstreamTimeout: UpstreamTimeout,
|
upstreamTimeout: UpstreamTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/client/firewall"
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
@@ -54,8 +53,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/relay"
|
"github.com/netbirdio/netbird/client/internal/relay"
|
||||||
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/syncstore"
|
||||||
"github.com/netbirdio/netbird/client/internal/updater"
|
"github.com/netbirdio/netbird/client/internal/updater"
|
||||||
"github.com/netbirdio/netbird/client/jobexec"
|
"github.com/netbirdio/netbird/client/jobexec"
|
||||||
cProto "github.com/netbirdio/netbird/client/proto"
|
cProto "github.com/netbirdio/netbird/client/proto"
|
||||||
@@ -65,13 +64,13 @@ import (
|
|||||||
mgm "github.com/netbirdio/netbird/shared/management/client"
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
"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/shared/netiputil"
|
|
||||||
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
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"
|
"github.com/netbirdio/netbird/util/capture"
|
||||||
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -148,6 +147,10 @@ type EngineConfig struct {
|
|||||||
|
|
||||||
LogPath string
|
LogPath string
|
||||||
TempDir string
|
TempDir string
|
||||||
|
|
||||||
|
// StateDir is the directory holding the state file. The sync response
|
||||||
|
// (network map) is serialized here on platforms that persist it to disk.
|
||||||
|
StateDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// EngineServices holds the external service dependencies required by the Engine.
|
// EngineServices holds the external service dependencies required by the Engine.
|
||||||
@@ -226,11 +229,16 @@ type Engine struct {
|
|||||||
|
|
||||||
afpacketCapture *capture.AFPacketCapture
|
afpacketCapture *capture.AFPacketCapture
|
||||||
|
|
||||||
// Sync response persistence (protected by syncRespMux)
|
// Sync response persistence (protected by syncRespMux).
|
||||||
syncRespMux sync.RWMutex
|
// syncStore is nil unless persistence has been enabled; its presence is
|
||||||
persistSyncResponse bool
|
// what marks persistence as active. The backend (disk or memory) is
|
||||||
latestSyncResponse *mgmProto.SyncResponse
|
// selected per-platform; see the syncstore package. syncStoreDir is where
|
||||||
flowManager nftypes.FlowManager
|
// a disk-backed store serializes to.
|
||||||
|
syncRespMux sync.RWMutex
|
||||||
|
syncStore syncstore.Store
|
||||||
|
syncStoreDir string
|
||||||
|
|
||||||
|
flowManager nftypes.FlowManager
|
||||||
|
|
||||||
// auto-update
|
// auto-update
|
||||||
updateManager *updater.Manager
|
updateManager *updater.Manager
|
||||||
@@ -292,6 +300,7 @@ func NewEngine(
|
|||||||
jobExecutor: jobexec.NewExecutor(),
|
jobExecutor: jobexec.NewExecutor(),
|
||||||
clientMetrics: services.ClientMetrics,
|
clientMetrics: services.ClientMetrics,
|
||||||
updateManager: services.UpdateManager,
|
updateManager: services.UpdateManager,
|
||||||
|
syncStoreDir: config.StateDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String())
|
log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String())
|
||||||
@@ -512,16 +521,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
|
|
||||||
e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)
|
e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)
|
||||||
|
|
||||||
e.dnsServer.SetRouteChecker(func(ip netip.Addr) bool {
|
e.dnsServer.SetRouteSources(e.routeManager.GetSelectedClientRoutes, e.routeManager.GetActiveClientRoutes)
|
||||||
for _, routes := range e.routeManager.GetSelectedClientRoutes() {
|
|
||||||
for _, r := range routes {
|
|
||||||
if r.Network.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = e.wgInterfaceCreate(); err != nil {
|
if err = e.wgInterfaceCreate(); err != nil {
|
||||||
log.Errorf("failed creating tunnel interface %s: [%s]", e.config.WgIfaceName, err.Error())
|
log.Errorf("failed creating tunnel interface %s: [%s]", e.config.WgIfaceName, err.Error())
|
||||||
@@ -529,6 +529,10 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
return fmt.Errorf("create wg interface: %w", err)
|
return fmt.Errorf("create wg interface: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filteredDevice := e.wgInterface.GetDevice(); filteredDevice != nil {
|
||||||
|
filteredDevice.SetPanicHandler(e.triggerClientRestart)
|
||||||
|
}
|
||||||
|
|
||||||
if err := e.createFirewall(); err != nil {
|
if err := e.createFirewall(); err != nil {
|
||||||
e.close()
|
e.close()
|
||||||
return err
|
return err
|
||||||
@@ -878,63 +882,25 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate)
|
e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.GetNetbirdConfig() != nil {
|
if err := e.updateNetbirdConfig(update.GetNetbirdConfig()); err != nil {
|
||||||
wCfg := update.GetNetbirdConfig()
|
return err
|
||||||
err := e.updateTURNs(wCfg.GetTurns())
|
}
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("update TURNs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.updateSTUNs(wCfg.GetStuns())
|
// Posture checks are bound to the network map presence:
|
||||||
if err != nil {
|
// NetworkMap != nil, checks present -> apply the received checks
|
||||||
return fmt.Errorf("update STUNs: %w", err)
|
// NetworkMap != nil, checks nil -> posture checks were removed, clear them
|
||||||
}
|
// NetworkMap == nil -> config-only update (e.g. relay token rotation),
|
||||||
|
// leave the previously applied checks untouched
|
||||||
var stunTurn []*stun.URI
|
nm := update.GetNetworkMap()
|
||||||
stunTurn = append(stunTurn, e.STUNs...)
|
if nm == nil {
|
||||||
stunTurn = append(stunTurn, e.TURNs...)
|
return nil
|
||||||
e.stunTurn.Store(stunTurn)
|
|
||||||
|
|
||||||
err = e.handleRelayUpdate(wCfg.GetRelay())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.handleFlowUpdate(wCfg.GetFlow())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("handle the flow configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
|
||||||
log.Warnf("Failed to update DNS server config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo update signal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := e.updateChecksIfNew(update.Checks); err != nil {
|
if err := e.updateChecksIfNew(update.Checks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nm := update.GetNetworkMap()
|
e.persistSyncResponse(update)
|
||||||
if nm == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist sync response under the dedicated lock (syncRespMux), not under syncMsgMux.
|
|
||||||
// Read the storage-enabled flag under the syncRespMux too.
|
|
||||||
e.syncRespMux.RLock()
|
|
||||||
enabled := e.persistSyncResponse
|
|
||||||
e.syncRespMux.RUnlock()
|
|
||||||
|
|
||||||
// Store sync response if persistence is enabled
|
|
||||||
if enabled {
|
|
||||||
e.syncRespMux.Lock()
|
|
||||||
e.latestSyncResponse = update
|
|
||||||
e.syncRespMux.Unlock()
|
|
||||||
|
|
||||||
log.Debugf("sync response persisted with serial %d", nm.GetSerial())
|
|
||||||
}
|
|
||||||
|
|
||||||
// only apply new changes and ignore old ones
|
// only apply new changes and ignore old ones
|
||||||
if err := e.updateNetworkMap(nm); err != nil {
|
if err := e.updateNetworkMap(nm); err != nil {
|
||||||
@@ -946,6 +912,64 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateNetbirdConfig applies the management-provided NetBird configuration:
|
||||||
|
// STUN/TURN and relay servers, flow logging and DNS settings. A nil config is a no-op,
|
||||||
|
// which is the case for sync updates carrying only a network map.
|
||||||
|
func (e *Engine) updateNetbirdConfig(wCfg *mgmProto.NetbirdConfig) error {
|
||||||
|
if wCfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.updateTURNs(wCfg.GetTurns()); err != nil {
|
||||||
|
return fmt.Errorf("update TURNs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.updateSTUNs(wCfg.GetStuns()); err != nil {
|
||||||
|
return fmt.Errorf("update STUNs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stunTurn []*stun.URI
|
||||||
|
stunTurn = append(stunTurn, e.STUNs...)
|
||||||
|
stunTurn = append(stunTurn, e.TURNs...)
|
||||||
|
e.stunTurn.Store(stunTurn)
|
||||||
|
|
||||||
|
if err := e.handleRelayUpdate(wCfg.GetRelay()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.handleFlowUpdate(wCfg.GetFlow()); err != nil {
|
||||||
|
return fmt.Errorf("handle the flow configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
||||||
|
log.Warnf("Failed to update DNS server config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo update signal
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// persistSyncResponse stores the full sync response so it can be restored on the next
|
||||||
|
// startup. Persistence is enabled only when syncStore is set. The dedicated syncRespMux
|
||||||
|
// (not syncMsgMux) is held for the whole Set so the store cannot be cleared (disabled /
|
||||||
|
// engine close) mid-call and have this write resurrect a file that was just removed.
|
||||||
|
func (e *Engine) persistSyncResponse(update *mgmProto.SyncResponse) {
|
||||||
|
e.syncRespMux.RLock()
|
||||||
|
defer e.syncRespMux.RUnlock()
|
||||||
|
|
||||||
|
if e.syncStore == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.syncStore.Set(update); err != nil {
|
||||||
|
log.Errorf("failed to persist sync response: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("sync response persisted with serial %d", update.GetNetworkMap().GetSerial())
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
|
func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
|
||||||
if update != nil {
|
if update != nil {
|
||||||
// when we receive token we expect valid address list too
|
// when we receive token we expect valid address list too
|
||||||
@@ -1053,11 +1077,17 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
|||||||
return ErrResetConnection
|
return ErrResetConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
if !e.config.DisableIPv6 && e.hasIPv6Changed(conf) {
|
if !e.config.DisableIPv6 {
|
||||||
log.Infof("peer IPv6 address changed, restarting client")
|
reset, err := e.reconcileIPv6(conf)
|
||||||
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
if err != nil {
|
||||||
e.clientCancel()
|
log.Warnf("reconcile IPv6 from PeerConfig: %v", err)
|
||||||
return ErrResetConnection
|
}
|
||||||
|
if reset {
|
||||||
|
log.Infof("peer IPv6 address changed value, restarting client")
|
||||||
|
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
||||||
|
e.clientCancel()
|
||||||
|
return ErrResetConnection
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.GetSshConfig() != nil {
|
if conf.GetSshConfig() != nil {
|
||||||
@@ -1072,31 +1102,65 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
|||||||
state.PubKey = e.config.WgPrivateKey.PublicKey().String()
|
state.PubKey = e.config.WgPrivateKey.PublicKey().String()
|
||||||
state.KernelInterface = !e.wgInterface.IsUserspaceBind()
|
state.KernelInterface = !e.wgInterface.IsUserspaceBind()
|
||||||
state.FQDN = conf.GetFqdn()
|
state.FQDN = conf.GetFqdn()
|
||||||
|
state.WgPort = e.config.WgPort
|
||||||
|
|
||||||
e.statusRecorder.UpdateLocalPeerState(state)
|
e.statusRecorder.UpdateLocalPeerState(state)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasIPv6Changed reports whether the IPv6 overlay address in the peer config
|
// reconcileIPv6 applies the management-supplied IPv6 overlay address to the
|
||||||
// differs from the configured address (added, removed, or changed).
|
// engine's WireGuard interface in place when possible. Three transitions:
|
||||||
// Compares against e.config.WgAddr (not the interface address, which may have
|
//
|
||||||
// been cleared by ClearIPv6 if OS assignment failed).
|
// - First v6 assignment (current had no v6, conf carries one): apply via
|
||||||
func (e *Engine) hasIPv6Changed(conf *mgmProto.PeerConfig) bool {
|
// WGIface.UpdateAddr, no reset. Critical for embedded clients whose
|
||||||
current := e.config.WgAddr
|
// boot config has no v6 — without this we reset on every fresh start
|
||||||
|
// once management has v6 enabled, orphaning any netstack listeners
|
||||||
|
// held outside the engine.
|
||||||
|
// - v6 removed (current had v6, conf carries none): clear in place, no
|
||||||
|
// reset.
|
||||||
|
// - v6 swapped to a different non-empty value: returns reset=true so the
|
||||||
|
// caller falls back to the engine-recreate path — the underlying
|
||||||
|
// interface address can't be safely swapped in place across all
|
||||||
|
// backends (gVisor netstack in particular fixes its address at
|
||||||
|
// CreateNetTUN time).
|
||||||
|
//
|
||||||
|
// Mutates e.config.WgAddr to match the applied state so subsequent
|
||||||
|
// PeerConfig comparisons are stable.
|
||||||
|
func (e *Engine) reconcileIPv6(conf *mgmProto.PeerConfig) (reset bool, err error) {
|
||||||
raw := conf.GetAddressV6()
|
raw := conf.GetAddressV6()
|
||||||
|
current := e.config.WgAddr
|
||||||
|
|
||||||
if len(raw) == 0 {
|
if len(raw) == 0 {
|
||||||
return current.HasIPv6()
|
if !current.HasIPv6() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
current.ClearIPv6()
|
||||||
|
e.config.WgAddr = current
|
||||||
|
if err := e.wgInterface.UpdateAddr(current); err != nil {
|
||||||
|
return false, fmt.Errorf("clear ipv6 on wg interface: %w", err)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix, err := netiputil.DecodePrefix(raw)
|
incoming := current
|
||||||
if err != nil {
|
if err := incoming.SetIPv6FromCompact(raw); err != nil {
|
||||||
log.Errorf("decode v6 overlay address: %v", err)
|
return false, fmt.Errorf("decode v6 overlay address: %w", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !current.HasIPv6() || current.IPv6 != prefix.Addr() || current.IPv6Net != prefix.Masked()
|
if !current.HasIPv6() {
|
||||||
|
e.config.WgAddr = incoming
|
||||||
|
if err := e.wgInterface.UpdateAddr(incoming); err != nil {
|
||||||
|
return false, fmt.Errorf("apply ipv6 on wg interface: %w", err)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.IPv6 == incoming.IPv6 && current.IPv6Net == incoming.IPv6Net {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) receiveJobEvents() {
|
func (e *Engine) receiveJobEvents() {
|
||||||
@@ -1150,6 +1214,7 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
|
|||||||
LogPath: e.config.LogPath,
|
LogPath: e.config.LogPath,
|
||||||
TempDir: e.config.TempDir,
|
TempDir: e.config.TempDir,
|
||||||
ClientMetrics: e.clientMetrics,
|
ClientMetrics: e.clientMetrics,
|
||||||
|
DaemonVersion: version.NetbirdVersion(),
|
||||||
RefreshStatus: func() {
|
RefreshStatus: func() {
|
||||||
e.RunHealthProbes(true)
|
e.RunHealthProbes(true)
|
||||||
},
|
},
|
||||||
@@ -1386,9 +1451,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
|
|
||||||
e.networkSerial = serial
|
e.networkSerial = serial
|
||||||
|
|
||||||
// Test received (upstream) servers for availability right away instead of upon usage.
|
|
||||||
// If no server of a server group responds this will disable the respective handler and retry later.
|
|
||||||
go e.dnsServer.ProbeAvailability()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1690,6 +1752,13 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
return e.ctx.Err()
|
return e.ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self-addressed heartbeat: the signal client's receive watchdog
|
||||||
|
// round-trips this through the server to confirm the receive stream
|
||||||
|
// is delivering. Liveness is already recorded before this handler.
|
||||||
|
if msg.GetBody().GetType() == sProto.Body_HEARTBEAT {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
conn, ok := e.peerStore.PeerConn(msg.Key)
|
conn, ok := e.peerStore.PeerConn(msg.Key)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("wrongly addressed message %s", msg.Key)
|
return fmt.Errorf("wrongly addressed message %s", msg.Key)
|
||||||
@@ -1825,6 +1894,18 @@ func (e *Engine) close() {
|
|||||||
if err := e.portForwardManager.GracefullyStop(ctx); err != nil {
|
if err := e.portForwardManager.GracefullyStop(ctx); err != nil {
|
||||||
log.Warnf("failed to gracefully stop port forwarding manager: %s", err)
|
log.Warnf("failed to gracefully stop port forwarding manager: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop any persisted sync response so its network map does not linger on
|
||||||
|
// disk after the engine stops (and cannot leak into a later run).
|
||||||
|
e.syncRespMux.Lock()
|
||||||
|
store := e.syncStore
|
||||||
|
e.syncStore = nil
|
||||||
|
e.syncRespMux.Unlock()
|
||||||
|
if store != nil {
|
||||||
|
if err := store.Clear(); err != nil {
|
||||||
|
log.Warnf("failed to clear persisted sync response on close: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, error) {
|
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, error) {
|
||||||
@@ -1876,7 +1957,6 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) {
|
|||||||
WGPrivKey: e.config.WgPrivateKey.String(),
|
WGPrivKey: e.config.WgPrivateKey.String(),
|
||||||
MTU: e.config.MTU,
|
MTU: e.config.MTU,
|
||||||
TransportNet: transportNet,
|
TransportNet: transportNet,
|
||||||
FilterFn: e.addrViaRoutes,
|
|
||||||
DisableDNS: e.config.DisableDNS,
|
DisableDNS: e.config.DisableDNS,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1932,7 +2012,7 @@ func (e *Engine) newDnsServer(dnsConfig *nbdns.Config) (dns.Server, error) {
|
|||||||
return dnsServer, nil
|
return dnsServer, nil
|
||||||
|
|
||||||
case "ios":
|
case "ios":
|
||||||
dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.mobileDep.HostDNSAddresses, e.statusRecorder, e.config.DisableDNS)
|
dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.statusRecorder, e.config.DisableDNS)
|
||||||
return dnsServer, nil
|
return dnsServer, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1979,6 +2059,29 @@ func (e *Engine) GetClientMetrics() *metrics.ClientMetrics {
|
|||||||
return e.clientMetrics
|
return e.clientMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performance bundles runtime-adjustable tunnel pool knobs.
|
||||||
|
// See Engine.SetPerformance. Nil fields are ignored.
|
||||||
|
type Performance struct {
|
||||||
|
PreallocatedBuffersPerPool *uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPerformance applies the given tuning to this engine's live Device.
|
||||||
|
func (e *Engine) SetPerformance(t Performance) error {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
if e.wgInterface == nil {
|
||||||
|
return fmt.Errorf("wg interface not initialized")
|
||||||
|
}
|
||||||
|
dev := e.wgInterface.GetWGDevice()
|
||||||
|
if dev == nil {
|
||||||
|
return fmt.Errorf("wg device not initialized")
|
||||||
|
}
|
||||||
|
if t.PreallocatedBuffersPerPool != nil {
|
||||||
|
dev.SetPreallocatedBuffersPerPool(*t.PreallocatedBuffersPerPool)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
||||||
iface, err := net.InterfaceByName(ifaceName)
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2101,21 +2204,6 @@ func (e *Engine) startNetworkMonitor() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) {
|
|
||||||
var vpnRoutes []netip.Prefix
|
|
||||||
for _, routes := range e.routeManager.GetClientRoutes() {
|
|
||||||
if len(routes) > 0 && routes[0] != nil {
|
|
||||||
vpnRoutes = append(vpnRoutes, routes[0].Network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isVpn, prefix := systemops.IsAddrRouted(addr, vpnRoutes); isVpn {
|
|
||||||
return true, prefix, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, netip.Prefix{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) stopDNSServer() {
|
func (e *Engine) stopDNSServer() {
|
||||||
if e.dnsServer == nil {
|
if e.dnsServer == nil {
|
||||||
return
|
return
|
||||||
@@ -2131,45 +2219,42 @@ func (e *Engine) stopDNSServer() {
|
|||||||
e.statusRecorder.UpdateDNSStates(nsGroupStates)
|
e.statusRecorder.UpdateDNSStates(nsGroupStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSyncResponsePersistence enables or disables sync response persistence
|
// SetSyncResponsePersistence enables or disables sync response persistence.
|
||||||
|
// The store is only instantiated while persistence is enabled; construction
|
||||||
|
// itself drops any stale data left over from an earlier run (see syncstore).
|
||||||
func (e *Engine) SetSyncResponsePersistence(enabled bool) {
|
func (e *Engine) SetSyncResponsePersistence(enabled bool) {
|
||||||
e.syncRespMux.Lock()
|
e.syncRespMux.Lock()
|
||||||
defer e.syncRespMux.Unlock()
|
defer e.syncRespMux.Unlock()
|
||||||
|
|
||||||
if enabled == e.persistSyncResponse {
|
if enabled == (e.syncStore != nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.persistSyncResponse = enabled
|
|
||||||
log.Debugf("Sync response persistence is set to %t", enabled)
|
log.Debugf("Sync response persistence is set to %t", enabled)
|
||||||
|
|
||||||
if !enabled {
|
if !enabled {
|
||||||
e.latestSyncResponse = nil
|
if err := e.syncStore.Clear(); err != nil {
|
||||||
|
log.Warnf("failed to clear persisted sync response: %v", err)
|
||||||
|
}
|
||||||
|
e.syncStore = nil
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.syncStore = syncstore.New(e.syncStoreDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestSyncResponse returns the stored sync response if persistence is enabled
|
// GetLatestSyncResponse returns the stored sync response if persistence is enabled
|
||||||
func (e *Engine) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) {
|
func (e *Engine) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) {
|
||||||
|
// Hold the lock for the whole Get so the store cannot be cleared
|
||||||
|
// (disabled / engine close) mid-call.
|
||||||
e.syncRespMux.RLock()
|
e.syncRespMux.RLock()
|
||||||
enabled := e.persistSyncResponse
|
defer e.syncRespMux.RUnlock()
|
||||||
latest := e.latestSyncResponse
|
|
||||||
e.syncRespMux.RUnlock()
|
|
||||||
|
|
||||||
if !enabled {
|
if e.syncStore == nil {
|
||||||
return nil, errors.New("sync response persistence is disabled")
|
return nil, errors.New("sync response persistence is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if latest == nil {
|
//nolint:nilnil
|
||||||
//nolint:nilnil
|
return e.syncStore.Get()
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Retrieving latest sync response with size %d bytes", proto.Size(latest))
|
|
||||||
sr, ok := proto.Clone(latest).(*mgmProto.SyncResponse)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to clone sync response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWgAddr returns the wireguard address
|
// GetWgAddr returns the wireguard address
|
||||||
@@ -2205,7 +2290,7 @@ func (e *Engine) updateDNSForwarder(
|
|||||||
enabled bool,
|
enabled bool,
|
||||||
fwdEntries []*dnsfwd.ForwarderEntry,
|
fwdEntries []*dnsfwd.ForwarderEntry,
|
||||||
) {
|
) {
|
||||||
if e.config.DisableServerRoutes {
|
if e.config.DisableServerRoutes || e.config.BlockInbound {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
305
client/internal/engine_reconcileipv6_test.go
Normal file
305
client/internal/engine_reconcileipv6_test.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reconcileIPv6 / updateConfig regression suite. Locks down the behavior that
|
||||||
|
// PR #5631 (main-side IPv6 overlay support) accidentally broke for embedded
|
||||||
|
// netstack clients: any first NetworkMap update that brings an IPv6 address
|
||||||
|
// used to trigger ErrResetConnection, which destroys the netstack and orphans
|
||||||
|
// every listener bound on it (proxy-side inbound listeners in particular).
|
||||||
|
// The fix in reconcileIPv6 distinguishes "v6 first-assigned" (apply in place)
|
||||||
|
// from "v6 swapped value" (must reset).
|
||||||
|
|
||||||
|
func mustEncodeV6Prefix(t *testing.T, p netip.Prefix) []byte {
|
||||||
|
t.Helper()
|
||||||
|
b, err := netiputil.EncodePrefix(p)
|
||||||
|
require.NoError(t, err, "encode v6 prefix %s", p)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconcileIPv6Fixture builds the smallest Engine the function under test
|
||||||
|
// needs: a config (with WgAddr being the load-bearing field) and a wgInterface
|
||||||
|
// whose UpdateAddr call we can observe.
|
||||||
|
func reconcileIPv6Fixture(t *testing.T, initial wgaddr.Address) (*Engine, *MockWGIface, *wgaddr.Address) {
|
||||||
|
t.Helper()
|
||||||
|
var applied wgaddr.Address
|
||||||
|
mock := &MockWGIface{
|
||||||
|
AddressFunc: func() wgaddr.Address { return initial },
|
||||||
|
UpdateAddrFunc: func(a wgaddr.Address) error {
|
||||||
|
applied = a
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
e := &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
clientCtx: ctx,
|
||||||
|
clientCancel: cancel,
|
||||||
|
config: &EngineConfig{WgAddr: initial},
|
||||||
|
wgInterface: mock,
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
return e, mock, &applied
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_FirstAssignment_AppliesInPlace(t *testing.T) {
|
||||||
|
// Embedded clients boot v4-only; management later assigns a v6 overlay.
|
||||||
|
// The fix: apply v6 in place, return reset=false. Pre-fix this case
|
||||||
|
// fell through to the "v6 changed" branch and reset the engine.
|
||||||
|
v4 := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
e, mock, applied := reconcileIPv6Fixture(t, v4)
|
||||||
|
|
||||||
|
v6Prefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: v4.String(),
|
||||||
|
AddressV6: mustEncodeV6Prefix(t, v6Prefix),
|
||||||
|
}
|
||||||
|
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, reset, "first v6 assignment must NOT request an engine reset")
|
||||||
|
|
||||||
|
require.True(t, e.config.WgAddr.HasIPv6(), "engine config must record the new v6")
|
||||||
|
assert.Equal(t, v6Prefix.Addr(), e.config.WgAddr.IPv6, "engine config v6 address must match")
|
||||||
|
assert.Equal(t, v6Prefix.Masked(), e.config.WgAddr.IPv6Net, "engine config v6 prefix must match")
|
||||||
|
|
||||||
|
require.True(t, applied.HasIPv6(), "WGIface.UpdateAddr must be called with v6 populated")
|
||||||
|
assert.Equal(t, v6Prefix.Addr(), applied.IPv6, "UpdateAddr must carry the new v6")
|
||||||
|
_ = mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_NoChange_NoOp(t *testing.T) {
|
||||||
|
// Steady state: management redelivers the same PeerConfig. No interface
|
||||||
|
// mutation, no reset. Guards against an infinite reset loop if the
|
||||||
|
// comparison ever drifts (e.g. address-vs-prefix masking bugs).
|
||||||
|
v6Prefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
require.NoError(t, addr.SetIPv6FromCompact(mustEncodeV6Prefix(t, v6Prefix)))
|
||||||
|
|
||||||
|
updateAddrCalled := false
|
||||||
|
mock := &MockWGIface{
|
||||||
|
AddressFunc: func() wgaddr.Address { return addr },
|
||||||
|
UpdateAddrFunc: func(a wgaddr.Address) error {
|
||||||
|
updateAddrCalled = true
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
e := &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
clientCtx: ctx,
|
||||||
|
clientCancel: cancel,
|
||||||
|
config: &EngineConfig{WgAddr: addr},
|
||||||
|
wgInterface: mock,
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: addr.String(),
|
||||||
|
AddressV6: mustEncodeV6Prefix(t, v6Prefix),
|
||||||
|
}
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, reset, "unchanged v6 must NOT trigger reset")
|
||||||
|
assert.False(t, updateAddrCalled, "unchanged v6 must NOT call UpdateAddr")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_Removed_AppliesInPlace(t *testing.T) {
|
||||||
|
// Management withdraws v6 (e.g. account toggled off the v6 group).
|
||||||
|
// Cleared in place, no reset.
|
||||||
|
v6Prefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
require.NoError(t, addr.SetIPv6FromCompact(mustEncodeV6Prefix(t, v6Prefix)))
|
||||||
|
|
||||||
|
e, _, applied := reconcileIPv6Fixture(t, addr)
|
||||||
|
e.config.WgAddr = addr
|
||||||
|
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: addr.String(),
|
||||||
|
AddressV6: nil,
|
||||||
|
}
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, reset, "v6 removed must NOT trigger reset")
|
||||||
|
|
||||||
|
assert.False(t, e.config.WgAddr.HasIPv6(), "engine config must reflect v6 cleared")
|
||||||
|
assert.False(t, applied.HasIPv6(), "UpdateAddr must receive cleared v6")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_PrefixLengthChanged_RequestsReset(t *testing.T) {
|
||||||
|
// Same v6 host, different mask (e.g. /64 → /80). Treated like a value
|
||||||
|
// change because the new netmask redefines the broadcast/scope.
|
||||||
|
oldPrefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
newPrefix := netip.MustParsePrefix("fd00::1/80")
|
||||||
|
|
||||||
|
addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
require.NoError(t, addr.SetIPv6FromCompact(mustEncodeV6Prefix(t, oldPrefix)))
|
||||||
|
|
||||||
|
updateAddrCalled := false
|
||||||
|
mock := &MockWGIface{
|
||||||
|
AddressFunc: func() wgaddr.Address { return addr },
|
||||||
|
UpdateAddrFunc: func(a wgaddr.Address) error {
|
||||||
|
updateAddrCalled = true
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
e := &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
clientCtx: ctx,
|
||||||
|
clientCancel: cancel,
|
||||||
|
config: &EngineConfig{WgAddr: addr},
|
||||||
|
wgInterface: mock,
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: addr.String(),
|
||||||
|
AddressV6: mustEncodeV6Prefix(t, newPrefix),
|
||||||
|
}
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, reset, "v6 prefix length change must request a reset")
|
||||||
|
assert.False(t, updateAddrCalled, "v6 prefix length change must NOT touch the interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_ValueChanged_RequestsReset(t *testing.T) {
|
||||||
|
// v6 was X, now Y. The netstack backend can't safely swap an existing
|
||||||
|
// address in place — fall back to the engine recreate path.
|
||||||
|
oldPrefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
newPrefix := netip.MustParsePrefix("fd00::2/64")
|
||||||
|
|
||||||
|
addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
require.NoError(t, addr.SetIPv6FromCompact(mustEncodeV6Prefix(t, oldPrefix)))
|
||||||
|
|
||||||
|
updateAddrCalled := false
|
||||||
|
mock := &MockWGIface{
|
||||||
|
AddressFunc: func() wgaddr.Address { return addr },
|
||||||
|
UpdateAddrFunc: func(a wgaddr.Address) error {
|
||||||
|
updateAddrCalled = true
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
e := &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
clientCtx: ctx,
|
||||||
|
clientCancel: cancel,
|
||||||
|
config: &EngineConfig{WgAddr: addr},
|
||||||
|
wgInterface: mock,
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: addr.String(),
|
||||||
|
AddressV6: mustEncodeV6Prefix(t, newPrefix),
|
||||||
|
}
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, reset, "v6 value change must request a reset")
|
||||||
|
assert.False(t, updateAddrCalled,
|
||||||
|
"v6 value change must NOT call UpdateAddr — caller will recreate the interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_InvalidBytes_ReturnsError(t *testing.T) {
|
||||||
|
// Corrupt PeerConfig.AddressV6 must not crash the engine and must not
|
||||||
|
// trigger a spurious reset.
|
||||||
|
v4 := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
e, _, applied := reconcileIPv6Fixture(t, v4)
|
||||||
|
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: v4.String(),
|
||||||
|
AddressV6: []byte{0x00}, // truncated, definitely not a valid prefix
|
||||||
|
}
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.Error(t, err, "malformed v6 bytes must surface an error")
|
||||||
|
assert.False(t, reset, "decode error must NOT request a reset")
|
||||||
|
assert.False(t, applied.HasIPv6(), "decode error must NOT touch the interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcileIPv6_UpdateAddrError_DoesNotPropagateReset(t *testing.T) {
|
||||||
|
// If WGIface.UpdateAddr fails (e.g. OS-side assignment error on a
|
||||||
|
// kernel device), reconcileIPv6 returns the error to the caller for
|
||||||
|
// logging — but it must NOT request a reset. The whole point of the
|
||||||
|
// fix is to AVOID the reset cascade on v6 transitions.
|
||||||
|
v4 := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
mock := &MockWGIface{
|
||||||
|
AddressFunc: func() wgaddr.Address { return v4 },
|
||||||
|
UpdateAddrFunc: func(_ wgaddr.Address) error { return errors.New("os refused address") },
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
e := &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
clientCtx: ctx,
|
||||||
|
clientCancel: cancel,
|
||||||
|
config: &EngineConfig{WgAddr: v4},
|
||||||
|
wgInterface: mock,
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
v6Prefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: v4.String(),
|
||||||
|
AddressV6: mustEncodeV6Prefix(t, v6Prefix),
|
||||||
|
}
|
||||||
|
reset, err := e.reconcileIPv6(conf)
|
||||||
|
require.Error(t, err, "UpdateAddr failure must surface")
|
||||||
|
assert.False(t, reset, "UpdateAddr failure must NOT request a reset")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateConfig_V6FirstAssignment_DoesNotResetEngine(t *testing.T) {
|
||||||
|
// The integration check: updateConfig must not return ErrResetConnection
|
||||||
|
// when the only change between current state and the new PeerConfig is
|
||||||
|
// "v6 added". Pre-fix this returned ErrResetConnection, tearing down
|
||||||
|
// every listener bound on the engine's netstack.
|
||||||
|
v4 := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
mock := &MockWGIface{
|
||||||
|
AddressFunc: func() wgaddr.Address { return v4 },
|
||||||
|
UpdateAddrFunc: func(_ wgaddr.Address) error { return nil },
|
||||||
|
IsUserspaceBindFunc: func() bool { return true },
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
e := &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
clientCtx: ctx,
|
||||||
|
clientCancel: cancel,
|
||||||
|
config: &EngineConfig{WgAddr: v4, WgPort: 51820},
|
||||||
|
wgInterface: mock,
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
statusRecorder: peer.NewRecorder("https://mgm.test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
v6Prefix := netip.MustParsePrefix("fd00::1/64")
|
||||||
|
conf := &mgmtProto.PeerConfig{
|
||||||
|
Address: v4.String(),
|
||||||
|
AddressV6: mustEncodeV6Prefix(t, v6Prefix),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.updateConfig(conf)
|
||||||
|
assert.NoError(t, err,
|
||||||
|
"updateConfig MUST NOT return ErrResetConnection when v6 is added for the first time — that's the bug fix")
|
||||||
|
assert.NotErrorIs(t, err, ErrResetConnection)
|
||||||
|
|
||||||
|
require.True(t, e.config.WgAddr.HasIPv6(), "engine config must record the assigned v6 after updateConfig")
|
||||||
|
assert.Equal(t, v6Prefix.Addr(), e.config.WgAddr.IPv6)
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/management/server/job"
|
"github.com/netbirdio/netbird/management/server/job"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||||
@@ -67,7 +67,6 @@ import (
|
|||||||
mgmt "github.com/netbirdio/netbird/shared/management/client"
|
mgmt "github.com/netbirdio/netbird/shared/management/client"
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
|
||||||
signal "github.com/netbirdio/netbird/shared/signal/client"
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
"github.com/netbirdio/netbird/shared/signal/proto"
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
signalServer "github.com/netbirdio/netbird/signal/server"
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
||||||
@@ -1641,7 +1640,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1707,82 +1706,12 @@ func getPeers(e *Engine) int {
|
|||||||
return len(e.peerStore.PeersPubKey())
|
return len(e.peerStore.PeersPubKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustEncodePrefix(t *testing.T, p netip.Prefix) []byte {
|
// The former TestEngine_hasIPv6Changed has been superseded by
|
||||||
t.Helper()
|
// engine_reconcileipv6_test.go — the underlying function (hasIPv6Changed)
|
||||||
b, err := netiputil.EncodePrefix(p)
|
// was replaced by reconcileIPv6, which applies "v6 added" / "v6 removed"
|
||||||
require.NoError(t, err)
|
// in place instead of demanding a full engine reset. The behavioral
|
||||||
return b
|
// matrix the old test enforced is now covered, with corrected expectations,
|
||||||
}
|
// by TestReconcileIPv6_* in that sibling file.
|
||||||
|
|
||||||
func TestEngine_hasIPv6Changed(t *testing.T) {
|
|
||||||
v4Only := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
|
||||||
|
|
||||||
v4v6 := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
|
||||||
v4v6.IPv6 = netip.MustParseAddr("fd00::1")
|
|
||||||
v4v6.IPv6Net = netip.MustParsePrefix("fd00::1/64").Masked()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
current wgaddr.Address
|
|
||||||
confV6 []byte
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no v6 before, no v6 now",
|
|
||||||
current: v4Only,
|
|
||||||
confV6: nil,
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no v6 before, v6 added",
|
|
||||||
current: v4Only,
|
|
||||||
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::1/64")),
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "had v6, now removed",
|
|
||||||
current: v4v6,
|
|
||||||
confV6: nil,
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "had v6, same v6",
|
|
||||||
current: v4v6,
|
|
||||||
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::1/64")),
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "had v6, different v6",
|
|
||||||
current: v4v6,
|
|
||||||
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::2/64")),
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "same v6 addr, different prefix length",
|
|
||||||
current: v4v6,
|
|
||||||
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::1/80")),
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "decode error keeps status quo",
|
|
||||||
current: v4Only,
|
|
||||||
confV6: []byte{1, 2, 3},
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
engine := &Engine{
|
|
||||||
config: &EngineConfig{WgAddr: tt.current},
|
|
||||||
}
|
|
||||||
conf := &mgmtProto.PeerConfig{
|
|
||||||
AddressV6: tt.confV6,
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.expected, engine.hasIPv6Changed(conf))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterAllowedIPs(t *testing.T) {
|
func TestFilterAllowedIPs(t *testing.T) {
|
||||||
v4v6Addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
v4v6Addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
|
|
||||||
|
nbversion "github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -11,7 +13,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func IsSupported(agentVersion string) bool {
|
func IsSupported(agentVersion string) bool {
|
||||||
if agentVersion == "development" {
|
if nbversion.IsDevelopmentVersion(agentVersion) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
// handle route changes
|
// handle route changes
|
||||||
case unix.RTM_ADD, syscall.RTM_DELETE:
|
case unix.RTM_ADD, syscall.RTM_DELETE:
|
||||||
route, err := parseRouteMessage(buf[:n])
|
route, flags, err := parseRouteMessage(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -66,6 +66,10 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
}
|
}
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case unix.RTM_ADD:
|
case unix.RTM_ADD:
|
||||||
|
if systemops.IgnoreAddedDefaultRoute(flags) {
|
||||||
|
log.Debugf("Network monitor: ignoring added default route via %s, interface %s, flags %#x", route.Gw, intf, flags)
|
||||||
|
continue
|
||||||
|
}
|
||||||
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
||||||
return nil
|
return nil
|
||||||
case unix.RTM_DELETE:
|
case unix.RTM_DELETE:
|
||||||
@@ -78,22 +82,26 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
func parseRouteMessage(buf []byte) (*systemops.Route, int, error) {
|
||||||
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse RIB: %v", err)
|
return nil, 0, fmt.Errorf("parse RIB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msgs) != 1 {
|
if len(msgs) != 1 {
|
||||||
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
return nil, 0, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, ok := msgs[0].(*route.RouteMessage)
|
msg, ok := msgs[0].(*route.RouteMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
return nil, 0, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemops.MsgToRoute(msg)
|
r, err := systemops.MsgToRoute(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return r, msg.Flags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer/id"
|
"github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
||||||
"github.com/netbirdio/netbird/client/internal/portforward"
|
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
@@ -899,7 +900,7 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to deterministic key if no NetBird PSK is configured
|
// Fallback to deterministic key if no NetBird PSK is configured
|
||||||
determKey, err := conn.rosenpassDetermKey()
|
determKey, err := rosenpass.DeterministicSeedKey(conn.config.LocalKey, conn.config.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
|
conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -908,26 +909,6 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|||||||
return determKey
|
return determKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: move this logic into Rosenpass package
|
|
||||||
func (conn *Conn) rosenpassDetermKey() (*wgtypes.Key, error) {
|
|
||||||
lk := []byte(conn.config.LocalKey)
|
|
||||||
rk := []byte(conn.config.Key) // remote key
|
|
||||||
var keyInput []byte
|
|
||||||
if string(lk) > string(rk) {
|
|
||||||
//nolint:gocritic
|
|
||||||
keyInput = append(lk[:16], rk[:16]...)
|
|
||||||
} else {
|
|
||||||
//nolint:gocritic
|
|
||||||
keyInput = append(rk[:16], lk[:16]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := wgtypes.NewKey(keyInput)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isController(config ConnConfig) bool {
|
func isController(config ConnConfig) bool {
|
||||||
return config.LocalKey > config.Key
|
return config.LocalKey > config.Key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type connStatusInputs struct {
|
|||||||
iceInProgress bool // a negotiation is currently in flight
|
iceInProgress bool // a negotiation is currently in flight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ConnStatus describe the status of a peer's connection
|
// ConnStatus describe the status of a peer's connection
|
||||||
type ConnStatus int32
|
type ConnStatus int32
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ type LocalPeerState struct {
|
|||||||
PubKey string
|
PubKey string
|
||||||
KernelInterface bool
|
KernelInterface bool
|
||||||
FQDN string
|
FQDN string
|
||||||
|
WgPort int
|
||||||
Routes map[string]struct{}
|
Routes map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,10 +186,14 @@ func (s *StatusChangeSubscription) Events() chan map[string]RouterState {
|
|||||||
return s.eventsChan
|
return s.eventsChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status holds a state of peers, signal, management connections and relays
|
// Status holds a state of peers, signal, management connections and relays.
|
||||||
|
// mux is an RWMutex so hot read paths (notably PeerStateByIP, called for
|
||||||
|
// every private-service request) don't contend against each other.
|
||||||
|
// Pure read methods take RLock; anything that mutates state takes Lock.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
mux sync.Mutex
|
mux sync.RWMutex
|
||||||
peers map[string]State
|
peers map[string]State
|
||||||
|
ipToKey map[string]string
|
||||||
changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription
|
changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription
|
||||||
signalState bool
|
signalState bool
|
||||||
signalError error
|
signalError error
|
||||||
@@ -227,6 +232,7 @@ type Status struct {
|
|||||||
func NewRecorder(mgmAddress string) *Status {
|
func NewRecorder(mgmAddress string) *Status {
|
||||||
return &Status{
|
return &Status{
|
||||||
peers: make(map[string]State),
|
peers: make(map[string]State),
|
||||||
|
ipToKey: make(map[string]string),
|
||||||
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),
|
||||||
@@ -278,13 +284,19 @@ func (d *Status) AddPeer(peerPubKey string, fqdn string, ip string, ipv6 string)
|
|||||||
Mux: new(sync.RWMutex),
|
Mux: new(sync.RWMutex),
|
||||||
}
|
}
|
||||||
d.peerListChangedForNotification = true
|
d.peerListChangedForNotification = true
|
||||||
|
if ipv6 != "" {
|
||||||
|
d.ipToKey[ipv6] = peerPubKey
|
||||||
|
}
|
||||||
|
if ip != "" {
|
||||||
|
d.ipToKey[ip] = peerPubKey
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeer adds peer to Daemon status map
|
// GetPeer adds peer to Daemon status map
|
||||||
func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
state, ok := d.peers[peerPubKey]
|
state, ok := d.peers[peerPubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -294,8 +306,8 @@ func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) PeerByIP(ip string) (string, bool) {
|
func (d *Status) PeerByIP(ip string) (string, bool) {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
for _, state := range d.peers {
|
for _, state := range d.peers {
|
||||||
if state.IP == ip {
|
if state.IP == ip {
|
||||||
@@ -305,17 +317,45 @@ func (d *Status) PeerByIP(ip string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerStateByIP returns the full peer State for the given tunnel IP.
|
||||||
|
// Matches against either the IPv4 (State.IP) or IPv6 (State.IPv6) tunnel
|
||||||
|
// address so dual-stack peers are reachable on either family. Only
|
||||||
|
// active peers are matched; peers moved into the offline slice by
|
||||||
|
// ReplaceOfflinePeers are intentionally treated as unknown.
|
||||||
|
func (d *Status) PeerStateByIP(ip string) (State, bool) {
|
||||||
|
if ip == "" {
|
||||||
|
return State{}, false
|
||||||
|
}
|
||||||
|
d.mux.RLock()
|
||||||
|
defer d.mux.RUnlock()
|
||||||
|
key, ok := d.ipToKey[ip]
|
||||||
|
if !ok {
|
||||||
|
return State{}, false
|
||||||
|
}
|
||||||
|
state, ok := d.peers[key]
|
||||||
|
if ok {
|
||||||
|
return state, true
|
||||||
|
}
|
||||||
|
return State{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// RemovePeer removes peer from Daemon status map
|
// RemovePeer removes peer from Daemon status map
|
||||||
func (d *Status) RemovePeer(peerPubKey string) error {
|
func (d *Status) RemovePeer(peerPubKey string) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
_, ok := d.peers[peerPubKey]
|
p, ok := d.peers[peerPubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("no peer with to remove")
|
return errors.New("no peer with to remove")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(d.peers, peerPubKey)
|
delete(d.peers, peerPubKey)
|
||||||
|
if mappedKey, exists := d.ipToKey[p.IP]; exists && mappedKey == peerPubKey {
|
||||||
|
delete(d.ipToKey, p.IP)
|
||||||
|
}
|
||||||
|
if mappedKey, exists := d.ipToKey[p.IPv6]; exists && mappedKey == peerPubKey {
|
||||||
|
delete(d.ipToKey, p.IPv6)
|
||||||
|
}
|
||||||
d.peerListChangedForNotification = true
|
d.peerListChangedForNotification = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -702,8 +742,8 @@ func (d *Status) UnsubscribePeerStateChanges(subscription *StatusChangeSubscript
|
|||||||
|
|
||||||
// GetLocalPeerState returns the local peer state
|
// GetLocalPeerState returns the local peer state
|
||||||
func (d *Status) GetLocalPeerState() LocalPeerState {
|
func (d *Status) GetLocalPeerState() LocalPeerState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return d.localPeer.Clone()
|
return d.localPeer.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,8 +949,8 @@ func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetRosenpassState() RosenpassState {
|
func (d *Status) GetRosenpassState() RosenpassState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return RosenpassState{
|
return RosenpassState{
|
||||||
d.rosenpassEnabled,
|
d.rosenpassEnabled,
|
||||||
d.rosenpassPermissive,
|
d.rosenpassPermissive,
|
||||||
@@ -918,14 +958,14 @@ func (d *Status) GetRosenpassState() RosenpassState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetLazyConnection() bool {
|
func (d *Status) GetLazyConnection() bool {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return d.lazyConnectionEnabled
|
return d.lazyConnectionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetManagementState() ManagementState {
|
func (d *Status) GetManagementState() ManagementState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return ManagementState{
|
return ManagementState{
|
||||||
d.mgmAddress,
|
d.mgmAddress,
|
||||||
d.managementState,
|
d.managementState,
|
||||||
@@ -951,8 +991,8 @@ func (d *Status) UpdateLatency(pubKey string, latency time.Duration) error {
|
|||||||
|
|
||||||
// IsLoginRequired determines if a peer's login has expired.
|
// IsLoginRequired determines if a peer's login has expired.
|
||||||
func (d *Status) IsLoginRequired() bool {
|
func (d *Status) IsLoginRequired() bool {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
// if peer is connected to the management then login is not expired
|
// if peer is connected to the management then login is not expired
|
||||||
if d.managementState {
|
if d.managementState {
|
||||||
@@ -967,8 +1007,8 @@ func (d *Status) IsLoginRequired() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetSignalState() SignalState {
|
func (d *Status) GetSignalState() SignalState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return SignalState{
|
return SignalState{
|
||||||
d.signalAddress,
|
d.signalAddress,
|
||||||
d.signalState,
|
d.signalState,
|
||||||
@@ -978,20 +1018,23 @@ func (d *Status) GetSignalState() SignalState {
|
|||||||
|
|
||||||
// GetRelayStates returns the stun/turn/permanent relay states
|
// GetRelayStates returns the stun/turn/permanent relay states
|
||||||
func (d *Status) GetRelayStates() []relay.ProbeResult {
|
func (d *Status) GetRelayStates() []relay.ProbeResult {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
if d.relayMgr == nil {
|
if d.relayMgr == nil {
|
||||||
return d.relayStates
|
return d.relayStates
|
||||||
}
|
}
|
||||||
|
|
||||||
// extend the list of stun, turn servers with relay address
|
// extend the list of stun, turn servers with the relay server connections
|
||||||
relayStates := slices.Clone(d.relayStates)
|
relayStates := slices.Clone(d.relayStates)
|
||||||
|
|
||||||
// if the server connection is not established then we will use the general address
|
states := d.relayMgr.RelayStates()
|
||||||
// in case of connection we will use the instance specific address
|
if len(states) == 0 {
|
||||||
instanceAddr, _, err := d.relayMgr.RelayInstanceAddress()
|
// no relay connection tracked yet; surface configured servers as
|
||||||
if err != nil {
|
// unavailable with the real reconnect error when known
|
||||||
// TODO add their status
|
err := relayClient.ErrRelayClientNotConnected
|
||||||
|
if connErr := d.relayMgr.RelayConnectError(); connErr != nil {
|
||||||
|
err = connErr
|
||||||
|
}
|
||||||
for _, r := range d.relayMgr.ServerURLs() {
|
for _, r := range d.relayMgr.ServerURLs() {
|
||||||
relayStates = append(relayStates, relay.ProbeResult{
|
relayStates = append(relayStates, relay.ProbeResult{
|
||||||
URI: r,
|
URI: r,
|
||||||
@@ -1001,15 +1044,19 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
|
|||||||
return relayStates
|
return relayStates
|
||||||
}
|
}
|
||||||
|
|
||||||
relayState := relay.ProbeResult{
|
for _, rs := range states {
|
||||||
URI: instanceAddr,
|
relayStates = append(relayStates, relay.ProbeResult{
|
||||||
|
URI: rs.URL,
|
||||||
|
Err: rs.Err,
|
||||||
|
Transport: rs.Transport,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return append(relayStates, relayState)
|
return relayStates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
if d.ingressGwMgr == nil {
|
if d.ingressGwMgr == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1018,16 +1065,16 @@ func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetDNSStates() []NSGroupState {
|
func (d *Status) GetDNSStates() []NSGroupState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
// shallow copy is good enough, as slices fields are currently not updated
|
// shallow copy is good enough, as slices fields are currently not updated
|
||||||
return slices.Clone(d.nsGroupStates)
|
return slices.Clone(d.nsGroupStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return maps.Clone(d.resolvedDomainsStates)
|
return maps.Clone(d.resolvedDomainsStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,8 +1090,8 @@ func (d *Status) GetFullStatus() FullStatus {
|
|||||||
LazyConnectionEnabled: d.GetLazyConnection(),
|
LazyConnectionEnabled: d.GetLazyConnection(),
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
fullStatus.LocalPeerState = d.localPeer
|
fullStatus.LocalPeerState = d.localPeer
|
||||||
|
|
||||||
@@ -1219,8 +1266,8 @@ func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) PeersStatus() (*configurer.Stats, error) {
|
func (d *Status) PeersStatus() (*configurer.Stats, error) {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
if d.wgIface == nil {
|
if d.wgIface == nil {
|
||||||
return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
|
return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
|
||||||
}
|
}
|
||||||
@@ -1326,6 +1373,7 @@ func (fs FullStatus) ToProto() *proto.FullStatus {
|
|||||||
pbFullStatus.LocalPeerState.PubKey = fs.LocalPeerState.PubKey
|
pbFullStatus.LocalPeerState.PubKey = fs.LocalPeerState.PubKey
|
||||||
pbFullStatus.LocalPeerState.KernelInterface = fs.LocalPeerState.KernelInterface
|
pbFullStatus.LocalPeerState.KernelInterface = fs.LocalPeerState.KernelInterface
|
||||||
pbFullStatus.LocalPeerState.Fqdn = fs.LocalPeerState.FQDN
|
pbFullStatus.LocalPeerState.Fqdn = fs.LocalPeerState.FQDN
|
||||||
|
pbFullStatus.LocalPeerState.WgPort = int32(fs.LocalPeerState.WgPort)
|
||||||
pbFullStatus.LocalPeerState.RosenpassPermissive = fs.RosenpassState.Permissive
|
pbFullStatus.LocalPeerState.RosenpassPermissive = fs.RosenpassState.Permissive
|
||||||
pbFullStatus.LocalPeerState.RosenpassEnabled = fs.RosenpassState.Enabled
|
pbFullStatus.LocalPeerState.RosenpassEnabled = fs.RosenpassState.Enabled
|
||||||
pbFullStatus.NumberOfForwardingRules = int32(fs.NumOfForwardingRules)
|
pbFullStatus.NumberOfForwardingRules = int32(fs.NumOfForwardingRules)
|
||||||
@@ -1364,6 +1412,7 @@ func (fs FullStatus) ToProto() *proto.FullStatus {
|
|||||||
pbRelayState := &proto.RelayState{
|
pbRelayState := &proto.RelayState{
|
||||||
URI: relayState.URI,
|
URI: relayState.URI,
|
||||||
Available: relayState.Err == nil,
|
Available: relayState.Err == nil,
|
||||||
|
Transport: relayState.Transport,
|
||||||
}
|
}
|
||||||
if err := relayState.Err; err != nil {
|
if err := relayState.Err; err != nil {
|
||||||
pbRelayState.Error = err.Error()
|
pbRelayState.Error = err.Error()
|
||||||
|
|||||||
@@ -63,6 +63,72 @@ func TestUpdatePeerState(t *testing.T) {
|
|||||||
assert.Equal(t, ip, state.IP, "ip should be equal")
|
assert.Equal(t, ip, state.IP, "ip should be equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStatus_PeerStateByIP(t *testing.T) {
|
||||||
|
status := NewRecorder("https://mgm")
|
||||||
|
req := require.New(t)
|
||||||
|
|
||||||
|
req.NoError(status.AddPeer("pk-1", "peer-1.netbird", "100.64.0.10", ""))
|
||||||
|
req.NoError(status.AddPeer("pk-2", "peer-2.netbird", "100.64.0.11", ""))
|
||||||
|
|
||||||
|
state, ok := status.PeerStateByIP("100.64.0.10")
|
||||||
|
req.True(ok, "known tunnel IP should resolve to a peer state")
|
||||||
|
req.Equal("pk-1", state.PubKey, "matching state must carry the right pub key")
|
||||||
|
req.Equal("peer-1.netbird", state.FQDN, "matching state must carry the right FQDN")
|
||||||
|
|
||||||
|
_, ok = status.PeerStateByIP("100.64.0.99")
|
||||||
|
req.False(ok, "unknown IP must report ok=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatus_PeerStateByIP_MatchesIPv6(t *testing.T) {
|
||||||
|
status := NewRecorder("https://mgm")
|
||||||
|
req := require.New(t)
|
||||||
|
|
||||||
|
req.NoError(status.AddPeer("pk-1", "peer-1.netbird", "100.64.0.10", "fd00::1"))
|
||||||
|
|
||||||
|
state, ok := status.PeerStateByIP("fd00::1")
|
||||||
|
req.True(ok, "IPv6-only match must resolve to the peer state")
|
||||||
|
req.Equal("pk-1", state.PubKey, "matching state must carry the right pub key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStatus_PeerStateByIP_IgnoresOfflinePeers documents that peers
|
||||||
|
// moved into the offline slice via ReplaceOfflinePeers are intentionally
|
||||||
|
// not resolvable by IP: only active peers can carry traffic, so callers
|
||||||
|
// (DNS filter, embed.Client.IdentityForIP) treat them as unknown.
|
||||||
|
func TestStatus_PeerStateByIP_IgnoresOfflinePeers(t *testing.T) {
|
||||||
|
status := NewRecorder("https://mgm")
|
||||||
|
req := require.New(t)
|
||||||
|
|
||||||
|
status.ReplaceOfflinePeers([]State{
|
||||||
|
{PubKey: "pk-offline", FQDN: "offline.netbird", IP: "100.64.0.20", IPv6: "fd00::20"},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, ok := status.PeerStateByIP("100.64.0.20")
|
||||||
|
req.False(ok, "offline peer must not resolve by IPv4 tunnel address")
|
||||||
|
|
||||||
|
_, ok = status.PeerStateByIP("fd00::20")
|
||||||
|
req.False(ok, "offline peer must not resolve by IPv6 tunnel address")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStatus_PeerStateByIP_RemovedPeer verifies RemovePeer drops the
|
||||||
|
// IP index entries for both address families.
|
||||||
|
func TestStatus_PeerStateByIP_RemovedPeer(t *testing.T) {
|
||||||
|
status := NewRecorder("https://mgm")
|
||||||
|
req := require.New(t)
|
||||||
|
|
||||||
|
req.NoError(status.AddPeer("pk-1", "peer-1.netbird", "100.64.0.10", "fd00::1"))
|
||||||
|
|
||||||
|
_, ok := status.PeerStateByIP("100.64.0.10")
|
||||||
|
req.True(ok, "active peer must resolve before removal")
|
||||||
|
|
||||||
|
req.NoError(status.RemovePeer("pk-1"))
|
||||||
|
|
||||||
|
_, ok = status.PeerStateByIP("100.64.0.10")
|
||||||
|
req.False(ok, "removed peer must not resolve by IPv4 tunnel address")
|
||||||
|
|
||||||
|
_, ok = status.PeerStateByIP("fd00::1")
|
||||||
|
req.False(ok, "removed peer must not resolve by IPv6 tunnel address")
|
||||||
|
}
|
||||||
|
|
||||||
func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
||||||
key := "abc"
|
key := "abc"
|
||||||
fqdn := "peer-a.netbird.local"
|
fqdn := "peer-a.netbird.local"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -165,10 +164,6 @@ func (w *WorkerICE) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HA
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if candidateViaRoutes(candidate, haRoutes) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.agent.AddRemoteCandidate(candidate); err != nil {
|
if err := w.agent.AddRemoteCandidate(candidate); err != nil {
|
||||||
w.log.Errorf("error while handling remote candidate")
|
w.log.Errorf("error while handling remote candidate")
|
||||||
return
|
return
|
||||||
@@ -589,34 +584,6 @@ func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive
|
|||||||
return ec, nil
|
return ec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool {
|
|
||||||
addr, err := netip.ParseAddr(candidate.Address())
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to parse IP address %s: %v", candidate.Address(), err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var routePrefixes []netip.Prefix
|
|
||||||
for _, routes := range clientRoutes {
|
|
||||||
if len(routes) > 0 && routes[0] != nil {
|
|
||||||
routePrefixes = append(routePrefixes, routes[0].Network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, prefix := range routePrefixes {
|
|
||||||
// default route is handled by route exclusion / ip rules
|
|
||||||
if prefix.Bits() == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix.Contains(addr) {
|
|
||||||
log.Debugf("Ignoring candidate [%s], its address is part of routed network %s", candidate.String(), prefix)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRelayCandidate(candidate ice.Candidate) bool {
|
func isRelayCandidate(candidate ice.Candidate) bool {
|
||||||
return candidate.Type() == ice.CandidateTypeRelay
|
return candidate.Type() == ice.CandidateTypeRelay
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,8 +179,10 @@ func getDefaultGateway() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dst := net.IPv4zero
|
dst := net.IPv4zero
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux.
|
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux/Android.
|
||||||
|
// TODO: on android/ios, use platform APIs (ConnectivityManager.getLinkProperties /
|
||||||
|
// NWPathMonitor) when netlink-based lookup is restricted or unavailable.
|
||||||
dst = net.IPv4(0, 0, 0, 1)
|
dst = net.IPv4(0, 0, 0, 1)
|
||||||
}
|
}
|
||||||
_, gateway, localIP, err = router.Route(dst)
|
_, gateway, localIP, err = router.Route(dst)
|
||||||
@@ -203,7 +205,7 @@ func getDefaultGateway6() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dst := net.IPv6zero
|
dst := net.IPv6zero
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
// ::2
|
// ::2
|
||||||
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user