mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-30 19:59:56 +00:00
Compare commits
133 Commits
feature/se
...
netmap_pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4988b6726e | ||
|
|
2552830184 | ||
|
|
3b8fc688f4 | ||
|
|
d82d62e818 | ||
|
|
0bf964dad7 | ||
|
|
297dcb3e24 | ||
|
|
bc22926fe0 | ||
|
|
d3f2ef9adb | ||
|
|
5bec1e8f03 | ||
|
|
74bb5c613e | ||
|
|
29dde908ae | ||
|
|
2d7b309004 | ||
|
|
5968cff242 | ||
|
|
cf43841b86 | ||
|
|
739e36a313 | ||
|
|
2bb5421631 | ||
|
|
998ade6e6d | ||
|
|
62f5467cd8 | ||
|
|
1b29995ece | ||
|
|
fd96b8c12f | ||
|
|
6dd6c3f398 | ||
|
|
d1422dcf09 | ||
|
|
615631567a | ||
|
|
f4daf59bcd | ||
|
|
ff2787e184 | ||
|
|
e20b62ad65 | ||
|
|
18b38943aa | ||
|
|
a400828b89 | ||
|
|
e2bb328a34 | ||
|
|
221b9c012c | ||
|
|
17b2044596 | ||
|
|
07101c59ac | ||
|
|
51b6f6291b | ||
|
|
2ebf26006a | ||
|
|
211a26019a | ||
|
|
6c26178ad5 | ||
|
|
af3b7e4497 | ||
|
|
e84f6527f7 | ||
|
|
ac9529ea8c | ||
|
|
f736ef9647 | ||
|
|
cf58bf1ba9 | ||
|
|
522b8ed969 | ||
|
|
c9e99659ea | ||
|
|
58c79f5878 | ||
|
|
15a0504fb1 | ||
|
|
883a1a8961 | ||
|
|
54192a94b7 | ||
|
|
8511687270 | ||
|
|
35b465fa4a | ||
|
|
fb87f751a5 | ||
|
|
679c7182a4 | ||
|
|
8c031ea6f0 | ||
|
|
60a9544656 | ||
|
|
d3710d4bb2 | ||
|
|
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 |
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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 privileged' -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 -e /client/testutil/privileged)
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: unit,client
|
||||||
|
|||||||
41
.github/workflows/golang-test-freebsd.yml
vendored
41
.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8
|
||||||
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
|
||||||
@@ -37,14 +48,14 @@ jobs:
|
|||||||
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
|
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
|
||||||
time go build -o netbird client/main.go
|
time go build -o netbird client/main.go
|
||||||
# check all component except management, since we do not support management server on freebsd
|
# check all component except management, since we do not support management server on freebsd
|
||||||
time go test -timeout 1m -failfast ./base62/...
|
time go test -tags privileged -timeout 1m -failfast ./base62/...
|
||||||
# NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use`
|
# NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use`
|
||||||
time go test -timeout 8m -failfast -v -p 1 ./client/...
|
time go test -tags privileged -timeout 8m -failfast -v -p 1 ./client/...
|
||||||
time go test -timeout 1m -failfast ./dns/...
|
time go test -tags privileged -timeout 1m -failfast ./dns/...
|
||||||
time go test -timeout 1m -failfast ./encryption/...
|
time go test -tags privileged -timeout 1m -failfast ./encryption/...
|
||||||
time go test -timeout 1m -failfast ./formatter/...
|
time go test -tags privileged -timeout 1m -failfast ./formatter/...
|
||||||
time go test -timeout 1m -failfast ./client/iface/...
|
time go test -tags privileged -timeout 1m -failfast ./client/iface/...
|
||||||
time go test -timeout 1m -failfast ./route/...
|
time go test -tags privileged -timeout 1m -failfast ./route/...
|
||||||
time go test -timeout 1m -failfast ./sharedsock/...
|
time go test -tags privileged -timeout 1m -failfast ./sharedsock/...
|
||||||
time go test -timeout 1m -failfast ./util/...
|
time go test -tags privileged -timeout 1m -failfast ./util/...
|
||||||
time go test -timeout 1m -failfast ./version/...
|
time go test -tags privileged -timeout 1m -failfast ./version/...
|
||||||
|
|||||||
207
.github/workflows/golang-test-linux.yml
vendored
207
.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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 -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@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
id: cache-restore
|
id: cache-restore
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -214,7 +229,7 @@ jobs:
|
|||||||
sh -c ' \
|
sh -c ' \
|
||||||
apk update; apk add --no-cache \
|
apk update; apk add --no-cache \
|
||||||
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
||||||
go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server)
|
go test -buildvcs=false -tags "devcert privileged" -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server -e /client/testutil/privileged)
|
||||||
'
|
'
|
||||||
|
|
||||||
test_relay:
|
test_relay:
|
||||||
@@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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 }}
|
||||||
@@ -522,20 +579,21 @@ 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 \
|
||||||
GIT_BRANCH=${{ github.ref_name }} \
|
|
||||||
go test -tags devcert -run=^$ -bench=. \
|
go test -tags devcert -run=^$ -bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
||||||
|
env:
|
||||||
|
GIT_BRANCH: ${{ github.ref_name }}
|
||||||
|
|
||||||
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 +624,12 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -577,10 +637,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -597,7 +657,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 }}
|
||||||
@@ -614,29 +674,32 @@ 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 \
|
||||||
GIT_BRANCH=${{ github.ref_name }} \
|
|
||||||
go test -tags=benchmark \
|
go test -tags=benchmark \
|
||||||
-run=^$ \
|
-run=^$ \
|
||||||
-bench=. \
|
-bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/server/http/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
env:
|
||||||
|
GIT_BRANCH: ${{ github.ref_name }}
|
||||||
|
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -644,10 +707,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -667,6 +730,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@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: netbirdio/netbird
|
||||||
|
flags: integration,management
|
||||||
|
|||||||
21
.github/workflows/golang-test-windows.yml
vendored
21
.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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\'
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' }
|
$packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' }
|
||||||
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
|
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
|
||||||
$cmd = "$goExe test -tags=devcert -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
|
$cmd = "$goExe test -tags `"devcert privileged`" -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
|
||||||
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
|
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
|
|||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@1bcf9fb12cf4aa7d266a90ae39939e61372fe520
|
||||||
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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@06116385d9baf250c9f4dcb4858b16962ea869c3 #v4.1.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 #v4.1.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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 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 }}" }'
|
||||||
|
|||||||
32
.github/workflows/test-infrastructure-files.yml
vendored
32
.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||||
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
|
||||||
@@ -205,7 +207,7 @@ jobs:
|
|||||||
- name: Build management docker image
|
- name: Build management docker image
|
||||||
working-directory: management
|
working-directory: management
|
||||||
run: |
|
run: |
|
||||||
docker build -t netbirdio/management:latest .
|
docker build -t netbirdio/management:latest --build-arg TARGETPLATFORM=. .
|
||||||
|
|
||||||
- name: Build signal binary
|
- name: Build signal binary
|
||||||
working-directory: signal
|
working-directory: signal
|
||||||
@@ -214,7 +216,7 @@ jobs:
|
|||||||
- name: Build signal docker image
|
- name: Build signal docker image
|
||||||
working-directory: signal
|
working-directory: signal
|
||||||
run: |
|
run: |
|
||||||
docker build -t netbirdio/signal:latest .
|
docker build -t netbirdio/signal:latest --build-arg TARGETPLATFORM=. .
|
||||||
|
|
||||||
- name: Build relay binary
|
- name: Build relay binary
|
||||||
working-directory: relay
|
working-directory: relay
|
||||||
@@ -223,7 +225,7 @@ jobs:
|
|||||||
- name: Build relay docker image
|
- name: Build relay docker image
|
||||||
working-directory: relay
|
working-directory: relay
|
||||||
run: |
|
run: |
|
||||||
docker build -t netbirdio/relay:latest .
|
docker build -t netbirdio/relay:latest --build-arg TARGETPLATFORM=. .
|
||||||
|
|
||||||
- name: run docker compose up
|
- name: run docker compose up
|
||||||
working-directory: infrastructure_files/artifacts
|
working-directory: infrastructure_files/artifacts
|
||||||
@@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.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
|
||||||
|
|
||||||
|
|||||||
866
.goreleaser.yaml
866
.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:
|
- "{{ .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:
|
- "{{ .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
|
- "{{ .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
|
- "{{ .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}}"
|
- "{{ .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:
|
- "{{ .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
|
- "{{ .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
|
||||||
@@ -922,9 +462,13 @@ checksum:
|
|||||||
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
- glob: ./release_files/install.sh
|
- glob: ./release_files/install.sh
|
||||||
- glob: ./infrastructure_files/getting-started.sh
|
- glob: ./infrastructure_files/getting-started.sh
|
||||||
|
- glob: ./infrastructure_files/getting-started-enterprise.sh
|
||||||
|
- glob: ./infrastructure_files/migrate-to-enterprise.sh
|
||||||
|
|
||||||
release:
|
release:
|
||||||
extra_files:
|
extra_files:
|
||||||
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
- glob: ./release_files/install.sh
|
- glob: ./release_files/install.sh
|
||||||
- glob: ./infrastructure_files/getting-started.sh
|
- glob: ./infrastructure_files/getting-started.sh
|
||||||
|
- glob: ./infrastructure_files/getting-started-enterprise.sh
|
||||||
|
- glob: ./infrastructure_files/migrate-to-enterprise.sh
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: lint lint-all lint-install setup-hooks
|
.PHONY: lint lint-all lint-install setup-hooks test-unit test-privileged
|
||||||
GOLANGCI_LINT := $(shell pwd)/bin/golangci-lint
|
GOLANGCI_LINT := $(shell pwd)/bin/golangci-lint
|
||||||
|
|
||||||
# Install golangci-lint locally if needed
|
# Install golangci-lint locally if needed
|
||||||
@@ -25,3 +25,15 @@ setup-hooks:
|
|||||||
@git config core.hooksPath .githooks
|
@git config core.hooksPath .githooks
|
||||||
@chmod +x .githooks/pre-push
|
@chmod +x .githooks/pre-push
|
||||||
@echo "✅ Git hooks configured! Pre-push will now run 'make lint'"
|
@echo "✅ Git hooks configured! Pre-push will now run 'make lint'"
|
||||||
|
|
||||||
|
# Host-safe unit tests: excludes the privileged-tagged tests (root / system-mutating).
|
||||||
|
# Runs as a normal user with no sudo and leaves host networking untouched.
|
||||||
|
test-unit:
|
||||||
|
@go test -tags devcert -timeout 10m ./...
|
||||||
|
|
||||||
|
# Privileged suite: runs the `privileged`-tagged tests inside a --privileged
|
||||||
|
# --cap-add=NET_ADMIN container via the ory/dockertest harness. Requires Docker.
|
||||||
|
# Narrow the run with env vars, e.g.:
|
||||||
|
# PRIV_RUN=TestNftablesManager PRIV_PKGS=./client/firewall/nftables/... make test-privileged
|
||||||
|
test-privileged:
|
||||||
|
@go test -tags 'devcert privileged' -timeout 30m -run TestRunPrivilegedSuiteInDocker -v ./client/testutil/privileged/...
|
||||||
|
|||||||
156
README.md
156
README.md
@@ -1,54 +1,46 @@
|
|||||||
|
|
||||||
<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 Agent Network (Beta)
|
||||||
|
> Identity-aware access control for AI agents — keyless access to LLM APIs and private
|
||||||
|
> resources over the encrypted NetBird tunnel. See [`agent-network/`](agent-network/) or
|
||||||
|
> read the docs at **[netbird.ai](https://netbird.ai)**.
|
||||||
|
|
||||||
**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.**
|
||||||
|
|
||||||
@@ -56,92 +48,92 @@
|
|||||||
|
|
||||||
**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.
|
||||||
|
|||||||
39
agent-network/README.md
Normal file
39
agent-network/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# NetBird Agent Network
|
||||||
|
|
||||||
|
Agent Network is NetBird's access control layer for AI agents and the people who run
|
||||||
|
them. It gives every agent a real identity, tied to your identity provider (IdP), and
|
||||||
|
governs what it can reach — the LLM APIs and AI gateways it can call, and the internal
|
||||||
|
resources it can access. Traffic flows only over the encrypted NetBird tunnel, scoped by
|
||||||
|
policy, with no API keys to leak.
|
||||||
|
|
||||||
|
> **Beta.** Agent Network is open source and can be self-hosted on your own
|
||||||
|
> infrastructure.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Agent Network is built on two existing NetBird capabilities:
|
||||||
|
|
||||||
|
- **Overlay network** — the encrypted WireGuard mesh between peers.
|
||||||
|
- **Reverse proxy** — a NetBird peer that terminates LLM requests, establishes the
|
||||||
|
caller's identity, evaluates policies/limits/guardrails, injects the upstream provider
|
||||||
|
key server-side, forwards to the API or gateway, and records usage.
|
||||||
|
|
||||||
|
LLM traffic is routed through the proxy's identity-aware pipeline, while internal
|
||||||
|
resources (databases, internal APIs, self-hosted models) are reached directly over
|
||||||
|
peer-to-peer WireGuard tunnels, governed by the same identities and access policies.
|
||||||
|
|
||||||
|
## Where the code lives
|
||||||
|
|
||||||
|
There is no separate "agent-network" service — it reuses the reverse-proxy and management
|
||||||
|
components:
|
||||||
|
|
||||||
|
- [`proxy/`](../proxy) — the NetBird reverse proxy that serves the agent network endpoint
|
||||||
|
and runs the per-request middleware pipeline.
|
||||||
|
- [`management/internals/modules/reverseproxy/`](../management/internals/modules/reverseproxy)
|
||||||
|
— the management-side control plane: providers, policies, guardrails, limits, routing,
|
||||||
|
and usage/access logs.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation, architecture, and quickstart:
|
||||||
|
**https://docs.netbird.io/agent-network**
|
||||||
@@ -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: string(activeProf.ID),
|
||||||
|
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: %w", 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 {
|
||||||
@@ -109,6 +138,41 @@ func addProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to service CLI interface: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
profileName := args[0]
|
||||||
|
|
||||||
|
id, err := addProfileOnDaemon(cmd.Context(), daemonClient, profileName, currUser.Username)
|
||||||
|
if err != nil {
|
||||||
|
return 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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to service CLI interface: %w", err)
|
return fmt.Errorf("connect to service CLI interface: %w", err)
|
||||||
@@ -121,21 +185,43 @@ func addProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
handle := args[0]
|
||||||
|
newProfilename := args[1]
|
||||||
|
|
||||||
profileName := args[0]
|
resp, err := daemonClient.RenameProfile(cmd.Context(), &proto.RenameProfileRequest{
|
||||||
|
Handle: handle,
|
||||||
_, err = daemonClient.AddProfile(cmd.Context(), &proto.AddProfileRequest{
|
Username: currUser.Username,
|
||||||
ProfileName: profileName,
|
NewProfileName: newProfilename,
|
||||||
Username: currUser.Username,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return wrapAmbiguityError(err, handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("Profile added successfully:", profileName)
|
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 +239,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 +259,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 +276,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 +299,46 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// addProfileOnDaemon issues the AddProfile RPC on an existing daemon client
|
||||||
|
// and returns the new profile's ID. It is the single entry point for profile
|
||||||
|
// creation, shared by `netbird profile add` and the `netbird up --profile
|
||||||
|
// <name>` auto-create path.
|
||||||
|
func addProfileOnDaemon(ctx context.Context, client proto.DaemonServiceClient, profileName, username string) (profilemanager.ID, error) {
|
||||||
|
resp, err := client.AddProfile(ctx, &proto.AddProfileRequest{
|
||||||
|
ProfileName: profileName,
|
||||||
|
Username: username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("add profile failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return profilemanager.ID(resp.Id), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
196
client/cmd/service_privileged_test.go
Normal file
196
client/cmd/service_privileged_test.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
//go:build privileged
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceStartTimeout = 10 * time.Second
|
||||||
|
serviceStopTimeout = 5 * time.Second
|
||||||
|
statusPollInterval = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// waitForServiceStatus waits for service to reach expected status with timeout
|
||||||
|
func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) {
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(statusPollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus)
|
||||||
|
case <-ticker.C:
|
||||||
|
status, err := s.Status()
|
||||||
|
if err != nil {
|
||||||
|
// Continue polling on transient errors
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status == expectedStatus {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServiceLifecycle tests the complete service lifecycle
|
||||||
|
func TestServiceLifecycle(t *testing.T) {
|
||||||
|
// TODO: Add support for Windows and macOS
|
||||||
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||||
|
t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("CONTAINER") == "true" {
|
||||||
|
t.Skip("Skipping service lifecycle test in container environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalServiceName := serviceName
|
||||||
|
serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
defer func() {
|
||||||
|
serviceName = originalServiceName
|
||||||
|
}()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir)
|
||||||
|
logLevel = "info"
|
||||||
|
daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir)
|
||||||
|
|
||||||
|
// Ensure cleanup even if a subtest fails and Stop/Uninstall subtests don't run.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cleanup: create service config: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cleanup: create service: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the subtests already cleaned up, there's nothing to do.
|
||||||
|
if _, err := s.Status(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Stop(); err != nil {
|
||||||
|
t.Errorf("cleanup: stop service: %v", err)
|
||||||
|
}
|
||||||
|
if err := s.Uninstall(); err != nil {
|
||||||
|
t.Errorf("cleanup: uninstall service: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("Install", func(t *testing.T) {
|
||||||
|
installCmd.SetContext(ctx)
|
||||||
|
err := installCmd.RunE(installCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
status, err := s.Status()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, service.StatusUnknown, status)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Start", func(t *testing.T) {
|
||||||
|
startCmd.SetContext(ctx)
|
||||||
|
err := startCmd.RunE(startCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Restart", func(t *testing.T) {
|
||||||
|
restartCmd.SetContext(ctx)
|
||||||
|
err := restartCmd.RunE(restartCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reconfigure", func(t *testing.T) {
|
||||||
|
originalLogLevel := logLevel
|
||||||
|
logLevel = "debug"
|
||||||
|
defer func() {
|
||||||
|
logLevel = originalLogLevel
|
||||||
|
}()
|
||||||
|
|
||||||
|
reconfigureCmd.SetContext(ctx)
|
||||||
|
err := reconfigureCmd.RunE(reconfigureCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Stop", func(t *testing.T) {
|
||||||
|
stopCmd.SetContext(ctx)
|
||||||
|
err := stopCmd.RunE(stopCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Uninstall", func(t *testing.T) {
|
||||||
|
uninstallCmd.SetContext(ctx)
|
||||||
|
err := uninstallCmd.RunE(uninstallCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.Status()
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -31,186 +27,6 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
serviceStartTimeout = 10 * time.Second
|
|
||||||
serviceStopTimeout = 5 * time.Second
|
|
||||||
statusPollInterval = 500 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// waitForServiceStatus waits for service to reach expected status with timeout
|
|
||||||
func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) {
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer timeoutCancel()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(statusPollInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus)
|
|
||||||
case <-ticker.C:
|
|
||||||
status, err := s.Status()
|
|
||||||
if err != nil {
|
|
||||||
// Continue polling on transient errors
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if status == expectedStatus {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestServiceLifecycle tests the complete service lifecycle
|
|
||||||
func TestServiceLifecycle(t *testing.T) {
|
|
||||||
// TODO: Add support for Windows and macOS
|
|
||||||
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
|
||||||
t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("CONTAINER") == "true" {
|
|
||||||
t.Skip("Skipping service lifecycle test in container environment")
|
|
||||||
}
|
|
||||||
|
|
||||||
originalServiceName := serviceName
|
|
||||||
serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix())
|
|
||||||
defer func() {
|
|
||||||
serviceName = originalServiceName
|
|
||||||
}()
|
|
||||||
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir)
|
|
||||||
logLevel = "info"
|
|
||||||
daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir)
|
|
||||||
|
|
||||||
// Ensure cleanup even if a subtest fails and Stop/Uninstall subtests don't run.
|
|
||||||
t.Cleanup(func() {
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cleanup: create service config: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cleanup: create service: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the subtests already cleaned up, there's nothing to do.
|
|
||||||
if _, err := s.Status(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Stop(); err != nil {
|
|
||||||
t.Errorf("cleanup: stop service: %v", err)
|
|
||||||
}
|
|
||||||
if err := s.Uninstall(); err != nil {
|
|
||||||
t.Errorf("cleanup: uninstall service: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
|
||||||
installCmd.SetContext(ctx)
|
|
||||||
err := installCmd.RunE(installCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
status, err := s.Status()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEqual(t, service.StatusUnknown, status)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Start", func(t *testing.T) {
|
|
||||||
startCmd.SetContext(ctx)
|
|
||||||
err := startCmd.RunE(startCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, running)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Restart", func(t *testing.T) {
|
|
||||||
restartCmd.SetContext(ctx)
|
|
||||||
err := restartCmd.RunE(restartCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, running)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Reconfigure", func(t *testing.T) {
|
|
||||||
originalLogLevel := logLevel
|
|
||||||
logLevel = "debug"
|
|
||||||
defer func() {
|
|
||||||
logLevel = originalLogLevel
|
|
||||||
}()
|
|
||||||
|
|
||||||
reconfigureCmd.SetContext(ctx)
|
|
||||||
err := reconfigureCmd.RunE(reconfigureCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, running)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Stop", func(t *testing.T) {
|
|
||||||
stopCmd.SetContext(ctx)
|
|
||||||
err := stopCmd.RunE(stopCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, stopped)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Uninstall", func(t *testing.T) {
|
|
||||||
uninstallCmd.SetContext(ctx)
|
|
||||||
err := uninstallCmd.RunE(uninstallCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = s.Status()
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestServiceEnvVars tests environment variable parsing
|
// TestServiceEnvVars tests environment variable parsing
|
||||||
func TestServiceEnvVars(t *testing.T) {
|
func TestServiceEnvVars(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"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/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
nbstatus "github.com/netbirdio/netbird/client/status"
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
@@ -111,11 +110,10 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := profilemanager.NewProfileManager()
|
// Resolve the active profile's display name via the daemon, which runs
|
||||||
var profName string
|
// as root and can read the per-user profile files. The local profile
|
||||||
if activeProf, err := pm.GetActiveProfile(); err == nil {
|
// manager only knows the active profile ID, not its display name.
|
||||||
profName = activeProf.Name
|
profName := getActiveProfileName(ctx)
|
||||||
}
|
|
||||||
|
|
||||||
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{
|
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{
|
||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
@@ -167,6 +165,25 @@ func getStatus(ctx context.Context, fullPeerStatus bool, shouldRunProbes bool) (
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getActiveProfileName asks the daemon for the active profile's display
|
||||||
|
// name. The daemon runs as root and can read the per-user profile files to
|
||||||
|
// resolve the ID to its human-readable name. Returns an empty string on any
|
||||||
|
// error so status output degrades gracefully.
|
||||||
|
func getActiveProfileName(ctx context.Context) string {
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
resp, err := proto.NewDaemonServiceClient(conn).GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.GetProfileName()
|
||||||
|
}
|
||||||
|
|
||||||
func parseFilters() error {
|
func parseFilters() error {
|
||||||
switch strings.ToLower(statusFilter) {
|
switch strings.ToLower(statusFilter) {
|
||||||
case "", "idle", "connecting", "connected":
|
case "", "idle", "connecting", "connected":
|
||||||
|
|||||||
@@ -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,16 +128,9 @@ 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)
|
if err := switchOrCreateProfile(cmd.Context(), pm, profileName, username.Username); 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 != nil {
|
|
||||||
return fmt.Errorf("switch profile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
profileSwitched = true
|
profileSwitched = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +145,52 @@ func upFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
|
return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// switchOrCreateProfile switches the active profile to the one identified by
|
||||||
|
// handle, creating it first when it does not exist yet. This restores the
|
||||||
|
// pre-0.73 behaviour where `netbird up --profile <name>` auto-creates a
|
||||||
|
// missing profile instead of failing.
|
||||||
|
func switchOrCreateProfile(ctx context.Context, pm *profilemanager.ProfileManager, handle, username string) error {
|
||||||
|
resolvedID, err := switchProfile(ctx, handle, username)
|
||||||
|
if err != nil {
|
||||||
|
st, ok := gstatus.FromError(err)
|
||||||
|
if !ok || st.Code() != codes.NotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Don't fail immediately on a create error: a concurrent run may
|
||||||
|
// have created the profile between the NotFound above and this
|
||||||
|
// call, in which case the retried switch still succeeds. Only
|
||||||
|
// surface the create error if the switch also fails.
|
||||||
|
_, createErr := createProfile(ctx, handle, username)
|
||||||
|
if resolvedID, err = switchProfile(ctx, handle, username); err != nil {
|
||||||
|
if createErr != nil {
|
||||||
|
return fmt.Errorf("create profile: %w", createErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pm.SwitchProfile(resolvedID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createProfile dials the daemon and creates a new profile with the given
|
||||||
|
// display name, returning its generated ID. Use addProfileOnDaemon directly
|
||||||
|
// when a daemon client is already available to reuse the connection.
|
||||||
|
func createProfile(ctx context.Context, profileName, username string) (profilemanager.ID, error) {
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
//nolint
|
||||||
|
return "", fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
|
"If the daemon is not running please run: "+
|
||||||
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
return addProfileOnDaemon(ctx, proto.NewDaemonServiceClient(conn), profileName, username)
|
||||||
|
}
|
||||||
|
|
||||||
func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
|
func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
|
||||||
// override the default profile filepath if provided
|
// override the default profile filepath if provided
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
@@ -190,7 +229,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 +300,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 +328,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 +369,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,12 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-startCtx.Done():
|
case <-startCtx.Done():
|
||||||
|
// ConnectClient.Stop now cancels its own run context and waits for the
|
||||||
|
// run loop to tear the engine down, so this cancel() is no longer
|
||||||
|
// required to break the deadlock and could be removed. It is kept as a
|
||||||
|
// defensive belt-and-suspenders: cancelling the parent context first
|
||||||
|
// guarantees the run loop is unblocked even if Stop's contract regresses.
|
||||||
|
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 +446,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 +529,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 {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build privileged
|
||||||
|
|
||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build !android && privileged
|
||||||
|
|
||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build privileged
|
||||||
|
|
||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build !android && privileged
|
||||||
|
|
||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build privileged
|
||||||
|
|
||||||
package iface
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux && !android
|
//go:build linux && !android && privileged
|
||||||
|
|
||||||
package wgproxy
|
package wgproxy
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux
|
//go:build !linux || !privileged
|
||||||
|
|
||||||
package wgproxy
|
package wgproxy
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux && !android
|
//go:build linux && !android && privileged
|
||||||
|
|
||||||
package wgproxy
|
package wgproxy
|
||||||
|
|
||||||
@@ -26,64 +26,6 @@ func compareUDPAddr(addr1, addr2 net.Addr) bool {
|
|||||||
return udpAddr1.IP.Equal(udpAddr2.IP) && udpAddr1.Port == udpAddr2.Port
|
return udpAddr1.IP.Equal(udpAddr2.IP) && udpAddr1.Port == udpAddr2.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRedirectAs_eBPF_IPv4 tests RedirectAs with eBPF proxy using IPv4 addresses
|
|
||||||
func TestRedirectAs_eBPF_IPv4(t *testing.T) {
|
|
||||||
wgPort := 51850
|
|
||||||
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
|
||||||
if err := ebpfProxy.Listen(); err != nil {
|
|
||||||
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := ebpfProxy.Free(); err != nil {
|
|
||||||
t.Errorf("failed to free ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
|
||||||
|
|
||||||
// NetBird UDP address of the remote peer
|
|
||||||
nbAddr := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("100.108.111.177"),
|
|
||||||
Port: 38746,
|
|
||||||
}
|
|
||||||
|
|
||||||
p2pEndpoint := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("192.168.0.56"),
|
|
||||||
Port: 51820,
|
|
||||||
}
|
|
||||||
|
|
||||||
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedirectAs_eBPF_IPv6 tests RedirectAs with eBPF proxy using IPv6 addresses
|
|
||||||
func TestRedirectAs_eBPF_IPv6(t *testing.T) {
|
|
||||||
wgPort := 51851
|
|
||||||
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
|
||||||
if err := ebpfProxy.Listen(); err != nil {
|
|
||||||
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := ebpfProxy.Free(); err != nil {
|
|
||||||
t.Errorf("failed to free ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
|
||||||
|
|
||||||
// NetBird UDP address of the remote peer
|
|
||||||
nbAddr := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("100.108.111.177"),
|
|
||||||
Port: 38746,
|
|
||||||
}
|
|
||||||
|
|
||||||
p2pEndpoint := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("fe80::56"),
|
|
||||||
Port: 51820,
|
|
||||||
}
|
|
||||||
|
|
||||||
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedirectAs_UDP_IPv4 tests RedirectAs with UDP proxy using IPv4 addresses
|
// TestRedirectAs_UDP_IPv4 tests RedirectAs with UDP proxy using IPv4 addresses
|
||||||
func TestRedirectAs_UDP_IPv4(t *testing.T) {
|
func TestRedirectAs_UDP_IPv4(t *testing.T) {
|
||||||
wgPort := 51852
|
wgPort := 51852
|
||||||
@@ -256,6 +198,64 @@ func testRedirectAs(t *testing.T, proxy Proxy, wgPort int, nbAddr, p2pEndpoint *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRedirectAs_eBPF_IPv4 tests RedirectAs with eBPF proxy using IPv4 addresses
|
||||||
|
func TestRedirectAs_eBPF_IPv4(t *testing.T) {
|
||||||
|
wgPort := 51850
|
||||||
|
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
||||||
|
if err := ebpfProxy.Listen(); err != nil {
|
||||||
|
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := ebpfProxy.Free(); err != nil {
|
||||||
|
t.Errorf("failed to free ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
||||||
|
|
||||||
|
// NetBird UDP address of the remote peer
|
||||||
|
nbAddr := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("100.108.111.177"),
|
||||||
|
Port: 38746,
|
||||||
|
}
|
||||||
|
|
||||||
|
p2pEndpoint := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("192.168.0.56"),
|
||||||
|
Port: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedirectAs_eBPF_IPv6 tests RedirectAs with eBPF proxy using IPv6 addresses
|
||||||
|
func TestRedirectAs_eBPF_IPv6(t *testing.T) {
|
||||||
|
wgPort := 51851
|
||||||
|
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
||||||
|
if err := ebpfProxy.Listen(); err != nil {
|
||||||
|
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := ebpfProxy.Free(); err != nil {
|
||||||
|
t.Errorf("failed to free ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
||||||
|
|
||||||
|
// NetBird UDP address of the remote peer
|
||||||
|
nbAddr := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("100.108.111.177"),
|
||||||
|
Port: 38746,
|
||||||
|
}
|
||||||
|
|
||||||
|
p2pEndpoint := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("fe80::56"),
|
||||||
|
Port: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
// TestRedirectAs_Multiple_Switches tests switching between multiple endpoints
|
// TestRedirectAs_Multiple_Switches tests switching between multiple endpoints
|
||||||
func TestRedirectAs_Multiple_Switches(t *testing.T) {
|
func TestRedirectAs_Multiple_Switches(t *testing.T) {
|
||||||
wgPort := 51856
|
wgPort := 51856
|
||||||
|
|||||||
@@ -260,23 +260,15 @@ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
|
|||||||
|
|
||||||
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
|
||||||
; Drop Run, App Paths and Uninstall entries left in the 32-bit registry view
|
; Create autostart registry entry based on checkbox
|
||||||
; or HKCU by legacy installers.
|
|
||||||
DetailPrint "Cleaning legacy 32-bit / HKCU entries..."
|
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
|
||||||
SetRegView 32
|
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
|
||||||
DeleteRegKey HKLM "${REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UI_REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UNINSTALL_PATH}"
|
|
||||||
SetRegView 64
|
|
||||||
|
|
||||||
DetailPrint "Autostart enabled: $AutostartEnabled"
|
DetailPrint "Autostart enabled: $AutostartEnabled"
|
||||||
${If} $AutostartEnabled == "1"
|
${If} $AutostartEnabled == "1"
|
||||||
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
||||||
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
||||||
${Else}
|
${Else}
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
DetailPrint "Autostart not enabled by user"
|
DetailPrint "Autostart not enabled by user"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
@@ -307,16 +299,11 @@ ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
|||||||
DetailPrint "Terminating Netbird UI process..."
|
DetailPrint "Terminating Netbird UI process..."
|
||||||
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
||||||
|
|
||||||
; Remove autostart entries from every view a previous installer may have used.
|
; Remove autostart registry entry
|
||||||
DetailPrint "Removing autostart registry entry if exists..."
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
SetRegView 32
|
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
|
||||||
DeleteRegKey HKLM "${REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UI_REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UNINSTALL_PATH}"
|
|
||||||
SetRegView 64
|
|
||||||
|
|
||||||
; Handle data deletion based on checkbox
|
; Handle data deletion based on checkbox
|
||||||
DetailPrint "Checking if user requested data deletion..."
|
DetailPrint "Checking if user requested data deletion..."
|
||||||
|
|||||||
@@ -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,10 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
@@ -53,6 +55,10 @@ var androidRunOverride func(c *ConnectClient, runningChan chan struct{}, logPath
|
|||||||
|
|
||||||
type ConnectClient struct {
|
type ConnectClient struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
runCancel context.CancelFunc
|
||||||
|
runExited chan struct{}
|
||||||
|
runOnce sync.Once
|
||||||
|
runStarted atomic.Bool
|
||||||
config *profilemanager.Config
|
config *profilemanager.Config
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
|
|
||||||
@@ -69,8 +75,14 @@ func NewConnectClient(
|
|||||||
config *profilemanager.Config,
|
config *profilemanager.Config,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
) *ConnectClient {
|
) *ConnectClient {
|
||||||
|
// Derive the run context here so Stop owns the cancel that unblocks the run
|
||||||
|
// loop. runCancel is set once at construction, so Stop can call it without
|
||||||
|
// racing the run loop's startup. Callers therefore need not cancel before Stop.
|
||||||
|
runCtx, runCancel := context.WithCancel(ctx)
|
||||||
return &ConnectClient{
|
return &ConnectClient{
|
||||||
ctx: ctx,
|
ctx: runCtx,
|
||||||
|
runCancel: runCancel,
|
||||||
|
runExited: make(chan struct{}),
|
||||||
config: config,
|
config: config,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
engineMutex: sync.Mutex{},
|
engineMutex: sync.Mutex{},
|
||||||
@@ -117,6 +129,8 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
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,11 +140,17 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
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 {
|
||||||
|
// Mark the loop as started and signal exit on return so Stop can wait for
|
||||||
|
// the loop to finish (and skip the wait if the loop never ran).
|
||||||
|
c.runStarted.Store(true)
|
||||||
|
defer c.runOnce.Do(func() { close(c.runExited) })
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
rec := c.statusRecorder
|
rec := c.statusRecorder
|
||||||
@@ -286,7 +306,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
log.Debug(err)
|
log.Debug(err)
|
||||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||||
state.Set(StatusNeedsLogin)
|
state.Set(StatusNeedsLogin)
|
||||||
_ = c.Stop()
|
c.runCancel()
|
||||||
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
||||||
}
|
}
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
@@ -346,6 +366,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)
|
||||||
@@ -401,14 +426,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.engine = nil
|
c.engine = nil
|
||||||
c.engineMutex.Unlock()
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
// todo: consider to remove this condition. Is not thread safe.
|
log.Infof("ensuring wg interface is removed, Netbird engine context cancelled")
|
||||||
// We should always call Stop(), but we need to verify that it is idempotent
|
|
||||||
if engine.wgInterface != nil {
|
|
||||||
log.Infof("ensuring %s is removed, Netbird engine context cancelled", engine.wgInterface.Name())
|
|
||||||
|
|
||||||
if err := engine.Stop(); err != nil {
|
if err := engine.Stop(); err != nil {
|
||||||
log.Errorf("Failed to stop engine: %v", err)
|
log.Errorf("Failed to stop engine: %v", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
c.statusRecorder.ClientTeardown()
|
c.statusRecorder.ClientTeardown()
|
||||||
|
|
||||||
@@ -424,12 +445,12 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.statusRecorder.ClientStart()
|
c.statusRecorder.ClientStart()
|
||||||
err = backoff.Retry(operation, backOff)
|
err = backoff.Retry(operation, backoff.WithContext(backOff, c.ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||||
state.Set(StatusNeedsLogin)
|
state.Set(StatusNeedsLogin)
|
||||||
_ = c.Stop()
|
c.runCancel()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -507,11 +528,9 @@ func (c *ConnectClient) Status() StatusType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) Stop() error {
|
func (c *ConnectClient) Stop() error {
|
||||||
engine := c.Engine()
|
c.runCancel()
|
||||||
if engine != nil {
|
if c.runStarted.Load() {
|
||||||
if err := engine.Stop(); err != nil {
|
<-c.runExited
|
||||||
return fmt.Errorf("stop engine: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -95,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)
|
||||||
@@ -436,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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,13 +51,20 @@ type cachedRecord struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolver caches critical NetBird infrastructure domains.
|
// Resolver caches critical NetBird infrastructure domains.
|
||||||
// records, refreshing, mgmtDomain and serverDomains are all guarded by mutex.
|
// records, refreshing, failedResolves, mgmtDomain and serverDomains are all
|
||||||
|
// guarded by mutex.
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
records map[dns.Question]*cachedRecord
|
records map[dns.Question]*cachedRecord
|
||||||
mgmtDomain *domain.Domain
|
mgmtDomain *domain.Domain
|
||||||
serverDomains *dnsconfig.ServerDomains
|
serverDomains *dnsconfig.ServerDomains
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
|
// failedResolves records the last failed initial resolve per domain so a
|
||||||
|
// domain that never resolves isn't retried on every server-domains update
|
||||||
|
// until refreshBackoff elapses. Entries are cleared on success and pruned
|
||||||
|
// to the current server-domains set.
|
||||||
|
failedResolves map[domain.Domain]time.Time
|
||||||
|
|
||||||
chain ChainResolver
|
chain ChainResolver
|
||||||
chainMaxPriority int
|
chainMaxPriority int
|
||||||
refreshGroup singleflight.Group
|
refreshGroup singleflight.Group
|
||||||
@@ -76,9 +83,10 @@ type Resolver struct {
|
|||||||
// NewResolver creates a new management domains cache resolver.
|
// NewResolver creates a new management domains cache resolver.
|
||||||
func NewResolver() *Resolver {
|
func NewResolver() *Resolver {
|
||||||
return &Resolver{
|
return &Resolver{
|
||||||
records: make(map[dns.Question]*cachedRecord),
|
records: make(map[dns.Question]*cachedRecord),
|
||||||
refreshing: make(map[dns.Question]*atomic.Bool),
|
refreshing: make(map[dns.Question]*atomic.Bool),
|
||||||
cacheTTL: resolveCacheTTL(),
|
failedResolves: make(map[domain.Domain]time.Time),
|
||||||
|
cacheTTL: resolveCacheTTL(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +181,9 @@ func (m *Resolver) continueToNext(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
|
|
||||||
// AddDomain resolves a domain and stores its A/AAAA records in the cache.
|
// AddDomain resolves a domain and stores its A/AAAA records in the cache.
|
||||||
// A family that resolves NODATA (nil err, zero records) evicts any stale
|
// A family that resolves NODATA (nil err, zero records) evicts any stale
|
||||||
// entry for that qtype.
|
// entry for that qtype. When one family hard-errors while the other succeeds,
|
||||||
|
// the resolved family is still cached but AddDomain returns an error so the
|
||||||
|
// caller retries the incomplete resolve rather than treating it as complete.
|
||||||
func (m *Resolver) AddDomain(ctx context.Context, d domain.Domain) error {
|
func (m *Resolver) AddDomain(ctx context.Context, d domain.Domain) error {
|
||||||
dnsName := strings.ToLower(dns.Fqdn(d.PunycodeString()))
|
dnsName := strings.ToLower(dns.Fqdn(d.PunycodeString()))
|
||||||
|
|
||||||
@@ -203,6 +213,10 @@ func (m *Resolver) AddDomain(ctx context.Context, d domain.Domain) error {
|
|||||||
log.Debugf("added/updated domain=%s with %d A records and %d AAAA records",
|
log.Debugf("added/updated domain=%s with %d A records and %d AAAA records",
|
||||||
d.SafeString(), len(aRecords), len(aaaaRecords))
|
d.SafeString(), len(aRecords), len(aaaaRecords))
|
||||||
|
|
||||||
|
if errA != nil || errAAAA != nil {
|
||||||
|
return fmt.Errorf("resolve %s: incomplete, a family failed: %w", d.SafeString(), errors.Join(errA, errAAAA))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +476,7 @@ func (m *Resolver) RemoveDomain(d domain.Domain) error {
|
|||||||
delete(m.records, qAAAA)
|
delete(m.records, qAAAA)
|
||||||
delete(m.refreshing, qA)
|
delete(m.refreshing, qA)
|
||||||
delete(m.refreshing, qAAAA)
|
delete(m.refreshing, qAAAA)
|
||||||
|
delete(m.failedResolves, d)
|
||||||
|
|
||||||
log.Debugf("removed domain=%s from cache", d.SafeString())
|
log.Debugf("removed domain=%s from cache", d.SafeString())
|
||||||
return nil
|
return nil
|
||||||
@@ -505,6 +520,7 @@ func (m *Resolver) UpdateFromServerDomains(ctx context.Context, serverDomains dn
|
|||||||
allDomains := m.extractDomainsFromServerDomains(updatedServerDomains)
|
allDomains := m.extractDomainsFromServerDomains(updatedServerDomains)
|
||||||
currentDomains := m.GetCachedDomains()
|
currentDomains := m.GetCachedDomains()
|
||||||
removedDomains = m.removeStaleDomains(currentDomains, allDomains)
|
removedDomains = m.removeStaleDomains(currentDomains, allDomains)
|
||||||
|
m.pruneFailedResolves(allDomains)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.addNewDomains(ctx, newDomains)
|
m.addNewDomains(ctx, newDomains)
|
||||||
@@ -577,13 +593,85 @@ func (m *Resolver) isManagementDomain(domain domain.Domain) bool {
|
|||||||
return m.mgmtDomain != nil && domain == *m.mgmtDomain
|
return m.mgmtDomain != nil && domain == *m.mgmtDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
// addNewDomains resolves and caches all domains from the update
|
// addNewDomains resolves and caches domains that are not yet in the cache,
|
||||||
|
// running the lookups concurrently. Domains already cached are skipped and left
|
||||||
|
// to the stale-while-revalidate refresh path, so a sync never re-resolves them
|
||||||
|
// synchronously: once NetBird owns the OS resolver the resolve runs through the
|
||||||
|
// handler chain and would otherwise dial the managed upstreams under the engine
|
||||||
|
// sync lock on every update.
|
||||||
func (m *Resolver) addNewDomains(ctx context.Context, newDomains domain.List) {
|
func (m *Resolver) addNewDomains(ctx context.Context, newDomains domain.List) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
seen := make(map[domain.Domain]struct{}, len(newDomains))
|
||||||
for _, newDomain := range newDomains {
|
for _, newDomain := range newDomains {
|
||||||
if err := m.AddDomain(ctx, newDomain); err != nil {
|
if _, dup := seen[newDomain]; dup {
|
||||||
log.Warnf("failed to add/update domain=%s: %v", newDomain.SafeString(), err)
|
continue
|
||||||
} else {
|
}
|
||||||
log.Debugf("added/updated management cache domain=%s", newDomain.SafeString())
|
seen[newDomain] = struct{}{}
|
||||||
|
|
||||||
|
if !m.needsResolve(newDomain) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(d domain.Domain) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := m.AddDomain(ctx, d); err != nil {
|
||||||
|
m.markResolveFailed(d)
|
||||||
|
log.Warnf("failed to add/update domain=%s: %v", d.SafeString(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.clearResolveFailed(d)
|
||||||
|
log.Debugf("added/updated management cache domain=%s", d.SafeString())
|
||||||
|
}(newDomain)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsResolve reports whether d should be resolved now. A recent failed or
|
||||||
|
// incomplete resolve gates retries on the backoff even when one family is
|
||||||
|
// already cached, so a transiently-failed family is retried instead of being
|
||||||
|
// treated as fully resolved. Otherwise a domain with any cached record is left
|
||||||
|
// to the stale-while-revalidate refresh path.
|
||||||
|
func (m *Resolver) needsResolve(d domain.Domain) bool {
|
||||||
|
dnsName := strings.ToLower(dns.Fqdn(d.PunycodeString()))
|
||||||
|
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
if failedAt, ok := m.failedResolves[d]; ok {
|
||||||
|
return time.Since(failedAt) >= refreshBackoff
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA} {
|
||||||
|
q := dns.Question{Name: dnsName, Qtype: qtype, Qclass: dns.ClassINET}
|
||||||
|
if _, ok := m.records[q]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Resolver) markResolveFailed(d domain.Domain) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
m.failedResolves[d] = time.Now()
|
||||||
|
m.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Resolver) clearResolveFailed(d domain.Domain) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
delete(m.failedResolves, d)
|
||||||
|
m.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pruneFailedResolves drops failure markers for domains no longer present in
|
||||||
|
// the server-domains set, keeping the map bounded to the current set (a
|
||||||
|
// failed-only domain has no cached record, so RemoveDomain never sees it).
|
||||||
|
func (m *Resolver) pruneFailedResolves(domains domain.List) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
for d := range m.failedResolves {
|
||||||
|
if !slices.Contains(domains, d) {
|
||||||
|
delete(m.failedResolves, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type fakeChain struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
calls map[string]int
|
calls map[string]int
|
||||||
answers map[string][]dns.RR
|
answers map[string][]dns.RR
|
||||||
|
qErr map[string]error
|
||||||
err error
|
err error
|
||||||
hasRoot bool
|
hasRoot bool
|
||||||
onLookup func()
|
onLookup func()
|
||||||
@@ -30,6 +31,7 @@ func newFakeChain() *fakeChain {
|
|||||||
return &fakeChain{
|
return &fakeChain{
|
||||||
calls: map[string]int{},
|
calls: map[string]int{},
|
||||||
answers: map[string][]dns.RR{},
|
answers: map[string][]dns.RR{},
|
||||||
|
qErr: map[string]error{},
|
||||||
hasRoot: true,
|
hasRoot: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,9 @@ func (f *fakeChain) ResolveInternal(ctx context.Context, msg *dns.Msg, maxPriori
|
|||||||
f.calls[key]++
|
f.calls[key]++
|
||||||
answers := f.answers[key]
|
answers := f.answers[key]
|
||||||
err := f.err
|
err := f.err
|
||||||
|
if err == nil {
|
||||||
|
err = f.qErr[key]
|
||||||
|
}
|
||||||
onLookup := f.onLookup
|
onLookup := f.onLookup
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
|
|
||||||
@@ -75,6 +80,12 @@ func (f *fakeChain) setAnswer(name string, qtype uint16, ip string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeChain) setErr(name string, qtype uint16, err error) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
f.qErr[name+"|"+dns.TypeToString[qtype]] = err
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeChain) callCount(name string, qtype uint16) int {
|
func (f *fakeChain) callCount(name string, qtype uint16) int {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
|
|||||||
183
client/internal/dns/mgmt/mgmt_resolve_test.go
Normal file
183
client/internal/dns/mgmt/mgmt_resolve_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package mgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A domain already in the cache must not be re-resolved on a subsequent server
|
||||||
|
// domains update; it is left to the stale-while-revalidate refresh path.
|
||||||
|
func TestResolver_UpdateFromServerDomains_SkipsCached(t *testing.T) {
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
chain.setAnswer("signal.example.com.", dns.TypeA, "10.0.0.2")
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
sd := dnsconfig.ServerDomains{Signal: domain.Domain("signal.example.com")}
|
||||||
|
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), sd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, chain.callCount("signal.example.com.", dns.TypeA),
|
||||||
|
"first update must resolve the domain")
|
||||||
|
|
||||||
|
_, err = r.UpdateFromServerDomains(context.Background(), sd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, chain.callCount("signal.example.com.", dns.TypeA),
|
||||||
|
"cached domain must not be re-resolved on a subsequent update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// New domains in a single update must resolve concurrently rather than serially.
|
||||||
|
func TestResolver_AddNewDomains_ResolvesConcurrently(t *testing.T) {
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
|
||||||
|
var inflight, maxInflight atomic.Int32
|
||||||
|
chain.onLookup = func() {
|
||||||
|
n := inflight.Add(1)
|
||||||
|
for {
|
||||||
|
old := maxInflight.Load()
|
||||||
|
if n <= old || maxInflight.CompareAndSwap(old, n) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
inflight.Add(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
relays := []domain.Domain{"a.example.com", "b.example.com", "c.example.com", "d.example.com"}
|
||||||
|
for _, d := range relays {
|
||||||
|
chain.setAnswer(dns.Fqdn(string(d)), dns.TypeA, "10.0.0.2")
|
||||||
|
}
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), dnsconfig.ServerDomains{Relay: relays})
|
||||||
|
require.NoError(t, err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, int(maxInflight.Load()), 2, "domains must resolve concurrently")
|
||||||
|
// Serial resolution of 4 domains would take at least 4*50ms; concurrent is far less.
|
||||||
|
assert.Less(t, elapsed, 300*time.Millisecond, "resolution should not be serial")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A domain that fails to resolve must not be retried on every update; the
|
||||||
|
// failure backoff suppresses re-resolution until it expires.
|
||||||
|
func TestResolver_UpdateFromServerDomains_BacksOffFailures(t *testing.T) {
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
chain.err = errors.New("resolve boom")
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
sd := dnsconfig.ServerDomains{Signal: domain.Domain("signal.example.com")}
|
||||||
|
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), sd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, chain.callCount("signal.example.com.", dns.TypeA),
|
||||||
|
"first update must attempt the resolve")
|
||||||
|
|
||||||
|
_, err = r.UpdateFromServerDomains(context.Background(), sd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, chain.callCount("signal.example.com.", dns.TypeA),
|
||||||
|
"failed resolve must back off and not retry on the next update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A domain listed under more than one server-domain type (e.g. STUN and TURN on
|
||||||
|
// the same host) must be resolved once per update, not once per occurrence.
|
||||||
|
func TestResolver_AddNewDomains_DedupesDuplicateDomains(t *testing.T) {
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
chain.setAnswer("dup.example.com.", dns.TypeA, "10.0.0.9")
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
sd := dnsconfig.ServerDomains{
|
||||||
|
Stuns: []domain.Domain{"dup.example.com"},
|
||||||
|
Turns: []domain.Domain{"dup.example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), sd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, chain.callCount("dup.example.com.", dns.TypeA),
|
||||||
|
"a domain appearing under multiple server-domain types must resolve once")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failure marker must be dropped once its domain leaves the server-domains set
|
||||||
|
// so the map stays bounded to the current set.
|
||||||
|
func TestResolver_UpdateFromServerDomains_PrunesFailedResolves(t *testing.T) {
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
chain.err = errors.New("resolve boom")
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), dnsconfig.ServerDomains{Signal: domain.Domain("gone.example.com")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.mutex.RLock()
|
||||||
|
_, marked := r.failedResolves[domain.Domain("gone.example.com")]
|
||||||
|
r.mutex.RUnlock()
|
||||||
|
require.True(t, marked, "failed resolve must be recorded")
|
||||||
|
|
||||||
|
_, err = r.UpdateFromServerDomains(context.Background(), dnsconfig.ServerDomains{Signal: domain.Domain("other.example.com")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.mutex.RLock()
|
||||||
|
_, stillMarked := r.failedResolves[domain.Domain("gone.example.com")]
|
||||||
|
r.mutex.RUnlock()
|
||||||
|
assert.False(t, stillMarked, "failure marker for a domain no longer in the set must be pruned")
|
||||||
|
}
|
||||||
|
|
||||||
|
// When one family hard-errors while the other resolves, the domain is cached
|
||||||
|
// for the working family but recorded as incomplete so the failed family is
|
||||||
|
// retried under backoff instead of being treated as fully resolved forever.
|
||||||
|
func TestResolver_AddNewDomains_RetriesPartialFamilyFailure(t *testing.T) {
|
||||||
|
d := domain.Domain("relay.example.com")
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
chain.setAnswer("relay.example.com.", dns.TypeA, "10.0.0.2")
|
||||||
|
chain.setErr("relay.example.com.", dns.TypeAAAA, errors.New("servfail"))
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), dnsconfig.ServerDomains{Relay: []domain.Domain{d}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r.mutex.RLock()
|
||||||
|
_, aCached := r.records[dns.Question{Name: "relay.example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}]
|
||||||
|
_, marked := r.failedResolves[d]
|
||||||
|
r.mutex.RUnlock()
|
||||||
|
require.True(t, aCached, "the working family must still be cached")
|
||||||
|
require.True(t, marked, "a partial failure must be recorded so the failed family is retried")
|
||||||
|
|
||||||
|
assert.False(t, r.needsResolve(d), "within the backoff window the domain is not retried")
|
||||||
|
|
||||||
|
r.mutex.Lock()
|
||||||
|
r.failedResolves[d] = time.Now().Add(-2 * refreshBackoff)
|
||||||
|
r.mutex.Unlock()
|
||||||
|
assert.True(t, r.needsResolve(d), "after the backoff elapses the domain is retried to pick up the missing family")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A family that returns NODATA (legitimately absent, e.g. an IPv4-only host) is
|
||||||
|
// not a failure: the domain must not be marked for retry, otherwise it would be
|
||||||
|
// re-resolved on every sync.
|
||||||
|
func TestResolver_AddNewDomains_NodataIsNotFailure(t *testing.T) {
|
||||||
|
d := domain.Domain("v4only.example.com")
|
||||||
|
r := NewResolver()
|
||||||
|
chain := newFakeChain()
|
||||||
|
chain.setAnswer("v4only.example.com.", dns.TypeA, "10.0.0.2")
|
||||||
|
r.SetChainResolver(chain, 50)
|
||||||
|
|
||||||
|
_, err := r.UpdateFromServerDomains(context.Background(), dnsconfig.ServerDomains{Relay: []domain.Domain{d}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r.mutex.RLock()
|
||||||
|
_, marked := r.failedResolves[d]
|
||||||
|
r.mutex.RUnlock()
|
||||||
|
assert.False(t, marked, "a NODATA family must not be recorded as a failure")
|
||||||
|
assert.False(t, r.needsResolve(d), "an IPv4-only host must not be re-resolved on later syncs")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -195,3 +207,35 @@ func FormatAnswers(answers []dns.RR) string {
|
|||||||
}
|
}
|
||||||
return "[" + strings.Join(parts, ", ") + "]"
|
return "[" + strings.Join(parts, ", ") + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StripOPT removes any OPT pseudo-RRs from the message's Extra section. Per
|
||||||
|
// RFC 6891 a responder must not include an OPT RR toward a client that did not
|
||||||
|
// advertise EDNS0.
|
||||||
|
func StripOPT(msg *dns.Msg) {
|
||||||
|
if len(msg.Extra) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := msg.Extra[:0]
|
||||||
|
for _, rr := range msg.Extra {
|
||||||
|
if _, ok := rr.(*dns.OPT); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, rr)
|
||||||
|
}
|
||||||
|
msg.Extra = out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractEDE returns the first Extended DNS Error (RFC 8914) option carried in
|
||||||
|
// the message, if present.
|
||||||
|
func ExtractEDE(msg *dns.Msg) (*dns.EDNS0_EDE, bool) {
|
||||||
|
opt := msg.IsEdns0()
|
||||||
|
if opt == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
for _, o := range opt.Option {
|
||||||
|
if ede, ok := o.(*dns.EDNS0_EDE); ok {
|
||||||
|
return ede, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|||||||
161
client/internal/dns/resutil/resolve_test.go
Normal file
161
client/internal/dns/resutil/resolve_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripOPT(t *testing.T) {
|
||||||
|
rm := &dns.Msg{
|
||||||
|
Extra: []dns.RR{
|
||||||
|
&dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}},
|
||||||
|
&dns.A{Hdr: dns.RR_Header{Name: "x.", Rrtype: dns.TypeA}, A: net.IPv4(1, 2, 3, 4)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
StripOPT(rm)
|
||||||
|
assert.Len(t, rm.Extra, 1, "OPT should be removed, A kept")
|
||||||
|
_, isOPT := rm.Extra[0].(*dns.OPT)
|
||||||
|
assert.False(t, isOPT, "remaining record must not be OPT")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractEDE(t *testing.T) {
|
||||||
|
t.Run("no edns", func(t *testing.T) {
|
||||||
|
_, ok := ExtractEDE(&dns.Msg{})
|
||||||
|
assert.False(t, ok, "message without OPT has no EDE")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("edns without ede", func(t *testing.T) {
|
||||||
|
rm := &dns.Msg{}
|
||||||
|
rm.SetEdns0(4096, false)
|
||||||
|
_, ok := ExtractEDE(rm)
|
||||||
|
assert.False(t, ok, "OPT without EDE option returns false")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with ede", func(t *testing.T) {
|
||||||
|
rm := &dns.Msg{}
|
||||||
|
opt := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
|
||||||
|
opt.Option = append(opt.Option, &dns.EDNS0_EDE{InfoCode: 49152, ExtraText: "upstream timeout"})
|
||||||
|
rm.Extra = append(rm.Extra, opt)
|
||||||
|
|
||||||
|
ede, ok := ExtractEDE(rm)
|
||||||
|
assert.True(t, ok, "EDE option should be found")
|
||||||
|
assert.Equal(t, uint16(49152), ede.InfoCode)
|
||||||
|
assert.Equal(t, "upstream timeout", ede.ExtraText)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -38,11 +39,15 @@ const (
|
|||||||
// defaultWarningDelayBase is the starting grace window before a
|
// defaultWarningDelayBase is the starting grace window before a
|
||||||
// "Nameserver group unreachable" event fires for a group that's
|
// "Nameserver group unreachable" event fires for a group that's
|
||||||
// never been healthy and only has overlay upstreams with no
|
// never been healthy and only has overlay upstreams with no
|
||||||
// Connected peer. Per-server and overridable; see warningDelayFor.
|
// Connected peer. Per-server and overridable via envWarningDelay;
|
||||||
defaultWarningDelayBase = 30 * time.Second
|
// see warningDelay.
|
||||||
|
defaultWarningDelayBase = 60 * time.Second
|
||||||
// warningDelayBonusCap caps the route-count bonus added to the
|
// warningDelayBonusCap caps the route-count bonus added to the
|
||||||
// base grace window. See warningDelayFor.
|
// base grace window. See warningDelay.
|
||||||
warningDelayBonusCap = 30 * time.Second
|
warningDelayBonusCap = 30 * time.Second
|
||||||
|
// envWarningDelay overrides defaultWarningDelayBase with a Go duration
|
||||||
|
// string (e.g. "90s", "2m"). Invalid or non-positive values are ignored.
|
||||||
|
envWarningDelay = "NB_DNS_HEALTH_WARNING_DELAY"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errNoUsableNameservers signals that a merged-domain group has no usable
|
// errNoUsableNameservers signals that a merged-domain group has no usable
|
||||||
@@ -135,7 +140,7 @@ type DefaultServer struct {
|
|||||||
disableSys bool
|
disableSys bool
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
service service
|
service service
|
||||||
dnsMuxMap registeredHandlerMap
|
dnsMuxHandlers []handlerWrapper
|
||||||
localResolver *local.Resolver
|
localResolver *local.Resolver
|
||||||
wgInterface WGIface
|
wgInterface WGIface
|
||||||
hostManager hostManager
|
hostManager hostManager
|
||||||
@@ -199,8 +204,6 @@ type handlerWrapper struct {
|
|||||||
priority int
|
priority int
|
||||||
}
|
}
|
||||||
|
|
||||||
type registeredHandlerMap map[types.HandlerID]handlerWrapper
|
|
||||||
|
|
||||||
// DefaultServerConfig holds configuration parameters for NewDefaultServer
|
// DefaultServerConfig holds configuration parameters for NewDefaultServer
|
||||||
type DefaultServerConfig struct {
|
type DefaultServerConfig struct {
|
||||||
WgInterface WGIface
|
WgInterface WGIface
|
||||||
@@ -289,7 +292,6 @@ func newDefaultServer(
|
|||||||
service: dnsService,
|
service: dnsService,
|
||||||
handlerChain: handlerChain,
|
handlerChain: handlerChain,
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
localResolver: local.NewResolver(),
|
localResolver: local.NewResolver(),
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
@@ -298,9 +300,14 @@ func newDefaultServer(
|
|||||||
hostManager: &noopHostConfigurator{},
|
hostManager: &noopHostConfigurator{},
|
||||||
mgmtCacheResolver: mgmtCacheResolver,
|
mgmtCacheResolver: mgmtCacheResolver,
|
||||||
currentConfigHash: ^uint64(0), // Initialize to max uint64 to ensure first config is always applied
|
currentConfigHash: ^uint64(0), // Initialize to max uint64 to ensure first config is always applied
|
||||||
warningDelayBase: defaultWarningDelayBase,
|
warningDelayBase: warningDelayBaseFromEnv(),
|
||||||
healthRefresh: make(chan struct{}, 1),
|
healthRefresh: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
|
// Wire the local resolver against the peer status recorder so it can
|
||||||
|
// suppress A/AAAA answers that point at disconnected peers (typical
|
||||||
|
// case: synthesised private-service records pointing at an embedded
|
||||||
|
// proxy peer that just went offline).
|
||||||
|
defaultServer.localResolver.SetPeerConnectivity(localPeerConnectivity{statusRecorder})
|
||||||
|
|
||||||
// register with root zone, handler chain takes care of the routing
|
// register with root zone, handler chain takes care of the routing
|
||||||
dnsService.RegisterMux(".", handlerChain)
|
dnsService.RegisterMux(".", handlerChain)
|
||||||
@@ -323,7 +330,7 @@ func (s *DefaultServer) SetRouteSources(selected, active func() route.HAMap) {
|
|||||||
type routeSettable interface {
|
type routeSettable interface {
|
||||||
setSelectedRoutes(func() route.HAMap)
|
setSelectedRoutes(func() route.HAMap)
|
||||||
}
|
}
|
||||||
for _, entry := range s.dnsMuxMap {
|
for _, entry := range s.dnsMuxHandlers {
|
||||||
if h, ok := entry.handler.(routeSettable); ok {
|
if h, ok := entry.handler.(routeSettable); ok {
|
||||||
h.setSelectedRoutes(selected)
|
h.setSelectedRoutes(selected)
|
||||||
}
|
}
|
||||||
@@ -772,13 +779,24 @@ func (s *DefaultServer) applyHostConfig() {
|
|||||||
// context is released rather than leaked until GC.
|
// context is released rather than leaked until GC.
|
||||||
func (s *DefaultServer) registerFallback() {
|
func (s *DefaultServer) registerFallback() {
|
||||||
originalNameservers := s.hostManager.getOriginalNameservers()
|
originalNameservers := s.hostManager.getOriginalNameservers()
|
||||||
if len(originalNameservers) == 0 {
|
|
||||||
|
serverIP := s.service.RuntimeIP()
|
||||||
|
var servers []netip.AddrPort
|
||||||
|
for _, ns := range originalNameservers {
|
||||||
|
if ns == serverIP {
|
||||||
|
log.Debugf("skipping original nameserver %s as it is the same as the server IP %s", ns, serverIP)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
servers = append(servers, netip.AddrPortFrom(ns, DefaultPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(servers) == 0 {
|
||||||
log.Debugf("no fallback upstreams to register; clearing PriorityFallback handler")
|
log.Debugf("no fallback upstreams to register; clearing PriorityFallback handler")
|
||||||
s.clearFallback()
|
s.clearFallback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("registering original nameservers %v as upstream handlers with priority %d", originalNameservers, PriorityFallback)
|
log.Infof("registering original nameservers %v as upstream handlers with priority %d", servers, PriorityFallback)
|
||||||
|
|
||||||
handler, err := newUpstreamResolver(
|
handler, err := newUpstreamResolver(
|
||||||
s.ctx,
|
s.ctx,
|
||||||
@@ -792,11 +810,6 @@ func (s *DefaultServer) registerFallback() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
handler.selectedRoutes = s.selectedRoutes
|
handler.selectedRoutes = s.selectedRoutes
|
||||||
|
|
||||||
var servers []netip.AddrPort
|
|
||||||
for _, ns := range originalNameservers {
|
|
||||||
servers = append(servers, netip.AddrPortFrom(ns, DefaultPort))
|
|
||||||
}
|
|
||||||
handler.addRace(servers)
|
handler.addRace(servers)
|
||||||
|
|
||||||
prev := s.fallbackHandler
|
prev := s.fallbackHandler
|
||||||
@@ -967,19 +980,23 @@ func (s *DefaultServer) usableNameServers(nameServers []nbdns.NameServer) []neti
|
|||||||
|
|
||||||
func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
||||||
// this will introduce a short period of time when the server is not able to handle DNS requests
|
// this will introduce a short period of time when the server is not able to handle DNS requests
|
||||||
for _, existing := range s.dnsMuxMap {
|
for _, existing := range s.dnsMuxHandlers {
|
||||||
s.deregisterHandler([]string{existing.domain}, existing.priority)
|
s.deregisterHandler([]string{existing.domain}, existing.priority)
|
||||||
existing.handler.Stop()
|
// The local resolver is a persistent singleton shared by every custom
|
||||||
|
// zone and reused across config updates. Its chain registrations are
|
||||||
|
// per-config and must be deregistered, but Stop() cancels its lookup
|
||||||
|
// context (breaking external CNAME-target resolution) and clears its
|
||||||
|
// records, so it must not be torn down here.
|
||||||
|
if existing.handler != s.localResolver {
|
||||||
|
existing.handler.Stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
muxUpdateMap := make(registeredHandlerMap)
|
|
||||||
|
|
||||||
for _, update := range muxUpdates {
|
for _, update := range muxUpdates {
|
||||||
s.registerHandler([]string{update.domain}, update.handler, update.priority)
|
s.registerHandler([]string{update.domain}, update.handler, update.priority)
|
||||||
muxUpdateMap[update.handler.ID()] = update
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.dnsMuxMap = muxUpdateMap
|
s.dnsMuxHandlers = muxUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateNSGroupStates records the new group set and pokes the refresher.
|
// updateNSGroupStates records the new group set and pokes the refresher.
|
||||||
@@ -1143,6 +1160,26 @@ func (s *DefaultServer) projectUnhealthy(p *nsGroupProj, servers []netip.AddrPor
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// warningDelayBaseFromEnv returns the base grace window, honoring
|
||||||
|
// envWarningDelay when it holds a valid positive Go duration. Invalid or
|
||||||
|
// non-positive values fall back to defaultWarningDelayBase.
|
||||||
|
func warningDelayBaseFromEnv() time.Duration {
|
||||||
|
val := os.Getenv(envWarningDelay)
|
||||||
|
if val == "" {
|
||||||
|
return defaultWarningDelayBase
|
||||||
|
}
|
||||||
|
d, err := time.ParseDuration(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("invalid %s value %q, using default %v: %v", envWarningDelay, val, defaultWarningDelayBase, err)
|
||||||
|
return defaultWarningDelayBase
|
||||||
|
}
|
||||||
|
if d <= 0 {
|
||||||
|
log.Warnf("%s must be positive, got %v, using default %v", envWarningDelay, d, defaultWarningDelayBase)
|
||||||
|
return defaultWarningDelayBase
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
// warningDelay returns the grace window for the given selected-route
|
// warningDelay returns the grace window for the given selected-route
|
||||||
// count. Scales gently: +1s per 100 routes, capped by
|
// count. Scales gently: +1s per 100 routes, capped by
|
||||||
// warningDelayBonusCap. Parallel handshakes mean handshake time grows
|
// warningDelayBonusCap. Parallel handshakes mean handshake time grows
|
||||||
@@ -1193,7 +1230,7 @@ func (s *DefaultServer) groupHasImmediateUpstream(servers []netip.AddrPort, snap
|
|||||||
// in more than one handler.
|
// in more than one handler.
|
||||||
func (s *DefaultServer) collectUpstreamHealth() map[netip.AddrPort]UpstreamHealth {
|
func (s *DefaultServer) collectUpstreamHealth() map[netip.AddrPort]UpstreamHealth {
|
||||||
merged := make(map[netip.AddrPort]UpstreamHealth)
|
merged := make(map[netip.AddrPort]UpstreamHealth)
|
||||||
for _, entry := range s.dnsMuxMap {
|
for _, entry := range s.dnsMuxHandlers {
|
||||||
reporter, ok := entry.handler.(upstreamHealthReporter)
|
reporter, ok := entry.handler.(upstreamHealthReporter)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -1386,3 +1423,25 @@ func (s *DefaultServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// localPeerConnectivity adapts *peer.Status to local.PeerConnectivity so
|
||||||
|
// the local resolver can ask "is this IP a known peer and is it
|
||||||
|
// connected?" without taking on the peer package as a dependency.
|
||||||
|
// A nil status recorder always reports known=false so the resolver
|
||||||
|
// short-circuits to the legacy "return everything" path.
|
||||||
|
type localPeerConnectivity struct {
|
||||||
|
status *peer.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnectedByIP looks the IP up in the peerstore and surfaces both
|
||||||
|
// the known and connected bits. Used by Resolver.filterDisconnectedPeerAnswers.
|
||||||
|
func (l localPeerConnectivity) IsConnectedByIP(ip string) (known, connected bool) {
|
||||||
|
if l.status == nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
state, ok := l.status.PeerStateByIP(ip)
|
||||||
|
if !ok {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
return true, state.ConnStatus == peer.StatusConnected
|
||||||
|
}
|
||||||
|
|||||||
485
client/internal/dns/server_privileged_test.go
Normal file
485
client/internal/dns/server_privileged_test.go
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
//go:build privileged
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
|
pfmock "github.com/netbirdio/netbird/client/iface/mocks"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/local"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateDNSServer(t *testing.T) {
|
||||||
|
|
||||||
|
nameServers := []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.4.4"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
initUpstreamMap []handlerWrapper
|
||||||
|
initLocalZones []nbdns.CustomZone
|
||||||
|
initSerial uint64
|
||||||
|
inputSerial uint64
|
||||||
|
inputUpdate nbdns.Config
|
||||||
|
shouldFail bool
|
||||||
|
expectedUpstreamMap []handlerWrapper
|
||||||
|
expectedLocalQs []dns.Question
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Initial Config Should Succeed",
|
||||||
|
initUpstreamMap: nil,
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
Domains: []string{"netbird.io"},
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
Primary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpstreamMap: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: "netbird.io",
|
||||||
|
priority: PriorityUpstream,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "netbird.cloud",
|
||||||
|
priority: PriorityLocal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: nbdns.RootZone,
|
||||||
|
priority: PriorityDefault,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLocalQs: []dns.Question{{Name: "peera.netbird.cloud.", Qtype: dns.TypeA, Qclass: dns.ClassINET}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New Config Should Succeed",
|
||||||
|
initLocalZones: []nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: 1, Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}},
|
||||||
|
initUpstreamMap: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: "netbird.cloud",
|
||||||
|
handler: &mockHandler{},
|
||||||
|
priority: PriorityUpstream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
Domains: []string{"netbird.io"},
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpstreamMap: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: "netbird.io",
|
||||||
|
priority: PriorityUpstream,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "netbird.cloud",
|
||||||
|
priority: PriorityLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLocalQs: []dns.Question{{Name: zoneRecords[0].Name, Qtype: 1, Qclass: 1}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Smaller Config Serial Should Be Skipped",
|
||||||
|
initLocalZones: []nbdns.CustomZone{},
|
||||||
|
initUpstreamMap: nil,
|
||||||
|
initSerial: 2,
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
|
||||||
|
initLocalZones: []nbdns.CustomZone{},
|
||||||
|
initUpstreamMap: nil,
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid NS Group Nameservers list Should Fail",
|
||||||
|
initLocalZones: []nbdns.CustomZone{},
|
||||||
|
initUpstreamMap: nil,
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Custom Zone Records list Should Skip",
|
||||||
|
initLocalZones: []nbdns.CustomZone{},
|
||||||
|
initUpstreamMap: nil,
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
Primary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpstreamMap: []handlerWrapper{{
|
||||||
|
domain: ".",
|
||||||
|
priority: PriorityDefault,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Config Should Succeed and Clean Maps",
|
||||||
|
initLocalZones: []nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}},
|
||||||
|
initUpstreamMap: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: zoneRecords[0].Name,
|
||||||
|
handler: &mockHandler{},
|
||||||
|
priority: PriorityUpstream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{ServiceEnable: true},
|
||||||
|
expectedUpstreamMap: nil,
|
||||||
|
expectedLocalQs: []dns.Question{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Disabled Service Should clean map",
|
||||||
|
initLocalZones: []nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}},
|
||||||
|
initUpstreamMap: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: zoneRecords[0].Name,
|
||||||
|
handler: &mockHandler{},
|
||||||
|
priority: PriorityUpstream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initSerial: 0,
|
||||||
|
inputSerial: 1,
|
||||||
|
inputUpdate: nbdns.Config{ServiceEnable: false},
|
||||||
|
expectedUpstreamMap: nil,
|
||||||
|
expectedLocalQs: []dns.Question{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
privKey, _ := wgtypes.GenerateKey()
|
||||||
|
newNet, err := stdnet.NewNet(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := iface.WGIFaceOpts{
|
||||||
|
IFaceName: fmt.Sprintf("utun230%d", n),
|
||||||
|
Address: wgaddr.MustParseWGAddress(fmt.Sprintf("100.66.100.%d/32", n+1)),
|
||||||
|
WGPort: 33100,
|
||||||
|
WGPrivKey: privKey.String(),
|
||||||
|
MTU: iface.DefaultMTU,
|
||||||
|
TransportNet: newNet,
|
||||||
|
}
|
||||||
|
|
||||||
|
wgIface, err := iface.NewWGIFace(opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = wgIface.Create()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = wgIface.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
||||||
|
WgInterface: wgIface,
|
||||||
|
CustomAddress: "",
|
||||||
|
StatusRecorder: peer.NewRecorder("mgm"),
|
||||||
|
StateManager: nil,
|
||||||
|
DisableSys: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = dnsServer.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = dnsServer.hostManager.restoreHostDNS()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dnsServer.dnsMuxHandlers = testCase.initUpstreamMap
|
||||||
|
dnsServer.localResolver.Update(testCase.initLocalZones)
|
||||||
|
dnsServer.updateSerial = testCase.initSerial
|
||||||
|
|
||||||
|
err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
|
||||||
|
if err != nil {
|
||||||
|
if testCase.shouldFail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("update dns server should not fail, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dnsServer.dnsMuxHandlers) != len(testCase.expectedUpstreamMap) {
|
||||||
|
t.Fatalf("update upstream failed, map size is different than expected, want %d, got %d", len(testCase.expectedUpstreamMap), len(dnsServer.dnsMuxHandlers))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expected := range testCase.expectedUpstreamMap {
|
||||||
|
found := false
|
||||||
|
for _, got := range dnsServer.dnsMuxHandlers {
|
||||||
|
if got.domain == expected.domain && got.priority == expected.priority {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("update upstream failed, handler for domain=%s priority=%d not found in dnsMuxHandlers: %#v", expected.domain, expected.priority, dnsServer.dnsMuxHandlers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, q := range testCase.expectedLocalQs {
|
||||||
|
dnsServer.localResolver.ServeDNS(responseWriter, &dns.Msg{
|
||||||
|
Question: []dns.Question{q},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(testCase.expectedLocalQs) > 0 {
|
||||||
|
assert.NotNil(t, responseMSG, "response message should not be nil")
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, responseMSG.Rcode, "response code should be success")
|
||||||
|
assert.NotEmpty(t, responseMSG.Answer, "response message should have answers")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
||||||
|
ov := os.Getenv("NB_WG_KERNEL_DISABLED")
|
||||||
|
defer t.Setenv("NB_WG_KERNEL_DISABLED", ov)
|
||||||
|
|
||||||
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
newNet, err := stdnet.NewNet(context.Background(), []string{"utun2301"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("create stdnet: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, _ := wgtypes.GeneratePrivateKey()
|
||||||
|
opts := iface.WGIFaceOpts{
|
||||||
|
IFaceName: "utun2301",
|
||||||
|
Address: wgaddr.MustParseWGAddress("100.66.100.1/32"),
|
||||||
|
WGPort: 33100,
|
||||||
|
WGPrivKey: privKey.String(),
|
||||||
|
MTU: iface.DefaultMTU,
|
||||||
|
TransportNet: newNet,
|
||||||
|
}
|
||||||
|
wgIface, err := iface.NewWGIFace(opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("build interface wireguard: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wgIface.Create()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("create and init wireguard interface: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = wgIface.Close(); err != nil {
|
||||||
|
t.Logf("close wireguard interface: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
packetfilter := pfmock.NewMockPacketFilter(ctrl)
|
||||||
|
packetfilter.EXPECT().FilterOutbound(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
packetfilter.EXPECT().SetUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
packetfilter.EXPECT().SetTCPPacketHook(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
|
if err := wgIface.SetFilter(packetfilter); err != nil {
|
||||||
|
t.Errorf("set packet filter: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
||||||
|
WgInterface: wgIface,
|
||||||
|
CustomAddress: "",
|
||||||
|
StatusRecorder: peer.NewRecorder("mgm"),
|
||||||
|
StateManager: nil,
|
||||||
|
DisableSys: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("create DNS server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dnsServer.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("run DNS server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = dnsServer.hostManager.restoreHostDNS(); err != nil {
|
||||||
|
t.Logf("restore DNS settings on the host: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dnsServer.dnsMuxHandlers = []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: zoneRecords[0].Name,
|
||||||
|
handler: &local.Resolver{},
|
||||||
|
priority: PriorityUpstream,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dnsServer.localResolver.Update([]nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}})
|
||||||
|
dnsServer.updateSerial = 0
|
||||||
|
|
||||||
|
nameServers := []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.4.4"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
update := nbdns.Config{
|
||||||
|
ServiceEnable: true,
|
||||||
|
CustomZones: []nbdns.CustomZone{
|
||||||
|
{
|
||||||
|
Domain: "netbird.cloud",
|
||||||
|
Records: zoneRecords,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
|
{
|
||||||
|
Domains: []string{"netbird.io"},
|
||||||
|
NameServers: nameServers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameServers: nameServers,
|
||||||
|
Primary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server with regular configuration
|
||||||
|
if err := dnsServer.UpdateDNSServer(1, update); err != nil {
|
||||||
|
t.Fatalf("update dns server should not fail, got error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
update2 := update
|
||||||
|
update2.ServiceEnable = false
|
||||||
|
// Disable the server, stop the listener
|
||||||
|
if err := dnsServer.UpdateDNSServer(2, update2); err != nil {
|
||||||
|
t.Fatalf("update dns server should not fail, got error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
update3 := update2
|
||||||
|
update3.NameServerGroups = update3.NameServerGroups[:1]
|
||||||
|
// But service still get updates and we checking that we handle
|
||||||
|
// internal state in the right way
|
||||||
|
if err := dnsServer.UpdateDNSServer(3, update3); err != nil {
|
||||||
|
t.Fatalf("update dns server should not fail, got error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -23,7 +22,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
pfmock "github.com/netbirdio/netbird/client/iface/mocks"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns/local"
|
"github.com/netbirdio/netbird/client/internal/dns/local"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns/test"
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
@@ -104,481 +102,6 @@ func init() {
|
|||||||
formatter.SetTextFormatter(log.StandardLogger())
|
formatter.SetTextFormatter(log.StandardLogger())
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDummyHandler(d string, servers []nbdns.NameServer) *upstreamResolverBase {
|
|
||||||
var srvs []netip.AddrPort
|
|
||||||
for _, srv := range servers {
|
|
||||||
srvs = append(srvs, srv.AddrPort())
|
|
||||||
}
|
|
||||||
u := &upstreamResolverBase{
|
|
||||||
domain: domain.Domain(d),
|
|
||||||
cancel: func() {},
|
|
||||||
}
|
|
||||||
u.addRace(srvs)
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateDNSServer(t *testing.T) {
|
|
||||||
|
|
||||||
nameServers := []nbdns.NameServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("8.8.8.8"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: 53,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("8.8.4.4"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: 53,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
dummyHandler := local.NewResolver()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
initUpstreamMap registeredHandlerMap
|
|
||||||
initLocalZones []nbdns.CustomZone
|
|
||||||
initSerial uint64
|
|
||||||
inputSerial uint64
|
|
||||||
inputUpdate nbdns.Config
|
|
||||||
shouldFail bool
|
|
||||||
expectedUpstreamMap registeredHandlerMap
|
|
||||||
expectedLocalQs []dns.Question
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Initial Config Should Succeed",
|
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{
|
|
||||||
ServiceEnable: true,
|
|
||||||
CustomZones: []nbdns.CustomZone{
|
|
||||||
{
|
|
||||||
Domain: "netbird.cloud",
|
|
||||||
Records: zoneRecords,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
|
||||||
{
|
|
||||||
Domains: []string{"netbird.io"},
|
|
||||||
NameServers: nameServers,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NameServers: nameServers,
|
|
||||||
Primary: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedUpstreamMap: registeredHandlerMap{
|
|
||||||
generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{
|
|
||||||
domain: "netbird.io",
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityUpstream,
|
|
||||||
},
|
|
||||||
dummyHandler.ID(): handlerWrapper{
|
|
||||||
domain: "netbird.cloud",
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityLocal,
|
|
||||||
},
|
|
||||||
generateDummyHandler(".", nameServers).ID(): handlerWrapper{
|
|
||||||
domain: nbdns.RootZone,
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityDefault,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedLocalQs: []dns.Question{{Name: "peera.netbird.cloud.", Qtype: dns.TypeA, Qclass: dns.ClassINET}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New Config Should Succeed",
|
|
||||||
initLocalZones: []nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: 1, Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}},
|
|
||||||
initUpstreamMap: registeredHandlerMap{
|
|
||||||
generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{
|
|
||||||
domain: "netbird.cloud",
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityUpstream,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{
|
|
||||||
ServiceEnable: true,
|
|
||||||
CustomZones: []nbdns.CustomZone{
|
|
||||||
{
|
|
||||||
Domain: "netbird.cloud",
|
|
||||||
Records: zoneRecords,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
|
||||||
{
|
|
||||||
Domains: []string{"netbird.io"},
|
|
||||||
NameServers: nameServers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedUpstreamMap: registeredHandlerMap{
|
|
||||||
generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{
|
|
||||||
domain: "netbird.io",
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityUpstream,
|
|
||||||
},
|
|
||||||
"local-resolver": handlerWrapper{
|
|
||||||
domain: "netbird.cloud",
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityLocal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedLocalQs: []dns.Question{{Name: zoneRecords[0].Name, Qtype: 1, Qclass: 1}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Smaller Config Serial Should Be Skipped",
|
|
||||||
initLocalZones: []nbdns.CustomZone{},
|
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
|
||||||
initSerial: 2,
|
|
||||||
inputSerial: 1,
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
|
|
||||||
initLocalZones: []nbdns.CustomZone{},
|
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{
|
|
||||||
ServiceEnable: true,
|
|
||||||
CustomZones: []nbdns.CustomZone{
|
|
||||||
{
|
|
||||||
Domain: "netbird.cloud",
|
|
||||||
Records: zoneRecords,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
|
||||||
{
|
|
||||||
NameServers: nameServers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid NS Group Nameservers list Should Fail",
|
|
||||||
initLocalZones: []nbdns.CustomZone{},
|
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{
|
|
||||||
ServiceEnable: true,
|
|
||||||
CustomZones: []nbdns.CustomZone{
|
|
||||||
{
|
|
||||||
Domain: "netbird.cloud",
|
|
||||||
Records: zoneRecords,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
|
||||||
{
|
|
||||||
NameServers: nameServers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shouldFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid Custom Zone Records list Should Skip",
|
|
||||||
initLocalZones: []nbdns.CustomZone{},
|
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{
|
|
||||||
ServiceEnable: true,
|
|
||||||
CustomZones: []nbdns.CustomZone{
|
|
||||||
{
|
|
||||||
Domain: "netbird.cloud",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
|
||||||
{
|
|
||||||
NameServers: nameServers,
|
|
||||||
Primary: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).ID(): handlerWrapper{
|
|
||||||
domain: ".",
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityDefault,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Config Should Succeed and Clean Maps",
|
|
||||||
initLocalZones: []nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}},
|
|
||||||
initUpstreamMap: registeredHandlerMap{
|
|
||||||
generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{
|
|
||||||
domain: zoneRecords[0].Name,
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityUpstream,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{ServiceEnable: true},
|
|
||||||
expectedUpstreamMap: make(registeredHandlerMap),
|
|
||||||
expectedLocalQs: []dns.Question{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Disabled Service Should clean map",
|
|
||||||
initLocalZones: []nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}},
|
|
||||||
initUpstreamMap: registeredHandlerMap{
|
|
||||||
generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{
|
|
||||||
domain: zoneRecords[0].Name,
|
|
||||||
handler: dummyHandler,
|
|
||||||
priority: PriorityUpstream,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initSerial: 0,
|
|
||||||
inputSerial: 1,
|
|
||||||
inputUpdate: nbdns.Config{ServiceEnable: false},
|
|
||||||
expectedUpstreamMap: make(registeredHandlerMap),
|
|
||||||
expectedLocalQs: []dns.Question{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for n, testCase := range testCases {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
privKey, _ := wgtypes.GenerateKey()
|
|
||||||
newNet, err := stdnet.NewNet(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := iface.WGIFaceOpts{
|
|
||||||
IFaceName: fmt.Sprintf("utun230%d", n),
|
|
||||||
Address: wgaddr.MustParseWGAddress(fmt.Sprintf("100.66.100.%d/32", n+1)),
|
|
||||||
WGPort: 33100,
|
|
||||||
WGPrivKey: privKey.String(),
|
|
||||||
MTU: iface.DefaultMTU,
|
|
||||||
TransportNet: newNet,
|
|
||||||
}
|
|
||||||
|
|
||||||
wgIface, err := iface.NewWGIFace(opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = wgIface.Create()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err = wgIface.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Log(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
|
||||||
WgInterface: wgIface,
|
|
||||||
CustomAddress: "",
|
|
||||||
StatusRecorder: peer.NewRecorder("mgm"),
|
|
||||||
StateManager: nil,
|
|
||||||
DisableSys: false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = dnsServer.Initialize()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err = dnsServer.hostManager.restoreHostDNS()
|
|
||||||
if err != nil {
|
|
||||||
t.Log(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
dnsServer.dnsMuxMap = testCase.initUpstreamMap
|
|
||||||
dnsServer.localResolver.Update(testCase.initLocalZones)
|
|
||||||
dnsServer.updateSerial = testCase.initSerial
|
|
||||||
|
|
||||||
err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
|
|
||||||
if err != nil {
|
|
||||||
if testCase.shouldFail {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatalf("update dns server should not fail, got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(dnsServer.dnsMuxMap) != len(testCase.expectedUpstreamMap) {
|
|
||||||
t.Fatalf("update upstream failed, map size is different than expected, want %d, got %d", len(testCase.expectedUpstreamMap), len(dnsServer.dnsMuxMap))
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range testCase.expectedUpstreamMap {
|
|
||||||
_, found := dnsServer.dnsMuxMap[key]
|
|
||||||
if !found {
|
|
||||||
t.Fatalf("update upstream failed, key %s was not found in the dnsMuxMap: %#v", key, dnsServer.dnsMuxMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseMSG *dns.Msg
|
|
||||||
responseWriter := &test.MockResponseWriter{
|
|
||||||
WriteMsgFunc: func(m *dns.Msg) error {
|
|
||||||
responseMSG = m
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, q := range testCase.expectedLocalQs {
|
|
||||||
dnsServer.localResolver.ServeDNS(responseWriter, &dns.Msg{
|
|
||||||
Question: []dns.Question{q},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(testCase.expectedLocalQs) > 0 {
|
|
||||||
assert.NotNil(t, responseMSG, "response message should not be nil")
|
|
||||||
assert.Equal(t, dns.RcodeSuccess, responseMSG.Rcode, "response code should be success")
|
|
||||||
assert.NotEmpty(t, responseMSG.Answer, "response message should have answers")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|
||||||
ov := os.Getenv("NB_WG_KERNEL_DISABLED")
|
|
||||||
defer t.Setenv("NB_WG_KERNEL_DISABLED", ov)
|
|
||||||
|
|
||||||
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
|
||||||
newNet, err := stdnet.NewNet(context.Background(), []string{"utun2301"})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("create stdnet: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
privKey, _ := wgtypes.GeneratePrivateKey()
|
|
||||||
opts := iface.WGIFaceOpts{
|
|
||||||
IFaceName: "utun2301",
|
|
||||||
Address: wgaddr.MustParseWGAddress("100.66.100.1/32"),
|
|
||||||
WGPort: 33100,
|
|
||||||
WGPrivKey: privKey.String(),
|
|
||||||
MTU: iface.DefaultMTU,
|
|
||||||
TransportNet: newNet,
|
|
||||||
}
|
|
||||||
wgIface, err := iface.NewWGIFace(opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("build interface wireguard: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wgIface.Create()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("create and init wireguard interface: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err = wgIface.Close(); err != nil {
|
|
||||||
t.Logf("close wireguard interface: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
packetfilter := pfmock.NewMockPacketFilter(ctrl)
|
|
||||||
packetfilter.EXPECT().FilterOutbound(gomock.Any(), gomock.Any()).AnyTimes()
|
|
||||||
packetfilter.EXPECT().SetUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
|
||||||
packetfilter.EXPECT().SetTCPPacketHook(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
|
||||||
|
|
||||||
if err := wgIface.SetFilter(packetfilter); err != nil {
|
|
||||||
t.Errorf("set packet filter: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
|
||||||
WgInterface: wgIface,
|
|
||||||
CustomAddress: "",
|
|
||||||
StatusRecorder: peer.NewRecorder("mgm"),
|
|
||||||
StateManager: nil,
|
|
||||||
DisableSys: false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("create DNS server: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dnsServer.Initialize()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("run DNS server: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err = dnsServer.hostManager.restoreHostDNS(); err != nil {
|
|
||||||
t.Logf("restore DNS settings on the host: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
dnsServer.dnsMuxMap = registeredHandlerMap{
|
|
||||||
"id1": handlerWrapper{
|
|
||||||
domain: zoneRecords[0].Name,
|
|
||||||
handler: &local.Resolver{},
|
|
||||||
priority: PriorityUpstream,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dnsServer.localResolver.Update([]nbdns.CustomZone{{Domain: "netbird.cloud", Records: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}}}})
|
|
||||||
dnsServer.updateSerial = 0
|
|
||||||
|
|
||||||
nameServers := []nbdns.NameServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("8.8.8.8"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: 53,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("8.8.4.4"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: 53,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
update := nbdns.Config{
|
|
||||||
ServiceEnable: true,
|
|
||||||
CustomZones: []nbdns.CustomZone{
|
|
||||||
{
|
|
||||||
Domain: "netbird.cloud",
|
|
||||||
Records: zoneRecords,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
|
||||||
{
|
|
||||||
Domains: []string{"netbird.io"},
|
|
||||||
NameServers: nameServers,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NameServers: nameServers,
|
|
||||||
Primary: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the server with regular configuration
|
|
||||||
if err := dnsServer.UpdateDNSServer(1, update); err != nil {
|
|
||||||
t.Fatalf("update dns server should not fail, got error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
update2 := update
|
|
||||||
update2.ServiceEnable = false
|
|
||||||
// Disable the server, stop the listener
|
|
||||||
if err := dnsServer.UpdateDNSServer(2, update2); err != nil {
|
|
||||||
t.Fatalf("update dns server should not fail, got error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
update3 := update2
|
|
||||||
update3.NameServerGroups = update3.NameServerGroups[:1]
|
|
||||||
// But service still get updates and we checking that we handle
|
|
||||||
// internal state in the right way
|
|
||||||
if err := dnsServer.UpdateDNSServer(3, update3); err != nil {
|
|
||||||
t.Fatalf("update dns server should not fail, got error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDNSServerStartStop(t *testing.T) {
|
func TestDNSServerStartStop(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -1029,15 +552,15 @@ func (m *mockService) RegisterMux(string, dns.Handler) {}
|
|||||||
func (m *mockService) DeregisterMux(string) {}
|
func (m *mockService) DeregisterMux(string) {}
|
||||||
|
|
||||||
func TestDefaultServer_UpdateMux(t *testing.T) {
|
func TestDefaultServer_UpdateMux(t *testing.T) {
|
||||||
baseMatchHandlers := registeredHandlerMap{
|
baseMatchHandlers := []handlerWrapper{
|
||||||
"upstream-group1": {
|
{
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-group1",
|
Id: "upstream-group1",
|
||||||
},
|
},
|
||||||
priority: PriorityUpstream,
|
priority: PriorityUpstream,
|
||||||
},
|
},
|
||||||
"upstream-group2": {
|
{
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-group2",
|
Id: "upstream-group2",
|
||||||
@@ -1046,15 +569,15 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
baseRootHandlers := registeredHandlerMap{
|
baseRootHandlers := []handlerWrapper{
|
||||||
"upstream-root1": {
|
{
|
||||||
domain: ".",
|
domain: ".",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-root1",
|
Id: "upstream-root1",
|
||||||
},
|
},
|
||||||
priority: PriorityDefault,
|
priority: PriorityDefault,
|
||||||
},
|
},
|
||||||
"upstream-root2": {
|
{
|
||||||
domain: ".",
|
domain: ".",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-root2",
|
Id: "upstream-root2",
|
||||||
@@ -1063,22 +586,22 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
baseMixedHandlers := registeredHandlerMap{
|
baseMixedHandlers := []handlerWrapper{
|
||||||
"upstream-group1": {
|
{
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-group1",
|
Id: "upstream-group1",
|
||||||
},
|
},
|
||||||
priority: PriorityUpstream,
|
priority: PriorityUpstream,
|
||||||
},
|
},
|
||||||
"upstream-group2": {
|
{
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-group2",
|
Id: "upstream-group2",
|
||||||
},
|
},
|
||||||
priority: PriorityUpstream - 1,
|
priority: PriorityUpstream - 1,
|
||||||
},
|
},
|
||||||
"upstream-other": {
|
{
|
||||||
domain: "other.com",
|
domain: "other.com",
|
||||||
handler: &mockHandler{
|
handler: &mockHandler{
|
||||||
Id: "upstream-other",
|
Id: "upstream-other",
|
||||||
@@ -1089,7 +612,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
initialHandlers registeredHandlerMap
|
initialHandlers []handlerWrapper
|
||||||
updates []handlerWrapper
|
updates []handlerWrapper
|
||||||
expectedHandlers map[string]string // map[HandlerID]domain
|
expectedHandlers map[string]string // map[HandlerID]domain
|
||||||
description string
|
description string
|
||||||
@@ -1373,32 +896,38 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
server := &DefaultServer{
|
server := &DefaultServer{
|
||||||
dnsMuxMap: tt.initialHandlers,
|
dnsMuxHandlers: tt.initialHandlers,
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
service: &mockService{},
|
service: &mockService{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the update
|
// Perform the update
|
||||||
server.updateMux(tt.updates)
|
server.updateMux(tt.updates)
|
||||||
|
|
||||||
// Verify the results
|
// Verify the results
|
||||||
assert.Equal(t, len(tt.expectedHandlers), len(server.dnsMuxMap),
|
assert.Equal(t, len(tt.expectedHandlers), len(server.dnsMuxHandlers),
|
||||||
"Number of handlers after update doesn't match expected")
|
"Number of handlers after update doesn't match expected")
|
||||||
|
|
||||||
// Check each expected handler
|
// Check each expected handler
|
||||||
for id, expectedDomain := range tt.expectedHandlers {
|
for id, expectedDomain := range tt.expectedHandlers {
|
||||||
handler, exists := server.dnsMuxMap[types.HandlerID(id)]
|
var found *handlerWrapper
|
||||||
assert.True(t, exists, "Expected handler %s not found", id)
|
for i := range server.dnsMuxHandlers {
|
||||||
if exists {
|
if server.dnsMuxHandlers[i].handler.ID() == types.HandlerID(id) {
|
||||||
assert.Equal(t, expectedDomain, handler.domain,
|
found = &server.dnsMuxHandlers[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.NotNil(t, found, "Expected handler %s not found", id)
|
||||||
|
if found != nil {
|
||||||
|
assert.Equal(t, expectedDomain, found.domain,
|
||||||
"Domain mismatch for handler %s", id)
|
"Domain mismatch for handler %s", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify no unexpected handlers exist
|
// Verify no unexpected handlers exist
|
||||||
for HandlerID := range server.dnsMuxMap {
|
for _, entry := range server.dnsMuxHandlers {
|
||||||
_, expected := tt.expectedHandlers[string(HandlerID)]
|
_, expected := tt.expectedHandlers[string(entry.handler.ID())]
|
||||||
assert.True(t, expected, "Unexpected handler found: %s", HandlerID)
|
assert.True(t, expected, "Unexpected handler found: %s", entry.handler.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the handlerChain state and order
|
// Verify the handlerChain state and order
|
||||||
@@ -1413,7 +942,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
|
|
||||||
// Verify handler exists in mux
|
// Verify handler exists in mux
|
||||||
foundInMux := false
|
foundInMux := false
|
||||||
for _, muxEntry := range server.dnsMuxMap {
|
for _, muxEntry := range server.dnsMuxHandlers {
|
||||||
if chainEntry.Handler == muxEntry.handler &&
|
if chainEntry.Handler == muxEntry.handler &&
|
||||||
chainEntry.Priority == muxEntry.priority &&
|
chainEntry.Priority == muxEntry.priority &&
|
||||||
chainEntry.Pattern == dns.Fqdn(muxEntry.domain) {
|
chainEntry.Pattern == dns.Fqdn(muxEntry.domain) {
|
||||||
@@ -1422,12 +951,108 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, foundInMux,
|
assert.True(t, foundInMux,
|
||||||
"Handler in chain not found in dnsMuxMap")
|
"Handler in chain not found in dnsMuxHandlers")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chainHasPattern reports whether the handler chain holds an entry registered
|
||||||
|
// for the given fqdn pattern at the given priority.
|
||||||
|
func chainHasPattern(s *DefaultServer, pattern string, priority int) bool {
|
||||||
|
for _, h := range s.handlerChain.handlers {
|
||||||
|
if h.OrigPattern == pattern && h.Priority == priority {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDefaultServer_UpdateMux_SharedHandlerZoneRemoval verifies that updateMux
|
||||||
|
// tracks each (handler, domain) registration independently when one handler
|
||||||
|
// serves multiple zones. Every custom zone is served by the same handler
|
||||||
|
// instance (the local resolver, whose ID is the constant "local-resolver"), so
|
||||||
|
// removing one zone must deregister exactly that zone's chain entry and leave
|
||||||
|
// the others in place. Tracking registrations by handler ID alone collapses all
|
||||||
|
// zones onto one entry, leaving removed zones in the chain to answer
|
||||||
|
// authoritatively with no records.
|
||||||
|
func TestDefaultServer_UpdateMux_SharedHandlerZoneRemoval(t *testing.T) {
|
||||||
|
// One handler serves every custom zone, mirroring s.localResolver.
|
||||||
|
shared := &mockHandler{Id: "local-resolver"}
|
||||||
|
|
||||||
|
server := &DefaultServer{
|
||||||
|
handlerChain: NewHandlerChain(),
|
||||||
|
service: &mockService{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two custom zones under the same handler. The surviving zone is registered
|
||||||
|
// last, mirroring the management emission order.
|
||||||
|
server.updateMux([]handlerWrapper{
|
||||||
|
{domain: "userzone.test", handler: shared, priority: PriorityLocal},
|
||||||
|
{domain: "peerzone.test", handler: shared, priority: PriorityLocal},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.True(t, chainHasPattern(server, "userzone.test.", PriorityLocal),
|
||||||
|
"userzone.test should be registered after the first update")
|
||||||
|
require.True(t, chainHasPattern(server, "peerzone.test.", PriorityLocal),
|
||||||
|
"peerzone.test should be registered after the first update")
|
||||||
|
|
||||||
|
// Remove one zone, keep the other.
|
||||||
|
server.updateMux([]handlerWrapper{
|
||||||
|
{domain: "peerzone.test", handler: shared, priority: PriorityLocal},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(t, chainHasPattern(server, "peerzone.test.", PriorityLocal),
|
||||||
|
"peerzone.test should remain after removing userzone.test")
|
||||||
|
assert.False(t, chainHasPattern(server, "userzone.test.", PriorityLocal),
|
||||||
|
"userzone.test handler must be deregistered, not leaked in the chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDefaultServer_UpdateMux_PreservesLocalResolver verifies that updateMux
|
||||||
|
// does not tear down the shared local resolver during reconfiguration. The
|
||||||
|
// resolver is a process-lifetime singleton reused across config updates;
|
||||||
|
// Stop() cancels its lookup context (breaking external CNAME-target
|
||||||
|
// resolution) and clears its records. updateMux must deregister its chain
|
||||||
|
// entries without stopping it. Records surviving a teardown update is the
|
||||||
|
// observable proxy: Stop() would have cleared them.
|
||||||
|
func TestDefaultServer_UpdateMux_PreservesLocalResolver(t *testing.T) {
|
||||||
|
resolver := local.NewResolver()
|
||||||
|
require.NoError(t, resolver.RegisterRecord(nbdns.SimpleRecord{
|
||||||
|
Name: "peer.netbird.cloud.",
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "10.0.0.1",
|
||||||
|
}))
|
||||||
|
|
||||||
|
server := &DefaultServer{
|
||||||
|
handlerChain: NewHandlerChain(),
|
||||||
|
service: &mockService{},
|
||||||
|
localResolver: resolver,
|
||||||
|
}
|
||||||
|
|
||||||
|
server.updateMux([]handlerWrapper{
|
||||||
|
{domain: "netbird.cloud", handler: resolver, priority: PriorityLocal},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove the zone. The resolver must survive so its records and lookup
|
||||||
|
// context stay intact for the next registration.
|
||||||
|
server.updateMux(nil)
|
||||||
|
|
||||||
|
var response *dns.Msg
|
||||||
|
resolver.ServeDNS(&test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
response = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, &dns.Msg{Question: []dns.Question{{Name: "peer.netbird.cloud.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}})
|
||||||
|
|
||||||
|
require.NotNil(t, response, "local resolver should answer after teardown")
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, response.Rcode,
|
||||||
|
"local resolver records must survive teardown; updateMux must not Stop() the shared resolver")
|
||||||
|
assert.NotEmpty(t, response.Answer, "answer should contain the surviving record")
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtraDomains(t *testing.T) {
|
func TestExtraDomains(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -2049,7 +1674,6 @@ func TestBuildUpstreamHandler_MergesGroupsPerDomain(t *testing.T) {
|
|||||||
localResolver: local.NewResolver(),
|
localResolver: local.NewResolver(),
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
hostManager: &noopHostConfigurator{},
|
hostManager: &noopHostConfigurator{},
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := []*nbdns.NameServerGroup{
|
groups := []*nbdns.NameServerGroup{
|
||||||
@@ -2207,7 +1831,7 @@ func TestEvaluateNSGroupHealth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// healthStubHandler is a minimal dnsMuxMap entry that exposes a fixed
|
// healthStubHandler is a minimal dnsMuxHandlers entry that exposes a fixed
|
||||||
// UpstreamHealth snapshot, letting tests drive recomputeNSGroupStates
|
// UpstreamHealth snapshot, letting tests drive recomputeNSGroupStates
|
||||||
// without spinning up real handlers.
|
// without spinning up real handlers.
|
||||||
type healthStubHandler struct {
|
type healthStubHandler struct {
|
||||||
@@ -2283,12 +1907,11 @@ func newProjTestFixture(t *testing.T) *projTestFixture {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
wgInterface: &mocWGIface{},
|
wgInterface: &mocWGIface{},
|
||||||
statusRecorder: recorder,
|
statusRecorder: recorder,
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
selectedRoutes: func() route.HAMap { return fx.selected },
|
selectedRoutes: func() route.HAMap { return fx.selected },
|
||||||
activeRoutes: func() route.HAMap { return fx.active },
|
activeRoutes: func() route.HAMap { return fx.active },
|
||||||
warningDelayBase: defaultWarningDelayBase,
|
warningDelayBase: defaultWarningDelayBase,
|
||||||
}
|
}
|
||||||
fx.server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: fx.stub, priority: PriorityUpstream}
|
fx.server.dnsMuxHandlers = []handlerWrapper{{domain: "example.com", handler: fx.stub, priority: PriorityUpstream}}
|
||||||
|
|
||||||
fx.server.mux.Lock()
|
fx.server.mux.Lock()
|
||||||
fx.server.updateNSGroupStates([]*nbdns.NameServerGroup{fx.group})
|
fx.server.updateNSGroupStates([]*nbdns.NameServerGroup{fx.group})
|
||||||
@@ -2395,7 +2018,6 @@ func TestProjection_OverlayAddrNoRouteDelaysWarning(t *testing.T) {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
wgInterface: &mocWGIface{},
|
wgInterface: &mocWGIface{},
|
||||||
statusRecorder: recorder,
|
statusRecorder: recorder,
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
selectedRoutes: func() route.HAMap { return nil },
|
selectedRoutes: func() route.HAMap { return nil },
|
||||||
activeRoutes: func() route.HAMap { return nil },
|
activeRoutes: func() route.HAMap { return nil },
|
||||||
warningDelayBase: 50 * time.Millisecond,
|
warningDelayBase: 50 * time.Millisecond,
|
||||||
@@ -2407,7 +2029,7 @@ func TestProjection_OverlayAddrNoRouteDelaysWarning(t *testing.T) {
|
|||||||
stub := &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{
|
stub := &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{
|
||||||
overlayPeer: {LastFail: time.Now(), LastErr: "timeout"},
|
overlayPeer: {LastFail: time.Now(), LastErr: "timeout"},
|
||||||
}}
|
}}
|
||||||
server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: stub, priority: PriorityUpstream}
|
server.dnsMuxHandlers = []handlerWrapper{{domain: "example.com", handler: stub, priority: PriorityUpstream}}
|
||||||
|
|
||||||
server.mux.Lock()
|
server.mux.Lock()
|
||||||
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
||||||
@@ -2444,7 +2066,6 @@ func TestProjection_StopClearsHealthState(t *testing.T) {
|
|||||||
service: NewServiceViaMemory(wgIface),
|
service: NewServiceViaMemory(wgIface),
|
||||||
hostManager: &noopHostConfigurator{},
|
hostManager: &noopHostConfigurator{},
|
||||||
extraDomains: map[domain.Domain]int{},
|
extraDomains: map[domain.Domain]int{},
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
statusRecorder: peer.NewRecorder("mgm"),
|
statusRecorder: peer.NewRecorder("mgm"),
|
||||||
selectedRoutes: func() route.HAMap { return nil },
|
selectedRoutes: func() route.HAMap { return nil },
|
||||||
activeRoutes: func() route.HAMap { return nil },
|
activeRoutes: func() route.HAMap { return nil },
|
||||||
@@ -2459,7 +2080,7 @@ func TestProjection_StopClearsHealthState(t *testing.T) {
|
|||||||
NameServers: []nbdns.NameServer{{IP: srv.Addr(), NSType: nbdns.UDPNameServerType, Port: int(srv.Port())}},
|
NameServers: []nbdns.NameServer{{IP: srv.Addr(), NSType: nbdns.UDPNameServerType, Port: int(srv.Port())}},
|
||||||
}
|
}
|
||||||
stub := &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{srv: {LastOk: time.Now()}}}
|
stub := &healthStubHandler{health: map[netip.AddrPort]UpstreamHealth{srv: {LastOk: time.Now()}}}
|
||||||
server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: stub, priority: PriorityUpstream}
|
server.dnsMuxHandlers = []handlerWrapper{{domain: "example.com", handler: stub, priority: PriorityUpstream}}
|
||||||
|
|
||||||
server.mux.Lock()
|
server.mux.Lock()
|
||||||
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
||||||
@@ -2484,6 +2105,32 @@ func TestProjection_StopClearsHealthState(t *testing.T) {
|
|||||||
// rule 3: startup failures while the peer is handshaking, then the peer
|
// rule 3: startup failures while the peer is handshaking, then the peer
|
||||||
// comes up and a query succeeds before the grace window elapses. No
|
// comes up and a query succeeds before the grace window elapses. No
|
||||||
// warning should ever have fired, and no recovery either.
|
// warning should ever have fired, and no recovery either.
|
||||||
|
func TestWarningDelayBaseFromEnv(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
set bool
|
||||||
|
val string
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
{name: "unset uses default", set: false, want: defaultWarningDelayBase},
|
||||||
|
{name: "valid override", set: true, val: "90s", want: 90 * time.Second},
|
||||||
|
{name: "valid minutes", set: true, val: "2m", want: 2 * time.Minute},
|
||||||
|
{name: "invalid falls back", set: true, val: "notaduration", want: defaultWarningDelayBase},
|
||||||
|
{name: "zero falls back", set: true, val: "0s", want: defaultWarningDelayBase},
|
||||||
|
{name: "negative falls back", set: true, val: "-30s", want: defaultWarningDelayBase},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Setenv(envWarningDelay, tc.val)
|
||||||
|
if !tc.set {
|
||||||
|
os.Unsetenv(envWarningDelay)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.want, warningDelayBaseFromEnv(), "grace window base")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProjection_OverlayRecoversDuringGrace(t *testing.T) {
|
func TestProjection_OverlayRecoversDuringGrace(t *testing.T) {
|
||||||
fx := newProjTestFixture(t)
|
fx := newProjTestFixture(t)
|
||||||
fx.server.warningDelayBase = 200 * time.Millisecond
|
fx.server.warningDelayBase = 200 * time.Millisecond
|
||||||
@@ -2595,7 +2242,6 @@ func TestProjection_MixedGroupEmitsImmediately(t *testing.T) {
|
|||||||
server := &DefaultServer{
|
server := &DefaultServer{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
statusRecorder: recorder,
|
statusRecorder: recorder,
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
selectedRoutes: func() route.HAMap { return overlayMap },
|
selectedRoutes: func() route.HAMap { return overlayMap },
|
||||||
activeRoutes: func() route.HAMap { return nil },
|
activeRoutes: func() route.HAMap { return nil },
|
||||||
warningDelayBase: time.Hour,
|
warningDelayBase: time.Hour,
|
||||||
@@ -2613,7 +2259,7 @@ func TestProjection_MixedGroupEmitsImmediately(t *testing.T) {
|
|||||||
overlay: {LastFail: time.Now(), LastErr: "timeout"},
|
overlay: {LastFail: time.Now(), LastErr: "timeout"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
server.dnsMuxMap["example.com"] = handlerWrapper{domain: "example.com", handler: stub, priority: PriorityUpstream}
|
server.dnsMuxHandlers = []handlerWrapper{{domain: "example.com", handler: stub, priority: PriorityUpstream}}
|
||||||
|
|
||||||
server.mux.Lock()
|
server.mux.Lock()
|
||||||
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
server.updateNSGroupStates([]*nbdns.NameServerGroup{group})
|
||||||
@@ -2640,7 +2286,6 @@ func TestDNSLoopPrevention(t *testing.T) {
|
|||||||
localResolver: local.NewResolver(),
|
localResolver: local.NewResolver(),
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
hostManager: &noopHostConfigurator{},
|
hostManager: &noopHostConfigurator{},
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@@ -443,29 +443,32 @@ func (u *upstreamResolverBase) queryUpstream(parentCtx context.Context, r *dns.M
|
|||||||
return raceResult{}, &upstreamFailure{upstream: upstream, reason: "no response"}
|
return raceResult{}, &upstreamFailure{upstream: upstream, reason: "no response"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A valid response means the upstream is reachable, whatever the Rcode.
|
||||||
|
u.markUpstreamOk(upstream)
|
||||||
|
|
||||||
proto := ""
|
proto := ""
|
||||||
if upstreamProto != nil {
|
if upstreamProto != nil {
|
||||||
proto = upstreamProto.protocol
|
proto = upstreamProto.protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if rm.Rcode == dns.RcodeServerFailure || rm.Rcode == dns.RcodeRefused {
|
if rm.Rcode == dns.RcodeServerFailure || rm.Rcode == dns.RcodeRefused {
|
||||||
|
// SERVFAIL and REFUSED are per-question outcomes (DNSSEC-bogus names,
|
||||||
|
// refused zones, transient recursion errors), not reachability
|
||||||
|
// problems: fail over for a better answer but keep the upstream healthy.
|
||||||
if code, ok := nonRetryableEDE(rm); ok {
|
if code, ok := nonRetryableEDE(rm); ok {
|
||||||
if !hadEdns {
|
if !hadEdns {
|
||||||
stripOPT(rm)
|
resutil.StripOPT(rm)
|
||||||
}
|
}
|
||||||
u.markUpstreamOk(upstream)
|
|
||||||
return raceResult{msg: rm, upstream: upstream, protocol: proto, ede: edeName(code)}, nil
|
return raceResult{msg: rm, upstream: upstream, protocol: proto, ede: edeName(code)}, nil
|
||||||
}
|
}
|
||||||
reason := dns.RcodeToString[rm.Rcode]
|
reason := dns.RcodeToString[rm.Rcode]
|
||||||
u.markUpstreamFail(upstream, reason)
|
|
||||||
return raceResult{}, &upstreamFailure{upstream: upstream, reason: reason}
|
return raceResult{}, &upstreamFailure{upstream: upstream, reason: reason}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hadEdns {
|
if !hadEdns {
|
||||||
stripOPT(rm)
|
resutil.StripOPT(rm)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.markUpstreamOk(upstream)
|
|
||||||
return raceResult{msg: rm, upstream: upstream, protocol: proto}, nil
|
return raceResult{msg: rm, upstream: upstream, protocol: proto}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,22 +523,6 @@ func upstreamUDPSize() uint16 {
|
|||||||
return dns.MinMsgSize
|
return dns.MinMsgSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// stripOPT removes any OPT pseudo-RRs from the response's Extra section so
|
|
||||||
// the response complies with RFC 6891 when the client did not advertise EDNS0.
|
|
||||||
func stripOPT(rm *dns.Msg) {
|
|
||||||
if len(rm.Extra) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out := rm.Extra[:0]
|
|
||||||
for _, rr := range rm.Extra {
|
|
||||||
if _, ok := rr.(*dns.OPT); ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, rr)
|
|
||||||
}
|
|
||||||
rm.Extra = out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *upstreamResolverBase) handleUpstreamError(err error, upstream netip.AddrPort, startTime time.Time) *upstreamFailure {
|
func (u *upstreamResolverBase) handleUpstreamError(err error, upstream netip.AddrPort, startTime time.Time) *upstreamFailure {
|
||||||
if !errors.Is(err, context.DeadlineExceeded) && !isTimeout(err) {
|
if !errors.Is(err, context.DeadlineExceeded) && !isTimeout(err) {
|
||||||
return &upstreamFailure{upstream: upstream, reason: err.Error()}
|
return &upstreamFailure{upstream: upstream, reason: err.Error()}
|
||||||
|
|||||||
@@ -517,6 +517,78 @@ func TestUpstreamResolver_HealthTracking(t *testing.T) {
|
|||||||
assert.NotContains(t, health, bad, "sibling upstream should not be queried when primary answers")
|
assert.NotContains(t, health, bad, "sibling upstream should not be queried when primary answers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestUpstreamResolver_HealthTracking_ResponseMeansReachable verifies that an
|
||||||
|
// upstream which answers with SERVFAIL or REFUSED is recorded as healthy:
|
||||||
|
// those are per-question outcomes from a reachable server and must not mark
|
||||||
|
// the upstream unhealthy. Only transport failures (timeouts) do.
|
||||||
|
func TestUpstreamResolver_HealthTracking_ResponseMeansReachable(t *testing.T) {
|
||||||
|
a := netip.MustParseAddrPort("192.0.2.10:53")
|
||||||
|
b := netip.MustParseAddrPort("192.0.2.11:53")
|
||||||
|
timeoutErr := &net.OpError{Op: "read", Err: fmt.Errorf("i/o timeout")}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
respA mockUpstreamResponse
|
||||||
|
respB mockUpstreamResponse
|
||||||
|
wantHealthy bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both SERVFAIL are reachable",
|
||||||
|
respA: mockUpstreamResponse{msg: buildMockResponse(dns.RcodeServerFailure, "")},
|
||||||
|
respB: mockUpstreamResponse{msg: buildMockResponse(dns.RcodeServerFailure, "")},
|
||||||
|
wantHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both REFUSED are reachable",
|
||||||
|
respA: mockUpstreamResponse{msg: buildMockResponse(dns.RcodeRefused, "")},
|
||||||
|
respB: mockUpstreamResponse{msg: buildMockResponse(dns.RcodeRefused, "")},
|
||||||
|
wantHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "timeout marks unhealthy",
|
||||||
|
respA: mockUpstreamResponse{err: timeoutErr},
|
||||||
|
respB: mockUpstreamResponse{err: timeoutErr},
|
||||||
|
wantHealthy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mockClient := &mockUpstreamResolverPerServer{
|
||||||
|
responses: map[string]mockUpstreamResponse{
|
||||||
|
a.String(): tc.respA,
|
||||||
|
b.String(): tc.respB,
|
||||||
|
},
|
||||||
|
rtt: time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resolver := &upstreamResolverBase{
|
||||||
|
ctx: ctx,
|
||||||
|
upstreamClient: mockClient,
|
||||||
|
upstreamTimeout: UpstreamTimeout,
|
||||||
|
}
|
||||||
|
resolver.addRace([]netip.AddrPort{a, b})
|
||||||
|
|
||||||
|
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, a, "primary upstream should have a health record")
|
||||||
|
if tc.wantHealthy {
|
||||||
|
assert.False(t, health[a].LastOk.IsZero(), "responding upstream should have LastOk set")
|
||||||
|
assert.True(t, health[a].LastFail.IsZero(), "responding upstream should not be marked failed")
|
||||||
|
assert.Empty(t, health[a].LastErr, "responding upstream should have no error")
|
||||||
|
} else {
|
||||||
|
assert.False(t, health[a].LastFail.IsZero(), "timed-out upstream should be marked failed")
|
||||||
|
assert.NotEmpty(t, health[a].LastErr, "timed-out upstream should record an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatFailures(t *testing.T) {
|
func TestFormatFailures(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -913,19 +985,6 @@ func TestEDEName(t *testing.T) {
|
|||||||
assert.Equal(t, "EDE 9999", edeName(9999), "unknown code falls back to numeric")
|
assert.Equal(t, "EDE 9999", edeName(9999), "unknown code falls back to numeric")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStripOPT(t *testing.T) {
|
|
||||||
rm := &dns.Msg{
|
|
||||||
Extra: []dns.RR{
|
|
||||||
&dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}},
|
|
||||||
&dns.A{Hdr: dns.RR_Header{Name: "x.", Rrtype: dns.TypeA}, A: net.IPv4(1, 2, 3, 4)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
stripOPT(rm)
|
|
||||||
assert.Len(t, rm.Extra, 1, "OPT should be removed, A kept")
|
|
||||||
_, isOPT := rm.Extra[0].(*dns.OPT)
|
|
||||||
assert.False(t, isOPT, "remaining record must not be OPT")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpstreamResolver_NonRetryableEDEShortCircuits(t *testing.T) {
|
func TestUpstreamResolver_NonRetryableEDEShortCircuits(t *testing.T) {
|
||||||
upstream1 := netip.MustParseAddrPort("192.0.2.1:53")
|
upstream1 := netip.MustParseAddrPort("192.0.2.1:53")
|
||||||
upstream2 := netip.MustParseAddrPort("192.0.2.2:53")
|
upstream2 := netip.MustParseAddrPort("192.0.2.2:53")
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ import (
|
|||||||
const errResolveFailed = "failed to resolve query for domain=%s: %v"
|
const errResolveFailed = "failed to resolve query for domain=%s: %v"
|
||||||
const upstreamTimeout = 15 * time.Second
|
const upstreamTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
// EDE info codes the forwarder emits on upstream failures so the querying
|
||||||
|
// client can see the reason without inspecting this peer's logs. They live in
|
||||||
|
// the RFC 8914 Private Use range (49152-65535); the Go resolver never exposes a
|
||||||
|
// real upstream EDE here, so these cannot collide with a genuine code.
|
||||||
|
const (
|
||||||
|
edeNetbirdUpstreamTimeout uint16 = 49152
|
||||||
|
edeNetbirdUpstreamFailure uint16 = 49153
|
||||||
|
)
|
||||||
|
|
||||||
type resolver interface {
|
type resolver interface {
|
||||||
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
||||||
}
|
}
|
||||||
@@ -220,7 +229,7 @@ func (f *DNSForwarder) handleDNSQuery(logger *log.Entry, w dns.ResponseWriter, q
|
|||||||
|
|
||||||
result := resutil.LookupIP(ctx, f.resolver, network, qname, question.Qtype)
|
result := resutil.LookupIP(ctx, f.resolver, network, qname, question.Qtype)
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
f.handleDNSError(ctx, logger, w, question, resp, qname, result, startTime)
|
f.handleDNSError(ctx, logger, w, question, resp, qname, result, query.IsEdns0() != nil, startTime)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +342,7 @@ func (f *DNSForwarder) handleDNSError(
|
|||||||
resp *dns.Msg,
|
resp *dns.Msg,
|
||||||
domain string,
|
domain string,
|
||||||
result resutil.LookupResult,
|
result resutil.LookupResult,
|
||||||
|
reqHasEdns bool,
|
||||||
startTime time.Time,
|
startTime time.Time,
|
||||||
) {
|
) {
|
||||||
qType := question.Qtype
|
qType := question.Qtype
|
||||||
@@ -374,6 +384,10 @@ func (f *DNSForwarder) handleDNSError(
|
|||||||
logger.Warnf(errResolveFailed, domain, result.Err)
|
logger.Warnf(errResolveFailed, domain, result.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if reqHasEdns {
|
||||||
|
attachEDE(resp, edeCodeFor(dnsErr), edeText(dnsErr))
|
||||||
|
}
|
||||||
|
|
||||||
f.writeResponse(logger, w, resp, domain, startTime)
|
f.writeResponse(logger, w, resp, domain, startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,3 +428,33 @@ func (f *DNSForwarder) getMatchingEntries(domain string) (route.ResID, []*Forwar
|
|||||||
|
|
||||||
return selectedResId, matches
|
return selectedResId, matches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// edeCodeFor maps an upstream lookup error to the NetBird EDE info code.
|
||||||
|
func edeCodeFor(dnsErr *net.DNSError) uint16 {
|
||||||
|
if dnsErr != nil && dnsErr.IsTimeout {
|
||||||
|
return edeNetbirdUpstreamTimeout
|
||||||
|
}
|
||||||
|
return edeNetbirdUpstreamFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
// edeText builds the EDE extra-text describing the class of upstream failure.
|
||||||
|
// It deliberately omits the upstream server address, which may be an internal
|
||||||
|
// resolver and is exposed to any client permitted to use the route; the full
|
||||||
|
// detail stays in the forwarder's local log.
|
||||||
|
func edeText(dnsErr *net.DNSError) string {
|
||||||
|
if dnsErr != nil && dnsErr.IsTimeout {
|
||||||
|
return "netbird forwarder: upstream timeout"
|
||||||
|
}
|
||||||
|
return "netbird forwarder: upstream failure"
|
||||||
|
}
|
||||||
|
|
||||||
|
// attachEDE adds an Extended DNS Error (RFC 8914) option to the response,
|
||||||
|
// creating the OPT pseudo-record if the response does not already carry one.
|
||||||
|
func attachEDE(resp *dns.Msg, code uint16, text string) {
|
||||||
|
opt := resp.IsEdns0()
|
||||||
|
if opt == nil {
|
||||||
|
resp.SetEdns0(dns.DefaultMsgSize, false)
|
||||||
|
opt = resp.IsEdns0()
|
||||||
|
}
|
||||||
|
opt.Option = append(opt.Option, &dns.EDNS0_EDE{InfoCode: code, ExtraText: text})
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/resutil"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns/test"
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
@@ -617,6 +618,85 @@ func TestDNSForwarder_ResponseCodes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDNSForwarder_UpstreamFailureEDE(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
lookupErr error
|
||||||
|
reqEdns bool
|
||||||
|
wantEDE bool
|
||||||
|
wantCode uint16
|
||||||
|
wantTextHas string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "timeout with edns0",
|
||||||
|
lookupErr: &net.DNSError{Err: "i/o timeout", Server: "10.0.0.53:53", IsTimeout: true},
|
||||||
|
reqEdns: true,
|
||||||
|
wantEDE: true,
|
||||||
|
wantCode: edeNetbirdUpstreamTimeout,
|
||||||
|
wantTextHas: "netbird forwarder: upstream timeout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server failure with edns0",
|
||||||
|
lookupErr: &net.DNSError{Err: "server misbehaving", Server: "10.0.0.53:53"},
|
||||||
|
reqEdns: true,
|
||||||
|
wantEDE: true,
|
||||||
|
wantCode: edeNetbirdUpstreamFailure,
|
||||||
|
wantTextHas: "netbird forwarder: upstream failure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no edns0 in request omits ede",
|
||||||
|
lookupErr: &net.DNSError{Err: "server misbehaving", Server: "10.0.0.53:53"},
|
||||||
|
reqEdns: false,
|
||||||
|
wantEDE: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockResolver := &MockResolver{}
|
||||||
|
forwarder := NewDNSForwarder(netip.MustParseAddrPort("127.0.0.1:0"), 300, nil, &peer.Status{}, nil)
|
||||||
|
forwarder.resolver = mockResolver
|
||||||
|
|
||||||
|
d, err := domain.FromString("example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
forwarder.UpdateDomains([]*ForwarderEntry{{Domain: d, ResID: "test-res"}})
|
||||||
|
|
||||||
|
mockResolver.On("LookupNetIP", mock.Anything, "ip4", "example.com.").
|
||||||
|
Return([]netip.Addr(nil), tt.lookupErr).Once()
|
||||||
|
|
||||||
|
query := &dns.Msg{}
|
||||||
|
query.SetQuestion("example.com.", dns.TypeA)
|
||||||
|
if tt.reqEdns {
|
||||||
|
query.SetEdns0(dns.DefaultMsgSize, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var writtenResp *dns.Msg
|
||||||
|
mockWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
writtenResp = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
forwarder.handleDNSQuery(log.NewEntry(log.StandardLogger()), mockWriter, query, time.Now())
|
||||||
|
mockResolver.AssertExpectations(t)
|
||||||
|
|
||||||
|
require.NotNil(t, writtenResp, "expected a response")
|
||||||
|
assert.Equal(t, dns.RcodeServerFailure, writtenResp.Rcode, "upstream failure must be SERVFAIL")
|
||||||
|
|
||||||
|
ede, ok := resutil.ExtractEDE(writtenResp)
|
||||||
|
if !tt.wantEDE {
|
||||||
|
assert.False(t, ok, "response must not carry EDE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.True(t, ok, "response must carry EDE")
|
||||||
|
assert.Equal(t, tt.wantCode, ede.InfoCode, "EDE info code")
|
||||||
|
assert.Contains(t, ede.ExtraText, tt.wantTextHas, "EDE extra-text")
|
||||||
|
assert.NotContains(t, ede.ExtraText, "10.0.0.53", "must not leak upstream server address")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDNSForwarder_TCPTruncation(t *testing.T) {
|
func TestDNSForwarder_TCPTruncation(t *testing.T) {
|
||||||
// Test that large UDP responses are truncated with TC bit set
|
// Test that large UDP responses are truncated with TC bit set
|
||||||
mockResolver := &MockResolver{}
|
mockResolver := &MockResolver{}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -72,6 +71,7 @@ import (
|
|||||||
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.
|
||||||
@@ -86,6 +86,8 @@ const (
|
|||||||
|
|
||||||
var ErrResetConnection = fmt.Errorf("reset connection")
|
var ErrResetConnection = fmt.Errorf("reset connection")
|
||||||
|
|
||||||
|
var ErrEngineAlreadyStarted = errors.New("engine already started")
|
||||||
|
|
||||||
type EngineConfig struct {
|
type EngineConfig struct {
|
||||||
WgPort int
|
WgPort int
|
||||||
WgIfaceName string
|
WgIfaceName string
|
||||||
@@ -148,6 +150,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.
|
||||||
@@ -195,6 +201,8 @@ type Engine struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
started bool
|
||||||
|
|
||||||
wgInterface WGIface
|
wgInterface WGIface
|
||||||
|
|
||||||
udpMux *udpmux.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
@@ -202,6 +210,12 @@ type Engine struct {
|
|||||||
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
||||||
networkSerial uint64
|
networkSerial uint64
|
||||||
|
|
||||||
|
// forwardingRules holds the ingress forward rules applied for the current target.
|
||||||
|
// Wholesale sections (incl. forward rules) run only on the first pass of a target;
|
||||||
|
// it is stashed here so the final, peer-converged pass can build the lazy-connection
|
||||||
|
// exclude list without recomputing them on every bounded peer pass.
|
||||||
|
forwardingRules []firewallManager.ForwardRule
|
||||||
|
|
||||||
networkMonitor *networkmonitor.NetworkMonitor
|
networkMonitor *networkmonitor.NetworkMonitor
|
||||||
|
|
||||||
sshServer sshServer
|
sshServer sshServer
|
||||||
@@ -226,11 +240,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
|
||||||
@@ -270,9 +289,15 @@ func NewEngine(
|
|||||||
services EngineServices,
|
services EngineServices,
|
||||||
mobileDep MobileDependency,
|
mobileDep MobileDependency,
|
||||||
) *Engine {
|
) *Engine {
|
||||||
|
// The engine is single-use: a fresh instance is built per connection
|
||||||
|
// cycle (see Client.run), so the run context is created once here rather
|
||||||
|
// than in Start.
|
||||||
|
ctx, cancel := context.WithCancel(clientCtx)
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
clientCtx: clientCtx,
|
clientCtx: clientCtx,
|
||||||
clientCancel: clientCancel,
|
clientCancel: clientCancel,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
signal: services.SignalClient,
|
signal: services.SignalClient,
|
||||||
signaler: peer.NewSignaler(services.SignalClient, config.WgPrivateKey),
|
signaler: peer.NewSignaler(services.SignalClient, config.WgPrivateKey),
|
||||||
mgmClient: services.MgmClient,
|
mgmClient: services.MgmClient,
|
||||||
@@ -292,6 +317,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())
|
||||||
@@ -304,8 +330,34 @@ func (e *Engine) Stop() error {
|
|||||||
log.Debugf("tried stopping engine that is nil")
|
log.Debugf("tried stopping engine that is nil")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
e.cancel()
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
|
|
||||||
|
e.stopLocked()
|
||||||
|
|
||||||
|
e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
timeout := e.calculateShutdownTimeout()
|
||||||
|
log.Debugf("waiting for goroutines to finish with timeout: %v", timeout)
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := waitWithContext(shutdownCtx, &e.shutdownWg); err != nil {
|
||||||
|
log.Warnf("shutdown timeout exceeded after %v, some goroutines may still be running", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("stopped Netbird Engine")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopLocked tears down everything Start may have brought up, in the order
|
||||||
|
// teardown requires (DNS before the interface goes down, flow manager after).
|
||||||
|
// The caller must hold syncMsgMux. It is shared by Stop and by Start's failure
|
||||||
|
// path, so a partially-initialized engine is cleaned up the same way; every
|
||||||
|
// step is nil-guarded. It does not wait on shutdownWg — the caller does that
|
||||||
|
// after releasing the lock, since the goroutines also take syncMsgMux.
|
||||||
|
func (e *Engine) stopLocked() {
|
||||||
if e.connMgr != nil {
|
if e.connMgr != nil {
|
||||||
e.connMgr.Close()
|
e.connMgr.Close()
|
||||||
}
|
}
|
||||||
@@ -356,10 +408,6 @@ func (e *Engine) Stop() error {
|
|||||||
// so dbus and friends don't complain because of a missing interface
|
// so dbus and friends don't complain because of a missing interface
|
||||||
e.stopDNSServer()
|
e.stopDNSServer()
|
||||||
|
|
||||||
if e.cancel != nil {
|
|
||||||
e.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
e.jobExecutorWG.Wait() // block until job goroutines finish
|
e.jobExecutorWG.Wait() // block until job goroutines finish
|
||||||
|
|
||||||
e.close()
|
e.close()
|
||||||
@@ -378,21 +426,6 @@ func (e *Engine) Stop() error {
|
|||||||
if err := e.stateManager.PersistState(context.Background()); err != nil {
|
if err := e.stateManager.PersistState(context.Background()); err != nil {
|
||||||
log.Errorf("failed to persist state: %v", err)
|
log.Errorf("failed to persist state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.syncMsgMux.Unlock()
|
|
||||||
|
|
||||||
timeout := e.calculateShutdownTimeout()
|
|
||||||
log.Debugf("waiting for goroutines to finish with timeout: %v", timeout)
|
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := waitWithContext(shutdownCtx, &e.shutdownWg); err != nil {
|
|
||||||
log.Warnf("shutdown timeout exceeded after %v, some goroutines may still be running", timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("stopped Netbird Engine")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateShutdownTimeout returns shutdown timeout: 10s base + 100ms per peer, capped at 30s.
|
// calculateShutdownTimeout returns shutdown timeout: 10s base + 100ms per peer, capped at 30s.
|
||||||
@@ -430,18 +463,38 @@ func waitWithContext(ctx context.Context, wg *sync.WaitGroup) error {
|
|||||||
// Start creates a new WireGuard tunnel interface and listens to events from Signal and Management services
|
// Start creates a new WireGuard tunnel interface and listens to events from Signal and Management services
|
||||||
// Connections to remote peers are not established here.
|
// Connections to remote peers are not established here.
|
||||||
// However, they will be established once an event with a list of peers to connect to will be received from Management Service
|
// However, they will be established once an event with a list of peers to connect to will be received from Management Service
|
||||||
func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) error {
|
func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) (err error) {
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
if err := iface.ValidateMTU(e.config.MTU); err != nil {
|
// The engine is single-use. Reject a duplicate start and a start on an
|
||||||
|
// already-stopped engine (run context cancelled).
|
||||||
|
if e.started {
|
||||||
|
return ErrEngineAlreadyStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctxErr := e.ctx.Err(); ctxErr != nil {
|
||||||
|
return fmt.Errorf("engine already stopped: %w", ctxErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.started = true
|
||||||
|
|
||||||
|
// Tear down any partially-initialized state on a failed start. Cancel the
|
||||||
|
// run context first so goroutines started before the failure (connMgr,
|
||||||
|
// srWatcher, monitors) unwind, then stopLocked mirrors Stop's teardown (we
|
||||||
|
// already hold syncMsgMux), cleaning up route/DNS/flow/state managers too,
|
||||||
|
// not just what close() covers.
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
e.cancel()
|
||||||
|
e.stopLocked()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = iface.ValidateMTU(e.config.MTU); err != nil {
|
||||||
return fmt.Errorf("invalid MTU configuration: %w", err)
|
return fmt.Errorf("invalid MTU configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.cancel != nil {
|
|
||||||
e.cancel()
|
|
||||||
}
|
|
||||||
e.ctx, e.cancel = context.WithCancel(e.clientCtx)
|
|
||||||
e.exposeManager = expose.NewManager(e.ctx, e.mgmClient)
|
e.exposeManager = expose.NewManager(e.ctx, e.mgmClient)
|
||||||
|
|
||||||
wgIface, err := e.newWgIface()
|
wgIface, err := e.newWgIface()
|
||||||
@@ -475,13 +528,11 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
|
|
||||||
initialRoutes, dnsConfig, dnsFeatureFlag, err := e.readInitialSettings()
|
initialRoutes, dnsConfig, dnsFeatureFlag, err := e.readInitialSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.close()
|
|
||||||
return fmt.Errorf("read initial settings: %w", err)
|
return fmt.Errorf("read initial settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsServer, err := e.newDnsServer(dnsConfig)
|
dnsServer, err := e.newDnsServer(dnsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.close()
|
|
||||||
return fmt.Errorf("create dns server: %w", err)
|
return fmt.Errorf("create dns server: %w", err)
|
||||||
}
|
}
|
||||||
e.dnsServer = dnsServer
|
e.dnsServer = dnsServer
|
||||||
@@ -516,12 +567,14 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
|
|
||||||
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())
|
||||||
e.close()
|
|
||||||
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()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +586,6 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
e.udpMux, err = e.wgInterface.Up()
|
e.udpMux, err = e.wgInterface.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to pull up wgInterface [%s]: %s", e.wgInterface.Name(), err.Error())
|
log.Errorf("failed to pull up wgInterface [%s]: %s", e.wgInterface.Name(), err.Error())
|
||||||
e.close()
|
|
||||||
return fmt.Errorf("up wg interface: %w", err)
|
return fmt.Errorf("up wg interface: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,9 +610,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
e.acl = acl.NewDefaultManager(e.firewall)
|
e.acl = acl.NewDefaultManager(e.firewall)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = e.dnsServer.Initialize()
|
if err := e.dnsServer.Initialize(); err != nil {
|
||||||
if err != nil {
|
|
||||||
e.close()
|
|
||||||
return fmt.Errorf("initialize dns server: %w", err)
|
return fmt.Errorf("initialize dns server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +622,9 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg)
|
e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg)
|
||||||
e.srWatcher.Start(peer.IsForceRelayed())
|
e.srWatcher.Start(peer.IsForceRelayed())
|
||||||
|
|
||||||
e.receiveSignalEvents()
|
if err = e.receiveSignalEvents(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
e.receiveManagementEvents()
|
e.receiveManagementEvents()
|
||||||
e.receiveJobEvents()
|
e.receiveJobEvents()
|
||||||
|
|
||||||
@@ -624,7 +676,6 @@ func (e *Engine) createFirewall() error {
|
|||||||
|
|
||||||
func (e *Engine) initFirewall() error {
|
func (e *Engine) initFirewall() error {
|
||||||
if err := e.routeManager.SetFirewall(e.firewall); err != nil {
|
if err := e.routeManager.SetFirewall(e.firewall); err != nil {
|
||||||
e.close()
|
|
||||||
return fmt.Errorf("set firewall: %w", err)
|
return fmt.Errorf("set firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +768,15 @@ func (e *Engine) blockLanAccess() {
|
|||||||
|
|
||||||
// modifyPeers updates peers that have been modified (e.g. IP address has been changed).
|
// modifyPeers updates peers that have been modified (e.g. IP address has been changed).
|
||||||
// It closes the existing connection, removes it from the peerConns map, and creates a new one.
|
// It closes the existing connection, removes it from the peerConns map, and creates a new one.
|
||||||
func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
// maxPeersPerSyncPass is the default per-pass cap on how many peers each of
|
||||||
|
// removePeers/modifyPeers/addNewPeers applies, so syncMsgMux is held only for a
|
||||||
|
// batch at a time and other subsystems can interleave between passes. It is
|
||||||
|
// passed in (not read globally) so tests can exercise the multi-pass path.
|
||||||
|
const maxPeersPerSyncPass = 300
|
||||||
|
|
||||||
|
// modifyPeers re-applies up to maxBatch changed peers per call. It returns true
|
||||||
|
// when more changed peers remained than the cap, so the caller re-runs.
|
||||||
|
func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig, maxBatch int) (bool, error) {
|
||||||
|
|
||||||
// first, check if peers have been modified
|
// first, check if peers have been modified
|
||||||
var modified []*mgmProto.RemotePeerConfig
|
var modified []*mgmProto.RemotePeerConfig
|
||||||
@@ -747,26 +806,32 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
more := false
|
||||||
|
if len(modified) > maxBatch {
|
||||||
|
modified = modified[:maxBatch]
|
||||||
|
more = true
|
||||||
|
}
|
||||||
|
|
||||||
// second, close all modified connections and remove them from the state map
|
// second, close all modified connections and remove them from the state map
|
||||||
for _, p := range modified {
|
for _, p := range modified {
|
||||||
err := e.removePeer(p.GetWgPubKey())
|
if err := e.removePeer(p.GetWgPubKey()); err != nil {
|
||||||
if err != nil {
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// third, add the peer connections again
|
// third, add the peer connections again
|
||||||
for _, p := range modified {
|
for _, p := range modified {
|
||||||
err := e.addNewPeer(p)
|
if err := e.addNewPeer(p); err != nil {
|
||||||
if err != nil {
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return more, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service.
|
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service.
|
||||||
// It also removes peers that have been modified (e.g. change of IP address). They will be added again in addPeers method.
|
// It also removes peers that have been modified (e.g. change of IP address). They will be added again in addPeers method.
|
||||||
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
// removePeers removes up to maxBatch peers per call. It returns true when more
|
||||||
|
// peers remained to remove than the cap, so the caller re-runs.
|
||||||
|
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig, maxBatch int) (bool, error) {
|
||||||
newPeers := make([]string, 0, len(peersUpdate))
|
newPeers := make([]string, 0, len(peersUpdate))
|
||||||
for _, p := range peersUpdate {
|
for _, p := range peersUpdate {
|
||||||
newPeers = append(newPeers, p.GetWgPubKey())
|
newPeers = append(newPeers, p.GetWgPubKey())
|
||||||
@@ -774,14 +839,19 @@ func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
|||||||
|
|
||||||
toRemove := util.SliceDiff(e.peerStore.PeersPubKey(), newPeers)
|
toRemove := util.SliceDiff(e.peerStore.PeersPubKey(), newPeers)
|
||||||
|
|
||||||
|
more := false
|
||||||
|
if len(toRemove) > maxBatch {
|
||||||
|
toRemove = toRemove[:maxBatch]
|
||||||
|
more = true
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range toRemove {
|
for _, p := range toRemove {
|
||||||
err := e.removePeer(p)
|
if err := e.removePeer(p); err != nil {
|
||||||
if err != nil {
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
log.Infof("removed peer %s", p)
|
log.Infof("removed peer %s", p)
|
||||||
}
|
}
|
||||||
return nil
|
return more, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) removeAllPeers() error {
|
func (e *Engine) removeAllPeers() error {
|
||||||
@@ -850,93 +920,117 @@ func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdate
|
|||||||
e.updateManager.SetVersion(autoUpdateSettings.Version, autoUpdateSettings.AlwaysUpdate)
|
e.updateManager.SetVersion(autoUpdateSettings.Version, autoUpdateSettings.AlwaysUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
// applySyncPass applies one bounded pass of the sync update under syncMsgMux and
|
||||||
started := time.Now()
|
// returns true if more peers remained than the per-pass cap. It is driven by the
|
||||||
defer func() {
|
// mapStateManager, which re-invokes it (releasing the lock between passes) until
|
||||||
duration := time.Since(started)
|
// the update is fully applied.
|
||||||
log.Infof("sync finished in %s", duration)
|
func (e *Engine) applySyncPass(update *mgmProto.SyncResponse, firstPass bool) (bool, error) {
|
||||||
e.clientMetrics.RecordSyncDuration(e.ctx, duration)
|
|
||||||
}()
|
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
// Check context INSIDE lock to ensure atomicity with shutdown
|
// Check context INSIDE lock to ensure atomicity with shutdown
|
||||||
if e.ctx.Err() != nil {
|
if e.ctx.Err() != nil {
|
||||||
return e.ctx.Err()
|
return false, e.ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil {
|
if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil {
|
||||||
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 false, 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 false, 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 false, err
|
||||||
}
|
|
||||||
|
|
||||||
nm := update.GetNetworkMap()
|
|
||||||
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 {
|
more, err := e.updateNetworkMap(nm, maxPeersPerSyncPass, firstPass)
|
||||||
return err
|
if err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.statusRecorder.PublishEvent(cProto.SystemEvent_INFO, cProto.SystemEvent_SYSTEM, "Network map updated", "", nil)
|
e.statusRecorder.PublishEvent(cProto.SystemEvent_INFO, cProto.SystemEvent_SYSTEM, "Network map updated", "", nil)
|
||||||
|
|
||||||
|
return more, 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
|
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) {
|
||||||
|
// Only persist updates that carry a network map. Config-only updates (e.g. relay
|
||||||
|
// token rotation, STUN/TURN) have a nil NetworkMap; persisting them would overwrite
|
||||||
|
// the last full map on disk and break restore-on-restart.
|
||||||
|
if update.GetNetworkMap() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1001,7 +1095,7 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error {
|
|||||||
}
|
}
|
||||||
e.checks = checks
|
e.checks = checks
|
||||||
|
|
||||||
info, err := system.GetInfoWithChecks(e.ctx, checks)
|
info, err := system.GetInfoWithChecks(e.ctx, checks, e.overlayAddresses()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to get system info with checks: %v", err)
|
log.Warnf("failed to get system info with checks: %v", err)
|
||||||
info = system.GetInfo(e.ctx)
|
info = system.GetInfo(e.ctx)
|
||||||
@@ -1032,6 +1126,20 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overlayAddresses returns our own WireGuard overlay address (v4 and v6) so it
|
||||||
|
// can be excluded from the reported network addresses; the interface coming and
|
||||||
|
// going otherwise churns the peer meta on the management server.
|
||||||
|
func (e *Engine) overlayAddresses() []netip.Addr {
|
||||||
|
var ips []netip.Addr
|
||||||
|
if e.config.WgAddr.IP.IsValid() {
|
||||||
|
ips = append(ips, e.config.WgAddr.IP)
|
||||||
|
}
|
||||||
|
if e.config.WgAddr.HasIPv6() {
|
||||||
|
ips = append(ips, e.config.WgAddr.IPv6)
|
||||||
|
}
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
||||||
if e.wgInterface == nil {
|
if e.wgInterface == nil {
|
||||||
return errors.New("wireguard interface is not initialized")
|
return errors.New("wireguard interface is not initialized")
|
||||||
@@ -1063,6 +1171,7 @@ 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)
|
||||||
|
|
||||||
@@ -1141,6 +1250,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)
|
||||||
},
|
},
|
||||||
@@ -1173,7 +1283,7 @@ func (e *Engine) receiveManagementEvents() {
|
|||||||
e.shutdownWg.Add(1)
|
e.shutdownWg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer e.shutdownWg.Done()
|
defer e.shutdownWg.Done()
|
||||||
info, err := system.GetInfoWithChecks(e.ctx, e.checks)
|
info, err := system.GetInfoWithChecks(e.ctx, e.checks, e.overlayAddresses()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to get system info with checks: %v", err)
|
log.Warnf("failed to get system info with checks: %v", err)
|
||||||
info = system.GetInfo(e.ctx)
|
info = system.GetInfo(e.ctx)
|
||||||
@@ -1197,7 +1307,19 @@ func (e *Engine) receiveManagementEvents() {
|
|||||||
e.config.DisableSSHAuth,
|
e.config.DisableSSHAuth,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = e.mgmClient.Sync(e.ctx, info, e.handleSync)
|
// The map-state manager converges the latest update in the background in
|
||||||
|
// bounded passes; the stream callback only hands it the newest target.
|
||||||
|
manager := newMapStateManager(e.applySyncPass, e.persistSyncResponse, func(d time.Duration) {
|
||||||
|
log.Infof("sync finished in %s", d)
|
||||||
|
e.clientMetrics.RecordSyncDuration(e.ctx, d)
|
||||||
|
})
|
||||||
|
e.shutdownWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer e.shutdownWg.Done()
|
||||||
|
manager.run(e.ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = e.mgmClient.Sync(e.ctx, info, manager.SetTarget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// happens if management is unavailable for a long time.
|
// happens if management is unavailable for a long time.
|
||||||
// We want to cancel the operation of the whole client
|
// We want to cancel the operation of the whole client
|
||||||
@@ -1248,21 +1370,104 @@ func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
// updateNetworkMap applies the wholesale parts (config, routes, ACL, DNS) in full
|
||||||
|
// and up to maxBatch peers per phase. It returns true when more peers remained
|
||||||
|
// than the cap, so the caller re-runs until convergence.
|
||||||
|
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap, maxBatch int, firstPass bool) (bool, error) {
|
||||||
// intentionally leave it before checking serial because for now it can happen that peer IP changed but serial didn't
|
// intentionally leave it before checking serial because for now it can happen that peer IP changed but serial didn't
|
||||||
if networkMap.GetPeerConfig() != nil {
|
if networkMap.GetPeerConfig() != nil {
|
||||||
err := e.updateConfig(networkMap.GetPeerConfig())
|
err := e.updateConfig(networkMap.GetPeerConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serial := networkMap.GetSerial()
|
serial := networkMap.GetSerial()
|
||||||
if e.networkSerial > serial {
|
if e.networkSerial > serial {
|
||||||
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
|
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wholesale sections (firewall/ACL, DNS, routes, forward rules) are applied
|
||||||
|
// up-front and only once per target: they are cheap, local, idempotent and must
|
||||||
|
// be in place before peers come up (fail-closed). On the bounded re-runs that only
|
||||||
|
// drain the remaining peer batches they are skipped — the applied forward rules are
|
||||||
|
// reused from e.forwardingRules for the lazy-exclude finalize.
|
||||||
|
if firstPass {
|
||||||
|
e.applyWholesale(networkMap, serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("got peers update from Management Service, total peers to connect to = %d", len(networkMap.GetRemotePeers()))
|
||||||
|
|
||||||
|
e.updateOfflinePeers(networkMap.GetOfflinePeers())
|
||||||
|
|
||||||
|
// Filter out own peer from the remote peers list
|
||||||
|
localPubKey := e.config.WgPrivateKey.PublicKey().String()
|
||||||
|
remotePeers := make([]*mgmProto.RemotePeerConfig, 0, len(networkMap.GetRemotePeers()))
|
||||||
|
for _, p := range networkMap.GetRemotePeers() {
|
||||||
|
if p.GetWgPubKey() != localPubKey {
|
||||||
|
remotePeers = append(remotePeers, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// needMore signals the caller to re-run when a peer phase hit its per-pass cap.
|
||||||
|
needMore := false
|
||||||
|
|
||||||
|
// cleanup request, most likely our peer has been deleted
|
||||||
|
if networkMap.GetRemotePeersIsEmpty() {
|
||||||
|
err := e.removeAllPeers()
|
||||||
|
e.statusRecorder.FinishPeerListModifications()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removeMore, err := e.removePeers(remotePeers, maxBatch)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyMore, err := e.modifyPeers(remotePeers, maxBatch)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addMore, err := e.addNewPeers(remotePeers, maxBatch)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
needMore = removeMore || modifyMore || addMore
|
||||||
|
|
||||||
|
e.statusRecorder.FinishPeerListModifications()
|
||||||
|
|
||||||
|
e.updatePeerSSHHostKeys(remotePeers)
|
||||||
|
|
||||||
|
if err := e.updateSSHClientConfig(remotePeers); err != nil {
|
||||||
|
log.Warnf("failed to update SSH client config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.updateSSHServerAuth(networkMap.GetSshAuth())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the exclude list only once peers have fully converged (this pass added
|
||||||
|
// the last batch). It needs all target peers present in the store, and
|
||||||
|
// ExcludePeer has replace-semantics — a partial set mid-convergence would be wrong.
|
||||||
|
if !needMore {
|
||||||
|
excludedLazyPeers := e.toExcludedLazyPeers(e.forwardingRules, remotePeers)
|
||||||
|
e.connMgr.SetExcludeList(e.ctx, excludedLazyPeers)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.networkSerial = serial
|
||||||
|
|
||||||
|
return needMore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyWholesale applies the cheap, local, idempotent map sections — lazy feature
|
||||||
|
// flag, firewall/legacy management, DNS, routes, ACL filtering, DNS forwarder and
|
||||||
|
// ingress forward rules — that must be in place before peers come up. It runs once
|
||||||
|
// per target (first pass only); the resulting forward rules are stashed in
|
||||||
|
// e.forwardingRules for the lazy-exclude finalize on the peer-converged pass.
|
||||||
|
func (e *Engine) applyWholesale(networkMap *mgmProto.NetworkMap, serial uint64) {
|
||||||
if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, networkMap.GetPeerConfig().GetLazyConnectionEnabled()); err != nil {
|
if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, networkMap.GetPeerConfig().GetLazyConnectionEnabled()); err != nil {
|
||||||
log.Errorf("failed to update lazy connection feature flag: %v", err)
|
log.Errorf("failed to update lazy connection feature flag: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1323,61 +1528,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to update forward rules, err: %v", err)
|
log.Errorf("failed to update forward rules, err: %v", err)
|
||||||
}
|
}
|
||||||
|
e.forwardingRules = forwardingRules
|
||||||
log.Debugf("got peers update from Management Service, total peers to connect to = %d", len(networkMap.GetRemotePeers()))
|
|
||||||
|
|
||||||
e.updateOfflinePeers(networkMap.GetOfflinePeers())
|
|
||||||
|
|
||||||
// Filter out own peer from the remote peers list
|
|
||||||
localPubKey := e.config.WgPrivateKey.PublicKey().String()
|
|
||||||
remotePeers := make([]*mgmProto.RemotePeerConfig, 0, len(networkMap.GetRemotePeers()))
|
|
||||||
for _, p := range networkMap.GetRemotePeers() {
|
|
||||||
if p.GetWgPubKey() != localPubKey {
|
|
||||||
remotePeers = append(remotePeers, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup request, most likely our peer has been deleted
|
|
||||||
if networkMap.GetRemotePeersIsEmpty() {
|
|
||||||
err := e.removeAllPeers()
|
|
||||||
e.statusRecorder.FinishPeerListModifications()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := e.removePeers(remotePeers)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.modifyPeers(remotePeers)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.addNewPeers(remotePeers)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.statusRecorder.FinishPeerListModifications()
|
|
||||||
|
|
||||||
e.updatePeerSSHHostKeys(remotePeers)
|
|
||||||
|
|
||||||
if err := e.updateSSHClientConfig(remotePeers); err != nil {
|
|
||||||
log.Warnf("failed to update SSH client config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.updateSSHServerAuth(networkMap.GetSshAuth())
|
|
||||||
}
|
|
||||||
|
|
||||||
// must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store
|
|
||||||
excludedLazyPeers := e.toExcludedLazyPeers(forwardingRules, remotePeers)
|
|
||||||
e.connMgr.SetExcludeList(e.ctx, excludedLazyPeers)
|
|
||||||
|
|
||||||
e.networkSerial = serial
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toDNSFeatureFlag(networkMap *mgmProto.NetworkMap) bool {
|
func toDNSFeatureFlag(networkMap *mgmProto.NetworkMap) bool {
|
||||||
@@ -1557,14 +1708,23 @@ func addrToString(addr netip.Addr) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addNewPeers adds peers that were not know before but arrived from the Management service with the update
|
// addNewPeers adds peers that were not know before but arrived from the Management service with the update
|
||||||
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
// addNewPeers adds up to maxBatch not-yet-present peers per call. It returns true
|
||||||
|
// when more new peers remained than the cap, so the caller re-runs.
|
||||||
|
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig, maxBatch int) (bool, error) {
|
||||||
|
added := 0
|
||||||
for _, p := range peersUpdate {
|
for _, p := range peersUpdate {
|
||||||
err := e.addNewPeer(p)
|
if _, ok := e.peerStore.PeerConn(p.GetWgPubKey()); ok {
|
||||||
if err != nil {
|
continue // already present (cheap skip), does not count toward the cap
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
if added >= maxBatch {
|
||||||
|
return true, nil // at least one more new peer remains
|
||||||
|
}
|
||||||
|
if err := e.addNewPeer(p); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
added++
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addNewPeer add peer if connection doesn't exist
|
// addNewPeer add peer if connection doesn't exist
|
||||||
@@ -1662,7 +1822,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
|
|||||||
}
|
}
|
||||||
|
|
||||||
// receiveSignalEvents connects to the Signal Service event stream to negotiate connection with remote peers
|
// receiveSignalEvents connects to the Signal Service event stream to negotiate connection with remote peers
|
||||||
func (e *Engine) receiveSignalEvents() {
|
func (e *Engine) receiveSignalEvents() error {
|
||||||
e.shutdownWg.Add(1)
|
e.shutdownWg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer e.shutdownWg.Done()
|
defer e.shutdownWg.Done()
|
||||||
@@ -1678,6 +1838,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)
|
||||||
@@ -1726,7 +1893,12 @@ func (e *Engine) receiveSignalEvents() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
e.signal.WaitStreamConnected()
|
// todo: consider to remove this blocker. I do not see benefit to block the Start operations
|
||||||
|
e.signal.WaitStreamConnected(e.ctx)
|
||||||
|
if err := e.ctx.Err(); err != nil {
|
||||||
|
return fmt.Errorf("wait for signal stream: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) parseNATExternalIPMappings() []string {
|
func (e *Engine) parseNATExternalIPMappings() []string {
|
||||||
@@ -1813,6 +1985,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) {
|
||||||
@@ -1864,7 +2048,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1967,6 +2150,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 {
|
||||||
@@ -2089,21 +2295,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
|
||||||
@@ -2119,45 +2310,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
|
||||||
@@ -2193,7 +2381,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
565
client/internal/engine_privileged_test.go
Normal file
565
client/internal/engine_privileged_test.go
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
//go:build privileged
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"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"
|
||||||
|
"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"
|
||||||
|
mgmt "github.com/netbirdio/netbird/shared/management/client"
|
||||||
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEngine_SSH(t *testing.T) {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKey, err := nbssh.GeneratePrivateKey(nbssh.ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
||||||
|
engine := NewEngine(
|
||||||
|
ctx, cancel,
|
||||||
|
&EngineConfig{
|
||||||
|
WgIfaceName: "utun101",
|
||||||
|
WgAddr: wgaddr.MustParseWGAddress("100.64.0.1/24"),
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
ServerSSHAllowed: true,
|
||||||
|
MTU: iface.DefaultMTU,
|
||||||
|
SSHKey: sshKey,
|
||||||
|
},
|
||||||
|
EngineServices{
|
||||||
|
SignalClient: &signal.MockClient{},
|
||||||
|
MgmClient: &mgmt.MockClient{},
|
||||||
|
RelayManager: relayMgr,
|
||||||
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
||||||
|
},
|
||||||
|
MobileDependency{},
|
||||||
|
)
|
||||||
|
|
||||||
|
engine.dnsServer = &dns.MockServer{
|
||||||
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.Start(nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := engine.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
peerWithSSH := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.21/24"},
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{
|
||||||
|
SshPubKey: []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH server is not enabled so SSH config of a remote peer should be ignored
|
||||||
|
networkMap := &mgmtProto.NetworkMap{
|
||||||
|
Serial: 6,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = engine.updateNetworkMap(networkMap, maxPeersPerSyncPass, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, engine.sshServer)
|
||||||
|
|
||||||
|
// SSH server is enabled, therefore SSH config should be applied
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 7,
|
||||||
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{
|
||||||
|
SshEnabled: true,
|
||||||
|
JwtConfig: &mgmtProto.JWTConfig{
|
||||||
|
Issuer: "test-issuer",
|
||||||
|
Audience: "test-audience",
|
||||||
|
KeysLocation: "test-keys",
|
||||||
|
MaxTokenAge: 3600,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = engine.updateNetworkMap(networkMap, maxPeersPerSyncPass, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
assert.NotNil(t, engine.sshServer)
|
||||||
|
|
||||||
|
// now remove peer
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 8,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = engine.updateNetworkMap(networkMap, maxPeersPerSyncPass, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// time.Sleep(250 * time.Millisecond)
|
||||||
|
assert.NotNil(t, engine.sshServer)
|
||||||
|
|
||||||
|
// now disable SSH server
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 9,
|
||||||
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{SshEnabled: false}},
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = engine.updateNetworkMap(networkMap, maxPeersPerSyncPass, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, engine.sshServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_Sync(t *testing.T) {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// feed updates to Engine via mocked Management client
|
||||||
|
updates := make(chan *mgmtProto.SyncResponse)
|
||||||
|
defer close(updates)
|
||||||
|
syncFunc := func(ctx context.Context, info *system.Info, msgHandler func(msg *mgmtProto.SyncResponse) error) error {
|
||||||
|
for msg := range updates {
|
||||||
|
err := msgHandler(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
||||||
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
||||||
|
WgIfaceName: "utun103",
|
||||||
|
WgAddr: wgaddr.MustParseWGAddress("100.64.0.1/24"),
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
MTU: iface.DefaultMTU,
|
||||||
|
}, EngineServices{
|
||||||
|
SignalClient: &signal.MockClient{},
|
||||||
|
MgmClient: &mgmt.MockClient{SyncFunc: syncFunc},
|
||||||
|
RelayManager: relayMgr,
|
||||||
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
||||||
|
}, MobileDependency{})
|
||||||
|
engine.ctx = ctx
|
||||||
|
|
||||||
|
engine.dnsServer = &dns.MockServer{
|
||||||
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := engine.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = engine.Start(nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer1 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.10/24"},
|
||||||
|
}
|
||||||
|
peer2 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.11/24"},
|
||||||
|
}
|
||||||
|
peer3 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.12/24"},
|
||||||
|
}
|
||||||
|
// 1st update with just 1 peer and serial larger than the current serial of the engine => apply update
|
||||||
|
updates <- &mgmtProto.SyncResponse{
|
||||||
|
NetworkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 10,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peer1, peer2, peer3},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.After(time.Second * 2)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("timeout while waiting for test to finish")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if getPeers(engine) == 3 && engine.networkSerial == 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_MultiplePeers(t *testing.T) {
|
||||||
|
// log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigServer, signalAddr, err := startSignal(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sigServer.Stop()
|
||||||
|
mgmtServer, mgmtAddr, err := startManagement(t, t.TempDir(), "../testdata/store.sql")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer mgmtServer.GracefulStop()
|
||||||
|
|
||||||
|
setupKey := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
||||||
|
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
engines := []*Engine{}
|
||||||
|
numPeers := 10
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(numPeers)
|
||||||
|
// create and start peers
|
||||||
|
for i := 0; i < numPeers; i++ {
|
||||||
|
j := i
|
||||||
|
go func() {
|
||||||
|
engine, err := createEngine(ctx, cancel, setupKey, j, mgmtAddr, signalAddr)
|
||||||
|
if err != nil {
|
||||||
|
wg.Done()
|
||||||
|
t.Errorf("unable to create the engine for peer %d with error %v", j, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
engine.dnsServer = &dns.MockServer{}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
guid := fmt.Sprintf("{%s}", uuid.New().String())
|
||||||
|
device.CustomWindowsGUIDString = strings.ToLower(guid)
|
||||||
|
err = engine.Start(nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to start engine for peer %d with error %v", j, err)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
engines = append(engines, engine)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until all have been created and started
|
||||||
|
wg.Wait()
|
||||||
|
if len(engines) != numPeers {
|
||||||
|
t.Fatal("not all peers were started")
|
||||||
|
}
|
||||||
|
// check whether all the peer have expected peers connected
|
||||||
|
|
||||||
|
expectedConnected := numPeers * (numPeers - 1)
|
||||||
|
|
||||||
|
// adjust according to timeouts
|
||||||
|
timeout := 50 * time.Second
|
||||||
|
timeoutChan := time.After(timeout)
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeoutChan:
|
||||||
|
t.Fatalf("waiting for expected connections timeout after %s", timeout.String())
|
||||||
|
break loop
|
||||||
|
case <-ticker.C:
|
||||||
|
totalConnected := 0
|
||||||
|
for _, engine := range engines {
|
||||||
|
totalConnected += getConnectedPeers(engine)
|
||||||
|
}
|
||||||
|
if totalConnected == expectedConnected {
|
||||||
|
log.Infof("total connected=%d", totalConnected)
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
log.Infof("total connected=%d", totalConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cleanup test
|
||||||
|
for n, peerEngine := range engines {
|
||||||
|
t.Logf("stopping peer with interface %s from multipeer test, loopIndex %d", peerEngine.wgInterface.Name(), n)
|
||||||
|
errStop := peerEngine.mgmClient.Close()
|
||||||
|
if errStop != nil {
|
||||||
|
log.Infoln("got error trying to close management clients from engine: ", errStop)
|
||||||
|
}
|
||||||
|
errStop = peerEngine.Stop()
|
||||||
|
if errStop != nil {
|
||||||
|
log.Infoln("got error trying to close testing peers engine: ", errStop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
kaep = keepalive.EnforcementPolicy{
|
||||||
|
MinTime: 15 * time.Second,
|
||||||
|
PermitWithoutStream: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
kasp = keepalive.ServerParameters{
|
||||||
|
MaxConnectionIdle: 15 * time.Second,
|
||||||
|
MaxConnectionAgeGrace: 5 * time.Second,
|
||||||
|
Time: 5 * time.Second,
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mgmtAddr string, signalAddr string) (*Engine, error) {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mgmtClient, err := mgmt.NewClient(ctx, mgmtAddr, key, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signalClient, err := signal.NewClient(ctx, signalAddr, key, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := system.GetInfo(ctx)
|
||||||
|
resp, err := mgmtClient.Register(setupKey, "", info, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ifaceName string
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
ifaceName = fmt.Sprintf("utun1%d", i)
|
||||||
|
} else {
|
||||||
|
ifaceName = fmt.Sprintf("wt%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wgPort := 33100 + i
|
||||||
|
conf := &EngineConfig{
|
||||||
|
WgIfaceName: ifaceName,
|
||||||
|
WgAddr: wgaddr.MustParseWGAddress(resp.PeerConfig.Address),
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: wgPort,
|
||||||
|
MTU: iface.DefaultMTU,
|
||||||
|
}
|
||||||
|
|
||||||
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
||||||
|
e, err := NewEngine(ctx, cancel, conf, EngineServices{
|
||||||
|
SignalClient: signalClient,
|
||||||
|
MgmClient: mgmtClient,
|
||||||
|
RelayManager: relayMgr,
|
||||||
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
||||||
|
}, MobileDependency{}), nil
|
||||||
|
e.ctx = ctx
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSignal(t *testing.T) (*grpc.Server, string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv, err := signalServer.NewServer(context.Background(), otel.Meter(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
proto.RegisterSignalExchangeServer(s, srv)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = s.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, lis.Addr().String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
config := &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: "localhost:10000",
|
||||||
|
},
|
||||||
|
Datadir: dataDir,
|
||||||
|
HttpConfig: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
|
|
||||||
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), testFile, config.Datadir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionsManager := permissions.NewManager(store)
|
||||||
|
peersManager := peers.NewManager(store, permissionsManager)
|
||||||
|
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||||
|
|
||||||
|
cacheStore, err := nbcache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond, 100)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
||||||
|
|
||||||
|
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 := server.NewAccountRequestBuffer(context.Background(), store)
|
||||||
|
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
|
||||||
|
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsManager, err := nbgrpc.NewTimeBasedAuthSecretsManager(updateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
||||||
|
go func() {
|
||||||
|
if err = s.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, lis.Addr().String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConnectedPeers returns a connection Status or nil if peer connection wasn't found
|
||||||
|
func getConnectedPeers(e *Engine) int {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
i := 0
|
||||||
|
for _, id := range e.peerStore.PeersPubKey() {
|
||||||
|
conn, _ := e.peerStore.PeerConn(id)
|
||||||
|
if conn.IsConnected() {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPeers(e *Engine) int {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
return len(e.peerStore.PeersPubKey())
|
||||||
|
}
|
||||||
@@ -6,37 +6,18 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
"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/grpc"
|
|
||||||
"google.golang.org/grpc/keepalive"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/management/server/job"
|
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
@@ -50,44 +31,17 @@ import (
|
|||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
nbssh "github.com/netbirdio/netbird/client/ssh"
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"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/integrations/port_forwarding"
|
|
||||||
"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"
|
|
||||||
"github.com/netbirdio/netbird/monotime"
|
"github.com/netbirdio/netbird/monotime"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
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"
|
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
signal "github.com/netbirdio/netbird/shared/signal/client"
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
"github.com/netbirdio/netbird/shared/signal/proto"
|
|
||||||
signalServer "github.com/netbirdio/netbird/signal/server"
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
kaep = keepalive.EnforcementPolicy{
|
|
||||||
MinTime: 15 * time.Second,
|
|
||||||
PermitWithoutStream: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
kasp = keepalive.ServerParameters{
|
|
||||||
MaxConnectionIdle: 15 * time.Second,
|
|
||||||
MaxConnectionAgeGrace: 5 * time.Second,
|
|
||||||
Time: 5 * time.Second,
|
|
||||||
Timeout: 2 * time.Second,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type MockWGIface struct {
|
type MockWGIface struct {
|
||||||
CreateFunc func() error
|
CreateFunc func() error
|
||||||
CreateOnAndroidFunc func(routeRange []string, ip string, domains []string) error
|
CreateOnAndroidFunc func(routeRange []string, ip string, domains []string) error
|
||||||
@@ -234,129 +188,6 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEngine_SSH(t *testing.T) {
|
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sshKey, err := nbssh.GeneratePrivateKey(nbssh.ED25519)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
||||||
engine := NewEngine(
|
|
||||||
ctx, cancel,
|
|
||||||
&EngineConfig{
|
|
||||||
WgIfaceName: "utun101",
|
|
||||||
WgAddr: wgaddr.MustParseWGAddress("100.64.0.1/24"),
|
|
||||||
WgPrivateKey: key,
|
|
||||||
WgPort: 33100,
|
|
||||||
ServerSSHAllowed: true,
|
|
||||||
MTU: iface.DefaultMTU,
|
|
||||||
SSHKey: sshKey,
|
|
||||||
},
|
|
||||||
EngineServices{
|
|
||||||
SignalClient: &signal.MockClient{},
|
|
||||||
MgmClient: &mgmt.MockClient{},
|
|
||||||
RelayManager: relayMgr,
|
|
||||||
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
||||||
},
|
|
||||||
MobileDependency{},
|
|
||||||
)
|
|
||||||
|
|
||||||
engine.dnsServer = &dns.MockServer{
|
|
||||||
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
err = engine.Start(nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err := engine.Stop()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
peerWithSSH := &mgmtProto.RemotePeerConfig{
|
|
||||||
WgPubKey: "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
||||||
AllowedIps: []string{"100.64.0.21/24"},
|
|
||||||
SshConfig: &mgmtProto.SSHConfig{
|
|
||||||
SshPubKey: []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH server is not enabled so SSH config of a remote peer should be ignored
|
|
||||||
networkMap := &mgmtProto.NetworkMap{
|
|
||||||
Serial: 6,
|
|
||||||
PeerConfig: nil,
|
|
||||||
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
|
||||||
RemotePeersIsEmpty: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = engine.updateNetworkMap(networkMap)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Nil(t, engine.sshServer)
|
|
||||||
|
|
||||||
// SSH server is enabled, therefore SSH config should be applied
|
|
||||||
networkMap = &mgmtProto.NetworkMap{
|
|
||||||
Serial: 7,
|
|
||||||
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
|
||||||
SshConfig: &mgmtProto.SSHConfig{
|
|
||||||
SshEnabled: true,
|
|
||||||
JwtConfig: &mgmtProto.JWTConfig{
|
|
||||||
Issuer: "test-issuer",
|
|
||||||
Audience: "test-audience",
|
|
||||||
KeysLocation: "test-keys",
|
|
||||||
MaxTokenAge: 3600,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
|
||||||
RemotePeersIsEmpty: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = engine.updateNetworkMap(networkMap)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
assert.NotNil(t, engine.sshServer)
|
|
||||||
|
|
||||||
// now remove peer
|
|
||||||
networkMap = &mgmtProto.NetworkMap{
|
|
||||||
Serial: 8,
|
|
||||||
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
|
||||||
RemotePeersIsEmpty: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = engine.updateNetworkMap(networkMap)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// time.Sleep(250 * time.Millisecond)
|
|
||||||
assert.NotNil(t, engine.sshServer)
|
|
||||||
|
|
||||||
// now disable SSH server
|
|
||||||
networkMap = &mgmtProto.NetworkMap{
|
|
||||||
Serial: 9,
|
|
||||||
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
|
||||||
SshConfig: &mgmtProto.SSHConfig{SshEnabled: false}},
|
|
||||||
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
|
||||||
RemotePeersIsEmpty: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = engine.updateNetworkMap(networkMap)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Nil(t, engine.sshServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEngine_SSHUpdateLogic(t *testing.T) {
|
func TestEngine_SSHUpdateLogic(t *testing.T) {
|
||||||
// Test that SSH server start/stop logic works based on config
|
// Test that SSH server start/stop logic works based on config
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
@@ -426,7 +257,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
||||||
@@ -602,7 +433,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
|
|
||||||
for _, c := range []testCase{case1, case2, case3, case4, case5, case6} {
|
for _, c := range []testCase{case1, case2, case3, case4, case5, case6} {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
err = engine.updateNetworkMap(c.networkMap)
|
_, err = engine.updateNetworkMap(c.networkMap, maxPeersPerSyncPass, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
return
|
return
|
||||||
@@ -629,97 +460,47 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestEngine_Sync(t *testing.T) {
|
// chunked apply: with a per-pass cap smaller than the number of peers, a
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
// single updateNetworkMap applies one batch and reports more==true; the
|
||||||
if err != nil {
|
// caller re-runs until convergence. (engine currently holds 0 peers.)
|
||||||
t.Fatal(err)
|
t.Run("chunked add converges over multiple passes", func(t *testing.T) {
|
||||||
return
|
nm := &mgmtProto.NetworkMap{
|
||||||
}
|
Serial: 6,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peer1, peer2, peer3},
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// feed updates to Engine via mocked Management client
|
|
||||||
updates := make(chan *mgmtProto.SyncResponse)
|
|
||||||
defer close(updates)
|
|
||||||
syncFunc := func(ctx context.Context, info *system.Info, msgHandler func(msg *mgmtProto.SyncResponse) error) error {
|
|
||||||
for msg := range updates {
|
|
||||||
err := msgHandler(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
||||||
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
||||||
WgIfaceName: "utun103",
|
|
||||||
WgAddr: wgaddr.MustParseWGAddress("100.64.0.1/24"),
|
|
||||||
WgPrivateKey: key,
|
|
||||||
WgPort: 33100,
|
|
||||||
MTU: iface.DefaultMTU,
|
|
||||||
}, EngineServices{
|
|
||||||
SignalClient: &signal.MockClient{},
|
|
||||||
MgmClient: &mgmt.MockClient{SyncFunc: syncFunc},
|
|
||||||
RelayManager: relayMgr,
|
|
||||||
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
||||||
}, MobileDependency{})
|
|
||||||
engine.ctx = ctx
|
|
||||||
|
|
||||||
engine.dnsServer = &dns.MockServer{
|
|
||||||
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err := engine.Stop()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = engine.Start(nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
peer1 := &mgmtProto.RemotePeerConfig{
|
|
||||||
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
||||||
AllowedIps: []string{"100.64.0.10/24"},
|
|
||||||
}
|
|
||||||
peer2 := &mgmtProto.RemotePeerConfig{
|
|
||||||
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
|
||||||
AllowedIps: []string{"100.64.0.11/24"},
|
|
||||||
}
|
|
||||||
peer3 := &mgmtProto.RemotePeerConfig{
|
|
||||||
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
|
||||||
AllowedIps: []string{"100.64.0.12/24"},
|
|
||||||
}
|
|
||||||
// 1st update with just 1 peer and serial larger than the current serial of the engine => apply update
|
|
||||||
updates <- &mgmtProto.SyncResponse{
|
|
||||||
NetworkMap: &mgmtProto.NetworkMap{
|
|
||||||
Serial: 10,
|
|
||||||
PeerConfig: nil,
|
|
||||||
RemotePeers: []*mgmtProto.RemotePeerConfig{peer1, peer2, peer3},
|
|
||||||
RemotePeersIsEmpty: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout := time.After(time.Second * 2)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
t.Fatalf("timeout while waiting for test to finish")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if getPeers(engine) == 3 && engine.networkSerial == 10 {
|
more, err := engine.updateNetworkMap(nm, 1, true)
|
||||||
break
|
require.NoError(t, err)
|
||||||
|
require.True(t, more, "pass 1 should signal more")
|
||||||
|
require.Len(t, engine.peerStore.PeersPubKey(), 1)
|
||||||
|
|
||||||
|
more, err = engine.updateNetworkMap(nm, 1, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, more, "pass 2 should signal more")
|
||||||
|
require.Len(t, engine.peerStore.PeersPubKey(), 2)
|
||||||
|
|
||||||
|
more, err = engine.updateNetworkMap(nm, 1, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, more, "pass 3 should converge")
|
||||||
|
require.Len(t, engine.peerStore.PeersPubKey(), 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("chunked remove converges over multiple passes", func(t *testing.T) {
|
||||||
|
nm := &mgmtProto.NetworkMap{
|
||||||
|
Serial: 7,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peer1}, // remove peer2, peer3
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
more, err := engine.updateNetworkMap(nm, 1, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, more, "pass 1 should signal more (2 to remove, cap 1)")
|
||||||
|
|
||||||
|
more, err = engine.updateNetworkMap(nm, 1, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, more, "pass 2 should converge")
|
||||||
|
require.Len(t, engine.peerStore.PeersPubKey(), 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
||||||
@@ -817,7 +598,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
@@ -890,7 +671,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = engine.updateNetworkMap(testCase.networkMap)
|
_, err = engine.updateNetworkMap(testCase.networkMap, maxPeersPerSyncPass, true)
|
||||||
assert.NoError(t, err, "shouldn't return error")
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
||||||
assert.Len(t, input.clientRoutes, testCase.expectedLen, "clientRoutes len should match")
|
assert.Len(t, input.clientRoutes, testCase.expectedLen, "clientRoutes len should match")
|
||||||
@@ -1024,7 +805,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
@@ -1094,7 +875,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = engine.updateNetworkMap(testCase.networkMap)
|
_, err = engine.updateNetworkMap(testCase.networkMap, maxPeersPerSyncPass, true)
|
||||||
assert.NoError(t, err, "shouldn't return error")
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
||||||
assert.Len(t, input.inputNSGroups, testCase.expectedZonesLen, "zones len should match")
|
assert.Len(t, input.inputNSGroups, testCase.expectedZonesLen, "zones len should match")
|
||||||
@@ -1105,104 +886,6 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEngine_MultiplePeers(t *testing.T) {
|
|
||||||
// log.SetLevel(log.DebugLevel)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
sigServer, signalAddr, err := startSignal(t)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer sigServer.Stop()
|
|
||||||
mgmtServer, mgmtAddr, err := startManagement(t, t.TempDir(), "../testdata/store.sql")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer mgmtServer.GracefulStop()
|
|
||||||
|
|
||||||
setupKey := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
|
||||||
|
|
||||||
mu := sync.Mutex{}
|
|
||||||
engines := []*Engine{}
|
|
||||||
numPeers := 10
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(numPeers)
|
|
||||||
// create and start peers
|
|
||||||
for i := 0; i < numPeers; i++ {
|
|
||||||
j := i
|
|
||||||
go func() {
|
|
||||||
engine, err := createEngine(ctx, cancel, setupKey, j, mgmtAddr, signalAddr)
|
|
||||||
if err != nil {
|
|
||||||
wg.Done()
|
|
||||||
t.Errorf("unable to create the engine for peer %d with error %v", j, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
engine.dnsServer = &dns.MockServer{}
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
guid := fmt.Sprintf("{%s}", uuid.New().String())
|
|
||||||
device.CustomWindowsGUIDString = strings.ToLower(guid)
|
|
||||||
err = engine.Start(nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unable to start engine for peer %d with error %v", j, err)
|
|
||||||
wg.Done()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
engines = append(engines, engine)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait until all have been created and started
|
|
||||||
wg.Wait()
|
|
||||||
if len(engines) != numPeers {
|
|
||||||
t.Fatal("not all peers was started")
|
|
||||||
}
|
|
||||||
// check whether all the peer have expected peers connected
|
|
||||||
|
|
||||||
expectedConnected := numPeers * (numPeers - 1)
|
|
||||||
|
|
||||||
// adjust according to timeouts
|
|
||||||
timeout := 50 * time.Second
|
|
||||||
timeoutChan := time.After(timeout)
|
|
||||||
ticker := time.NewTicker(time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeoutChan:
|
|
||||||
t.Fatalf("waiting for expected connections timeout after %s", timeout.String())
|
|
||||||
break loop
|
|
||||||
case <-ticker.C:
|
|
||||||
totalConnected := 0
|
|
||||||
for _, engine := range engines {
|
|
||||||
totalConnected += getConnectedPeers(engine)
|
|
||||||
}
|
|
||||||
if totalConnected == expectedConnected {
|
|
||||||
log.Infof("total connected=%d", totalConnected)
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
log.Infof("total connected=%d", totalConnected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// cleanup test
|
|
||||||
for n, peerEngine := range engines {
|
|
||||||
t.Logf("stopping peer with interface %s from multipeer test, loopIndex %d", peerEngine.wgInterface.Name(), n)
|
|
||||||
errStop := peerEngine.mgmClient.Close()
|
|
||||||
if errStop != nil {
|
|
||||||
log.Infoln("got error trying to close management clients from engine: ", errStop)
|
|
||||||
}
|
|
||||||
errStop = peerEngine.Stop()
|
|
||||||
if errStop != nil {
|
|
||||||
log.Infoln("got error trying to close testing peers engine: ", errStop)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_ParseNATExternalIPMappings(t *testing.T) {
|
func Test_ParseNATExternalIPMappings(t *testing.T) {
|
||||||
ifaceList, err := net.Interfaces()
|
ifaceList, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1526,187 +1209,6 @@ func TestCompareNetIPLists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mgmtAddr string, signalAddr string) (*Engine, error) {
|
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mgmtClient, err := mgmt.NewClient(ctx, mgmtAddr, key, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
signalClient, err := signal.NewClient(ctx, signalAddr, key, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
info := system.GetInfo(ctx)
|
|
||||||
resp, err := mgmtClient.Register(setupKey, "", info, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ifaceName string
|
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
ifaceName = fmt.Sprintf("utun1%d", i)
|
|
||||||
} else {
|
|
||||||
ifaceName = fmt.Sprintf("wt%d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wgPort := 33100 + i
|
|
||||||
conf := &EngineConfig{
|
|
||||||
WgIfaceName: ifaceName,
|
|
||||||
WgAddr: wgaddr.MustParseWGAddress(resp.PeerConfig.Address),
|
|
||||||
WgPrivateKey: key,
|
|
||||||
WgPort: wgPort,
|
|
||||||
MTU: iface.DefaultMTU,
|
|
||||||
}
|
|
||||||
|
|
||||||
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
||||||
e, err := NewEngine(ctx, cancel, conf, EngineServices{
|
|
||||||
SignalClient: signalClient,
|
|
||||||
MgmClient: mgmtClient,
|
|
||||||
RelayManager: relayMgr,
|
|
||||||
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
||||||
}, MobileDependency{}), nil
|
|
||||||
e.ctx = ctx
|
|
||||||
return e, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func startSignal(t *testing.T) (*grpc.Server, string, error) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", "localhost:0")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv, err := signalServer.NewServer(context.Background(), otel.Meter(""))
|
|
||||||
require.NoError(t, err)
|
|
||||||
proto.RegisterSignalExchangeServer(s, srv)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err = s.Serve(lis); err != nil {
|
|
||||||
log.Fatalf("failed to serve: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return s, lis.Addr().String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, string, error) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
config := &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: "localhost:10000",
|
|
||||||
},
|
|
||||||
Datadir: dataDir,
|
|
||||||
HttpConfig: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", "localhost:0")
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
|
||||||
|
|
||||||
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), testFile, config.Datadir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
t.Cleanup(cleanUp)
|
|
||||||
|
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
permissionsManager := permissions.NewManager(store)
|
|
||||||
peersManager := peers.NewManager(store, permissionsManager)
|
|
||||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
|
||||||
|
|
||||||
cacheStore, err := nbcache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond, 100)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
|
||||||
|
|
||||||
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 := server.NewAccountRequestBuffer(context.Background(), store)
|
|
||||||
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
|
|
||||||
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
secretsManager, err := nbgrpc.NewTimeBasedAuthSecretsManager(updateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
|
||||||
go func() {
|
|
||||||
if err = s.Serve(lis); err != nil {
|
|
||||||
log.Fatalf("failed to serve: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return s, lis.Addr().String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getConnectedPeers returns a connection Status or nil if peer connection wasn't found
|
|
||||||
func getConnectedPeers(e *Engine) int {
|
|
||||||
e.syncMsgMux.Lock()
|
|
||||||
defer e.syncMsgMux.Unlock()
|
|
||||||
i := 0
|
|
||||||
for _, id := range e.peerStore.PeersPubKey() {
|
|
||||||
conn, _ := e.peerStore.PeerConn(id)
|
|
||||||
if conn.IsConnected() {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPeers(e *Engine) int {
|
|
||||||
e.syncMsgMux.Lock()
|
|
||||||
defer e.syncMsgMux.Unlock()
|
|
||||||
|
|
||||||
return len(e.peerStore.PeersPubKey())
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustEncodePrefix(t *testing.T, p netip.Prefix) []byte {
|
func mustEncodePrefix(t *testing.T, p netip.Prefix) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
b, err := netiputil.EncodePrefix(p)
|
b, err := netiputil.EncodePrefix(p)
|
||||||
|
|||||||
@@ -119,10 +119,6 @@ func (d *BindListener) ReadPackets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.peerCfg.Log.Debugf("removing lazy endpoint for peer %s", d.peerCfg.PublicKey)
|
d.peerCfg.Log.Debugf("removing lazy endpoint for peer %s", d.peerCfg.PublicKey)
|
||||||
if err := d.wgIface.RemovePeer(d.peerCfg.PublicKey); err != nil {
|
|
||||||
d.peerCfg.Log.Errorf("failed to remove endpoint: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = d.lazyConn.Close()
|
_ = d.lazyConn.Close()
|
||||||
d.bind.RemoveEndpoint(d.fakeIP)
|
d.bind.RemoveEndpoint(d.fakeIP)
|
||||||
d.done.Done()
|
d.done.Done()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user