mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-31 13:09:55 +00:00
Compare commits
43 Commits
main
...
nmap/combi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9124832651 | ||
|
|
d0c27ad854 | ||
|
|
1d8d9ff2ec | ||
|
|
9e236ac20e | ||
|
|
efa6a3f502 | ||
|
|
5fbcdeceac | ||
|
|
3a1bbeba90 | ||
|
|
728057ef15 | ||
|
|
582cd70086 | ||
|
|
9bbbafaf69 | ||
|
|
672b057aa0 | ||
|
|
b9a0186200 | ||
|
|
9083bdb977 | ||
|
|
b194af48b8 | ||
|
|
4543780ef0 | ||
|
|
2de0283971 | ||
|
|
c948d7398f | ||
|
|
13d26106f8 | ||
|
|
3e83164bcd | ||
|
|
6568c905c6 | ||
|
|
aa9a1a42f5 | ||
|
|
5ae6c25ac0 | ||
|
|
1d906e411d | ||
|
|
3012228b91 | ||
|
|
85851bc477 | ||
|
|
fed4f1b024 | ||
|
|
70e84d5228 | ||
|
|
57529c7f18 | ||
|
|
fd99bc072d | ||
|
|
40e6ec16c6 | ||
|
|
ec476d5072 | ||
|
|
550ae5558e | ||
|
|
46494bd860 | ||
|
|
c7bff8f074 | ||
|
|
3a95f39f2c | ||
|
|
6b4d4076f4 | ||
|
|
63d2217d8a | ||
|
|
0bfccd65d2 | ||
|
|
26d778374b | ||
|
|
5ec8bebfa5 | ||
|
|
cefb37e920 | ||
|
|
5a16c812fd | ||
|
|
285bbc5ffb |
45
.github/dependabot.yml
vendored
45
.github/dependabot.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
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*"
|
|
||||||
105
.github/workflows/check-license-dependencies.yml
vendored
105
.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,10 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Check for problematic license dependencies
|
- name: Check for problematic license dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -59,57 +56,55 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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 ""
|
||||||
|
|
||||||
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
# Filter out dependencies that are only pulled in by internal AGPL packages
|
||||||
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
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")
|
||||||
|
|
||||||
if [ -n "$COPYLEFT_DEPS" ]; then
|
# Check if any importer is NOT in management/signal/relay
|
||||||
echo "Found copyleft licensed dependencies:"
|
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
||||||
echo "$COPYLEFT_DEPS"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Filter out dependencies that are only pulled in by internal AGPL packages
|
if [ -n "$BSD_IMPORTER" ]; then
|
||||||
INCOMPATIBLE=""
|
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
||||||
while IFS=',' read -r package url license; do
|
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
||||||
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
else
|
||||||
# Find ALL packages that import this GPL package using go list
|
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
||||||
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
|
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
|
||||||
fi
|
done <<< "$COPYLEFT_DEPS"
|
||||||
|
|
||||||
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
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,10 +8,11 @@ jobs:
|
|||||||
post:
|
post:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: roots/discourse-topic-github-release-action@557d74ea05b6cc0c47f555c1d5d28a89d904005b # v1.1.0
|
- uses: roots/discourse-topic-github-release-action@main
|
||||||
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: releases
|
discourse-tags:
|
||||||
|
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,9 +15,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@v4
|
||||||
with:
|
- uses: git-town/action@v1.2.1
|
||||||
persist-credentials: false
|
|
||||||
- uses: git-town/action@3d8b878379abb1ee393fb49865a28b4a6c2cd3b0 # v1.2.1
|
|
||||||
with:
|
with:
|
||||||
skip-single-stacks: true
|
skip-single-stacks: true
|
||||||
|
|||||||
9
.github/workflows/golang-test-darwin.yml
vendored
9
.github/workflows/golang-test-darwin.yml
vendored
@@ -16,18 +16,16 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -46,3 +44,4 @@ jobs:
|
|||||||
|
|
||||||
- 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 -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)
|
||||||
|
|
||||||
|
|||||||
19
.github/workflows/golang-test-freebsd.yml
vendored
19
.github/workflows/golang-test-freebsd.yml
vendored
@@ -15,28 +15,17 @@ jobs:
|
|||||||
name: "Client / Unit"
|
name: "Client / Unit"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Read Go version from go.mod
|
|
||||||
id: goversion
|
|
||||||
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Test in FreeBSD
|
- name: Test in FreeBSD
|
||||||
id: test
|
id: test
|
||||||
env:
|
uses: vmactions/freebsd-vm@v1
|
||||||
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
|
||||||
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "15.0"
|
release: "14.2"
|
||||||
envs: "GO_VERSION"
|
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y curl pkgconf xorg
|
pkg install -y curl pkgconf xorg
|
||||||
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
GO_TARBALL="go1.25.3.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"
|
||||||
|
|||||||
120
.github/workflows/golang-test-linux.yml
vendored
120
.github/workflows/golang-test-linux.yml
vendored
@@ -18,11 +18,9 @@ jobs:
|
|||||||
management: ${{ steps.filter.outputs.management }}
|
management: ${{ steps.filter.outputs.management }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
@@ -30,7 +28,7 @@ jobs:
|
|||||||
- 'management/**'
|
- 'management/**'
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -41,7 +39,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -115,16 +113,14 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -135,7 +131,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -162,16 +158,14 @@ jobs:
|
|||||||
|
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -183,7 +177,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
id: cache-restore
|
id: cache-restore
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -237,12 +231,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -257,7 +249,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -285,16 +277,14 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -308,7 +298,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -334,16 +324,14 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -358,7 +346,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -382,21 +370,19 @@ jobs:
|
|||||||
|
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -407,7 +393,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -424,7 +410,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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -451,13 +437,13 @@ jobs:
|
|||||||
|
|
||||||
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
|
||||||
@@ -488,12 +474,10 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -504,7 +488,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -521,7 +505,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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -545,13 +529,13 @@ jobs:
|
|||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["amd64"]
|
arch: [ 'amd64' ]
|
||||||
store: ["sqlite", "postgres"]
|
store: [ 'sqlite', 'postgres' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -582,12 +566,10 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -598,7 +580,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -615,7 +597,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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -641,22 +623,20 @@ jobs:
|
|||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["amd64"]
|
arch: [ 'amd64' ]
|
||||||
store: ["sqlite", "postgres"]
|
store: [ 'sqlite', 'postgres']
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -667,7 +647,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
|
|||||||
19
.github/workflows/golang-test-windows.yml
vendored
19
.github/workflows/golang-test-windows.yml
vendored
@@ -18,12 +18,10 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
@@ -35,7 +33,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -46,15 +44,16 @@ 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:
|
||||||
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
destination: ${{ env.downloadPath }}\wintun.zip
|
file-name: wintun.zip
|
||||||
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||||
|
|
||||||
- name: Decompressing wintun files
|
- name: Decompressing wintun files
|
||||||
run: tar -xvf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
||||||
|
|
||||||
|
|||||||
14
.github/workflows/golangci-lint.yml
vendored
14
.github/workflows/golangci-lint.yml
vendored
@@ -15,11 +15,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
uses: codespell-project/actions-codespell@v2
|
||||||
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/**
|
||||||
@@ -40,15 +38,13 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -56,7 +52,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@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
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,9 +22,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
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,25 +16,23 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
uses: android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
cmdline-tools-version: 8512546
|
cmdline-tools-version: 8512546
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
|
uses: actions/setup-java@v4
|
||||||
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
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
|
||||||
@@ -54,11 +52,9 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const title = context.payload.pull_request.title;
|
const title = context.payload.pull_request.title;
|
||||||
|
|||||||
68
.github/workflows/proto-version-check.yml
vendored
68
.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||||
@@ -20,66 +20,34 @@ jobs:
|
|||||||
per_page: 100,
|
per_page: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const modifiedPbFiles = files.filter(
|
const pbFiles = files.filter(f => f.filename.endsWith('.pb.go'));
|
||||||
f => f.filename.endsWith('.pb.go') && f.status === 'modified'
|
const missingPatch = pbFiles.filter(f => !f.patch).map(f => f.filename);
|
||||||
);
|
if (missingPatch.length > 0) {
|
||||||
if (modifiedPbFiles.length === 0) {
|
core.setFailed(
|
||||||
console.log('No modified .pb.go files to check');
|
`Cannot inspect patch data for:\n` +
|
||||||
|
missingPatch.map(f => `- ${f}`).join('\n') +
|
||||||
|
`\nThis can happen with very large PRs. Verify proto versions manually.`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const versionPattern = /^[+-]\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
|
||||||
const versionPattern = /^\s*\/\/\s+protoc(?:-gen-go)?\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 = [];
|
const violations = [];
|
||||||
for (const file of modifiedPbFiles) {
|
|
||||||
const [base, head] = await Promise.all([
|
for (const file of pbFiles) {
|
||||||
getVersionHeader(file.filename, baseSha),
|
const changed = file.patch
|
||||||
getVersionHeader(file.filename, headSha),
|
.split('\n')
|
||||||
]);
|
.filter(line => versionPattern.test(line));
|
||||||
if (!base.ok || !head.ok) {
|
if (changed.length > 0) {
|
||||||
core.warning(
|
|
||||||
`Skipping ${file.filename}: 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.filename,
|
||||||
base: base.lines,
|
lines: changed,
|
||||||
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.file}:\n${v.lines.map(l => ' ' + l).join('\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(
|
||||||
|
|||||||
164
.github/workflows/release.yml
vendored
164
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.5"
|
SIGN_PIPE_VER: "v0.1.4"
|
||||||
GORELEASER_VER: "v2.14.3"
|
GORELEASER_VER: "v2.14.3"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
@@ -24,9 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Generate FreeBSD port diff
|
- name: Generate FreeBSD port diff
|
||||||
run: bash release_files/freebsd-port-diff.sh
|
run: bash release_files/freebsd-port-diff.sh
|
||||||
@@ -53,26 +51,19 @@ 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'
|
||||||
env:
|
uses: vmactions/freebsd-vm@v1
|
||||||
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
|
||||||
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "15.0"
|
release: "15.0"
|
||||||
envs: "GO_VERSION"
|
|
||||||
prepare: |
|
prepare: |
|
||||||
# Install required packages
|
# Install required packages
|
||||||
pkg install -y git curl portlint
|
pkg install -y git curl portlint go
|
||||||
|
|
||||||
# Install Go for building
|
# Install Go for building
|
||||||
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
GO_TARBALL="go1.25.5.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"
|
||||||
@@ -114,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: freebsd-port-files
|
name: freebsd-port-files
|
||||||
path: |
|
path: |
|
||||||
@@ -133,25 +124,26 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
flags: ""
|
flags: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
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') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -164,18 +156,18 @@ jobs:
|
|||||||
- name: check git status
|
- name: check git status
|
||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a #v4.0.0
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
|
uses: docker/setup-buildx-action@v2
|
||||||
- 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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v1
|
||||||
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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -199,7 +191,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@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --clean ${{ env.flags }}
|
args: release --clean ${{ env.flags }}
|
||||||
@@ -290,28 +282,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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload linux packages
|
- name: upload linux packages
|
||||||
id: upload_linux_packages
|
id: upload_linux_packages
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-packages
|
name: linux-packages
|
||||||
path: dist/netbird_linux**
|
path: dist/netbird_linux**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload windows packages
|
- name: upload windows packages
|
||||||
id: upload_windows_packages
|
id: upload_windows_packages
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-packages
|
name: windows-packages
|
||||||
path: dist/netbird_windows**
|
path: dist/netbird_windows**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload macos packages
|
- name: upload macos packages
|
||||||
id: upload_macos_packages
|
id: upload_macos_packages
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-packages
|
name: macos-packages
|
||||||
path: dist/netbird_darwin**
|
path: dist/netbird_darwin**
|
||||||
@@ -322,26 +314,27 @@ 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: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
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') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -382,7 +375,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@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
uses: goreleaser/goreleaser-action@v4
|
||||||
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 }}
|
||||||
@@ -411,7 +404,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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -425,17 +418,16 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -449,7 +441,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@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
uses: goreleaser/goreleaser-action@v4
|
||||||
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 }}
|
||||||
@@ -457,7 +449,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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -482,26 +474,27 @@ jobs:
|
|||||||
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
||||||
downloadPath: '${{ github.workspace }}\temp'
|
downloadPath: '${{ github.workspace }}\temp'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
uses: booxmedialtd/ws-action-parse-semver@v1
|
||||||
|
with:
|
||||||
|
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
||||||
|
version_extractor_regex: '\/v(.*)$'
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Add 7-Zip to PATH
|
- 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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Download UI release artifacts
|
- name: Download UI release artifacts
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: release-ui
|
path: release-ui
|
||||||
@@ -521,27 +514,29 @@ 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:
|
||||||
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
destination: ${{ env.downloadPath }}\wintun.zip
|
file-name: wintun.zip
|
||||||
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||||
|
|
||||||
- name: Decompress wintun files
|
- name: Decompress wintun files
|
||||||
run: tar -xvf "${{ env.downloadPath }}\wintun.zip" -C ${{ env.downloadPath }}
|
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -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:
|
||||||
url: https://pkgs.netbird.io/mesa3d/MesaForWindows-x64-20.1.8.7z
|
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
|
||||||
destination: ${{ env.downloadPath }}\mesa3d.7z
|
file-name: mesa3d.7z
|
||||||
sha256: 71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
|
||||||
|
|
||||||
- name: Extract Mesa3D driver (amd64 only)
|
- name: Extract Mesa3D driver (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
@@ -552,38 +547,35 @@ 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: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
uses: carlosperate/download-file-action@v2
|
||||||
with:
|
with:
|
||||||
url: https://pkgs.netbird.io/nsis/EnVar_plugin.zip
|
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
|
||||||
destination: ${{ github.workspace }}\envar_plugin.zip
|
file-name: envar_plugin.zip
|
||||||
sha256: e9aa92de351345ed82795251d838f1ae9041ba35af9d381a5780c7843b01f56a
|
location: ${{ github.workspace }}
|
||||||
|
|
||||||
- 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:
|
||||||
url: https://pkgs.netbird.io/nsis/ShellExecAsUser_amd64-Unicode.7z
|
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
|
||||||
destination: ${{ github.workspace }}\ShellExecAsUser_amd64-Unicode.7z
|
file-name: ShellExecAsUser_amd64-Unicode.7z
|
||||||
sha256: 0a55ea25c7330a92cec028eda8afcaf1b1a7092e0dfb77c21c8f654564b4ff9d
|
location: ${{ github.workspace }}
|
||||||
|
|
||||||
- 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
|
||||||
shell: pwsh
|
uses: joncloud/makensis-action@v3.3
|
||||||
|
with:
|
||||||
|
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
|
||||||
|
script-file: client/installer.nsis
|
||||||
|
arguments: "/V4 /DARCH=${{ matrix.arch }}"
|
||||||
env:
|
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
|
||||||
@@ -600,7 +592,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload installer artifacts
|
- name: Upload installer artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-installer-test-${{ matrix.arch }}
|
name: windows-installer-test-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -619,7 +611,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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
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 }}
|
||||||
@@ -711,7 +703,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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: Sign bin and installer
|
workflow: Sign bin and installer
|
||||||
repo: netbirdio/sign-pipelines
|
repo: netbirdio/sign-pipelines
|
||||||
|
|||||||
2
.github/workflows/sync-main.yml
vendored
2
.github/workflows/sync-main.yml
vendored
@@ -14,7 +14,7 @@ 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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: sync-main.yml
|
workflow: sync-main.yml
|
||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
|
|||||||
8
.github/workflows/sync-tag.yml
vendored
8
.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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -42,7 +42,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 ios-client submodule bump
|
- name: Trigger ios-client submodule bump
|
||||||
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
|
|||||||
26
.github/workflows/test-infrastructure-files.yml
vendored
26
.github/workflows/test-infrastructure-files.yml
vendored
@@ -6,10 +6,10 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "infrastructure_files/**"
|
- 'infrastructure_files/**'
|
||||||
- ".github/workflows/test-infrastructure-files.yml"
|
- '.github/workflows/test-infrastructure-files.yml'
|
||||||
- "management/cmd/**"
|
- 'management/cmd/**'
|
||||||
- "signal/cmd/**"
|
- 'signal/cmd/**'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
store: ["sqlite", "postgres", "mysql"]
|
store: [ 'sqlite', 'postgres', 'mysql' ]
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
||||||
@@ -68,17 +68,15 @@ jobs:
|
|||||||
run: sudo apt-get install -y curl
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -141,8 +139,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
|
||||||
@@ -256,9 +254,7 @@ jobs:
|
|||||||
run: sudo apt-get install -y jq
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
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
|
||||||
|
|||||||
6
.github/workflows/update-docs.yml
vendored
6
.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,7 +13,7 @@ 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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: generate api pages
|
workflow: generate api pages
|
||||||
repo: netbirdio/docs
|
repo: netbirdio/docs
|
||||||
|
|||||||
15
.github/workflows/wasm-build-validation.yml
vendored
15
.github/workflows/wasm-build-validation.yml
vendored
@@ -19,17 +19,15 @@ jobs:
|
|||||||
GOARCH: wasm
|
GOARCH: wasm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
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@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
@@ -44,11 +42,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Build Wasm client
|
- name: Build Wasm client
|
||||||
@@ -69,3 +65,4 @@ jobs:
|
|||||||
echo "Wasm binary size (${SIZE_MB}MB) exceeds 56MB limit!"
|
echo "Wasm binary size (${SIZE_MB}MB) exceeds 56MB limit!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
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, _ := validator.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
iv, _ := integrations.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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"
|
||||||
@@ -85,12 +84,6 @@ 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.
|
||||||
@@ -101,26 +94,6 @@ 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
|
||||||
@@ -202,7 +175,6 @@ 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,
|
||||||
@@ -220,13 +192,6 @@ 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,
|
||||||
@@ -440,21 +405,6 @@ 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 isn't in this client's peer
|
|
||||||
// roster — callers should treat that as "unknown peer".
|
|
||||||
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()
|
||||||
@@ -523,25 +473,6 @@ 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.
|
||||||
|
|||||||
@@ -260,15 +260,23 @@ 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}"
|
||||||
|
|
||||||
; Create autostart registry entry based on checkbox
|
; Drop Run, App Paths and Uninstall entries left in the 32-bit registry view
|
||||||
|
; 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}
|
||||||
|
|
||||||
@@ -299,11 +307,16 @@ 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 registry entry
|
; Remove autostart entries from every view a previous installer may have used.
|
||||||
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,13 +360,7 @@ func isRedirectURLPortUsed(redirectURL string, excludedRanges []excludedPortRang
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// FreeBSD 15 disables connecting to INADDR_ANY (0.0.0.0) as a localhost
|
addr := fmt.Sprintf(":%s", port)
|
||||||
// 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
|
||||||
|
|||||||
@@ -339,7 +339,8 @@ 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:
|
||||||
return strings.HasSuffix(qname, "."+entry.Pattern)
|
parts := strings.Split(strings.TrimSuffix(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,54 +164,6 @@ 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 {
|
||||||
@@ -321,19 +273,6 @@ 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,19 +26,6 @@ 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
|
||||||
@@ -46,11 +33,6 @@ 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
|
||||||
@@ -67,15 +49,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -122,7 +95,6 @@ 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)
|
||||||
@@ -464,78 +436,6 @@ 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) == 0 {
|
|
||||||
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,21 +30,6 @@ 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.",
|
||||||
@@ -2667,114 +2652,3 @@ 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"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -301,11 +301,6 @@ func newDefaultServer(
|
|||||||
warningDelayBase: defaultWarningDelayBase,
|
warningDelayBase: defaultWarningDelayBase,
|
||||||
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)
|
||||||
@@ -1391,25 +1386,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,9 +61,11 @@ import (
|
|||||||
cProto "github.com/netbirdio/netbird/client/proto"
|
cProto "github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
mgm "github.com/netbirdio/netbird/shared/management/client"
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
nbnetworkmap "github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
||||||
@@ -202,6 +204,13 @@ 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
|
||||||
|
|
||||||
|
// latestComponents is the most-recent NetworkMapComponents decoded from
|
||||||
|
// a NetworkMapEnvelope (capability=3 peers only). Held alongside the
|
||||||
|
// NetworkMap that Calculate() produced from it so Step 3 incremental
|
||||||
|
// updates have a base to apply changes against. nil for legacy-format
|
||||||
|
// peers. Guarded by syncMsgMux.
|
||||||
|
latestComponents *types.NetworkMapComponents
|
||||||
|
|
||||||
networkMonitor *networkmonitor.NetworkMonitor
|
networkMonitor *networkmonitor.NetworkMonitor
|
||||||
|
|
||||||
sshServer sshServer
|
sshServer sshServer
|
||||||
@@ -865,8 +874,12 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return e.ctx.Err()
|
return e.ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil {
|
// Envelope sync responses carry PeerConfig at the top level; legacy
|
||||||
e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate)
|
// NetworkMap syncs carry it under NetworkMap.PeerConfig.
|
||||||
|
if pc := update.GetPeerConfig(); pc != nil {
|
||||||
|
e.handleAutoUpdateVersion(pc.GetAutoUpdate())
|
||||||
|
} else if nm := update.GetNetworkMap(); nm != nil && nm.GetPeerConfig() != nil {
|
||||||
|
e.handleAutoUpdateVersion(nm.GetPeerConfig().GetAutoUpdate())
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.GetNetbirdConfig() != nil {
|
if update.GetNetbirdConfig() != nil {
|
||||||
@@ -907,11 +920,45 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nm := update.GetNetworkMap()
|
var (
|
||||||
|
nm *mgmProto.NetworkMap
|
||||||
|
components *types.NetworkMapComponents
|
||||||
|
)
|
||||||
|
if envelope := update.GetNetworkMapEnvelope(); envelope != nil {
|
||||||
|
// Components-format peer: decode the envelope back to typed
|
||||||
|
// components, run Calculate() locally, and convert to the wire
|
||||||
|
// NetworkMap shape the rest of the engine consumes. Components are
|
||||||
|
// retained so future incremental updates (Step 3) can apply deltas
|
||||||
|
// instead of doing a full reconstruction.
|
||||||
|
localKey := e.config.WgPrivateKey.PublicKey().String()
|
||||||
|
dnsName := ""
|
||||||
|
if pc := update.GetPeerConfig(); pc != nil {
|
||||||
|
// PeerConfig.Fqdn = "<dns_label>.<dns_domain>" — extract the
|
||||||
|
// shared domain by stripping the peer's own label prefix. Falls
|
||||||
|
// back to empty if the FQDN doesn't have the expected shape.
|
||||||
|
dnsName = extractDNSDomainFromFQDN(pc.GetFqdn())
|
||||||
|
}
|
||||||
|
result, err := nbnetworkmap.EnvelopeToNetworkMap(e.ctx, envelope, localKey, dnsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode network map envelope: %w", err)
|
||||||
|
}
|
||||||
|
nm = result.NetworkMap
|
||||||
|
components = result.Components
|
||||||
|
} else {
|
||||||
|
nm = update.GetNetworkMap()
|
||||||
|
}
|
||||||
if nm == nil {
|
if nm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only retain the components view when the server sent the envelope
|
||||||
|
// path. A legacy proto.NetworkMap means components == nil; writing it
|
||||||
|
// here would clobber a previously-cached snapshot, breaking the Step 3
|
||||||
|
// incremental-delta base on a future envelope sync.
|
||||||
|
if components != nil {
|
||||||
|
e.latestComponents = components
|
||||||
|
}
|
||||||
|
|
||||||
// Persist sync response under the dedicated lock (syncRespMux), not under syncMsgMux.
|
// Persist sync response under the dedicated lock (syncRespMux), not under syncMsgMux.
|
||||||
// Read the storage-enabled flag under the syncRespMux too.
|
// Read the storage-enabled flag under the syncRespMux too.
|
||||||
e.syncRespMux.RLock()
|
e.syncRespMux.RLock()
|
||||||
@@ -937,6 +984,19 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractDNSDomainFromFQDN returns the trailing dotted domain part of the
|
||||||
|
// receiving peer's FQDN — the same value the management server fills as
|
||||||
|
// dnsName when it builds the legacy NetworkMap. "peer42.netbird.cloud" →
|
||||||
|
// "netbird.cloud". An empty string is returned for unrecognized formats.
|
||||||
|
func extractDNSDomainFromFQDN(fqdn string) string {
|
||||||
|
for i := 0; i < len(fqdn); i++ {
|
||||||
|
if fqdn[i] == '.' && i+1 < len(fqdn) {
|
||||||
|
return fqdn[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -1967,29 +2027,6 @@ 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 {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/management/server/job"
|
"github.com/netbirdio/netbird/management/server/job"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
"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/controller"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||||
@@ -66,8 +66,8 @@ import (
|
|||||||
"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"
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
signal "github.com/netbirdio/netbird/shared/signal/client"
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
"github.com/netbirdio/netbird/shared/signal/proto"
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
signalServer "github.com/netbirdio/netbird/signal/server"
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
||||||
@@ -1641,7 +1641,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
// handle route changes
|
// handle route changes
|
||||||
case unix.RTM_ADD, syscall.RTM_DELETE:
|
case unix.RTM_ADD, syscall.RTM_DELETE:
|
||||||
route, flags, err := parseRouteMessage(buf[:n])
|
route, err := parseRouteMessage(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -66,10 +66,6 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
}
|
}
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case unix.RTM_ADD:
|
case unix.RTM_ADD:
|
||||||
if systemops.IgnoreAddedDefaultRoute(flags) {
|
|
||||||
log.Debugf("Network monitor: ignoring added default route via %s, interface %s, flags %#x", route.Gw, intf, flags)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
||||||
return nil
|
return nil
|
||||||
case unix.RTM_DELETE:
|
case unix.RTM_DELETE:
|
||||||
@@ -82,26 +78,22 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRouteMessage(buf []byte) (*systemops.Route, int, error) {
|
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
||||||
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("parse RIB: %v", err)
|
return nil, fmt.Errorf("parse RIB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msgs) != 1 {
|
if len(msgs) != 1 {
|
||||||
return nil, 0, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, ok := msgs[0].(*route.RouteMessage)
|
msg, ok := msgs[0].(*route.RouteMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, 0, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := systemops.MsgToRoute(msg)
|
return systemops.MsgToRoute(msg)
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return r, msg.Flags, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer/id"
|
"github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
||||||
"github.com/netbirdio/netbird/client/internal/portforward"
|
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||||
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
@@ -900,7 +899,7 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to deterministic key if no NetBird PSK is configured
|
// Fallback to deterministic key if no NetBird PSK is configured
|
||||||
determKey, err := rosenpass.DeterministicSeedKey(conn.config.LocalKey, conn.config.Key)
|
determKey, err := conn.rosenpassDetermKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
|
conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -909,6 +908,26 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|||||||
return determKey
|
return determKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: move this logic into Rosenpass package
|
||||||
|
func (conn *Conn) rosenpassDetermKey() (*wgtypes.Key, error) {
|
||||||
|
lk := []byte(conn.config.LocalKey)
|
||||||
|
rk := []byte(conn.config.Key) // remote key
|
||||||
|
var keyInput []byte
|
||||||
|
if string(lk) > string(rk) {
|
||||||
|
//nolint:gocritic
|
||||||
|
keyInput = append(lk[:16], rk[:16]...)
|
||||||
|
} else {
|
||||||
|
//nolint:gocritic
|
||||||
|
keyInput = append(rk[:16], lk[:16]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := wgtypes.NewKey(keyInput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &key, nil
|
||||||
|
}
|
||||||
|
|
||||||
func isController(config ConnConfig) bool {
|
func isController(config ConnConfig) bool {
|
||||||
return config.LocalKey > config.Key
|
return config.LocalKey > config.Key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,12 +185,9 @@ func (s *StatusChangeSubscription) Events() chan map[string]RouterState {
|
|||||||
return s.eventsChan
|
return s.eventsChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status holds a state of peers, signal, management connections and relays.
|
// Status holds a state of peers, signal, management connections and relays
|
||||||
// mux is an RWMutex so hot read paths (notably PeerStateByIP, called for
|
|
||||||
// every private-service request) don't contend against each other.
|
|
||||||
// Pure read methods take RLock; anything that mutates state takes Lock.
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
mux sync.RWMutex
|
mux sync.Mutex
|
||||||
peers map[string]State
|
peers map[string]State
|
||||||
changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription
|
changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription
|
||||||
signalState bool
|
signalState bool
|
||||||
@@ -286,8 +283,8 @@ func (d *Status) AddPeer(peerPubKey string, fqdn string, ip string, ipv6 string)
|
|||||||
|
|
||||||
// GetPeer adds peer to Daemon status map
|
// GetPeer adds peer to Daemon status map
|
||||||
func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
state, ok := d.peers[peerPubKey]
|
state, ok := d.peers[peerPubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -297,8 +294,8 @@ func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) PeerByIP(ip string) (string, bool) {
|
func (d *Status) PeerByIP(ip string) (string, bool) {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
for _, state := range d.peers {
|
for _, state := range d.peers {
|
||||||
if state.IP == ip {
|
if state.IP == ip {
|
||||||
@@ -308,25 +305,6 @@ func (d *Status) PeerByIP(ip string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerStateByIP returns the full peer State for the given tunnel IP.
|
|
||||||
// Matches against either the IPv4 (State.IP) or IPv6 (State.IPv6) tunnel
|
|
||||||
// address so dual-stack peers are reachable on either family. Returns the
|
|
||||||
// zero State and false when no peer matches or the input is empty.
|
|
||||||
func (d *Status) PeerStateByIP(ip string) (State, bool) {
|
|
||||||
if ip == "" {
|
|
||||||
return State{}, false
|
|
||||||
}
|
|
||||||
d.mux.RLock()
|
|
||||||
defer d.mux.RUnlock()
|
|
||||||
|
|
||||||
for _, state := range d.peers {
|
|
||||||
if (state.IP != "" && state.IP == ip) || (state.IPv6 != "" && state.IPv6 == ip) {
|
|
||||||
return state, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return State{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemovePeer removes peer from Daemon status map
|
// RemovePeer removes peer from Daemon status map
|
||||||
func (d *Status) RemovePeer(peerPubKey string) error {
|
func (d *Status) RemovePeer(peerPubKey string) error {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
@@ -724,8 +702,8 @@ func (d *Status) UnsubscribePeerStateChanges(subscription *StatusChangeSubscript
|
|||||||
|
|
||||||
// GetLocalPeerState returns the local peer state
|
// GetLocalPeerState returns the local peer state
|
||||||
func (d *Status) GetLocalPeerState() LocalPeerState {
|
func (d *Status) GetLocalPeerState() LocalPeerState {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
return d.localPeer.Clone()
|
return d.localPeer.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,8 +909,8 @@ func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetRosenpassState() RosenpassState {
|
func (d *Status) GetRosenpassState() RosenpassState {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
return RosenpassState{
|
return RosenpassState{
|
||||||
d.rosenpassEnabled,
|
d.rosenpassEnabled,
|
||||||
d.rosenpassPermissive,
|
d.rosenpassPermissive,
|
||||||
@@ -940,14 +918,14 @@ func (d *Status) GetRosenpassState() RosenpassState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetLazyConnection() bool {
|
func (d *Status) GetLazyConnection() bool {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
return d.lazyConnectionEnabled
|
return d.lazyConnectionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetManagementState() ManagementState {
|
func (d *Status) GetManagementState() ManagementState {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
return ManagementState{
|
return ManagementState{
|
||||||
d.mgmAddress,
|
d.mgmAddress,
|
||||||
d.managementState,
|
d.managementState,
|
||||||
@@ -973,8 +951,8 @@ func (d *Status) UpdateLatency(pubKey string, latency time.Duration) error {
|
|||||||
|
|
||||||
// IsLoginRequired determines if a peer's login has expired.
|
// IsLoginRequired determines if a peer's login has expired.
|
||||||
func (d *Status) IsLoginRequired() bool {
|
func (d *Status) IsLoginRequired() bool {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
// if peer is connected to the management then login is not expired
|
// if peer is connected to the management then login is not expired
|
||||||
if d.managementState {
|
if d.managementState {
|
||||||
@@ -989,8 +967,8 @@ func (d *Status) IsLoginRequired() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetSignalState() SignalState {
|
func (d *Status) GetSignalState() SignalState {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
return SignalState{
|
return SignalState{
|
||||||
d.signalAddress,
|
d.signalAddress,
|
||||||
d.signalState,
|
d.signalState,
|
||||||
@@ -1000,8 +978,8 @@ func (d *Status) GetSignalState() SignalState {
|
|||||||
|
|
||||||
// GetRelayStates returns the stun/turn/permanent relay states
|
// GetRelayStates returns the stun/turn/permanent relay states
|
||||||
func (d *Status) GetRelayStates() []relay.ProbeResult {
|
func (d *Status) GetRelayStates() []relay.ProbeResult {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
if d.relayMgr == nil {
|
if d.relayMgr == nil {
|
||||||
return d.relayStates
|
return d.relayStates
|
||||||
}
|
}
|
||||||
@@ -1030,8 +1008,8 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
if d.ingressGwMgr == nil {
|
if d.ingressGwMgr == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1040,16 +1018,16 @@ func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetDNSStates() []NSGroupState {
|
func (d *Status) GetDNSStates() []NSGroupState {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
// shallow copy is good enough, as slices fields are currently not updated
|
// shallow copy is good enough, as slices fields are currently not updated
|
||||||
return slices.Clone(d.nsGroupStates)
|
return slices.Clone(d.nsGroupStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
return maps.Clone(d.resolvedDomainsStates)
|
return maps.Clone(d.resolvedDomainsStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,8 +1043,8 @@ func (d *Status) GetFullStatus() FullStatus {
|
|||||||
LazyConnectionEnabled: d.GetLazyConnection(),
|
LazyConnectionEnabled: d.GetLazyConnection(),
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
fullStatus.LocalPeerState = d.localPeer
|
fullStatus.LocalPeerState = d.localPeer
|
||||||
|
|
||||||
@@ -1241,8 +1219,8 @@ func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) PeersStatus() (*configurer.Stats, error) {
|
func (d *Status) PeersStatus() (*configurer.Stats, error) {
|
||||||
d.mux.RLock()
|
d.mux.Lock()
|
||||||
defer d.mux.RUnlock()
|
defer d.mux.Unlock()
|
||||||
if d.wgIface == nil {
|
if d.wgIface == nil {
|
||||||
return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
|
return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,33 +63,6 @@ func TestUpdatePeerState(t *testing.T) {
|
|||||||
assert.Equal(t, ip, state.IP, "ip should be equal")
|
assert.Equal(t, ip, state.IP, "ip should be equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatus_PeerStateByIP(t *testing.T) {
|
|
||||||
status := NewRecorder("https://mgm")
|
|
||||||
req := require.New(t)
|
|
||||||
|
|
||||||
req.NoError(status.AddPeer("pk-1", "peer-1.netbird", "100.64.0.10", ""))
|
|
||||||
req.NoError(status.AddPeer("pk-2", "peer-2.netbird", "100.64.0.11", ""))
|
|
||||||
|
|
||||||
state, ok := status.PeerStateByIP("100.64.0.10")
|
|
||||||
req.True(ok, "known tunnel IP should resolve to a peer state")
|
|
||||||
req.Equal("pk-1", state.PubKey, "matching state must carry the right pub key")
|
|
||||||
req.Equal("peer-1.netbird", state.FQDN, "matching state must carry the right FQDN")
|
|
||||||
|
|
||||||
_, ok = status.PeerStateByIP("100.64.0.99")
|
|
||||||
req.False(ok, "unknown IP must report ok=false")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatus_PeerStateByIP_MatchesIPv6(t *testing.T) {
|
|
||||||
status := NewRecorder("https://mgm")
|
|
||||||
req := require.New(t)
|
|
||||||
|
|
||||||
req.NoError(status.AddPeer("pk-1", "peer-1.netbird", "100.64.0.10", "fd00::1"))
|
|
||||||
|
|
||||||
state, ok := status.PeerStateByIP("fd00::1")
|
|
||||||
req.True(ok, "IPv6-only match must resolve to the peer state")
|
|
||||||
req.Equal("pk-1", state.PubKey, "matching state must carry the right pub key")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
||||||
key := "abc"
|
key := "abc"
|
||||||
fqdn := "peer-a.netbird.local"
|
fqdn := "peer-a.netbird.local"
|
||||||
|
|||||||
@@ -179,10 +179,8 @@ func getDefaultGateway() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dst := net.IPv4zero
|
dst := net.IPv4zero
|
||||||
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
if runtime.GOOS == "linux" {
|
||||||
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux/Android.
|
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux.
|
||||||
// TODO: on android/ios, use platform APIs (ConnectivityManager.getLinkProperties /
|
|
||||||
// NWPathMonitor) when netlink-based lookup is restricted or unavailable.
|
|
||||||
dst = net.IPv4(0, 0, 0, 1)
|
dst = net.IPv4(0, 0, 0, 1)
|
||||||
}
|
}
|
||||||
_, gateway, localIP, err = router.Route(dst)
|
_, gateway, localIP, err = router.Route(dst)
|
||||||
@@ -205,7 +203,7 @@ func getDefaultGateway6() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dst := net.IPv6zero
|
dst := net.IPv6zero
|
||||||
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
if runtime.GOOS == "linux" {
|
||||||
// ::2
|
// ::2
|
||||||
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,6 @@ func hashRosenpassKey(key []byte) string {
|
|||||||
return hex.EncodeToString(hasher.Sum(nil))
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// rpServer is the subset of rp.Server used by Manager. Defined as an interface
|
|
||||||
// so tests can substitute a mock without spinning up a real UDP server.
|
|
||||||
type rpServer interface {
|
|
||||||
AddPeer(rp.PeerConfig) (rp.PeerID, error)
|
|
||||||
RemovePeer(rp.PeerID) error
|
|
||||||
Run() error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
ifaceName string
|
ifaceName string
|
||||||
spk []byte
|
spk []byte
|
||||||
@@ -45,7 +36,7 @@ type Manager struct {
|
|||||||
preSharedKey *[32]byte
|
preSharedKey *[32]byte
|
||||||
rpPeerIDs map[string]*rp.PeerID
|
rpPeerIDs map[string]*rp.PeerID
|
||||||
rpWgHandler *NetbirdHandler
|
rpWgHandler *NetbirdHandler
|
||||||
server rpServer
|
server *rp.Server
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
port int
|
port int
|
||||||
wgIface PresharedKeySetter
|
wgIface PresharedKeySetter
|
||||||
@@ -60,22 +51,7 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error)
|
|||||||
|
|
||||||
rpKeyHash := hashRosenpassKey(public)
|
rpKeyHash := hashRosenpassKey(public)
|
||||||
log.Tracef("generated new rosenpass key pair with public key %s", rpKeyHash)
|
log.Tracef("generated new rosenpass key pair with public key %s", rpKeyHash)
|
||||||
return &Manager{
|
return &Manager{ifaceName: wgIfaceName, rpKeyHash: rpKeyHash, spk: public, ssk: secret, preSharedKey: (*[32]byte)(preSharedKey), rpPeerIDs: make(map[string]*rp.PeerID), lock: sync.Mutex{}}, nil
|
||||||
ifaceName: wgIfaceName,
|
|
||||||
rpKeyHash: rpKeyHash,
|
|
||||||
spk: public,
|
|
||||||
ssk: secret,
|
|
||||||
preSharedKey: (*[32]byte)(preSharedKey),
|
|
||||||
rpPeerIDs: make(map[string]*rp.PeerID),
|
|
||||||
// rpWgHandler is created here (instead of only in generateConfig) so it
|
|
||||||
// is never nil between NewManager and Run(). Otherwise an early
|
|
||||||
// OnConnected call (race observed on Android, issue #4341) panics on
|
|
||||||
// nil receiver in addPeer -> m.rpWgHandler.AddPeer. generateConfig will
|
|
||||||
// replace it with a fresh handler on each Run() to clear stale peer
|
|
||||||
// state from previous engine sessions.
|
|
||||||
rpWgHandler: NewNetbirdHandler(),
|
|
||||||
lock: sync.Mutex{},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetPubKey() []byte {
|
func (m *Manager) GetPubKey() []byte {
|
||||||
@@ -89,16 +65,6 @@ func (m *Manager) GetAddress() *net.UDPAddr {
|
|||||||
|
|
||||||
// addPeer adds a new peer to the Rosenpass server
|
// addPeer adds a new peer to the Rosenpass server
|
||||||
func (m *Manager) addPeer(rosenpassPubKey []byte, rosenpassAddr string, wireGuardIP string, wireGuardPubKey string) error {
|
func (m *Manager) addPeer(rosenpassPubKey []byte, rosenpassAddr string, wireGuardIP string, wireGuardPubKey string) error {
|
||||||
// Defense in depth against issue #4341 (Android crash): if Run() has not
|
|
||||||
// completed yet, m.server / m.rpWgHandler may be nil. Return an explicit
|
|
||||||
// error instead of panicking on nil-receiver dereference.
|
|
||||||
if m.server == nil {
|
|
||||||
return fmt.Errorf("rosenpass server not initialized")
|
|
||||||
}
|
|
||||||
if m.rpWgHandler == nil {
|
|
||||||
return fmt.Errorf("rosenpass wg handler not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
pcfg := rp.PeerConfig{PublicKey: rosenpassPubKey}
|
pcfg := rp.PeerConfig{PublicKey: rosenpassPubKey}
|
||||||
if m.preSharedKey != nil {
|
if m.preSharedKey != nil {
|
||||||
@@ -113,16 +79,6 @@ func (m *Manager) addPeer(rosenpassPubKey []byte, rosenpassAddr string, wireGuar
|
|||||||
if pcfg.Endpoint, err = net.ResolveUDPAddr("udp", peerAddr); err != nil {
|
if pcfg.Endpoint, err = net.ResolveUDPAddr("udp", peerAddr); err != nil {
|
||||||
return fmt.Errorf("failed to resolve peer endpoint address: %w", err)
|
return fmt.Errorf("failed to resolve peer endpoint address: %w", err)
|
||||||
}
|
}
|
||||||
// Our local Rosenpass UDP server binds on the IPv6 wildcard ([::]) — see
|
|
||||||
// GetAddress(). The remote peer's endpoint (pcfg.Endpoint) is the destination
|
|
||||||
// our server will sendto when initiating handshakes. ResolveUDPAddr returns a
|
|
||||||
// 4-byte IPv4 for IPv4 hosts, which the kernel rejects (EDESTADDRREQ) when
|
|
||||||
// sent from an AF_INET6 socket. Normalize the remote endpoint to IPv4-mapped
|
|
||||||
// IPv6 so its address family matches our listening socket.
|
|
||||||
// TODO: maybe bind the Rosenpass UDP server to the peer wg IP addr
|
|
||||||
if v4 := pcfg.Endpoint.IP.To4(); v4 != nil {
|
|
||||||
pcfg.Endpoint.IP = v4.To16()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
peerID, err := m.server.AddPeer(pcfg)
|
peerID, err := m.server.AddPeer(pcfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,31 +182,24 @@ func (m *Manager) Run() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := rp.NewUDPServer(conf)
|
m.server, err = rp.NewUDPServer(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.lock.Lock()
|
|
||||||
m.server = server
|
|
||||||
m.lock.Unlock()
|
|
||||||
|
|
||||||
log.Infof("starting rosenpass server on port %d", m.port)
|
log.Infof("starting rosenpass server on port %d", m.port)
|
||||||
|
|
||||||
return server.Run()
|
return m.server.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the Rosenpass server
|
// Close closes the Rosenpass server
|
||||||
func (m *Manager) Close() error {
|
func (m *Manager) Close() error {
|
||||||
m.lock.Lock()
|
if m.server != nil {
|
||||||
server := m.server
|
err := m.server.Close()
|
||||||
m.server = nil
|
if err != nil {
|
||||||
m.lock.Unlock()
|
log.Errorf("failed closing local rosenpass server")
|
||||||
if server == nil {
|
}
|
||||||
return nil
|
m.server = nil
|
||||||
}
|
|
||||||
if err := server.Close(); err != nil {
|
|
||||||
log.Errorf("failed closing local rosenpass server: %v", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,412 +1,14 @@
|
|||||||
package rosenpass
|
package rosenpass
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
rp "cunicu.li/go-rosenpass"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- test doubles -----------------------------------------------------------
|
|
||||||
|
|
||||||
type addPeerCall struct {
|
|
||||||
cfg rp.PeerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type removePeerCall struct {
|
|
||||||
id rp.PeerID
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockServer struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
addCalls []addPeerCall
|
|
||||||
removed []removePeerCall
|
|
||||||
nextID rp.PeerID
|
|
||||||
addErr error
|
|
||||||
removeErr error
|
|
||||||
closed bool
|
|
||||||
ran bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockServer) AddPeer(cfg rp.PeerConfig) (rp.PeerID, error) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
m.addCalls = append(m.addCalls, addPeerCall{cfg: cfg})
|
|
||||||
if m.addErr != nil {
|
|
||||||
return rp.PeerID{}, m.addErr
|
|
||||||
}
|
|
||||||
// Increment a byte in nextID so distinct peers get distinct IDs.
|
|
||||||
m.nextID[0]++
|
|
||||||
return m.nextID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockServer) RemovePeer(id rp.PeerID) error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
m.removed = append(m.removed, removePeerCall{id: id})
|
|
||||||
return m.removeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockServer) Run() error { m.ran = true; return nil }
|
|
||||||
func (m *mockServer) Close() error { m.closed = true; return nil }
|
|
||||||
|
|
||||||
type setPSKCall struct {
|
|
||||||
peerKey string
|
|
||||||
psk wgtypes.Key
|
|
||||||
updateOnly bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockIface struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
calls []setPSKCall
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIface) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
m.calls = append(m.calls, setPSKCall{peerKey: peerKey, psk: psk, updateOnly: updateOnly})
|
|
||||||
return m.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTestManager builds a Manager with deterministic spk so tie-break
|
|
||||||
// against a peer pubkey is controllable from tests. The provided spk byte
|
|
||||||
// becomes the first byte; remaining bytes are zero.
|
|
||||||
func newTestManager(spkFirstByte byte, mock *mockServer) *Manager {
|
|
||||||
spk := make([]byte, 32)
|
|
||||||
spk[0] = spkFirstByte
|
|
||||||
return &Manager{
|
|
||||||
ifaceName: "wt0",
|
|
||||||
spk: spk,
|
|
||||||
ssk: make([]byte, 32),
|
|
||||||
rpKeyHash: "test-hash",
|
|
||||||
rpPeerIDs: make(map[string]*rp.PeerID),
|
|
||||||
rpWgHandler: NewNetbirdHandler(),
|
|
||||||
server: mock,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validWGKey returns a deterministic 32-byte wireguard public key (base64).
|
|
||||||
func validWGKey(t *testing.T, lastByte byte) string {
|
|
||||||
t.Helper()
|
|
||||||
var k wgtypes.Key
|
|
||||||
k[31] = lastByte
|
|
||||||
return k.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- pure helpers ----------------------------------------------------------
|
|
||||||
|
|
||||||
func TestHashRosenpassKey_Deterministic(t *testing.T) {
|
|
||||||
key := []byte("hello-rosenpass")
|
|
||||||
require.Equal(t, hashRosenpassKey(key), hashRosenpassKey(key))
|
|
||||||
require.Len(t, hashRosenpassKey(key), 64) // sha256 hex
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHashRosenpassKey_DifferentInputsDifferOutputs(t *testing.T) {
|
|
||||||
require.NotEqual(t, hashRosenpassKey([]byte("a")), hashRosenpassKey([]byte("b")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetLogLevel_DefaultWhenUnset(t *testing.T) {
|
|
||||||
// Snapshot + unset to exercise the LookupEnv ok=false branch. t.Setenv
|
|
||||||
// can only set, not delete, so do it manually with restore via t.Cleanup.
|
|
||||||
prev, hadPrev := os.LookupEnv(defaultLogLevelVar)
|
|
||||||
require.NoError(t, os.Unsetenv(defaultLogLevelVar))
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if hadPrev {
|
|
||||||
_ = os.Setenv(defaultLogLevelVar, prev)
|
|
||||||
} else {
|
|
||||||
_ = os.Unsetenv(defaultLogLevelVar)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
require.Equal(t, defaultLog.String(), getLogLevel().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetLogLevel_Cases(t *testing.T) {
|
|
||||||
cases := map[string]string{
|
|
||||||
"debug": "DEBUG",
|
|
||||||
"info": "INFO",
|
|
||||||
"warn": "WARN",
|
|
||||||
"error": "ERROR",
|
|
||||||
"unknown": "INFO", // default fallback
|
|
||||||
}
|
|
||||||
for input, wantStr := range cases {
|
|
||||||
input, wantStr := input, wantStr
|
|
||||||
t.Run(input, func(t *testing.T) {
|
|
||||||
t.Setenv(defaultLogLevelVar, input)
|
|
||||||
require.Equal(t, wantStr, getLogLevel().String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindRandomAvailableUDPPort(t *testing.T) {
|
func TestFindRandomAvailableUDPPort(t *testing.T) {
|
||||||
port, err := findRandomAvailableUDPPort()
|
port, err := findRandomAvailableUDPPort()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Greater(t, port, 0)
|
require.Greater(t, port, 0)
|
||||||
require.LessOrEqual(t, port, 65535)
|
require.LessOrEqual(t, port, 65535)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- addPeer ---------------------------------------------------------------
|
|
||||||
|
|
||||||
func TestAddPeer_HigherLocalPubkey_SetsEndpoint(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv) // local spk lexicographically larger
|
|
||||||
|
|
||||||
remotePubKey := make([]byte, 32) // remote spk = all zeros (smaller)
|
|
||||||
err := m.addPeer(remotePubKey, "rosenpass-host:7000", "100.1.1.1", validWGKey(t, 1))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, srv.addCalls, 1)
|
|
||||||
|
|
||||||
ep := srv.addCalls[0].cfg.Endpoint
|
|
||||||
require.NotNil(t, ep, "initiator side must set Endpoint")
|
|
||||||
require.Equal(t, 7000, ep.Port)
|
|
||||||
require.Equal(t, "100.1.1.1", ep.IP.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_HigherLocalPubkey_EndpointIPIsIPv4Mapped(t *testing.T) {
|
|
||||||
// Regression guard for the EDESTADDRREQ fix: Endpoint.IP must be 16-byte
|
|
||||||
// (IPv4-mapped IPv6) so it matches the AF_INET6 listening socket family.
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", validWGKey(t, 1))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ep := srv.addCalls[0].cfg.Endpoint
|
|
||||||
require.NotNil(t, ep)
|
|
||||||
require.Len(t, ep.IP, 16, "IPv4 endpoint must be normalized to 16-byte v4-mapped form")
|
|
||||||
require.True(t, ep.IP.To4() != nil, "Endpoint must still be detected as IPv4")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_LowerLocalPubkey_LeavesEndpointNil(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0x00, srv) // local spk smaller
|
|
||||||
|
|
||||||
remotePubKey := make([]byte, 32)
|
|
||||||
remotePubKey[0] = 0xFF
|
|
||||||
err := m.addPeer(remotePubKey, "rp:5000", "100.1.1.1", validWGKey(t, 2))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Nil(t, srv.addCalls[0].cfg.Endpoint, "responder side must NOT set Endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_PresharedKeyPropagated(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
psk := &wgtypes.Key{0x42}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
m.preSharedKey = (*[32]byte)(psk)
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", validWGKey(t, 3))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, [32]byte(*psk), [32]byte(srv.addCalls[0].cfg.PresharedKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_InvalidRosenpassAddr_ReturnsError(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv) // initiator path → parses rosenpassAddr
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "not-a-host-port", "100.1.1.1", validWGKey(t, 1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Empty(t, srv.addCalls, "server.AddPeer must not run when address parse fails")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_InvalidWireGuardPubKey_ReturnsError(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", "not-a-valid-key")
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_ServerError_Propagates(t *testing.T) {
|
|
||||||
srv := &mockServer{addErr: errors.New("boom")}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", validWGKey(t, 1))
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regression guard for issue #4341 (Android crash). If Run() has not completed
|
|
||||||
// before OnConnected fires, m.rpWgHandler or m.server may be nil. Without the
|
|
||||||
// nil guards, m.rpWgHandler.AddPeer panics on nil receiver.
|
|
||||||
func TestAddPeer_NilHandler_ReturnsErrorNoCrash(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
m.rpWgHandler = nil // simulate Run() not yet completed
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", validWGKey(t, 1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "wg handler not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_NilServer_ReturnsErrorNoCrash(t *testing.T) {
|
|
||||||
m := newTestManager(0xFF, nil)
|
|
||||||
m.server = nil // simulate Run() not yet completed
|
|
||||||
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", validWGKey(t, 1))
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "server not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewManager must pre-initialize rpWgHandler so the nil-receiver crash from
|
|
||||||
// issue #4341 cannot occur in the window between NewManager and Run().
|
|
||||||
func TestNewManager_PreInitializesHandler(t *testing.T) {
|
|
||||||
psk := wgtypes.Key{}
|
|
||||||
m, err := NewManager(&psk, "wt0")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, m.rpWgHandler, "rpWgHandler must be initialized in NewManager")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPeer_RecordsPeerID(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
wgKey := validWGKey(t, 5)
|
|
||||||
err := m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", wgKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Contains(t, m.rpPeerIDs, wgKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- OnConnected / OnDisconnected ------------------------------------------
|
|
||||||
|
|
||||||
func TestOnConnected_NilRemotePubKey_NoAddPeer(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
m.OnConnected(validWGKey(t, 1), nil, "100.1.1.1", "rp:5000")
|
|
||||||
require.Empty(t, srv.addCalls, "nil remote rosenpass pubkey must skip AddPeer")
|
|
||||||
require.Empty(t, m.rpPeerIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnConnected_ValidPubKey_CallsAddPeer(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
wgKey := validWGKey(t, 1)
|
|
||||||
m.OnConnected(wgKey, make([]byte, 32), "100.1.1.1", "rp:5000")
|
|
||||||
require.Len(t, srv.addCalls, 1)
|
|
||||||
require.Contains(t, m.rpPeerIDs, wgKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnDisconnected_UnknownPeer_NoOp(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
m.OnDisconnected(validWGKey(t, 99))
|
|
||||||
require.Empty(t, srv.removed, "unknown peer key must not call RemovePeer")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnDisconnected_KnownPeer_CallsRemoveAndForgets(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
wgKey := validWGKey(t, 1)
|
|
||||||
require.NoError(t, m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", wgKey))
|
|
||||||
require.Contains(t, m.rpPeerIDs, wgKey)
|
|
||||||
|
|
||||||
m.OnDisconnected(wgKey)
|
|
||||||
require.Len(t, srv.removed, 1)
|
|
||||||
require.NotContains(t, m.rpPeerIDs, wgKey, "peer must be forgotten after disconnect")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- IsPresharedKeyInitialized ---------------------------------------------
|
|
||||||
|
|
||||||
func TestIsPresharedKeyInitialized_UnknownPeer_ReturnsFalse(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
require.False(t, m.IsPresharedKeyInitialized(validWGKey(t, 1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsPresharedKeyInitialized_AddedButNotHandshaken_ReturnsFalse(t *testing.T) {
|
|
||||||
srv := &mockServer{}
|
|
||||||
m := newTestManager(0xFF, srv)
|
|
||||||
|
|
||||||
wgKey := validWGKey(t, 2)
|
|
||||||
require.NoError(t, m.addPeer(make([]byte, 32), "rp:5000", "100.1.1.1", wgKey))
|
|
||||||
require.False(t, m.IsPresharedKeyInitialized(wgKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- NetbirdHandler.outputKey ----------------------------------------------
|
|
||||||
|
|
||||||
func TestHandler_OutputKey_FirstCallUsesUpdateOnlyFalse(t *testing.T) {
|
|
||||||
h := NewNetbirdHandler()
|
|
||||||
iface := &mockIface{}
|
|
||||||
h.SetInterface(iface)
|
|
||||||
|
|
||||||
pid := rp.PeerID{0x01}
|
|
||||||
wgKey := wgtypes.Key{0xAA}
|
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgKey))
|
|
||||||
|
|
||||||
psk := rp.Key{0xBB}
|
|
||||||
h.HandshakeCompleted(pid, psk)
|
|
||||||
|
|
||||||
require.Len(t, iface.calls, 1)
|
|
||||||
require.False(t, iface.calls[0].updateOnly, "first PSK rotation must use updateOnly=false")
|
|
||||||
require.Equal(t, wgKey.String(), iface.calls[0].peerKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_OutputKey_SubsequentCallsUseUpdateOnlyTrue(t *testing.T) {
|
|
||||||
h := NewNetbirdHandler()
|
|
||||||
iface := &mockIface{}
|
|
||||||
h.SetInterface(iface)
|
|
||||||
|
|
||||||
pid := rp.PeerID{0x02}
|
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgtypes.Key{0xCC}))
|
|
||||||
|
|
||||||
h.HandshakeCompleted(pid, rp.Key{0x01}) // first
|
|
||||||
h.HandshakeCompleted(pid, rp.Key{0x02}) // second
|
|
||||||
|
|
||||||
require.Len(t, iface.calls, 2)
|
|
||||||
require.False(t, iface.calls[0].updateOnly)
|
|
||||||
require.True(t, iface.calls[1].updateOnly, "subsequent rotations must use updateOnly=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_OutputKey_NilInterface_NoCrashNoCall(t *testing.T) {
|
|
||||||
h := NewNetbirdHandler()
|
|
||||||
// no SetInterface — iface remains nil
|
|
||||||
pid := rp.PeerID{0x03}
|
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgtypes.Key{}))
|
|
||||||
|
|
||||||
// Must not panic.
|
|
||||||
h.HandshakeCompleted(pid, rp.Key{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_OutputKey_UnknownPeer_NoCall(t *testing.T) {
|
|
||||||
h := NewNetbirdHandler()
|
|
||||||
iface := &mockIface{}
|
|
||||||
h.SetInterface(iface)
|
|
||||||
|
|
||||||
h.HandshakeCompleted(rp.PeerID{0xFF}, rp.Key{})
|
|
||||||
require.Empty(t, iface.calls, "unknown peer id must not trigger SetPresharedKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_RemovePeer_ClearsInitializedState(t *testing.T) {
|
|
||||||
h := NewNetbirdHandler()
|
|
||||||
iface := &mockIface{}
|
|
||||||
h.SetInterface(iface)
|
|
||||||
|
|
||||||
pid := rp.PeerID{0x04}
|
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgtypes.Key{0xDD}))
|
|
||||||
h.HandshakeCompleted(pid, rp.Key{0x01})
|
|
||||||
require.True(t, h.IsPeerInitialized(pid))
|
|
||||||
|
|
||||||
h.RemovePeer(pid)
|
|
||||||
require.False(t, h.IsPeerInitialized(pid), "RemovePeer must clear initialized flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_SetInterfaceAfterAddPeer_StillReceivesKey(t *testing.T) {
|
|
||||||
h := NewNetbirdHandler()
|
|
||||||
pid := rp.PeerID{0x05}
|
|
||||||
wgKey := wgtypes.Key{0xEE}
|
|
||||||
h.AddPeer(pid, "wt0", rp.Key(wgKey))
|
|
||||||
|
|
||||||
iface := &mockIface{}
|
|
||||||
h.SetInterface(iface) // set after AddPeer
|
|
||||||
|
|
||||||
h.HandshakeCompleted(pid, rp.Key{0x42})
|
|
||||||
require.Len(t, iface.calls, 1)
|
|
||||||
require.Equal(t, wgKey.String(), iface.calls[0].peerKey)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package rosenpass
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeterministicSeedKey derives a 32-byte WireGuard preshared key from a pair
|
|
||||||
// of peer public keys. Both peers, given the same key pair, produce the same
|
|
||||||
// output regardless of which side runs the function: the inputs are ordered
|
|
||||||
// lexicographically before concatenation.
|
|
||||||
//
|
|
||||||
// NetBird uses this value as the initial Rosenpass-side preshared key when no
|
|
||||||
// explicit account-level PSK is configured, so both peers converge on the same
|
|
||||||
// PSK before the first post-quantum handshake completes.
|
|
||||||
//
|
|
||||||
// The resulting key MUST NOT be treated as quantum-safe: it is deterministic
|
|
||||||
// from public keys and exists only to seed WireGuard until Rosenpass rotates
|
|
||||||
// in a real post-quantum PSK.
|
|
||||||
func DeterministicSeedKey(localKey, remoteKey string) (*wgtypes.Key, error) {
|
|
||||||
lk := []byte(localKey)
|
|
||||||
rk := []byte(remoteKey)
|
|
||||||
if len(lk) < 16 || len(rk) < 16 {
|
|
||||||
return nil, fmt.Errorf("rosenpass: peer keys must be at least 16 bytes (got local=%d, remote=%d)", len(lk), len(rk))
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyInput []byte
|
|
||||||
if localKey > remoteKey {
|
|
||||||
keyInput = append(keyInput, lk[:16]...)
|
|
||||||
keyInput = append(keyInput, rk[:16]...)
|
|
||||||
} else {
|
|
||||||
keyInput = append(keyInput, rk[:16]...)
|
|
||||||
keyInput = append(keyInput, lk[:16]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := wgtypes.NewKey(keyInput)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rosenpass: deterministic seed key: %w", err)
|
|
||||||
}
|
|
||||||
return &key, nil
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package rosenpass
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDeterministicSeedKey_SameForBothSides(t *testing.T) {
|
|
||||||
// Peer A and peer B must derive the same PSK regardless of which side
|
|
||||||
// computes it: the function orders inputs internally.
|
|
||||||
a := strings.Repeat("a", 32)
|
|
||||||
b := strings.Repeat("b", 32)
|
|
||||||
|
|
||||||
keyAB, err := DeterministicSeedKey(a, b)
|
|
||||||
require.NoError(t, err)
|
|
||||||
keyBA, err := DeterministicSeedKey(b, a)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, keyAB.String(), keyBA.String(), "swapping arguments must yield identical key")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeterministicSeedKey_ChangesWithKeys(t *testing.T) {
|
|
||||||
a := strings.Repeat("a", 32)
|
|
||||||
b := strings.Repeat("b", 32)
|
|
||||||
c := strings.Repeat("c", 32)
|
|
||||||
|
|
||||||
keyAB, err := DeterministicSeedKey(a, b)
|
|
||||||
require.NoError(t, err)
|
|
||||||
keyAC, err := DeterministicSeedKey(a, c)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEqual(t, keyAB.String(), keyAC.String(), "different peer pair must yield different key")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeterministicSeedKey_TooShortKey_ReturnsError(t *testing.T) {
|
|
||||||
short := "short" // < 16 bytes
|
|
||||||
long := strings.Repeat("x", 32)
|
|
||||||
|
|
||||||
_, err := DeterministicSeedKey(short, long)
|
|
||||||
require.Error(t, err)
|
|
||||||
_, err = DeterministicSeedKey(long, short)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
//go:build dragonfly || freebsd || netbsd || openbsd
|
|
||||||
|
|
||||||
package systemops
|
|
||||||
|
|
||||||
// IgnoreAddedDefaultRoute reports whether an RTM_ADD default route with the
|
|
||||||
// given flags should be ignored by the network monitor.
|
|
||||||
func IgnoreAddedDefaultRoute(flags int) bool {
|
|
||||||
return filterRoutesByFlags(flags)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
//go:build darwin
|
|
||||||
|
|
||||||
package systemops
|
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
// IgnoreAddedDefaultRoute reports whether an RTM_ADD default route with the
|
|
||||||
// given flags should be ignored by the network monitor. Scoped routes
|
|
||||||
// (RTF_IFSCOPE) are tied to a specific interface index and cannot replace the
|
|
||||||
// unscoped default the kernel uses for general egress, so flapping ones (e.g.
|
|
||||||
// Wi-Fi calling IMS tunnels on ipsec0, Docker bridges, scoped utun defaults)
|
|
||||||
// must not trigger an engine restart.
|
|
||||||
func IgnoreAddedDefaultRoute(flags int) bool {
|
|
||||||
if filterRoutesByFlags(flags) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if flags&unix.RTF_IFSCOPE != 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -96,19 +96,17 @@ func (m *Manager) Stop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
cancel := m.cancel
|
defer m.mu.Unlock()
|
||||||
done := m.done
|
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
if cancel == nil {
|
if m.cancel == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cancel()
|
m.cancel()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-done:
|
case <-m.done:
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -64,6 +64,13 @@
|
|||||||
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
|
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
</Component>
|
</Component>
|
||||||
|
<!-- Drop the HKCU Run\Netbird value written by legacy NSIS installers. -->
|
||||||
|
<Component Id="NetbirdLegacyHKCUCleanup" Guid="*">
|
||||||
|
<RegistryValue Root="HKCU" Key="Software\NetBird GmbH\Installer"
|
||||||
|
Name="LegacyHKCUCleanup" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
<RemoveRegistryValue Root="HKCU"
|
||||||
|
Key="Software\Microsoft\Windows\CurrentVersion\Run" Name="Netbird" />
|
||||||
|
</Component>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
<StandardDirectory Id="CommonAppDataFolder">
|
<StandardDirectory Id="CommonAppDataFolder">
|
||||||
@@ -76,10 +83,28 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<!-- Drop Run, App Paths and Uninstall entries written by legacy NSIS
|
||||||
|
installers into the 32-bit registry view (HKLM\Software\Wow6432Node). -->
|
||||||
|
<Component Id="NetbirdLegacyWow6432Cleanup" Directory="NetbirdInstallDir"
|
||||||
|
Guid="bda5d628-16bd-4086-b2c1-5099d8d51763" Bitness="always32">
|
||||||
|
<RegistryValue Root="HKLM" Key="Software\NetBird GmbH\Installer"
|
||||||
|
Name="LegacyWow6432Cleanup" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
<RemoveRegistryValue Root="HKLM"
|
||||||
|
Key="Software\Microsoft\Windows\CurrentVersion\Run" Name="Netbird" />
|
||||||
|
<RemoveRegistryKey Action="removeOnInstall" Root="HKLM"
|
||||||
|
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\Netbird" />
|
||||||
|
<RemoveRegistryKey Action="removeOnInstall" Root="HKLM"
|
||||||
|
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\Netbird-ui" />
|
||||||
|
<RemoveRegistryKey Action="removeOnInstall" Root="HKLM"
|
||||||
|
Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Netbird" />
|
||||||
|
</Component>
|
||||||
|
|
||||||
<ComponentGroup Id="NetbirdFilesComponent">
|
<ComponentGroup Id="NetbirdFilesComponent">
|
||||||
<ComponentRef Id="NetbirdFiles" />
|
<ComponentRef Id="NetbirdFiles" />
|
||||||
<ComponentRef Id="NetbirdAumidRegistry" />
|
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||||
<ComponentRef Id="NetbirdAutoStart" />
|
<ComponentRef Id="NetbirdAutoStart" />
|
||||||
|
<ComponentRef Id="NetbirdLegacyHKCUCleanup" />
|
||||||
|
<ComponentRef Id="NetbirdLegacyWow6432Cleanup" />
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
|
|
||||||
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
"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/controller"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||||
@@ -315,7 +315,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, settingsManagerMock, eventStore, cacheStore)
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, settingsManagerMock, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/zcalusic/sysinfo"
|
"github.com/zcalusic/sysinfo"
|
||||||
|
|
||||||
@@ -28,11 +29,19 @@ func UpdateStaticInfoAsync() {
|
|||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
kernelName, kernelVersion, kernelPlatform := kernelInfo()
|
info := _getInfo()
|
||||||
|
for strings.Contains(info, "broken pipe") {
|
||||||
|
info = _getInfo()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
osStr := strings.ReplaceAll(info, "\n", "")
|
||||||
|
osStr = strings.ReplaceAll(osStr, "\r\n", "")
|
||||||
|
osInfo := strings.Split(osStr, " ")
|
||||||
|
|
||||||
osName, osVersion := readOsReleaseFile()
|
osName, osVersion := readOsReleaseFile()
|
||||||
if osName == "" {
|
if osName == "" {
|
||||||
osName = kernelName
|
osName = osInfo[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
systemHostname, _ := os.Hostname()
|
systemHostname, _ := os.Hostname()
|
||||||
@@ -49,8 +58,8 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gio := &Info{
|
gio := &Info{
|
||||||
Kernel: kernelName,
|
Kernel: osInfo[0],
|
||||||
Platform: kernelPlatform,
|
Platform: osInfo[2],
|
||||||
OS: osName,
|
OS: osName,
|
||||||
OSVersion: osVersion,
|
OSVersion: osVersion,
|
||||||
Hostname: extractDeviceName(ctx, systemHostname),
|
Hostname: extractDeviceName(ctx, systemHostname),
|
||||||
@@ -58,7 +67,7 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
CPUs: runtime.NumCPU(),
|
CPUs: runtime.NumCPU(),
|
||||||
NetbirdVersion: version.NetbirdVersion(),
|
NetbirdVersion: version.NetbirdVersion(),
|
||||||
UIVersion: extractUserAgent(ctx),
|
UIVersion: extractUserAgent(ctx),
|
||||||
KernelVersion: kernelVersion,
|
KernelVersion: osInfo[1],
|
||||||
NetworkAddresses: addrs,
|
NetworkAddresses: addrs,
|
||||||
SystemSerialNumber: si.SystemSerialNumber,
|
SystemSerialNumber: si.SystemSerialNumber,
|
||||||
SystemProductName: si.SystemProductName,
|
SystemProductName: si.SystemProductName,
|
||||||
@@ -69,12 +78,18 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
func kernelInfo() (string, string, string) {
|
func _getInfo() string {
|
||||||
var uts unix.Utsname
|
cmd := exec.Command("uname", "-srio")
|
||||||
if err := unix.Uname(&uts); err != nil {
|
cmd.Stdin = strings.NewReader("some")
|
||||||
return "", "", ""
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("getInfo: %s", err)
|
||||||
}
|
}
|
||||||
return unix.ByteSliceToString(uts.Sysname[:]), unix.ByteSliceToString(uts.Release[:]), unix.ByteSliceToString(uts.Machine[:])
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sysInfo() (string, string, string) {
|
func sysInfo() (string, string, string) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
certValidationTimeout = 5 * time.Minute
|
certValidationTimeout = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) {
|
func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) {
|
||||||
@@ -47,31 +46,17 @@ func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, cert
|
|||||||
|
|
||||||
promise := conn.wsHandlers.Call("onCertificateRequest", certInfo)
|
promise := conn.wsHandlers.Call("onCertificateRequest", certInfo)
|
||||||
|
|
||||||
resultChan := make(chan bool, 1)
|
resultChan := make(chan bool)
|
||||||
errorChan := make(chan error, 1)
|
errorChan := make(chan error)
|
||||||
|
|
||||||
// Release from inside the callbacks so a post-timeout promise resolution
|
promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
// does not invoke an already-released func.
|
result := args[0].Bool()
|
||||||
var thenFn, catchFn js.Func
|
resultChan <- result
|
||||||
var releaseOnce sync.Once
|
|
||||||
release := func() {
|
|
||||||
releaseOnce.Do(func() {
|
|
||||||
thenFn.Release()
|
|
||||||
catchFn.Release()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
thenFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
|
||||||
defer release()
|
|
||||||
resultChan <- args[0].Bool()
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
catchFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
|
||||||
defer release()
|
|
||||||
errorChan <- fmt.Errorf("certificate validation failed")
|
errorChan <- fmt.Errorf("certificate validation failed")
|
||||||
return nil
|
return nil
|
||||||
})
|
}))
|
||||||
|
|
||||||
promise.Call("then", thenFn).Call("catch", catchFn)
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -58,8 +57,6 @@ type RDCleanPathProxy struct {
|
|||||||
}
|
}
|
||||||
activeConnections map[string]*proxyConnection
|
activeConnections map[string]*proxyConnection
|
||||||
destinations map[string]string
|
destinations map[string]string
|
||||||
pendingHandlers map[string]js.Func
|
|
||||||
nextID atomic.Uint64
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,15 +66,8 @@ type proxyConnection struct {
|
|||||||
rdpConn net.Conn
|
rdpConn net.Conn
|
||||||
tlsConn *tls.Conn
|
tlsConn *tls.Conn
|
||||||
wsHandlers js.Value
|
wsHandlers js.Value
|
||||||
// Go-side callbacks exposed to JS. js.FuncOf pins the Go closure in a
|
ctx context.Context
|
||||||
// global handle map and MUST be released, otherwise every connection
|
cancel context.CancelFunc
|
||||||
// leaks the Go memory the closure captures.
|
|
||||||
wsHandlerFn js.Func
|
|
||||||
onMessageFn js.Func
|
|
||||||
onCloseFn js.Func
|
|
||||||
cleanupOnce sync.Once
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRDCleanPathProxy creates a new RDCleanPath proxy
|
// NewRDCleanPathProxy creates a new RDCleanPath proxy
|
||||||
@@ -90,11 +80,7 @@ func NewRDCleanPathProxy(client interface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateProxy creates a new proxy endpoint for the given destination.
|
// CreateProxy creates a new proxy endpoint for the given destination
|
||||||
// The registered handler fn and its destinations/pendingHandlers entries are
|
|
||||||
// only released once a connection is established and cleanupConnection runs.
|
|
||||||
// If a caller invokes CreateProxy but never connects to the returned URL,
|
|
||||||
// those entries stay pinned for the lifetime of the page.
|
|
||||||
func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
|
func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
|
||||||
destination := net.JoinHostPort(hostname, port)
|
destination := net.JoinHostPort(hostname, port)
|
||||||
|
|
||||||
@@ -102,7 +88,7 @@ func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
|
|||||||
resolve := args[0]
|
resolve := args[0]
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
proxyID := fmt.Sprintf("proxy_%d", p.nextID.Add(1))
|
proxyID := fmt.Sprintf("proxy_%d", len(p.activeConnections))
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
if p.destinations == nil {
|
if p.destinations == nil {
|
||||||
@@ -114,7 +100,7 @@ func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
|
|||||||
proxyURL := fmt.Sprintf("%s://%s/%s", RDCleanPathProxyScheme, RDCleanPathProxyHost, proxyID)
|
proxyURL := fmt.Sprintf("%s://%s/%s", RDCleanPathProxyScheme, RDCleanPathProxyHost, proxyID)
|
||||||
|
|
||||||
// Register the WebSocket handler for this specific proxy
|
// Register the WebSocket handler for this specific proxy
|
||||||
handlerFn := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
js.Global().Set(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID), js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return js.ValueOf("error: requires WebSocket argument")
|
return js.ValueOf("error: requires WebSocket argument")
|
||||||
}
|
}
|
||||||
@@ -122,14 +108,7 @@ func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
|
|||||||
ws := args[0]
|
ws := args[0]
|
||||||
p.HandleWebSocketConnection(ws, proxyID)
|
p.HandleWebSocketConnection(ws, proxyID)
|
||||||
return nil
|
return nil
|
||||||
})
|
}))
|
||||||
p.mu.Lock()
|
|
||||||
if p.pendingHandlers == nil {
|
|
||||||
p.pendingHandlers = make(map[string]js.Func)
|
|
||||||
}
|
|
||||||
p.pendingHandlers[proxyID] = handlerFn
|
|
||||||
p.mu.Unlock()
|
|
||||||
js.Global().Set(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID), handlerFn)
|
|
||||||
|
|
||||||
log.Infof("Created RDCleanPath proxy endpoint: %s for destination: %s", proxyURL, destination)
|
log.Infof("Created RDCleanPath proxy endpoint: %s for destination: %s", proxyURL, destination)
|
||||||
resolve.Invoke(proxyURL)
|
resolve.Invoke(proxyURL)
|
||||||
@@ -163,10 +142,6 @@ func (p *RDCleanPathProxy) HandleWebSocketConnection(ws js.Value, proxyID string
|
|||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.activeConnections[proxyID] = conn
|
p.activeConnections[proxyID] = conn
|
||||||
if fn, ok := p.pendingHandlers[proxyID]; ok {
|
|
||||||
conn.wsHandlerFn = fn
|
|
||||||
delete(p.pendingHandlers, proxyID)
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
p.setupWebSocketHandlers(ws, conn)
|
p.setupWebSocketHandlers(ws, conn)
|
||||||
@@ -175,7 +150,7 @@ func (p *RDCleanPathProxy) HandleWebSocketConnection(ws js.Value, proxyID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnection) {
|
func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnection) {
|
||||||
conn.onMessageFn = js.FuncOf(func(this js.Value, args []js.Value) any {
|
ws.Set("onGoMessage", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -183,15 +158,13 @@ func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnec
|
|||||||
data := args[0]
|
data := args[0]
|
||||||
go p.handleWebSocketMessage(conn, data)
|
go p.handleWebSocketMessage(conn, data)
|
||||||
return nil
|
return nil
|
||||||
})
|
}))
|
||||||
ws.Set("onGoMessage", conn.onMessageFn)
|
|
||||||
|
|
||||||
conn.onCloseFn = js.FuncOf(func(_ js.Value, args []js.Value) any {
|
ws.Set("onGoClose", js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||||
log.Debug("WebSocket closed by JavaScript")
|
log.Debug("WebSocket closed by JavaScript")
|
||||||
conn.cancel()
|
conn.cancel()
|
||||||
return nil
|
return nil
|
||||||
})
|
}))
|
||||||
ws.Set("onGoClose", conn.onCloseFn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) handleWebSocketMessage(conn *proxyConnection, data js.Value) {
|
func (p *RDCleanPathProxy) handleWebSocketMessage(conn *proxyConnection, data js.Value) {
|
||||||
@@ -288,49 +261,25 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) {
|
func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) {
|
||||||
conn.cleanupOnce.Do(func() {
|
log.Debugf("Cleaning up connection %s", conn.id)
|
||||||
log.Debugf("Cleaning up connection %s", conn.id)
|
conn.cancel()
|
||||||
conn.cancel()
|
if conn.tlsConn != nil {
|
||||||
if conn.tlsConn != nil {
|
log.Debug("Closing TLS connection")
|
||||||
log.Debug("Closing TLS connection")
|
if err := conn.tlsConn.Close(); err != nil {
|
||||||
if err := conn.tlsConn.Close(); err != nil {
|
log.Debugf("Error closing TLS connection: %v", err)
|
||||||
log.Debugf("Error closing TLS connection: %v", err)
|
|
||||||
}
|
|
||||||
conn.tlsConn = nil
|
|
||||||
}
|
}
|
||||||
if conn.rdpConn != nil {
|
conn.tlsConn = nil
|
||||||
log.Debug("Closing TCP connection")
|
}
|
||||||
if err := conn.rdpConn.Close(); err != nil {
|
if conn.rdpConn != nil {
|
||||||
log.Debugf("Error closing TCP connection: %v", err)
|
log.Debug("Closing TCP connection")
|
||||||
}
|
if err := conn.rdpConn.Close(); err != nil {
|
||||||
conn.rdpConn = nil
|
log.Debugf("Error closing TCP connection: %v", err)
|
||||||
}
|
}
|
||||||
js.Global().Delete(fmt.Sprintf("handleRDCleanPathWebSocket_%s", conn.id))
|
conn.rdpConn = nil
|
||||||
|
}
|
||||||
// Detach before releasing so late JS calls surface as TypeError instead
|
p.mu.Lock()
|
||||||
// of silent "call to released function".
|
delete(p.activeConnections, conn.id)
|
||||||
if conn.wsHandlers.Truthy() {
|
p.mu.Unlock()
|
||||||
conn.wsHandlers.Set("onGoMessage", js.Undefined())
|
|
||||||
conn.wsHandlers.Set("onGoClose", js.Undefined())
|
|
||||||
}
|
|
||||||
|
|
||||||
// wsHandlerFn may be zero-value if the pending handler lookup missed.
|
|
||||||
if conn.wsHandlerFn.Truthy() {
|
|
||||||
conn.wsHandlerFn.Release()
|
|
||||||
}
|
|
||||||
if conn.onMessageFn.Truthy() {
|
|
||||||
conn.onMessageFn.Release()
|
|
||||||
}
|
|
||||||
if conn.onCloseFn.Truthy() {
|
|
||||||
conn.onCloseFn.Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
delete(p.activeConnections, conn.id)
|
|
||||||
delete(p.destinations, conn.id)
|
|
||||||
delete(p.pendingHandlers, conn.id)
|
|
||||||
p.mu.Unlock()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) {
|
func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func CreateJSInterface(client *Client) js.Value {
|
func CreateJSInterface(client *Client) js.Value {
|
||||||
jsInterface := js.Global().Get("Object").Call("create", js.Null())
|
jsInterface := js.Global().Get("Object").Call("create", js.Null())
|
||||||
|
|
||||||
writeFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
|
jsInterface.Set("write", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return js.ValueOf(false)
|
return js.ValueOf(false)
|
||||||
}
|
}
|
||||||
@@ -32,10 +32,9 @@ func CreateJSInterface(client *Client) js.Value {
|
|||||||
|
|
||||||
_, err := client.Write(bytes)
|
_, err := client.Write(bytes)
|
||||||
return js.ValueOf(err == nil)
|
return js.ValueOf(err == nil)
|
||||||
})
|
}))
|
||||||
jsInterface.Set("write", writeFunc)
|
|
||||||
|
|
||||||
resizeFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
|
jsInterface.Set("resize", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return js.ValueOf(false)
|
return js.ValueOf(false)
|
||||||
}
|
}
|
||||||
@@ -43,26 +42,14 @@ func CreateJSInterface(client *Client) js.Value {
|
|||||||
rows := args[1].Int()
|
rows := args[1].Int()
|
||||||
err := client.Resize(cols, rows)
|
err := client.Resize(cols, rows)
|
||||||
return js.ValueOf(err == nil)
|
return js.ValueOf(err == nil)
|
||||||
})
|
}))
|
||||||
jsInterface.Set("resize", resizeFunc)
|
|
||||||
|
|
||||||
closeFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
|
jsInterface.Set("close", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
client.Close()
|
client.Close()
|
||||||
return js.Undefined()
|
return js.Undefined()
|
||||||
})
|
}))
|
||||||
jsInterface.Set("close", closeFunc)
|
|
||||||
|
|
||||||
go func() {
|
go readLoop(client, jsInterface)
|
||||||
readLoop(client, jsInterface)
|
|
||||||
// Detach before releasing so late JS calls surface as TypeError instead
|
|
||||||
// of silent "call to released function".
|
|
||||||
jsInterface.Set("write", js.Undefined())
|
|
||||||
jsInterface.Set("resize", js.Undefined())
|
|
||||||
jsInterface.Set("close", js.Undefined())
|
|
||||||
writeFunc.Release()
|
|
||||||
resizeFunc.Release()
|
|
||||||
closeFunc.Release()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return jsInterface
|
return jsInterface
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,10 +67,6 @@ func init() {
|
|||||||
rootCmd.AddCommand(newTokenCommands())
|
rootCmd.AddCommand(newTokenCommands())
|
||||||
}
|
}
|
||||||
|
|
||||||
func RootCmd() *cobra.Command {
|
|
||||||
return rootCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
return rootCmd.Execute()
|
return rootCmd.Execute()
|
||||||
}
|
}
|
||||||
@@ -172,7 +168,7 @@ func initializeConfig() error {
|
|||||||
// serverInstances holds all server instances created during startup.
|
// serverInstances holds all server instances created during startup.
|
||||||
type serverInstances struct {
|
type serverInstances struct {
|
||||||
relaySrv *relayServer.Server
|
relaySrv *relayServer.Server
|
||||||
mgmtSrv mgmtServer.Server
|
mgmtSrv *mgmtServer.BaseServer
|
||||||
signalSrv *signalServer.Server
|
signalSrv *signalServer.Server
|
||||||
healthcheck *healthcheck.Server
|
healthcheck *healthcheck.Server
|
||||||
stunServer *stun.Server
|
stunServer *stun.Server
|
||||||
@@ -328,24 +324,19 @@ func setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s, ok := servers.mgmtSrv.GetContainer(mgmtServer.ContainerKeyBaseServer); ok {
|
servers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {
|
||||||
if baseServer, ok := s.(*mgmtServer.BaseServer); ok {
|
grpcSrv := s.GRPCServer()
|
||||||
baseServer.AfterInit(func(s *mgmtServer.BaseServer) {
|
|
||||||
grpcSrv := s.GRPCServer()
|
|
||||||
|
|
||||||
if servers.signalSrv != nil {
|
if servers.signalSrv != nil {
|
||||||
proto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)
|
proto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)
|
||||||
log.Infof("Signal server registered on port %s", cfg.Server.ListenAddress)
|
log.Infof("Signal server registered on port %s", cfg.Server.ListenAddress)
|
||||||
}
|
|
||||||
|
|
||||||
s.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), s.IDPHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))
|
|
||||||
if servers.relaySrv != nil {
|
|
||||||
log.Infof("Relay WebSocket handler added (path: /relay)")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
s.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))
|
||||||
|
if servers.relaySrv != nil {
|
||||||
|
log.Infof("Relay WebSocket handler added (path: /relay)")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {
|
func startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {
|
||||||
@@ -355,32 +346,38 @@ func startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *
|
|||||||
log.Infof("Relay WebSocket multiplexed on management port (no separate relay listener)")
|
log.Infof("Relay WebSocket multiplexed on management port (no separate relay listener)")
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Go(func() {
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
log.Infof("running metrics server: %s%s", metricsServer.Addr, metricsServer.Endpoint)
|
log.Infof("running metrics server: %s%s", metricsServer.Addr, metricsServer.Endpoint)
|
||||||
if err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
if err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("failed to start metrics server: %v", err)
|
log.Fatalf("failed to start metrics server: %v", err)
|
||||||
}
|
}
|
||||||
})
|
}()
|
||||||
|
|
||||||
wg.Go(func() {
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
if err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
if err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("failed to start healthcheck server: %v", err)
|
log.Fatalf("failed to start healthcheck server: %v", err)
|
||||||
}
|
}
|
||||||
})
|
}()
|
||||||
|
|
||||||
if stunServer != nil {
|
if stunServer != nil {
|
||||||
wg.Go(func() {
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
if err := stunServer.Listen(); err != nil {
|
if err := stunServer.Listen(); err != nil {
|
||||||
if errors.Is(err, stun.ErrServerClosed) {
|
if errors.Is(err, stun.ErrServerClosed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Errorf("STUN server error: %v", err)
|
log.Errorf("STUN server error: %v", err)
|
||||||
}
|
}
|
||||||
})
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv mgmtServer.Server, metricsServer *sharedMetrics.Metrics) error {
|
func shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
if err := httpHealthcheck.Shutdown(ctx); err != nil {
|
if err := httpHealthcheck.Shutdown(ctx); err != nil {
|
||||||
@@ -494,7 +491,7 @@ func handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {
|
|||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (mgmtServer.Server, error) {
|
func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {
|
||||||
mgmt := cfg.Management
|
mgmt := cfg.Management
|
||||||
|
|
||||||
// Extract port from listen address
|
// Extract port from listen address
|
||||||
@@ -505,7 +502,7 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (m
|
|||||||
}
|
}
|
||||||
mgmtPort, _ := strconv.Atoi(portStr)
|
mgmtPort, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
mgmtSrv := newServer(
|
mgmtSrv := mgmtServer.NewServer(
|
||||||
&mgmtServer.Config{
|
&mgmtServer.Config{
|
||||||
NbConfig: mgmtConfig,
|
NbConfig: mgmtConfig,
|
||||||
DNSDomain: "",
|
DNSDomain: "",
|
||||||
@@ -524,7 +521,7 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (m
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic
|
// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic
|
||||||
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, idpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
|
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
|
||||||
wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))
|
wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))
|
||||||
|
|
||||||
var relayAcceptFn func(conn listener.Conn)
|
var relayAcceptFn func(conn listener.Conn)
|
||||||
@@ -559,10 +556,6 @@ func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, id
|
|||||||
http.Error(w, "Relay service not enabled", http.StatusNotFound)
|
http.Error(w, "Relay service not enabled", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embedded IdP (Dex)
|
|
||||||
case idpHandler != nil && strings.HasPrefix(r.URL.Path, "/oauth2"):
|
|
||||||
idpHandler.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
// Management HTTP API (default)
|
// Management HTTP API (default)
|
||||||
default:
|
default:
|
||||||
httpHandler.ServeHTTP(w, r)
|
httpHandler.ServeHTTP(w, r)
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
mgmtServer "github.com/netbirdio/netbird/management/internals/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
var newServer = func(cfg *mgmtServer.Config) mgmtServer.Server {
|
|
||||||
return mgmtServer.NewServer(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetNewServer(fn func(*mgmtServer.Config) mgmtServer.Server) {
|
|
||||||
newServer = fn
|
|
||||||
}
|
|
||||||
@@ -53,6 +53,9 @@ type NameServerGroup struct {
|
|||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
// AccountID is a reference to Account that this object belongs
|
// AccountID is a reference to Account that this object belongs
|
||||||
AccountID string `gorm:"index"`
|
AccountID string `gorm:"index"`
|
||||||
|
// AccountSeqID is a per-account monotonically increasing identifier used as the
|
||||||
|
// compact wire id when sending NetworkMap components to capable peers.
|
||||||
|
AccountSeqID uint32 `json:"-" gorm:"index:idx_nameserver_groups_account_seq_id;not null;default:0"`
|
||||||
// Name group name
|
// Name group name
|
||||||
Name string
|
Name string
|
||||||
// Description group description
|
// Description group description
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -3,7 +3,7 @@ module github.com/netbirdio/netbird
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cunicu.li/go-rosenpass v0.5.42
|
cunicu.li/go-rosenpass v0.4.0
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.4
|
github.com/golang/protobuf v1.5.4
|
||||||
@@ -19,8 +19,8 @@ require (
|
|||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
google.golang.org/grpc v1.80.0
|
google.golang.org/grpc v1.80.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
@@ -38,7 +38,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
|
||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/caddyserver/certmagic v0.21.3
|
github.com/caddyserver/certmagic v0.21.3
|
||||||
github.com/cilium/ebpf v0.19.0
|
github.com/cilium/ebpf v0.15.0
|
||||||
github.com/coder/websocket v1.8.14
|
github.com/coder/websocket v1.8.14
|
||||||
github.com/coreos/go-iptables v0.7.0
|
github.com/coreos/go-iptables v0.7.0
|
||||||
github.com/coreos/go-oidc/v3 v3.18.0
|
github.com/coreos/go-oidc/v3 v3.18.0
|
||||||
@@ -60,7 +60,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/google/nftables v0.3.0
|
github.com/google/nftables v0.3.0
|
||||||
github.com/gopacket/gopacket v1.4.0
|
github.com/gopacket/gopacket v1.1.1
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
||||||
@@ -335,7 +335,7 @@ replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-2024
|
|||||||
|
|
||||||
replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949
|
replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949
|
||||||
|
|
||||||
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f
|
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0
|
||||||
|
|
||||||
replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6
|
replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6
|
||||||
|
|
||||||
|
|||||||
26
go.sum
26
go.sum
@@ -7,8 +7,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
|
|||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:b8xUw3004wk+3ipBhu0VU4RtUJsegMIiqjxSK4++lzA=
|
codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:b8xUw3004wk+3ipBhu0VU4RtUJsegMIiqjxSK4++lzA=
|
||||||
codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
||||||
cunicu.li/go-rosenpass v0.5.42 h1:fRDsGwCxd7DhDgZI1Pxeo8GtNyq8BESZJ7w2/BGGJtU=
|
cunicu.li/go-rosenpass v0.4.0 h1:LtPtBgFWY/9emfgC4glKLEqS0MJTylzV6+ChRhiZERw=
|
||||||
cunicu.li/go-rosenpass v0.5.42/go.mod h1:YRBeyKOe/gWpSX2kpDUec5p9t0XOLsshTguId5gTGVg=
|
cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJplf/M=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||||
@@ -111,8 +111,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
|||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
|
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
||||||
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
|
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
@@ -225,8 +225,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum
|
|||||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
@@ -307,8 +307,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA
|
|||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||||
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
|
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
|
||||||
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
|
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
|
||||||
github.com/gopacket/gopacket v1.4.0 h1:cr1OlFpzksCkZHNO0eLjaSSOrMQnpPXg0j6qHIY3y2U=
|
github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw=
|
||||||
github.com/gopacket/gopacket v1.4.0/go.mod h1:EpvsxINeehp5qj4YMKMLf2/dekdhKn2IIAO/ZOifS7o=
|
github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs=
|
||||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
@@ -390,8 +390,6 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
|||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
|
|
||||||
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
|
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||||
@@ -501,8 +499,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax
|
|||||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
||||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
|
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
|
||||||
github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f h1:ff2D57RBjWtyQ2wVwJOxOgXAXOe/J2lJWtSX0Bz/BRk=
|
github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0 h1:h/QnNzm7xzHPm+gajcblYUOclrW2FeNeDlUNj6tTWKQ=
|
||||||
github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||||
@@ -902,8 +900,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type Controller struct {
|
|||||||
EphemeralPeersManager ephemeral.Manager
|
EphemeralPeersManager ephemeral.Manager
|
||||||
|
|
||||||
accountUpdateLocks sync.Map
|
accountUpdateLocks sync.Map
|
||||||
sendAccountUpdateLocks sync.Map
|
affectedPeerUpdateLocks sync.Map
|
||||||
updateAccountPeersBufferInterval atomic.Int64
|
updateAccountPeersBufferInterval atomic.Int64
|
||||||
// dnsDomain is used for peer resolution. This is appended to the peer's name
|
// dnsDomain is used for peer resolution. This is appended to the peer's name
|
||||||
dnsDomain string
|
dnsDomain string
|
||||||
@@ -55,6 +55,15 @@ type Controller struct {
|
|||||||
proxyController port_forwarding.Controller
|
proxyController port_forwarding.Controller
|
||||||
|
|
||||||
integratedPeerValidator integrated_validator.IntegratedValidator
|
integratedPeerValidator integrated_validator.IntegratedValidator
|
||||||
|
|
||||||
|
// componentsDisabled is the kill switch for the component-based wire
|
||||||
|
// format. When true the controller emits legacy proto.NetworkMap to every
|
||||||
|
// peer regardless of capability — used to roll back instantly via a
|
||||||
|
// management restart from a bad components encoder.
|
||||||
|
//
|
||||||
|
// Set once in NewController from NB_NETWORK_MAP_COMPONENTS_DISABLE and
|
||||||
|
// never written after — readers race-free without a mutex.
|
||||||
|
componentsDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type bufferUpdate struct {
|
type bufferUpdate struct {
|
||||||
@@ -63,6 +72,13 @@ type bufferUpdate struct {
|
|||||||
update atomic.Bool
|
update atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bufferAffectedUpdate struct {
|
||||||
|
sendMu sync.Mutex
|
||||||
|
dataMu sync.Mutex
|
||||||
|
next *time.Timer
|
||||||
|
peerIDs map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
var _ network_map.Controller = (*Controller)(nil)
|
var _ network_map.Controller = (*Controller)(nil)
|
||||||
|
|
||||||
func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config) *Controller {
|
func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config) *Controller {
|
||||||
@@ -81,12 +97,30 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
|
|||||||
settingsManager: settingsManager,
|
settingsManager: settingsManager,
|
||||||
dnsDomain: dnsDomain,
|
dnsDomain: dnsDomain,
|
||||||
config: config,
|
config: config,
|
||||||
|
componentsDisabled: parseBoolEnv("NB_NETWORK_MAP_COMPONENTS_DISABLE"),
|
||||||
|
|
||||||
proxyController: proxyController,
|
proxyController: proxyController,
|
||||||
EphemeralPeersManager: ephemeralPeersManager,
|
EphemeralPeersManager: ephemeralPeersManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerNeedsComponents reports whether the gRPC layer should emit the
|
||||||
|
// component-based wire format for this peer. Combines the peer's advertised
|
||||||
|
// capability with the controller-level kill switch — callers ask exactly
|
||||||
|
// this question, so encapsulating it removes accidental double-checks.
|
||||||
|
func (c *Controller) PeerNeedsComponents(p *nbpeer.Peer) bool {
|
||||||
|
return p != nil && p.SupportsComponentNetworkMap() && !c.componentsDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBoolEnv reads an env var via strconv.ParseBool so callers accept the
|
||||||
|
// usual "1/t/T/TRUE/true/True" set instead of being strict about a single
|
||||||
|
// literal — matches the convention used elsewhere in the codebase
|
||||||
|
// (e.g. event.go's NB_TRAFFIC_EVENT_*) and reduces operator surprises.
|
||||||
|
func parseBoolEnv(key string) bool {
|
||||||
|
v, _ := strconv.ParseBool(os.Getenv(key))
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Controller) OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *network_map.UpdateMessage, error) {
|
func (c *Controller) OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *network_map.UpdateMessage, error) {
|
||||||
peer, err := c.repo.GetPeerByID(ctx, accountID, peerID)
|
peer, err := c.repo.GetPeerByID(ctx, accountID, peerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,7 +146,7 @@ func (c *Controller) CountStreams() int {
|
|||||||
return c.peersUpdateManager.CountStreams()
|
return c.peersUpdateManager.CountStreams()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string) error {
|
||||||
log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName())
|
log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName())
|
||||||
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -175,10 +209,6 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.accountManagerMetrics != nil {
|
|
||||||
c.accountManagerMetrics.CountNmapTriggered(string(reason.Resource), string(reason.Operation))
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
semaphore <- struct{}{}
|
semaphore <- struct{}{}
|
||||||
go func(p *nbpeer.Peer) {
|
go func(p *nbpeer.Peer) {
|
||||||
@@ -196,18 +226,26 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
|
|
||||||
remotePeerNetworkMap := account.GetPeerNetworkMapFromComponents(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
result := account.GetPeerNetworkMapResult(ctx, p.ID, c.componentsDisabled, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
|
||||||
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
proxyNetworkMap := proxyNetworkMaps[p.ID]
|
||||||
if ok {
|
if result.NetworkMap != nil && proxyNetworkMap != nil {
|
||||||
remotePeerNetworkMap.Merge(proxyNetworkMap)
|
result.NetworkMap.Merge(proxyNetworkMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerGroups := account.GetPeerGroups(p.ID)
|
peerGroups := account.GetPeerGroups(p.ID)
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
var update *proto.SyncResponse
|
||||||
|
if result.IsComponents() {
|
||||||
|
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||||
|
// the client merges it into Calculate()'s output the same
|
||||||
|
// way the legacy server did via NetworkMap.Merge.
|
||||||
|
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, result.Components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
} else {
|
||||||
|
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, result.NetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
}
|
||||||
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||||
|
|
||||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||||
@@ -225,51 +263,155 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) bufferSendUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
|
||||||
log.WithContext(ctx).Tracef("buffer sending update peers for account %s from %s", accountID, util.GetCallerName())
|
|
||||||
|
|
||||||
if c.accountManagerMetrics != nil {
|
|
||||||
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
|
||||||
}
|
|
||||||
|
|
||||||
bufUpd, _ := c.sendAccountUpdateLocks.LoadOrStore(accountID, &bufferUpdate{})
|
|
||||||
b := bufUpd.(*bufferUpdate)
|
|
||||||
|
|
||||||
if !b.mu.TryLock() {
|
|
||||||
b.update.Store(true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.next != nil {
|
|
||||||
b.next.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
|
||||||
if !b.update.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.update.Store(false)
|
|
||||||
if b.next == nil {
|
|
||||||
b.next = time.AfterFunc(time.Duration(c.updateAccountPeersBufferInterval.Load()), func() {
|
|
||||||
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.next.Reset(time.Duration(c.updateAccountPeersBufferInterval.Load()))
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePeers updates all peers that belong to an account.
|
// UpdatePeers updates all peers that belong to an account.
|
||||||
// Should be called when changes have to be synced to peers.
|
// Should be called when changes have to be synced to peers.
|
||||||
func (c *Controller) UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
func (c *Controller) UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
||||||
if c.accountManagerMetrics != nil {
|
if c.accountManagerMetrics != nil {
|
||||||
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
||||||
}
|
}
|
||||||
return c.sendUpdateAccountPeers(ctx, accountID, reason)
|
return c.sendUpdateAccountPeers(ctx, accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAffectedPeers updates only the specified peers that belong to an account.
|
||||||
|
func (c *Controller) UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error {
|
||||||
|
if len(peerIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.sendUpdateForAffectedPeers(ctx, accountID, peerIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error {
|
||||||
|
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: account %s, %d affected peers: %v (caller: %s)", accountID, len(peerIDs), peerIDs, util.GetCallerName())
|
||||||
|
|
||||||
|
if !c.hasConnectedPeers(peerIDs) {
|
||||||
|
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: no connected peers among %v, skipping", peerIDs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalStart := time.Now()
|
||||||
|
|
||||||
|
peersToUpdate := c.filterConnectedAffectedPeers(account, peerIDs)
|
||||||
|
if len(peersToUpdate) == 0 {
|
||||||
|
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: no peers to update (affected peers not found in account or no channels)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: sending network map to %d connected peers", len(peersToUpdate))
|
||||||
|
|
||||||
|
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get validate peers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
semaphore := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
account.InjectProxyPolicies(ctx)
|
||||||
|
dnsCache := &cache.DNSConfigCache{}
|
||||||
|
dnsDomain := c.GetDNSDomain(account.Settings)
|
||||||
|
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMapsAll(ctx, accountID, account.Peers)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to get proxy network maps: %v", err)
|
||||||
|
return fmt.Errorf("failed to get proxy network maps: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
extraSetting, err := c.settingsManager.GetExtraSettings(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get flow enabled status: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
||||||
|
|
||||||
|
accountZones, err := c.repo.GetAccountZones(ctx, account.Id)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to get account zones: %v", err)
|
||||||
|
return fmt.Errorf("failed to get account zones: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range peersToUpdate {
|
||||||
|
wg.Add(1)
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
go func(p *nbpeer.Peer) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
postureChecks, err := c.getPeerPostureChecks(account, p.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Debugf("failed to get posture checks for peer %s: %v", p.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
||||||
|
start = time.Now()
|
||||||
|
|
||||||
|
result := account.GetPeerNetworkMapResult(ctx, p.ID, c.componentsDisabled, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
|
||||||
|
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||||
|
|
||||||
|
proxyNetworkMap := proxyNetworkMaps[p.ID]
|
||||||
|
if result.NetworkMap != nil && proxyNetworkMap != nil {
|
||||||
|
result.NetworkMap.Merge(proxyNetworkMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerGroups := account.GetPeerGroups(p.ID)
|
||||||
|
start = time.Now()
|
||||||
|
var update *proto.SyncResponse
|
||||||
|
if result.IsComponents() {
|
||||||
|
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, result.Components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
} else {
|
||||||
|
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, result.NetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
}
|
||||||
|
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||||
|
|
||||||
|
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||||
|
Update: update,
|
||||||
|
MessageType: network_map.MessageTypeNetworkMap,
|
||||||
|
})
|
||||||
|
}(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if c.accountManagerMetrics != nil {
|
||||||
|
c.accountManagerMetrics.CountUpdateAccountPeersDuration(time.Since(globalStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) hasConnectedPeers(peerIDs []string) bool {
|
||||||
|
for _, id := range peerIDs {
|
||||||
|
if c.peersUpdateManager.HasChannel(id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) filterConnectedAffectedPeers(account *types.Account, peerIDs []string) []*nbpeer.Peer {
|
||||||
|
affected := make(map[string]struct{}, len(peerIDs))
|
||||||
|
for _, id := range peerIDs {
|
||||||
|
affected[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*nbpeer.Peer
|
||||||
|
for _, peer := range account.Peers {
|
||||||
|
if _, ok := affected[peer.ID]; ok && c.peersUpdateManager.HasChannel(peer.ID) {
|
||||||
|
result = append(result, peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error {
|
func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error {
|
||||||
@@ -318,11 +460,11 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
remotePeerNetworkMap := account.GetPeerNetworkMapFromComponents(ctx, peerId, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
result := account.GetPeerNetworkMapResult(ctx, peerId, c.componentsDisabled, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
proxyNetworkMap := proxyNetworkMaps[peer.ID]
|
||||||
if ok {
|
if result.NetworkMap != nil && proxyNetworkMap != nil {
|
||||||
remotePeerNetworkMap.Merge(proxyNetworkMap)
|
result.NetworkMap.Merge(proxyNetworkMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
extraSettings, err := c.settingsManager.GetExtraSettings(ctx, peer.AccountID)
|
extraSettings, err := c.settingsManager.GetExtraSettings(ctx, peer.AccountID)
|
||||||
@@ -333,7 +475,12 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
|||||||
peerGroups := account.GetPeerGroups(peerId)
|
peerGroups := account.GetPeerGroups(peerId)
|
||||||
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
||||||
|
|
||||||
update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
var update *proto.SyncResponse
|
||||||
|
if result.IsComponents() {
|
||||||
|
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, result.Components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
} else {
|
||||||
|
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, result.NetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
}
|
||||||
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
||||||
Update: update,
|
Update: update,
|
||||||
MessageType: network_map.MessageTypeNetworkMap,
|
MessageType: network_map.MessageTypeNetworkMap,
|
||||||
@@ -363,14 +510,14 @@ func (c *Controller) BufferUpdateAccountPeers(ctx context.Context, accountID str
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
_ = c.sendUpdateAccountPeers(ctx, accountID)
|
||||||
if !b.update.Load() {
|
if !b.update.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.update.Store(false)
|
b.update.Store(false)
|
||||||
if b.next == nil {
|
if b.next == nil {
|
||||||
b.next = time.AfterFunc(time.Duration(c.updateAccountPeersBufferInterval.Load()), func() {
|
b.next = time.AfterFunc(time.Duration(c.updateAccountPeersBufferInterval.Load()), func() {
|
||||||
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
_ = c.sendUpdateAccountPeers(ctx, accountID)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -380,6 +527,161 @@ func (c *Controller) BufferUpdateAccountPeers(ctx context.Context, accountID str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValidatedPeerWithComponents is the components-format counterpart of
|
||||||
|
// GetValidatedPeerWithMap. It returns raw NetworkMapComponents for capable
|
||||||
|
// peers along with the proxy NetworkMap fragment (BYOP / port-forwarding
|
||||||
|
// data the legacy server folds in via NetworkMap.Merge). The gRPC layer
|
||||||
|
// encodes both into the wire envelope. The caller is responsible for
|
||||||
|
// checking peer capability + componentsDisabled before dispatching here —
|
||||||
|
// this method does NOT branch on capability itself.
|
||||||
|
func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
|
if isRequiresApproval {
|
||||||
|
network, err := c.repo.GetAccountNetwork(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
return peer, &types.NetworkMapComponents{Network: network.Copy()}, nil, nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
account.InjectProxyPolicies(ctx)
|
||||||
|
|
||||||
|
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
postureChecks, err := c.getPeerPostureChecks(account, peer.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountZones, err := c.repo.GetAccountZones(ctx, account.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the proxy network map fragment for this peer alongside the
|
||||||
|
// components — same single-account-load path the streaming controller
|
||||||
|
// uses, so initial-sync delivers BYOP/forwarding patches synchronously
|
||||||
|
// instead of waiting for the next streaming push.
|
||||||
|
proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMaps(ctx, account.Id, peer.ID, account.Peers)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to get proxy network maps: %v", err)
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsDomain := c.GetDNSDomain(account.Settings)
|
||||||
|
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
||||||
|
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
components := account.GetPeerNetworkMapComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
||||||
|
|
||||||
|
return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferUpdateAffectedPeers accumulates peer IDs and flushes them after the buffer interval.
|
||||||
|
func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
|
||||||
|
if len(peerIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.accountManagerMetrics != nil {
|
||||||
|
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Tracef("buffer updating %d affected peers for account %s from %s", len(peerIDs), accountID, util.GetCallerName())
|
||||||
|
|
||||||
|
bufUpd, _ := c.affectedPeerUpdateLocks.LoadOrStore(accountID, &bufferAffectedUpdate{
|
||||||
|
peerIDs: make(map[string]struct{}),
|
||||||
|
})
|
||||||
|
b := bufUpd.(*bufferAffectedUpdate)
|
||||||
|
|
||||||
|
b.addPeerIDs(peerIDs)
|
||||||
|
|
||||||
|
if !b.sendMu.TryLock() {
|
||||||
|
// Another goroutine is already sending; it will pick up our IDs on its next drain.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.stopTimer()
|
||||||
|
|
||||||
|
collected := b.drainPeerIDs()
|
||||||
|
go func() {
|
||||||
|
defer b.sendMu.Unlock()
|
||||||
|
_ = c.sendUpdateForAffectedPeers(ctx, accountID, collected)
|
||||||
|
|
||||||
|
// Check if more peer IDs accumulated while we were sending.
|
||||||
|
if !b.hasPending() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule a debounced flush for the newly accumulated IDs.
|
||||||
|
b.setTimer(time.Duration(c.updateAccountPeersBufferInterval.Load()), func() {
|
||||||
|
ids := b.drainPeerIDs()
|
||||||
|
if len(ids) > 0 {
|
||||||
|
_ = c.sendUpdateForAffectedPeers(ctx, accountID, ids)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bufferAffectedUpdate) addPeerIDs(ids []string) {
|
||||||
|
b.dataMu.Lock()
|
||||||
|
for _, id := range ids {
|
||||||
|
b.peerIDs[id] = struct{}{}
|
||||||
|
}
|
||||||
|
b.dataMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bufferAffectedUpdate) drainPeerIDs() []string {
|
||||||
|
b.dataMu.Lock()
|
||||||
|
defer b.dataMu.Unlock()
|
||||||
|
if len(b.peerIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]string, 0, len(b.peerIDs))
|
||||||
|
for id := range b.peerIDs {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
b.peerIDs = make(map[string]struct{})
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bufferAffectedUpdate) hasPending() bool {
|
||||||
|
b.dataMu.Lock()
|
||||||
|
defer b.dataMu.Unlock()
|
||||||
|
return len(b.peerIDs) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bufferAffectedUpdate) stopTimer() {
|
||||||
|
b.dataMu.Lock()
|
||||||
|
defer b.dataMu.Unlock()
|
||||||
|
if b.next != nil {
|
||||||
|
b.next.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bufferAffectedUpdate) setTimer(d time.Duration, f func()) {
|
||||||
|
b.dataMu.Lock()
|
||||||
|
defer b.dataMu.Unlock()
|
||||||
|
if b.next == nil {
|
||||||
|
b.next = time.AfterFunc(d, f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.next.Reset(d)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
if isRequiresApproval {
|
if isRequiresApproval {
|
||||||
network, err := c.repo.GetAccountNetwork(ctx, accountID)
|
network, err := c.repo.GetAccountNetwork(ctx, accountID)
|
||||||
@@ -577,21 +879,24 @@ func isPeerInPolicySourceGroups(account *types.Account, peerID string, policy *t
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) OnPeersUpdated(ctx context.Context, accountID string, peerIDs []string) error {
|
func (c *Controller) OnPeersUpdated(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
||||||
err := c.bufferSendUpdateAccountPeers(ctx, accountID, types.UpdateReason{Resource: types.UpdateResourcePeer, Operation: types.UpdateOperationUpdate})
|
if len(affectedPeerIDs) == 0 {
|
||||||
if err != nil {
|
log.WithContext(ctx).Tracef("no affected peers for peer update in account %s, skipping", accountID)
|
||||||
log.WithContext(ctx).Errorf("failed to buffer update account peers for peer update in account %s: %v", accountID, err)
|
return nil
|
||||||
}
|
}
|
||||||
|
return c.BufferUpdateAffectedPeers(ctx, accountID, affectedPeerIDs, types.UpdateReason{Resource: types.UpdateResourcePeer, Operation: types.UpdateOperationUpdate})
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) OnPeersAdded(ctx context.Context, accountID string, peerIDs []string) error {
|
func (c *Controller) OnPeersAdded(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
||||||
log.WithContext(ctx).Debugf("OnPeersAdded call to add peers: %v", peerIDs)
|
log.WithContext(ctx).Debugf("OnPeersAdded call to add peers: %v", peerIDs)
|
||||||
return c.bufferSendUpdateAccountPeers(ctx, accountID, types.UpdateReason{Resource: types.UpdateResourcePeer, Operation: types.UpdateOperationCreate})
|
if len(affectedPeerIDs) == 0 {
|
||||||
|
log.WithContext(ctx).Tracef("no affected peers for peer add in account %s, skipping", accountID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.BufferUpdateAffectedPeers(ctx, accountID, affectedPeerIDs, types.UpdateReason{Resource: types.UpdateResourcePeer, Operation: types.UpdateOperationCreate})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string) error {
|
func (c *Controller) OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
||||||
network, err := c.repo.GetAccountNetwork(ctx, accountID)
|
network, err := c.repo.GetAccountNetwork(ctx, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -624,7 +929,11 @@ func (c *Controller) OnPeersDeleted(ctx context.Context, accountID string, peerI
|
|||||||
c.peersUpdateManager.CloseChannel(ctx, peerID)
|
c.peersUpdateManager.CloseChannel(ctx, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.bufferSendUpdateAccountPeers(ctx, accountID, types.UpdateReason{Resource: types.UpdateResourcePeer, Operation: types.UpdateOperationDelete})
|
if len(affectedPeerIDs) == 0 {
|
||||||
|
log.WithContext(ctx).Tracef("no affected peers for peer delete in account %s, skipping network map update", accountID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.BufferUpdateAffectedPeers(ctx, accountID, affectedPeerIDs, types.UpdateReason{Resource: types.UpdateResourcePeer, Operation: types.UpdateOperationDelete})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
|
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
|
||||||
|
|||||||
@@ -19,17 +19,23 @@ const (
|
|||||||
|
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
||||||
|
UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error
|
||||||
|
BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error
|
||||||
UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error
|
UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error
|
||||||
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
||||||
GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, p *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error)
|
GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, p *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||||
|
GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, p *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||||
|
// PeerNeedsComponents combines the peer's advertised capability with the
|
||||||
|
// kill-switch flag — the only public predicate gRPC layers should ask.
|
||||||
|
PeerNeedsComponents(p *nbpeer.Peer) bool
|
||||||
GetDNSDomain(settings *types.Settings) string
|
GetDNSDomain(settings *types.Settings) string
|
||||||
StartWarmup(context.Context)
|
StartWarmup(context.Context)
|
||||||
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
|
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
|
||||||
CountStreams() int
|
CountStreams() int
|
||||||
|
|
||||||
OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string) error
|
OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string, affectedPeerIDs []string) error
|
||||||
OnPeersAdded(ctx context.Context, accountID string, peerIDs []string) error
|
OnPeersAdded(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error
|
||||||
OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string) error
|
OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error
|
||||||
DisconnectPeers(ctx context.Context, accountId string, peerIDs []string)
|
DisconnectPeers(ctx context.Context, accountId string, peerIDs []string)
|
||||||
OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *UpdateMessage, error)
|
OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *UpdateMessage, error)
|
||||||
OnPeerDisconnected(ctx context.Context, accountID string, peerID string)
|
OnPeerDisconnected(ctx context.Context, accountID string, peerID string)
|
||||||
|
|||||||
@@ -57,6 +57,20 @@ func (mr *MockControllerMockRecorder) BufferUpdateAccountPeers(ctx, accountID, r
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAccountPeers", reflect.TypeOf((*MockController)(nil).BufferUpdateAccountPeers), ctx, accountID, reason)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAccountPeers", reflect.TypeOf((*MockController)(nil).BufferUpdateAccountPeers), ctx, accountID, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BufferUpdateAffectedPeers mocks base method.
|
||||||
|
func (m *MockController) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "BufferUpdateAffectedPeers", ctx, accountID, peerIDs, reason)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferUpdateAffectedPeers indicates an expected call of BufferUpdateAffectedPeers.
|
||||||
|
func (mr *MockControllerMockRecorder) BufferUpdateAffectedPeers(ctx, accountID, peerIDs, reason any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAffectedPeers", reflect.TypeOf((*MockController)(nil).BufferUpdateAffectedPeers), ctx, accountID, peerIDs, reason)
|
||||||
|
}
|
||||||
|
|
||||||
// CountStreams mocks base method.
|
// CountStreams mocks base method.
|
||||||
func (m *MockController) CountStreams() int {
|
func (m *MockController) CountStreams() int {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -130,6 +144,39 @@ func (mr *MockControllerMockRecorder) GetValidatedPeerWithMap(ctx, isRequiresApp
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeerWithMap", reflect.TypeOf((*MockController)(nil).GetValidatedPeerWithMap), ctx, isRequiresApproval, accountID, p)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeerWithMap", reflect.TypeOf((*MockController)(nil).GetValidatedPeerWithMap), ctx, isRequiresApproval, accountID, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValidatedPeerWithComponents mocks base method.
|
||||||
|
func (m *MockController) GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, p *peer.Peer) (*peer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetValidatedPeerWithComponents", ctx, isRequiresApproval, accountID, p)
|
||||||
|
ret0, _ := ret[0].(*peer.Peer)
|
||||||
|
ret1, _ := ret[1].(*types.NetworkMapComponents)
|
||||||
|
ret2, _ := ret[2].(*types.NetworkMap)
|
||||||
|
ret3, _ := ret[3].([]*posture.Checks)
|
||||||
|
ret4, _ := ret[4].(int64)
|
||||||
|
ret5, _ := ret[5].(error)
|
||||||
|
return ret0, ret1, ret2, ret3, ret4, ret5
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValidatedPeerWithComponents indicates an expected call of GetValidatedPeerWithComponents.
|
||||||
|
func (mr *MockControllerMockRecorder) GetValidatedPeerWithComponents(ctx, isRequiresApproval, accountID, p any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeerWithComponents", reflect.TypeOf((*MockController)(nil).GetValidatedPeerWithComponents), ctx, isRequiresApproval, accountID, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerNeedsComponents mocks base method.
|
||||||
|
func (m *MockController) PeerNeedsComponents(p *peer.Peer) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "PeerNeedsComponents", p)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerNeedsComponents indicates an expected call of PeerNeedsComponents.
|
||||||
|
func (mr *MockControllerMockRecorder) PeerNeedsComponents(p any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeerNeedsComponents", reflect.TypeOf((*MockController)(nil).PeerNeedsComponents), p)
|
||||||
|
}
|
||||||
|
|
||||||
// OnPeerConnected mocks base method.
|
// OnPeerConnected mocks base method.
|
||||||
func (m *MockController) OnPeerConnected(ctx context.Context, accountID, peerID string) (chan *UpdateMessage, error) {
|
func (m *MockController) OnPeerConnected(ctx context.Context, accountID, peerID string) (chan *UpdateMessage, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -158,45 +205,45 @@ func (mr *MockControllerMockRecorder) OnPeerDisconnected(ctx, accountID, peerID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersAdded mocks base method.
|
// OnPeersAdded mocks base method.
|
||||||
func (m *MockController) OnPeersAdded(ctx context.Context, accountID string, peerIDs []string) error {
|
func (m *MockController) OnPeersAdded(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "OnPeersAdded", ctx, accountID, peerIDs)
|
ret := m.ctrl.Call(m, "OnPeersAdded", ctx, accountID, peerIDs, affectedPeerIDs)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersAdded indicates an expected call of OnPeersAdded.
|
// OnPeersAdded indicates an expected call of OnPeersAdded.
|
||||||
func (mr *MockControllerMockRecorder) OnPeersAdded(ctx, accountID, peerIDs any) *gomock.Call {
|
func (mr *MockControllerMockRecorder) OnPeersAdded(ctx, accountID, peerIDs, affectedPeerIDs any) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersAdded", reflect.TypeOf((*MockController)(nil).OnPeersAdded), ctx, accountID, peerIDs)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersAdded", reflect.TypeOf((*MockController)(nil).OnPeersAdded), ctx, accountID, peerIDs, affectedPeerIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersDeleted mocks base method.
|
// OnPeersDeleted mocks base method.
|
||||||
func (m *MockController) OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string) error {
|
func (m *MockController) OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "OnPeersDeleted", ctx, accountID, peerIDs)
|
ret := m.ctrl.Call(m, "OnPeersDeleted", ctx, accountID, peerIDs, affectedPeerIDs)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersDeleted indicates an expected call of OnPeersDeleted.
|
// OnPeersDeleted indicates an expected call of OnPeersDeleted.
|
||||||
func (mr *MockControllerMockRecorder) OnPeersDeleted(ctx, accountID, peerIDs any) *gomock.Call {
|
func (mr *MockControllerMockRecorder) OnPeersDeleted(ctx, accountID, peerIDs, affectedPeerIDs any) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersDeleted", reflect.TypeOf((*MockController)(nil).OnPeersDeleted), ctx, accountID, peerIDs)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersDeleted", reflect.TypeOf((*MockController)(nil).OnPeersDeleted), ctx, accountID, peerIDs, affectedPeerIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersUpdated mocks base method.
|
// OnPeersUpdated mocks base method.
|
||||||
func (m *MockController) OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string) error {
|
func (m *MockController) OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string, affectedPeerIDs []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "OnPeersUpdated", ctx, accountId, peerIDs)
|
ret := m.ctrl.Call(m, "OnPeersUpdated", ctx, accountId, peerIDs, affectedPeerIDs)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersUpdated indicates an expected call of OnPeersUpdated.
|
// OnPeersUpdated indicates an expected call of OnPeersUpdated.
|
||||||
func (mr *MockControllerMockRecorder) OnPeersUpdated(ctx, accountId, peerIDs any) *gomock.Call {
|
func (mr *MockControllerMockRecorder) OnPeersUpdated(ctx, accountId, peerIDs, affectedPeerIDs any) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersUpdated", reflect.TypeOf((*MockController)(nil).OnPeersUpdated), ctx, accountId, peerIDs)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersUpdated", reflect.TypeOf((*MockController)(nil).OnPeersUpdated), ctx, accountId, peerIDs, affectedPeerIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartWarmup mocks base method.
|
// StartWarmup mocks base method.
|
||||||
@@ -250,3 +297,17 @@ func (mr *MockControllerMockRecorder) UpdateAccountPeers(ctx, accountID, reason
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountPeers", reflect.TypeOf((*MockController)(nil).UpdateAccountPeers), ctx, accountID, reason)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountPeers", reflect.TypeOf((*MockController)(nil).UpdateAccountPeers), ctx, accountID, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAffectedPeers mocks base method.
|
||||||
|
func (m *MockController) UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateAffectedPeers", ctx, accountID, peerIDs)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAffectedPeers indicates an expected call of UpdateAffectedPeers.
|
||||||
|
func (mr *MockControllerMockRecorder) UpdateAffectedPeers(ctx, accountID, peerIDs any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAffectedPeers", reflect.TypeOf((*MockController)(nil).UpdateAffectedPeers), ctx, accountID, peerIDs)
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (p *PeersUpdateManager) SendUpdate(ctx context.Context, peerID string, upda
|
|||||||
found = true
|
found = true
|
||||||
select {
|
select {
|
||||||
case channel <- update:
|
case channel <- update:
|
||||||
log.WithContext(ctx).Tracef("update was sent to channel for peer %s", peerID)
|
log.WithContext(ctx).Debugf("update was sent to channel for peer %s", peerID)
|
||||||
default:
|
default:
|
||||||
dropped = true
|
dropped = true
|
||||||
log.WithContext(ctx).Warnf("channel for peer %s is %d full or closed", peerID, len(channel))
|
log.WithContext(ctx).Warnf("channel for peer %s is %d full or closed", peerID, len(channel))
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package peers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
@@ -36,14 +35,6 @@ type Manager interface {
|
|||||||
SetAccountManager(accountManager account.Manager)
|
SetAccountManager(accountManager account.Manager)
|
||||||
GetPeerID(ctx context.Context, peerKey string) (string, error)
|
GetPeerID(ctx context.Context, peerKey string) (string, error)
|
||||||
CreateProxyPeer(ctx context.Context, accountID string, peerKey string, cluster string) error
|
CreateProxyPeer(ctx context.Context, accountID string, peerKey string, cluster string) error
|
||||||
// GetPeerByTunnelIP looks up a peer in accountID by its WireGuard tunnel IP.
|
|
||||||
// Returns nil with an error when no match exists. No permission check;
|
|
||||||
// callers (the proxy's ValidateTunnelPeer RPC) are trusted server components.
|
|
||||||
GetPeerByTunnelIP(ctx context.Context, accountID string, ip net.IP) (*peer.Peer, error)
|
|
||||||
// GetPeerWithGroups returns the peer and the list of *types.Group it belongs
|
|
||||||
// to. Used by the proxy's auth path to authorise a request by the calling
|
|
||||||
// peer's group memberships.
|
|
||||||
GetPeerWithGroups(ctx context.Context, accountID, peerID string) (*peer.Peer, []*types.Group, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type managerImpl struct {
|
type managerImpl struct {
|
||||||
@@ -75,7 +66,7 @@ func (m *managerImpl) SetAccountManager(accountManager account.Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error) {
|
func (m *managerImpl) GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error) {
|
||||||
allowed, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
allowed, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -88,7 +79,7 @@ func (m *managerImpl) GetPeer(ctx context.Context, accountID, userID, peerID str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) {
|
func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) {
|
||||||
allowed, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
allowed, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -108,26 +99,6 @@ func (m *managerImpl) GetPeersByGroupIDs(ctx context.Context, accountID string,
|
|||||||
return m.store.GetPeersByGroupIDs(ctx, accountID, groupsIDs)
|
return m.store.GetPeersByGroupIDs(ctx, accountID, groupsIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerByTunnelIP delegates to the store's indexed lookup.
|
|
||||||
func (m *managerImpl) GetPeerByTunnelIP(ctx context.Context, accountID string, ip net.IP) (*peer.Peer, error) {
|
|
||||||
return m.store.GetPeerByIP(ctx, store.LockingStrengthNone, accountID, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerWithGroups returns the peer plus its group memberships. Any store
|
|
||||||
// error returns (nil, nil, err) so callers never receive a valid peer
|
|
||||||
// alongside a non-nil error.
|
|
||||||
func (m *managerImpl) GetPeerWithGroups(ctx context.Context, accountID, peerID string) (*peer.Peer, []*types.Group, error) {
|
|
||||||
p, err := m.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
groups, err := m.store.GetPeerGroups(ctx, store.LockingStrengthNone, accountID, peerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return p, groups, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *managerImpl) DeletePeers(ctx context.Context, accountID string, peerIDs []string, userID string, checkConnected bool) error {
|
func (m *managerImpl) DeletePeers(ctx context.Context, accountID string, peerIDs []string, userID string, checkConnected bool) error {
|
||||||
settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package peers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
net "net"
|
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
account "github.com/netbirdio/netbird/management/server/account"
|
account "github.com/netbirdio/netbird/management/server/account"
|
||||||
integrated_validator "github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
integrated_validator "github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
||||||
peer "github.com/netbirdio/netbird/management/server/peer"
|
peer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
types "github.com/netbirdio/netbird/management/server/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockManager is a mock of Manager interface.
|
// MockManager is a mock of Manager interface.
|
||||||
@@ -40,20 +38,6 @@ func (m *MockManager) EXPECT() *MockManagerMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateProxyPeer mocks base method.
|
|
||||||
func (m *MockManager) CreateProxyPeer(ctx context.Context, accountID, peerKey, cluster string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CreateProxyPeer", ctx, accountID, peerKey, cluster)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateProxyPeer indicates an expected call of CreateProxyPeer.
|
|
||||||
func (mr *MockManagerMockRecorder) CreateProxyPeer(ctx, accountID, peerKey, cluster interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProxyPeer", reflect.TypeOf((*MockManager)(nil).CreateProxyPeer), ctx, accountID, peerKey, cluster)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePeers mocks base method.
|
// DeletePeers mocks base method.
|
||||||
func (m *MockManager) DeletePeers(ctx context.Context, accountID string, peerIDs []string, userID string, checkConnected bool) error {
|
func (m *MockManager) DeletePeers(ctx context.Context, accountID string, peerIDs []string, userID string, checkConnected bool) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -113,21 +97,6 @@ func (mr *MockManagerMockRecorder) GetPeerAccountID(ctx, peerID interface{}) *go
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerAccountID", reflect.TypeOf((*MockManager)(nil).GetPeerAccountID), ctx, peerID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerAccountID", reflect.TypeOf((*MockManager)(nil).GetPeerAccountID), ctx, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerByTunnelIP mocks base method.
|
|
||||||
func (m *MockManager) GetPeerByTunnelIP(ctx context.Context, accountID string, ip net.IP) (*peer.Peer, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetPeerByTunnelIP", ctx, accountID, ip)
|
|
||||||
ret0, _ := ret[0].(*peer.Peer)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerByTunnelIP indicates an expected call of GetPeerByTunnelIP.
|
|
||||||
func (mr *MockManagerMockRecorder) GetPeerByTunnelIP(ctx, accountID, ip interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerByTunnelIP", reflect.TypeOf((*MockManager)(nil).GetPeerByTunnelIP), ctx, accountID, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerID mocks base method.
|
// GetPeerID mocks base method.
|
||||||
func (m *MockManager) GetPeerID(ctx context.Context, peerKey string) (string, error) {
|
func (m *MockManager) GetPeerID(ctx context.Context, peerKey string) (string, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -143,22 +112,6 @@ func (mr *MockManagerMockRecorder) GetPeerID(ctx, peerKey interface{}) *gomock.C
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerID", reflect.TypeOf((*MockManager)(nil).GetPeerID), ctx, peerKey)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerID", reflect.TypeOf((*MockManager)(nil).GetPeerID), ctx, peerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerWithGroups mocks base method.
|
|
||||||
func (m *MockManager) GetPeerWithGroups(ctx context.Context, accountID, peerID string) (*peer.Peer, []*types.Group, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetPeerWithGroups", ctx, accountID, peerID)
|
|
||||||
ret0, _ := ret[0].(*peer.Peer)
|
|
||||||
ret1, _ := ret[1].([]*types.Group)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerWithGroups indicates an expected call of GetPeerWithGroups.
|
|
||||||
func (mr *MockManagerMockRecorder) GetPeerWithGroups(ctx, accountID, peerID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerWithGroups", reflect.TypeOf((*MockManager)(nil).GetPeerWithGroups), ctx, accountID, peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeersByGroupIDs mocks base method.
|
// GetPeersByGroupIDs mocks base method.
|
||||||
func (m *MockManager) GetPeersByGroupIDs(ctx context.Context, accountID string, groupsIDs []string) ([]*peer.Peer, error) {
|
func (m *MockManager) GetPeersByGroupIDs(ctx context.Context, accountID string, groupsIDs []string) ([]*peer.Peer, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -209,3 +162,17 @@ func (mr *MockManagerMockRecorder) SetNetworkMapController(networkMapController
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetworkMapController", reflect.TypeOf((*MockManager)(nil).SetNetworkMapController), networkMapController)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetworkMapController", reflect.TypeOf((*MockManager)(nil).SetNetworkMapController), networkMapController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateProxyPeer mocks base method.
|
||||||
|
func (m *MockManager) CreateProxyPeer(ctx context.Context, accountID string, peerKey string, cluster string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CreateProxyPeer", ctx, accountID, peerKey, cluster)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProxyPeer indicates an expected call of CreateProxyPeer.
|
||||||
|
func (mr *MockManagerMockRecorder) CreateProxyPeer(ctx, accountID, peerKey, cluster interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProxyPeer", reflect.TypeOf((*MockManager)(nil).CreateProxyPeer), ctx, accountID, peerKey, cluster)
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (m *managerImpl) SaveAccessLog(ctx context.Context, logEntry *accesslogs.Ac
|
|||||||
|
|
||||||
// GetAllAccessLogs retrieves access logs for an account with pagination and filtering
|
// GetAllAccessLogs retrieves access logs for an account with pagination and filtering
|
||||||
func (m *managerImpl) GetAllAccessLogs(ctx context.Context, accountID, userID string, filter *accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {
|
func (m *managerImpl) GetAllAccessLogs(ctx context.Context, accountID, userID string, filter *accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, status.NewPermissionValidationError(err)
|
return nil, 0, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ type Domain struct {
|
|||||||
// SupportsCrowdSec is populated at query time from proxy cluster capabilities.
|
// SupportsCrowdSec is populated at query time from proxy cluster capabilities.
|
||||||
// Not persisted.
|
// Not persisted.
|
||||||
SupportsCrowdSec *bool `gorm:"-"`
|
SupportsCrowdSec *bool `gorm:"-"`
|
||||||
// SupportsPrivate is populated at query time from proxy cluster capabilities. Not persisted.
|
|
||||||
SupportsPrivate *bool `gorm:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventMeta returns activity event metadata for a domain
|
// EventMeta returns activity event metadata for a domain
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ func domainToApi(d *domain.Domain) api.ReverseProxyDomain {
|
|||||||
SupportsCustomPorts: d.SupportsCustomPorts,
|
SupportsCustomPorts: d.SupportsCustomPorts,
|
||||||
RequireSubdomain: d.RequireSubdomain,
|
RequireSubdomain: d.RequireSubdomain,
|
||||||
SupportsCrowdsec: d.SupportsCrowdSec,
|
SupportsCrowdsec: d.SupportsCrowdSec,
|
||||||
SupportsPrivate: d.SupportsPrivate,
|
|
||||||
}
|
}
|
||||||
if d.TargetCluster != "" {
|
if d.TargetCluster != "" {
|
||||||
resp.TargetCluster = &d.TargetCluster
|
resp.TargetCluster = &d.TargetCluster
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ type proxyManager interface {
|
|||||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -57,7 +56,7 @@ func NewManager(store store, proxyMgr proxyManager, permissionsManager permissio
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*domain.Domain, error) {
|
func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*domain.Domain, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -94,7 +93,6 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
|||||||
d.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, cluster)
|
d.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, cluster)
|
||||||
d.RequireSubdomain = m.proxyManager.ClusterRequireSubdomain(ctx, cluster)
|
d.RequireSubdomain = m.proxyManager.ClusterRequireSubdomain(ctx, cluster)
|
||||||
d.SupportsCrowdSec = m.proxyManager.ClusterSupportsCrowdSec(ctx, cluster)
|
d.SupportsCrowdSec = m.proxyManager.ClusterSupportsCrowdSec(ctx, cluster)
|
||||||
d.SupportsPrivate = m.proxyManager.ClusterSupportsPrivate(ctx, cluster)
|
|
||||||
ret = append(ret, d)
|
ret = append(ret, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +109,6 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
|||||||
if d.TargetCluster != "" {
|
if d.TargetCluster != "" {
|
||||||
cd.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, d.TargetCluster)
|
cd.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, d.TargetCluster)
|
||||||
cd.SupportsCrowdSec = m.proxyManager.ClusterSupportsCrowdSec(ctx, d.TargetCluster)
|
cd.SupportsCrowdSec = m.proxyManager.ClusterSupportsCrowdSec(ctx, d.TargetCluster)
|
||||||
cd.SupportsPrivate = m.proxyManager.ClusterSupportsPrivate(ctx, d.TargetCluster)
|
|
||||||
}
|
}
|
||||||
// Custom domains never require a subdomain by default since
|
// Custom domains never require a subdomain by default since
|
||||||
// the account owns them and should be able to use the bare domain.
|
// the account owns them and should be able to use the bare domain.
|
||||||
@@ -122,7 +119,7 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) CreateDomain(ctx context.Context, accountID, userID, domainName, targetCluster string) (*domain.Domain, error) {
|
func (m Manager) CreateDomain(ctx context.Context, accountID, userID, domainName, targetCluster string) (*domain.Domain, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -163,7 +160,7 @@ func (m Manager) CreateDomain(ctx context.Context, accountID, userID, domainName
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) DeleteDomain(ctx context.Context, accountID, userID, domainID string) error {
|
func (m Manager) DeleteDomain(ctx context.Context, accountID, userID, domainID string) error {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -187,7 +184,7 @@ func (m Manager) DeleteDomain(ctx context.Context, accountID, userID, domainID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID string) {
|
func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID string) {
|
||||||
ok, _, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"accountID": accountID,
|
"accountID": accountID,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type mockProxyManager struct {
|
type mockProxyManager struct {
|
||||||
getActiveClusterAddressesFunc func(ctx context.Context) ([]string, error)
|
getActiveClusterAddressesFunc func(ctx context.Context) ([]string, error)
|
||||||
getActiveClusterAddressesForAccountFunc func(ctx context.Context, accountID string) ([]string, error)
|
getActiveClusterAddressesForAccountFunc func(ctx context.Context, accountID string) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +40,6 @@ func (m *mockProxyManager) ClusterSupportsCrowdSec(_ context.Context, _ string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockProxyManager) ClusterSupportsPrivate(_ context.Context, _ string) *bool {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetClusterAllowList_BYOPMergedWithPublic(t *testing.T) {
|
func TestGetClusterAllowList_BYOPMergedWithPublic(t *testing.T) {
|
||||||
pm := &mockProxyManager{
|
pm := &mockProxyManager{
|
||||||
getActiveClusterAddressesForAccountFunc: func(_ context.Context, accID string) ([]string, error) {
|
getActiveClusterAddressesForAccountFunc: func(_ context.Context, accID string) ([]string, error) {
|
||||||
@@ -155,3 +151,4 @@ func TestGetClusterAllowList_PublicEmpty_BYOPOnly(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{"byop.example.com"}, result)
|
assert.Equal(t, []string{"byop.example.com"}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ type Manager interface {
|
|||||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool
|
|
||||||
CleanupStale(ctx context.Context, inactivityDuration time.Duration) error
|
CleanupStale(ctx context.Context, inactivityDuration time.Duration) error
|
||||||
GetAccountProxy(ctx context.Context, accountID string) (*Proxy, error)
|
GetAccountProxy(ctx context.Context, accountID string) (*Proxy, error)
|
||||||
CountAccountProxies(ctx context.Context, accountID string) (int64, error)
|
CountAccountProxies(ctx context.Context, accountID string) (int64, error)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ type store interface {
|
|||||||
GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||||
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||||
GetClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
GetClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
||||||
GetClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool
|
|
||||||
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
|
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
|
||||||
GetProxyByAccountID(ctx context.Context, accountID string) (*proxy.Proxy, error)
|
GetProxyByAccountID(ctx context.Context, accountID string) (*proxy.Proxy, error)
|
||||||
CountProxiesByAccountID(ctx context.Context, accountID string) (int64, error)
|
CountProxiesByAccountID(ctx context.Context, accountID string) (int64, error)
|
||||||
@@ -138,11 +137,6 @@ func (m Manager) ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string
|
|||||||
return m.store.GetClusterSupportsCrowdSec(ctx, clusterAddr)
|
return m.store.GetClusterSupportsCrowdSec(ctx, clusterAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClusterSupportsPrivate reports whether any active proxy claims the private capability (nil = unreported).
|
|
||||||
func (m Manager) ClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool {
|
|
||||||
return m.store.GetClusterSupportsPrivate(ctx, clusterAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanupStale removes proxies that haven't sent heartbeat in the specified duration
|
// CleanupStale removes proxies that haven't sent heartbeat in the specified duration
|
||||||
func (m *Manager) CleanupStale(ctx context.Context, inactivityDuration time.Duration) error {
|
func (m *Manager) CleanupStale(ctx context.Context, inactivityDuration time.Duration) error {
|
||||||
if err := m.store.CleanupStaleProxies(ctx, inactivityDuration); err != nil {
|
if err := m.store.CleanupStaleProxies(ctx, inactivityDuration); err != nil {
|
||||||
@@ -184,3 +178,4 @@ func (m *Manager) DeleteAccountCluster(ctx context.Context, clusterAddress, acco
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type mockStore struct {
|
type mockStore struct {
|
||||||
saveProxyFunc func(ctx context.Context, p *proxy.Proxy) error
|
saveProxyFunc func(ctx context.Context, p *proxy.Proxy) error
|
||||||
disconnectProxyFunc func(ctx context.Context, proxyID, sessionID string) error
|
disconnectProxyFunc func(ctx context.Context, proxyID, sessionID string) error
|
||||||
updateProxyHeartbeatFunc func(ctx context.Context, p *proxy.Proxy) error
|
updateProxyHeartbeatFunc func(ctx context.Context, p *proxy.Proxy) error
|
||||||
getActiveProxyClusterAddressesFunc func(ctx context.Context) ([]string, error)
|
getActiveProxyClusterAddressesFunc func(ctx context.Context) ([]string, error)
|
||||||
getActiveProxyClusterAddressesForAccFunc func(ctx context.Context, accountID string) ([]string, error)
|
getActiveProxyClusterAddressesForAccFunc func(ctx context.Context, accountID string) ([]string, error)
|
||||||
cleanupStaleProxiesFunc func(ctx context.Context, d time.Duration) error
|
cleanupStaleProxiesFunc func(ctx context.Context, d time.Duration) error
|
||||||
getProxyByAccountIDFunc func(ctx context.Context, accountID string) (*proxy.Proxy, error)
|
getProxyByAccountIDFunc func(ctx context.Context, accountID string) (*proxy.Proxy, error)
|
||||||
countProxiesByAccountIDFunc func(ctx context.Context, accountID string) (int64, error)
|
countProxiesByAccountIDFunc func(ctx context.Context, accountID string) (int64, error)
|
||||||
isClusterAddressConflictingFunc func(ctx context.Context, clusterAddress, accountID string) (bool, error)
|
isClusterAddressConflictingFunc func(ctx context.Context, clusterAddress, accountID string) (bool, error)
|
||||||
deleteAccountClusterFunc func(ctx context.Context, clusterAddress, accountID string) error
|
deleteAccountClusterFunc func(ctx context.Context, clusterAddress, accountID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockStore) SaveProxy(ctx context.Context, p *proxy.Proxy) error {
|
func (m *mockStore) SaveProxy(ctx context.Context, p *proxy.Proxy) error {
|
||||||
@@ -99,9 +99,6 @@ func (m *mockStore) GetClusterRequireSubdomain(_ context.Context, _ string) *boo
|
|||||||
func (m *mockStore) GetClusterSupportsCrowdSec(_ context.Context, _ string) *bool {
|
func (m *mockStore) GetClusterSupportsCrowdSec(_ context.Context, _ string) *bool {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (m *mockStore) GetClusterSupportsPrivate(_ context.Context, _ string) *bool {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestManager(s store) *Manager {
|
func newTestManager(s store) *Manager {
|
||||||
meter := noop.NewMeterProvider().Meter("test")
|
meter := noop.NewMeterProvider().Meter("test")
|
||||||
|
|||||||
@@ -92,20 +92,6 @@ func (mr *MockManagerMockRecorder) ClusterSupportsCrowdSec(ctx, clusterAddr inte
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSupportsCrowdSec", reflect.TypeOf((*MockManager)(nil).ClusterSupportsCrowdSec), ctx, clusterAddr)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSupportsCrowdSec", reflect.TypeOf((*MockManager)(nil).ClusterSupportsCrowdSec), ctx, clusterAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClusterSupportsPrivate mocks base method.
|
|
||||||
func (m *MockManager) ClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ClusterSupportsPrivate", ctx, clusterAddr)
|
|
||||||
ret0, _ := ret[0].(*bool)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClusterSupportsPrivate indicates an expected call of ClusterSupportsPrivate.
|
|
||||||
func (mr *MockManagerMockRecorder) ClusterSupportsPrivate(ctx, clusterAddr interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSupportsPrivate", reflect.TypeOf((*MockManager)(nil).ClusterSupportsPrivate), ctx, clusterAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect mocks base method.
|
// Connect mocks base method.
|
||||||
func (m *MockManager) Connect(ctx context.Context, proxyID, sessionID, clusterAddress, ipAddress string, accountID *string, capabilities *Capabilities) (*Proxy, error) {
|
func (m *MockManager) Connect(ctx context.Context, proxyID, sessionID, clusterAddress, ipAddress string, accountID *string, capabilities *Capabilities) (*Proxy, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ type Capabilities struct {
|
|||||||
RequireSubdomain *bool
|
RequireSubdomain *bool
|
||||||
// SupportsCrowdsec indicates whether this proxy has CrowdSec configured.
|
// SupportsCrowdsec indicates whether this proxy has CrowdSec configured.
|
||||||
SupportsCrowdsec *bool
|
SupportsCrowdsec *bool
|
||||||
// Private indicates whether this proxy supports inbound access via Wireguard
|
|
||||||
// tunnel and netbird-only authentication policies
|
|
||||||
Private *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy represents a reverse proxy instance
|
// Proxy represents a reverse proxy instance
|
||||||
@@ -70,9 +67,10 @@ type Cluster struct {
|
|||||||
Type ClusterType
|
Type ClusterType
|
||||||
Online bool
|
Online bool
|
||||||
ConnectedProxies int
|
ConnectedProxies int
|
||||||
// *bool: nil = no proxy reported the capability; the dashboard renders that as unknown.
|
// Capability flags. *bool because nil means "no proxy reported a
|
||||||
|
// capability for this cluster" — the dashboard renders these as
|
||||||
|
// unknown rather than false.
|
||||||
SupportsCustomPorts *bool
|
SupportsCustomPorts *bool
|
||||||
RequireSubdomain *bool
|
RequireSubdomain *bool
|
||||||
SupportsCrowdSec *bool
|
SupportsCrowdSec *bool
|
||||||
Private *bool
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (h *handler) createToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, ctx, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Create)
|
ok, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
@@ -76,13 +76,13 @@ func (h *handler) createToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.store.SaveProxyAccessToken(ctx, &generated.ProxyAccessToken); err != nil {
|
if err := h.store.SaveProxyAccessToken(r.Context(), &generated.ProxyAccessToken); err != nil {
|
||||||
util.WriteErrorResponse("failed to save token", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to save token", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := toProxyTokenCreatedResponse(generated)
|
resp := toProxyTokenCreatedResponse(generated)
|
||||||
util.WriteJSONObject(ctx, w, resp)
|
util.WriteJSONObject(r.Context(), w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) listTokens(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) listTokens(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -92,7 +92,7 @@ func (h *handler) listTokens(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, ctx, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Read)
|
ok, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
@@ -102,7 +102,7 @@ func (h *handler) listTokens(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, err := h.store.GetProxyAccessTokensByAccountID(ctx, store.LockingStrengthNone, userAuth.AccountId)
|
tokens, err := h.store.GetProxyAccessTokensByAccountID(r.Context(), store.LockingStrengthNone, userAuth.AccountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteErrorResponse("failed to list tokens", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to list tokens", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
@@ -113,7 +113,7 @@ func (h *handler) listTokens(w http.ResponseWriter, r *http.Request) {
|
|||||||
resp = append(resp, toProxyTokenResponse(token))
|
resp = append(resp, toProxyTokenResponse(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
util.WriteJSONObject(ctx, w, resp)
|
util.WriteJSONObject(r.Context(), w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) revokeToken(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) revokeToken(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -123,7 +123,7 @@ func (h *handler) revokeToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, ctx, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Delete)
|
ok, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
@@ -139,7 +139,7 @@ func (h *handler) revokeToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := h.store.GetProxyAccessTokenByID(ctx, store.LockingStrengthNone, tokenID)
|
token, err := h.store.GetProxyAccessTokenByID(r.Context(), store.LockingStrengthNone, tokenID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := status.FromError(err); ok && s.ErrorType == status.NotFound {
|
if s, ok := status.FromError(err); ok && s.ErrorType == status.NotFound {
|
||||||
util.WriteErrorResponse("token not found", http.StatusNotFound, w)
|
util.WriteErrorResponse("token not found", http.StatusNotFound, w)
|
||||||
@@ -154,12 +154,12 @@ func (h *handler) revokeToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.store.RevokeProxyAccessToken(ctx, tokenID); err != nil {
|
if err := h.store.RevokeProxyAccessToken(r.Context(), tokenID); err != nil {
|
||||||
util.WriteErrorResponse("failed to revoke token", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to revoke token", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.WriteJSONObject(ctx, w, util.EmptyObject{})
|
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProxyTokenResponse(token *types.ProxyAccessToken) api.ProxyToken {
|
func toProxyTokenResponse(token *types.ProxyAccessToken) api.ProxyToken {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestCreateToken_AccountScoped(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Create).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Create).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
@@ -90,7 +90,7 @@ func TestCreateToken_WithExpiration(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
@@ -115,7 +115,7 @@ func TestCreateToken_EmptyName(t *testing.T) {
|
|||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
permissionsManager: permsMgr,
|
permissionsManager: permsMgr,
|
||||||
@@ -135,7 +135,7 @@ func TestCreateToken_PermissionDenied(t *testing.T) {
|
|||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(false, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(false, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
permissionsManager: permsMgr,
|
permissionsManager: permsMgr,
|
||||||
@@ -164,7 +164,7 @@ func TestListTokens(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Read).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Read).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
@@ -202,7 +202,7 @@ func TestRevokeToken_Success(t *testing.T) {
|
|||||||
mockStore.EXPECT().RevokeProxyAccessToken(gomock.Any(), "tok-1").Return(nil)
|
mockStore.EXPECT().RevokeProxyAccessToken(gomock.Any(), "tok-1").Return(nil)
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Delete).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Delete).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
@@ -231,7 +231,7 @@ func TestRevokeToken_WrongAccount(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
@@ -258,7 +258,7 @@ func TestRevokeToken_ManagementWideToken(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
permsMgr := permissions.NewMockManager(ctrl)
|
permsMgr := permissions.NewMockManager(ctrl)
|
||||||
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(true, context.Background(), nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(true, nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
|
|||||||
@@ -204,7 +204,6 @@ func (h *handler) getClusters(w http.ResponseWriter, r *http.Request) {
|
|||||||
SupportsCustomPorts: c.SupportsCustomPorts,
|
SupportsCustomPorts: c.SupportsCustomPorts,
|
||||||
RequireSubdomain: c.RequireSubdomain,
|
RequireSubdomain: c.RequireSubdomain,
|
||||||
SupportsCrowdsec: c.SupportsCrowdSec,
|
SupportsCrowdsec: c.SupportsCrowdSec,
|
||||||
Private: c.Private,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ type CapabilityProvider interface {
|
|||||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -120,7 +119,7 @@ func (m *Manager) StartExposeReaper(ctx context.Context) {
|
|||||||
// capability flags reported by its active proxies so the dashboard can
|
// capability flags reported by its active proxies so the dashboard can
|
||||||
// render feature support without a second round-trip.
|
// render feature support without a second round-trip.
|
||||||
func (m *Manager) GetClusters(ctx context.Context, accountID, userID string) ([]proxy.Cluster, error) {
|
func (m *Manager) GetClusters(ctx context.Context, accountID, userID string) ([]proxy.Cluster, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -137,7 +136,6 @@ func (m *Manager) GetClusters(ctx context.Context, accountID, userID string) ([]
|
|||||||
clusters[i].SupportsCustomPorts = m.capabilities.ClusterSupportsCustomPorts(ctx, clusters[i].Address)
|
clusters[i].SupportsCustomPorts = m.capabilities.ClusterSupportsCustomPorts(ctx, clusters[i].Address)
|
||||||
clusters[i].RequireSubdomain = m.capabilities.ClusterRequireSubdomain(ctx, clusters[i].Address)
|
clusters[i].RequireSubdomain = m.capabilities.ClusterRequireSubdomain(ctx, clusters[i].Address)
|
||||||
clusters[i].SupportsCrowdSec = m.capabilities.ClusterSupportsCrowdSec(ctx, clusters[i].Address)
|
clusters[i].SupportsCrowdSec = m.capabilities.ClusterSupportsCrowdSec(ctx, clusters[i].Address)
|
||||||
clusters[i].Private = m.capabilities.ClusterSupportsPrivate(ctx, clusters[i].Address)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clusters, nil
|
return clusters, nil
|
||||||
@@ -146,7 +144,7 @@ func (m *Manager) GetClusters(ctx context.Context, accountID, userID string) ([]
|
|||||||
// DeleteAccountCluster removes all proxy registrations for the given cluster address
|
// DeleteAccountCluster removes all proxy registrations for the given cluster address
|
||||||
// owned by the account.
|
// owned by the account.
|
||||||
func (m *Manager) DeleteAccountCluster(ctx context.Context, accountID, userID, clusterAddress string) error {
|
func (m *Manager) DeleteAccountCluster(ctx context.Context, accountID, userID, clusterAddress string) error {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -158,7 +156,7 @@ func (m *Manager) DeleteAccountCluster(ctx context.Context, accountID, userID, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) {
|
func (m *Manager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -210,9 +208,6 @@ func (m *Manager) replaceHostByLookup(ctx context.Context, accountID string, s *
|
|||||||
target.Host = resource.Domain
|
target.Host = resource.Domain
|
||||||
case service.TargetTypeSubnet:
|
case service.TargetTypeSubnet:
|
||||||
// For subnets we do not do any lookups on the resource
|
// For subnets we do not do any lookups on the resource
|
||||||
case service.TargetTypeCluster:
|
|
||||||
// Cluster targets carry the upstream address on target_id; the
|
|
||||||
// proxy resolves the destination at request time.
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown target type: %s", target.TargetType)
|
return fmt.Errorf("unknown target type: %s", target.TargetType)
|
||||||
}
|
}
|
||||||
@@ -222,7 +217,7 @@ func (m *Manager) replaceHostByLookup(ctx context.Context, accountID string, s *
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetService(ctx context.Context, accountID, userID, serviceID string) (*service.Service, error) {
|
func (m *Manager) GetService(ctx context.Context, accountID, userID, serviceID string) (*service.Service, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -243,7 +238,7 @@ func (m *Manager) GetService(ctx context.Context, accountID, userID, serviceID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) CreateService(ctx context.Context, accountID, userID string, s *service.Service) (*service.Service, error) {
|
func (m *Manager) CreateService(ctx context.Context, accountID, userID string, s *service.Service) (*service.Service, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -528,7 +523,7 @@ func (m *Manager) checkDomainAvailable(ctx context.Context, transaction store.St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) UpdateService(ctx context.Context, accountID, userID string, service *service.Service) (*service.Service, error) {
|
func (m *Manager) UpdateService(ctx context.Context, accountID, userID string, service *service.Service) (*service.Service, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Update)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -784,10 +779,6 @@ func validateTargetReferences(ctx context.Context, transaction store.Store, acco
|
|||||||
if err := validateResourceTarget(ctx, transaction, accountID, target); err != nil {
|
if err := validateResourceTarget(ctx, transaction, accountID, target); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case service.TargetTypeCluster:
|
|
||||||
if err := validateClusterTarget(target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return status.Errorf(status.InvalidArgument, "unknown target type %q for target %q", target.TargetType, target.TargetId)
|
return status.Errorf(status.InvalidArgument, "unknown target type %q for target %q", target.TargetType, target.TargetId)
|
||||||
}
|
}
|
||||||
@@ -795,13 +786,6 @@ func validateTargetReferences(ctx context.Context, transaction store.Store, acco
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateClusterTarget(target *service.Target) error {
|
|
||||||
if !target.Options.DirectUpstream {
|
|
||||||
return status.Errorf(status.InvalidArgument, "cluster target %s has direct upstream disabled", target.Host)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePeerTarget(ctx context.Context, transaction store.Store, accountID string, target *service.Target) error {
|
func validatePeerTarget(ctx context.Context, transaction store.Store, accountID string, target *service.Target) error {
|
||||||
if _, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, target.TargetId); err != nil {
|
if _, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, target.TargetId); err != nil {
|
||||||
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
|
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
|
||||||
@@ -836,7 +820,7 @@ func validateResourceTargetType(target *service.Target, resource *resourcetypes.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceID string) error {
|
func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceID string) error {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -876,7 +860,7 @@ func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DeleteAllServices(ctx context.Context, accountID, userID string) error {
|
func (m *Manager) DeleteAllServices(ctx context.Context, accountID, userID string) error {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -978,14 +962,12 @@ func (m *Manager) ReloadAllServicesForAccount(ctx context.Context, accountID str
|
|||||||
return fmt.Errorf("failed to get services: %w", err)
|
return fmt.Errorf("failed to get services: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcCfg := m.proxyController.GetOIDCValidationConfig()
|
|
||||||
|
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
err = m.replaceHostByLookup(ctx, accountID, s)
|
err = m.replaceHostByLookup(ctx, accountID, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to replace host by lookup for service %s: %w", s.ID, err)
|
return fmt.Errorf("failed to replace host by lookup for service %s: %w", s.ID, err)
|
||||||
}
|
}
|
||||||
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, s.ToProtoMapping(service.Update, "", oidcCfg), s.ProxyCluster)
|
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, s.ToProtoMapping(service.Update, "", m.proxyController.GetOIDCValidationConfig()), s.ProxyCluster)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1172,7 +1172,7 @@ func TestDeleteService_DeletesTargets(t *testing.T) {
|
|||||||
|
|
||||||
mockPerms.EXPECT().
|
mockPerms.EXPECT().
|
||||||
ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete).
|
ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
mockAcct.EXPECT().
|
mockAcct.EXPECT().
|
||||||
StoreEvent(ctx, userID, service.ID, accountID, activity.ServiceDeleted, gomock.Any())
|
StoreEvent(ctx, userID, service.ID, accountID, activity.ServiceDeleted, gomock.Any())
|
||||||
mockAcct.EXPECT().
|
mockAcct.EXPECT().
|
||||||
@@ -1344,66 +1344,3 @@ func TestValidateSubdomainRequirement(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateTargetReferences_ClusterTargetSkipsLookup(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
mockStore := store.NewMockStore(ctrl)
|
|
||||||
accountID := "test-account"
|
|
||||||
|
|
||||||
// No peer or resource lookups must be issued for cluster targets.
|
|
||||||
targets := []*rpservice.Target{
|
|
||||||
{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: rpservice.TargetTypeCluster,
|
|
||||||
Options: rpservice.TargetOptions{DirectUpstream: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
require.NoError(t, validateTargetReferences(ctx, mockStore, accountID, targets), "cluster target must validate without store lookups")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidateTargetReferences_ClusterTargetRequiresDirectUpstream pins the
|
|
||||||
// store-side check that cluster targets must opt into the host-stack dial
|
|
||||||
// path. Without DirectUpstream the proxy would route this target through
|
|
||||||
// the embedded NetBird client and fail on every request.
|
|
||||||
func TestValidateTargetReferences_ClusterTargetRequiresDirectUpstream(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
mockStore := store.NewMockStore(ctrl)
|
|
||||||
accountID := "test-account"
|
|
||||||
|
|
||||||
targets := []*rpservice.Target{
|
|
||||||
{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: rpservice.TargetTypeCluster,
|
|
||||||
Host: "backend.lan",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := validateTargetReferences(ctx, mockStore, accountID, targets)
|
|
||||||
require.Error(t, err, "cluster target without direct_upstream must be rejected")
|
|
||||||
assert.ErrorContains(t, err, "direct upstream disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceHostByLookup_SkipsClusterTarget(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
mockStore := store.NewMockStore(ctrl)
|
|
||||||
accountID := "test-account"
|
|
||||||
|
|
||||||
mgr := &Manager{store: mockStore}
|
|
||||||
|
|
||||||
svc := &rpservice.Service{
|
|
||||||
ID: "svc-1",
|
|
||||||
AccountID: accountID,
|
|
||||||
Targets: []*rpservice.Target{
|
|
||||||
{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: rpservice.TargetTypeCluster,
|
|
||||||
Host: "127.0.0.1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, mgr.replaceHostByLookup(ctx, accountID, svc), "cluster target must not trigger peer/resource lookup")
|
|
||||||
assert.Equal(t, "127.0.0.1", svc.Targets[0].Host, "operator-supplied host must be preserved for cluster target")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,11 +45,10 @@ const (
|
|||||||
StatusCertificateFailed Status = "certificate_failed"
|
StatusCertificateFailed Status = "certificate_failed"
|
||||||
StatusError Status = "error"
|
StatusError Status = "error"
|
||||||
|
|
||||||
TargetTypePeer TargetType = "peer"
|
TargetTypePeer TargetType = "peer"
|
||||||
TargetTypeHost TargetType = "host"
|
TargetTypeHost TargetType = "host"
|
||||||
TargetTypeDomain TargetType = "domain"
|
TargetTypeDomain TargetType = "domain"
|
||||||
TargetTypeSubnet TargetType = "subnet"
|
TargetTypeSubnet TargetType = "subnet"
|
||||||
TargetTypeCluster TargetType = "cluster"
|
|
||||||
|
|
||||||
SourcePermanent = "permanent"
|
SourcePermanent = "permanent"
|
||||||
SourceEphemeral = "ephemeral"
|
SourceEphemeral = "ephemeral"
|
||||||
@@ -61,11 +60,6 @@ type TargetOptions struct {
|
|||||||
SessionIdleTimeout time.Duration `json:"session_idle_timeout,omitempty"`
|
SessionIdleTimeout time.Duration `json:"session_idle_timeout,omitempty"`
|
||||||
PathRewrite PathRewriteMode `json:"path_rewrite,omitempty"`
|
PathRewrite PathRewriteMode `json:"path_rewrite,omitempty"`
|
||||||
CustomHeaders map[string]string `gorm:"serializer:json" json:"custom_headers,omitempty"`
|
CustomHeaders map[string]string `gorm:"serializer:json" json:"custom_headers,omitempty"`
|
||||||
// DirectUpstream bypasses the proxy's embedded NetBird client and dials
|
|
||||||
// the target via the proxy host's network stack. Useful for upstreams
|
|
||||||
// reachable without WireGuard (public APIs, LAN services, localhost
|
|
||||||
// sidecars). Default false.
|
|
||||||
DirectUpstream bool `json:"direct_upstream,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Target struct {
|
type Target struct {
|
||||||
@@ -73,7 +67,7 @@ type Target struct {
|
|||||||
AccountID string `gorm:"index:idx_target_account;not null" json:"-"`
|
AccountID string `gorm:"index:idx_target_account;not null" json:"-"`
|
||||||
ServiceID string `gorm:"index:idx_service_targets;not null" json:"-"`
|
ServiceID string `gorm:"index:idx_service_targets;not null" json:"-"`
|
||||||
Path *string `json:"path,omitempty"`
|
Path *string `json:"path,omitempty"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"` // the Host field is only used for subnet targets, otherwise ignored
|
||||||
Port uint16 `gorm:"index:idx_target_port" json:"port"`
|
Port uint16 `gorm:"index:idx_target_port" json:"port"`
|
||||||
Protocol string `gorm:"index:idx_target_protocol" json:"protocol"`
|
Protocol string `gorm:"index:idx_target_protocol" json:"protocol"`
|
||||||
TargetId string `gorm:"index:idx_target_id" json:"target_id"`
|
TargetId string `gorm:"index:idx_target_id" json:"target_id"`
|
||||||
@@ -206,10 +200,6 @@ type Service struct {
|
|||||||
Mode string `gorm:"default:'http'"`
|
Mode string `gorm:"default:'http'"`
|
||||||
ListenPort uint16
|
ListenPort uint16
|
||||||
PortAutoAssigned bool
|
PortAutoAssigned bool
|
||||||
// Private marks the service as NetBird-only: auth via ValidateTunnelPeer against AccessGroups instead of SSO. HTTP-only.
|
|
||||||
Private bool
|
|
||||||
// AccessGroups is the group ID allowlist for inbound peers on private services. Mutually exclusive with bearer SSO.
|
|
||||||
AccessGroups []string `json:"access_groups,omitempty" gorm:"serializer:json"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitNewRecord generates a new unique ID and resets metadata for a newly created
|
// InitNewRecord generates a new unique ID and resets metadata for a newly created
|
||||||
@@ -309,12 +299,6 @@ func (s *Service) ToAPIResponse() *api.Service {
|
|||||||
Mode: &mode,
|
Mode: &mode,
|
||||||
ListenPort: &listenPort,
|
ListenPort: &listenPort,
|
||||||
PortAutoAssigned: &s.PortAutoAssigned,
|
PortAutoAssigned: &s.PortAutoAssigned,
|
||||||
Private: &s.Private,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.AccessGroups) > 0 {
|
|
||||||
groups := append([]string(nil), s.AccessGroups...)
|
|
||||||
resp.AccessGroups = &groups
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ProxyCluster != "" {
|
if s.ProxyCluster != "" {
|
||||||
@@ -324,7 +308,6 @@ func (s *Service) ToAPIResponse() *api.Service {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToProtoMapping converts the service into the wire format the proxy consumes.
|
|
||||||
func (s *Service) ToProtoMapping(operation Operation, authToken string, oidcConfig proxy.OIDCValidationConfig) *proto.ProxyMapping {
|
func (s *Service) ToProtoMapping(operation Operation, authToken string, oidcConfig proxy.OIDCValidationConfig) *proto.ProxyMapping {
|
||||||
pathMappings := s.buildPathMappings()
|
pathMappings := s.buildPathMappings()
|
||||||
|
|
||||||
@@ -366,7 +349,6 @@ func (s *Service) ToProtoMapping(operation Operation, authToken string, oidcConf
|
|||||||
RewriteRedirects: s.RewriteRedirects,
|
RewriteRedirects: s.RewriteRedirects,
|
||||||
Mode: s.Mode,
|
Mode: s.Mode,
|
||||||
ListenPort: int32(s.ListenPort), //nolint:gosec
|
ListenPort: int32(s.ListenPort), //nolint:gosec
|
||||||
Private: s.Private,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r := restrictionsToProto(s.Restrictions); r != nil {
|
if r := restrictionsToProto(s.Restrictions); r != nil {
|
||||||
@@ -473,8 +455,7 @@ func pathRewriteToProto(mode PathRewriteMode) proto.PathRewriteMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func targetOptionsToAPI(opts TargetOptions) *api.ServiceTargetOptions {
|
func targetOptionsToAPI(opts TargetOptions) *api.ServiceTargetOptions {
|
||||||
if !opts.SkipTLSVerify && opts.RequestTimeout == 0 && opts.SessionIdleTimeout == 0 &&
|
if !opts.SkipTLSVerify && opts.RequestTimeout == 0 && opts.SessionIdleTimeout == 0 && opts.PathRewrite == "" && len(opts.CustomHeaders) == 0 {
|
||||||
opts.PathRewrite == "" && len(opts.CustomHeaders) == 0 && !opts.DirectUpstream {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
apiOpts := &api.ServiceTargetOptions{}
|
apiOpts := &api.ServiceTargetOptions{}
|
||||||
@@ -496,22 +477,17 @@ func targetOptionsToAPI(opts TargetOptions) *api.ServiceTargetOptions {
|
|||||||
if len(opts.CustomHeaders) > 0 {
|
if len(opts.CustomHeaders) > 0 {
|
||||||
apiOpts.CustomHeaders = &opts.CustomHeaders
|
apiOpts.CustomHeaders = &opts.CustomHeaders
|
||||||
}
|
}
|
||||||
if opts.DirectUpstream {
|
|
||||||
apiOpts.DirectUpstream = &opts.DirectUpstream
|
|
||||||
}
|
|
||||||
return apiOpts
|
return apiOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
func targetOptionsToProto(opts TargetOptions) *proto.PathTargetOptions {
|
func targetOptionsToProto(opts TargetOptions) *proto.PathTargetOptions {
|
||||||
if !opts.SkipTLSVerify && opts.PathRewrite == "" && opts.RequestTimeout == 0 &&
|
if !opts.SkipTLSVerify && opts.PathRewrite == "" && opts.RequestTimeout == 0 && len(opts.CustomHeaders) == 0 {
|
||||||
len(opts.CustomHeaders) == 0 && !opts.DirectUpstream {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
popts := &proto.PathTargetOptions{
|
popts := &proto.PathTargetOptions{
|
||||||
SkipTlsVerify: opts.SkipTLSVerify,
|
SkipTlsVerify: opts.SkipTLSVerify,
|
||||||
PathRewrite: pathRewriteToProto(opts.PathRewrite),
|
PathRewrite: pathRewriteToProto(opts.PathRewrite),
|
||||||
CustomHeaders: opts.CustomHeaders,
|
CustomHeaders: opts.CustomHeaders,
|
||||||
DirectUpstream: opts.DirectUpstream,
|
|
||||||
}
|
}
|
||||||
if opts.RequestTimeout != 0 {
|
if opts.RequestTimeout != 0 {
|
||||||
popts.RequestTimeout = durationpb.New(opts.RequestTimeout)
|
popts.RequestTimeout = durationpb.New(opts.RequestTimeout)
|
||||||
@@ -561,9 +537,6 @@ func targetOptionsFromAPI(idx int, o *api.ServiceTargetOptions) (TargetOptions,
|
|||||||
if o.CustomHeaders != nil {
|
if o.CustomHeaders != nil {
|
||||||
opts.CustomHeaders = *o.CustomHeaders
|
opts.CustomHeaders = *o.CustomHeaders
|
||||||
}
|
}
|
||||||
if o.DirectUpstream != nil {
|
|
||||||
opts.DirectUpstream = *o.DirectUpstream
|
|
||||||
}
|
|
||||||
return opts, nil
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,14 +551,6 @@ func (s *Service) FromAPIRequest(req *api.ServiceRequest, accountID string) erro
|
|||||||
if req.ListenPort != nil {
|
if req.ListenPort != nil {
|
||||||
s.ListenPort = uint16(*req.ListenPort) //nolint:gosec
|
s.ListenPort = uint16(*req.ListenPort) //nolint:gosec
|
||||||
}
|
}
|
||||||
if req.Private != nil {
|
|
||||||
s.Private = *req.Private
|
|
||||||
}
|
|
||||||
if req.AccessGroups != nil {
|
|
||||||
s.AccessGroups = append([]string(nil), *req.AccessGroups...)
|
|
||||||
} else {
|
|
||||||
s.AccessGroups = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
targets, err := targetsFromAPI(accountID, req.Targets)
|
targets, err := targetsFromAPI(accountID, req.Targets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -775,9 +740,6 @@ func (s *Service) Validate() error {
|
|||||||
if err := validateAccessRestrictions(&s.Restrictions); err != nil {
|
if err := validateAccessRestrictions(&s.Restrictions); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.validatePrivateRequirements(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s.Mode {
|
switch s.Mode {
|
||||||
case ModeHTTP:
|
case ModeHTTP:
|
||||||
@@ -791,23 +753,6 @@ func (s *Service) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validatePrivateRequirements enforces the private-service contract: HTTP mode, ≥1 access group, no bearer auth.
|
|
||||||
func (s *Service) validatePrivateRequirements() error {
|
|
||||||
if !s.Private {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if s.Mode != "" && s.Mode != ModeHTTP {
|
|
||||||
return fmt.Errorf("private services only support HTTP mode, got %q", s.Mode)
|
|
||||||
}
|
|
||||||
if len(s.AccessGroups) == 0 {
|
|
||||||
return errors.New("private services require at least one access group")
|
|
||||||
}
|
|
||||||
if s.Auth.BearerAuth != nil && s.Auth.BearerAuth.Enabled {
|
|
||||||
return errors.New("private services cannot enable bearer auth (SSO): NetBird-only access and SSO are mutually exclusive")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) validateHTTPMode() error {
|
func (s *Service) validateHTTPMode() error {
|
||||||
if s.Domain == "" {
|
if s.Domain == "" {
|
||||||
return errors.New("service domain is required")
|
return errors.New("service domain is required")
|
||||||
@@ -854,21 +799,11 @@ func (s *Service) validateHTTPTargets() error {
|
|||||||
for i, target := range s.Targets {
|
for i, target := range s.Targets {
|
||||||
switch target.TargetType {
|
switch target.TargetType {
|
||||||
case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
|
case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
|
||||||
// Host is normally overwritten by replaceHostByLookup with the
|
// host field will be ignored
|
||||||
// resolved peer IP / resource address; operator-supplied values
|
|
||||||
// are honored only when DirectUpstream is set. Validate the
|
|
||||||
// override here so misconfigured hosts fail fast at API time.
|
|
||||||
if err := validateDirectUpstreamHost(i, target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case TargetTypeSubnet:
|
case TargetTypeSubnet:
|
||||||
if target.Host == "" {
|
if target.Host == "" {
|
||||||
return fmt.Errorf("target %d has empty host but target_type is %q", i, target.TargetType)
|
return fmt.Errorf("target %d has empty host but target_type is %q", i, target.TargetType)
|
||||||
}
|
}
|
||||||
case TargetTypeCluster:
|
|
||||||
if err := validateClusterTarget(i, target); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("target %d has invalid target_type %q", i, target.TargetType)
|
return fmt.Errorf("target %d has invalid target_type %q", i, target.TargetType)
|
||||||
}
|
}
|
||||||
@@ -886,67 +821,25 @@ func (s *Service) validateHTTPTargets() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateClusterTarget cluster targets should not have empty hosts and should have direct upstream enabled.
|
|
||||||
func validateClusterTarget(idx int, target *Target) error {
|
|
||||||
host := strings.TrimSpace(target.Host)
|
|
||||||
if host == "" {
|
|
||||||
return fmt.Errorf("target %d: has empty host", idx)
|
|
||||||
}
|
|
||||||
if !target.Options.DirectUpstream {
|
|
||||||
return fmt.Errorf("target %d: %s has direct upstream disabled", idx, target.Host)
|
|
||||||
}
|
|
||||||
return validateDirectUpstreamHost(idx, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateDirectUpstreamHost validates the operator-supplied Host on a
|
|
||||||
// peer/host/domain target when DirectUpstream is set. Empty Host is
|
|
||||||
// allowed — the lookup fills in the default peer IP / resource address.
|
|
||||||
// Without DirectUpstream the Host value is silently overwritten by
|
|
||||||
// replaceHostByLookup, so we don't validate it (preserves the historical
|
|
||||||
// behaviour where APIs accepted any value and dropped it). Non-empty
|
|
||||||
// Host with DirectUpstream must look like a hostname or IP and must
|
|
||||||
// not carry a port (port lives on Target.Port).
|
|
||||||
func validateDirectUpstreamHost(idx int, target *Target) error {
|
|
||||||
if !target.Options.DirectUpstream {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
host := strings.TrimSpace(target.Host)
|
|
||||||
if host == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strings.ContainsAny(host, " \t/") {
|
|
||||||
return fmt.Errorf("target %d: host %q contains invalid characters", idx, host)
|
|
||||||
}
|
|
||||||
if _, _, err := net.SplitHostPort(host); err == nil {
|
|
||||||
return fmt.Errorf("target %d: host %q must not include a port (set target.port instead)", idx, host)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) validateL4Target(target *Target) error {
|
func (s *Service) validateL4Target(target *Target) error {
|
||||||
// L4 services have a single target; per-target disable is meaningless
|
// L4 services have a single target; per-target disable is meaningless
|
||||||
// (use the service-level Enabled flag instead). Force it on so that
|
// (use the service-level Enabled flag instead). Force it on so that
|
||||||
// buildPathMappings always includes the target in the proto.
|
// buildPathMappings always includes the target in the proto.
|
||||||
target.Enabled = true
|
target.Enabled = true
|
||||||
|
|
||||||
|
if target.Port == 0 {
|
||||||
|
return errors.New("target port is required for L4 services")
|
||||||
|
}
|
||||||
if target.TargetId == "" {
|
if target.TargetId == "" {
|
||||||
return errors.New("target_id is required for L4 services")
|
return errors.New("target_id is required for L4 services")
|
||||||
}
|
}
|
||||||
if target.TargetType != TargetTypeCluster && target.Port == 0 {
|
|
||||||
return errors.New("target port is required for L4 services")
|
|
||||||
}
|
|
||||||
switch target.TargetType {
|
switch target.TargetType {
|
||||||
case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
|
case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
|
||||||
if err := validateDirectUpstreamHost(0, target); err != nil {
|
// OK
|
||||||
return err
|
|
||||||
}
|
|
||||||
case TargetTypeSubnet:
|
case TargetTypeSubnet:
|
||||||
if target.Host == "" {
|
if target.Host == "" {
|
||||||
return errors.New("target host is required for subnet targets")
|
return errors.New("target host is required for subnet targets")
|
||||||
}
|
}
|
||||||
case TargetTypeCluster:
|
|
||||||
// target_id carries the cluster address; the proxy resolves
|
|
||||||
// the upstream at request time.
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid target_type %q for L4 service", target.TargetType)
|
return fmt.Errorf("invalid target_type %q for L4 service", target.TargetType)
|
||||||
}
|
}
|
||||||
@@ -1281,11 +1174,6 @@ func (s *Service) Copy() *Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessGroups []string
|
|
||||||
if len(s.AccessGroups) > 0 {
|
|
||||||
accessGroups = append([]string(nil), s.AccessGroups...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
AccountID: s.AccountID,
|
AccountID: s.AccountID,
|
||||||
@@ -1307,8 +1195,6 @@ func (s *Service) Copy() *Service {
|
|||||||
Mode: s.Mode,
|
Mode: s.Mode,
|
||||||
ListenPort: s.ListenPort,
|
ListenPort: s.ListenPort,
|
||||||
PortAutoAssigned: s.PortAutoAssigned,
|
PortAutoAssigned: s.PortAutoAssigned,
|
||||||
Private: s.Private,
|
|
||||||
AccessGroups: accessGroups,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||||
"github.com/netbirdio/netbird/shared/hash/argon2id"
|
"github.com/netbirdio/netbird/shared/hash/argon2id"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1117,191 +1116,3 @@ func TestValidate_HeaderAuths(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "exceeds maximum length")
|
assert.Contains(t, err.Error(), "exceeds maximum length")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidate_HTTPClusterTarget(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Host: "backend.lan",
|
|
||||||
Options: TargetOptions{DirectUpstream: true},
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
require.NoError(t, rp.Validate(), "HTTP cluster target with target_id, host, and direct_upstream must validate")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_HTTPClusterTarget_RequiresTargetId(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Host: "backend.lan",
|
|
||||||
Options: TargetOptions{DirectUpstream: true},
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
assert.ErrorContains(t, rp.Validate(), "empty target_id", "cluster target must reject empty target_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidate_HTTPClusterTarget_RequiresHost pins the new cluster-target
|
|
||||||
// rule that operator-supplied Host is mandatory: cluster targets dial the
|
|
||||||
// upstream via the host network stack (direct_upstream is implied), so an
|
|
||||||
// empty Host leaves the proxy with nothing to dial.
|
|
||||||
func TestValidate_HTTPClusterTarget_RequiresHost(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Options: TargetOptions{DirectUpstream: true},
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
assert.ErrorContains(t, rp.Validate(), "empty host", "cluster target must reject empty host")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidate_HTTPClusterTarget_RequiresDirectUpstream pins the second
|
|
||||||
// half of the cluster-target rule: DirectUpstream must be true so the
|
|
||||||
// stdlib transport branch in MultiTransport is taken. Without it the
|
|
||||||
// embedded NetBird client would try to dial the cluster address through
|
|
||||||
// the WG tunnel, which is the wrong network for a cluster upstream.
|
|
||||||
func TestValidate_HTTPClusterTarget_RequiresDirectUpstream(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Host: "backend.lan",
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
assert.ErrorContains(t, rp.Validate(), "direct upstream disabled", "cluster target must reject direct_upstream=false")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_L4ClusterTarget(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Mode = ModeTCP
|
|
||||||
rp.ListenPort = 9000
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "tcp",
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
require.NoError(t, rp.Validate(), "L4 cluster target must validate without an explicit port")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestService_Copy_RoundtripsPrivate(t *testing.T) {
|
|
||||||
svc := validProxy()
|
|
||||||
svc.Private = true
|
|
||||||
svc.AccessGroups = []string{"grp-admins", "grp-ops"}
|
|
||||||
cp := svc.Copy()
|
|
||||||
require.NotNil(t, cp)
|
|
||||||
assert.True(t, cp.Private)
|
|
||||||
assert.Equal(t, []string{"grp-admins", "grp-ops"}, cp.AccessGroups)
|
|
||||||
|
|
||||||
cp.Private = false
|
|
||||||
assert.True(t, svc.Private)
|
|
||||||
|
|
||||||
cp.AccessGroups[0] = "grp-other"
|
|
||||||
assert.Equal(t, []string{"grp-admins", "grp-ops"}, svc.AccessGroups)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestService_APIRoundtrip_Private(t *testing.T) {
|
|
||||||
enabled := true
|
|
||||||
private := true
|
|
||||||
accessGroups := []string{"grp-admins"}
|
|
||||||
targets := []api.ServiceTarget{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: api.ServiceTargetTargetType("cluster"),
|
|
||||||
Protocol: "http",
|
|
||||||
Port: 80,
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
req := &api.ServiceRequest{
|
|
||||||
Name: "svc-private",
|
|
||||||
Domain: "myapp.eu.proxy.netbird.io",
|
|
||||||
Enabled: enabled,
|
|
||||||
Private: &private,
|
|
||||||
AccessGroups: &accessGroups,
|
|
||||||
Targets: &targets,
|
|
||||||
}
|
|
||||||
|
|
||||||
svc := &Service{}
|
|
||||||
require.NoError(t, svc.FromAPIRequest(req, "acc-1"))
|
|
||||||
assert.True(t, svc.Private)
|
|
||||||
assert.Equal(t, []string{"grp-admins"}, svc.AccessGroups)
|
|
||||||
|
|
||||||
resp := svc.ToAPIResponse()
|
|
||||||
require.NotNil(t, resp.Private)
|
|
||||||
assert.True(t, *resp.Private)
|
|
||||||
require.NotNil(t, resp.AccessGroups)
|
|
||||||
assert.Equal(t, []string{"grp-admins"}, *resp.AccessGroups)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_Private_RequiresAccessGroups(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Private = true
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Host: "backend.lan",
|
|
||||||
Options: TargetOptions{DirectUpstream: true},
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
assert.ErrorContains(t, rp.Validate(), "access group")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_Private_RejectsBearerAuth(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Private = true
|
|
||||||
rp.AccessGroups = []string{"grp-admins"}
|
|
||||||
rp.Auth.BearerAuth = &BearerAuthConfig{
|
|
||||||
Enabled: true,
|
|
||||||
DistributionGroups: []string{"grp-sso"},
|
|
||||||
}
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Host: "backend.lan",
|
|
||||||
Options: TargetOptions{DirectUpstream: true},
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
assert.ErrorContains(t, rp.Validate(), "mutually exclusive")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_Private_AcceptsNonClusterTargets(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Private = true
|
|
||||||
rp.AccessGroups = []string{"grp-admins"}
|
|
||||||
require.NoError(t, rp.Validate())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_Private_AcceptsClusterTargetWithAccessGroups(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Private = true
|
|
||||||
rp.AccessGroups = []string{"grp-admins"}
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "http",
|
|
||||||
Host: "backend.lan",
|
|
||||||
Options: TargetOptions{DirectUpstream: true},
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
require.NoError(t, rp.Validate())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidate_Private_RejectsNonHTTPMode(t *testing.T) {
|
|
||||||
rp := validProxy()
|
|
||||||
rp.Private = true
|
|
||||||
rp.AccessGroups = []string{"grp-admins"}
|
|
||||||
rp.Mode = ModeTCP
|
|
||||||
rp.Targets = []*Target{{
|
|
||||||
TargetId: "eu.proxy.netbird.io",
|
|
||||||
TargetType: TargetTypeCluster,
|
|
||||||
Protocol: "tcp",
|
|
||||||
Enabled: true,
|
|
||||||
}}
|
|
||||||
assert.ErrorContains(t, rp.Validate(), "HTTP")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,20 +20,6 @@ type KeyPair struct {
|
|||||||
type Claims struct {
|
type Claims struct {
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
Method auth.Method `json:"method"`
|
Method auth.Method `json:"method"`
|
||||||
// Email is the calling user's email address. Carried so the
|
|
||||||
// proxy can stamp identity on upstream requests (e.g.
|
|
||||||
// x-litellm-end-user-id) without an extra management
|
|
||||||
// round-trip on every cookie-bearing request.
|
|
||||||
Email string `json:"email,omitempty"`
|
|
||||||
// Groups carries the user's group IDs so the proxy can stamp them
|
|
||||||
// onto upstream requests (X-NetBird-Groups) from the cookie path
|
|
||||||
// without an extra management round-trip.
|
|
||||||
Groups []string `json:"groups,omitempty"`
|
|
||||||
// GroupNames carries the human-readable display names for the ids
|
|
||||||
// in Groups, ordered identically (positional pairing). Slice may be
|
|
||||||
// shorter than Groups for tokens minted before names were
|
|
||||||
// resolvable; the consumer falls back to ids for missing positions.
|
|
||||||
GroupNames []string `json:"group_names,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateKeyPair() (*KeyPair, error) {
|
func GenerateKeyPair() (*KeyPair, error) {
|
||||||
@@ -48,13 +34,7 @@ func GenerateKeyPair() (*KeyPair, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignToken mints a session JWT for the given user and domain. email,
|
func SignToken(privKeyB64, userID, domain string, method auth.Method, expiration time.Duration) (string, error) {
|
||||||
// groups, and groupNames, when non-empty, are embedded so the proxy can
|
|
||||||
// authorise and stamp identity for policy-aware middlewares without a
|
|
||||||
// management round-trip on every cookie-bearing request. groupNames
|
|
||||||
// pairs positionally with groups; pass nil when names couldn't be
|
|
||||||
// resolved.
|
|
||||||
func SignToken(privKeyB64, userID, email, domain string, method auth.Method, groups, groupNames []string, expiration time.Duration) (string, error) {
|
|
||||||
privKeyBytes, err := base64.StdEncoding.DecodeString(privKeyB64)
|
privKeyBytes, err := base64.StdEncoding.DecodeString(privKeyB64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("decode private key: %w", err)
|
return "", fmt.Errorf("decode private key: %w", err)
|
||||||
@@ -76,10 +56,7 @@ func SignToken(privKeyB64, userID, email, domain string, method auth.Method, gro
|
|||||||
IssuedAt: jwt.NewNumericDate(now),
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
NotBefore: jwt.NewNumericDate(now),
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
},
|
},
|
||||||
Method: method,
|
Method: method,
|
||||||
Email: email,
|
|
||||||
Groups: append([]string(nil), groups...),
|
|
||||||
GroupNames: append([]string(nil), groupNames...),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewManager(store store.Store, accountManager account.Manager, permissionsMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllZones(ctx context.Context, accountID, userID string) ([]*zones.Zone, error) {
|
func (m *managerImpl) GetAllZones(ctx context.Context, accountID, userID string) ([]*zones.Zone, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func (m *managerImpl) GetAllZones(ctx context.Context, accountID, userID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetZone(ctx context.Context, accountID, userID, zoneID string) (*zones.Zone, error) {
|
func (m *managerImpl) GetZone(ctx context.Context, accountID, userID, zoneID string) (*zones.Zone, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ func (m *managerImpl) GetZone(ctx context.Context, accountID, userID, zoneID str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) CreateZone(ctx context.Context, accountID, userID string, zone *zones.Zone) (*zones.Zone, error) {
|
func (m *managerImpl) CreateZone(ctx context.Context, accountID, userID string, zone *zones.Zone) (*zones.Zone, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Create)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func (m *managerImpl) CreateZone(ctx context.Context, accountID, userID string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) UpdateZone(ctx context.Context, accountID, userID string, updatedZone *zones.Zone) (*zones.Zone, error) {
|
func (m *managerImpl) UpdateZone(ctx context.Context, accountID, userID string, updatedZone *zones.Zone) (*zones.Zone, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ func (m *managerImpl) UpdateZone(ctx context.Context, accountID, userID string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) DeleteZone(ctx context.Context, accountID, userID, zoneID string) error {
|
func (m *managerImpl) DeleteZone(ctx context.Context, accountID, userID, zoneID string) error {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Delete)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func TestManagerImpl_GetAllZones(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
|
result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -95,7 +95,7 @@ func TestManagerImpl_GetAllZones(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
|
result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -112,7 +112,7 @@ func TestManagerImpl_GetAllZones(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(false, ctx, status.Errorf(status.Internal, "permission check failed"))
|
Return(false, status.Errorf(status.Internal, "permission check failed"))
|
||||||
|
|
||||||
result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
|
result, err := manager.GetAllZones(ctx, testAccountID, testUserID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -134,7 +134,7 @@ func TestManagerImpl_GetZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.GetZone(ctx, testAccountID, testUserID, zone.ID)
|
result, err := manager.GetZone(ctx, testAccountID, testUserID, zone.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -150,7 +150,7 @@ func TestManagerImpl_GetZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.GetZone(ctx, testAccountID, testUserID, testZoneID)
|
result, err := manager.GetZone(ctx, testAccountID, testUserID, testZoneID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -179,7 +179,7 @@ func TestManagerImpl_CreateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
assert.Equal(t, testUserID, initiatorID)
|
assert.Equal(t, testUserID, initiatorID)
|
||||||
@@ -212,7 +212,7 @@ func TestManagerImpl_CreateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -235,7 +235,7 @@ func TestManagerImpl_CreateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -261,7 +261,7 @@ func TestManagerImpl_CreateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -293,7 +293,7 @@ func TestManagerImpl_CreateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -319,7 +319,7 @@ func TestManagerImpl_CreateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
result, err := manager.CreateZone(ctx, testAccountID, testUserID, inputZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -354,7 +354,7 @@ func TestManagerImpl_UpdateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
storeEventCalled := false
|
storeEventCalled := false
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
@@ -394,7 +394,7 @@ func TestManagerImpl_UpdateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
|
result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -418,7 +418,7 @@ func TestManagerImpl_UpdateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
|
result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -441,7 +441,7 @@ func TestManagerImpl_UpdateZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
|
result, err := manager.UpdateZone(ctx, testAccountID, testUserID, updatedZone)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -471,7 +471,7 @@ func TestManagerImpl_DeleteZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
storeEventCallCount := 0
|
storeEventCallCount := 0
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
@@ -503,7 +503,7 @@ func TestManagerImpl_DeleteZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
storeEventCalled := false
|
storeEventCalled := false
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
@@ -529,7 +529,7 @@ func TestManagerImpl_DeleteZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
err := manager.DeleteZone(ctx, testAccountID, testUserID, testZoneID)
|
err := manager.DeleteZone(ctx, testAccountID, testUserID, testZoneID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -545,7 +545,7 @@ func TestManagerImpl_DeleteZone(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
err := manager.DeleteZone(ctx, testAccountID, testUserID, "non-existent-zone")
|
err := manager.DeleteZone(ctx, testAccountID, testUserID, "non-existent-zone")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewManager(store store.Store, accountManager account.Manager, permissionsMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllRecords(ctx context.Context, accountID, userID, zoneID string) ([]*records.Record, error) {
|
func (m *managerImpl) GetAllRecords(ctx context.Context, accountID, userID, zoneID string) ([]*records.Record, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func (m *managerImpl) GetAllRecords(ctx context.Context, accountID, userID, zone
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetRecord(ctx context.Context, accountID, userID, zoneID, recordID string) (*records.Record, error) {
|
func (m *managerImpl) GetRecord(ctx context.Context, accountID, userID, zoneID, recordID string) (*records.Record, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ func (m *managerImpl) GetRecord(ctx context.Context, accountID, userID, zoneID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) CreateRecord(ctx context.Context, accountID, userID, zoneID string, record *records.Record) (*records.Record, error) {
|
func (m *managerImpl) CreateRecord(ctx context.Context, accountID, userID, zoneID string, record *records.Record) (*records.Record, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Create)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func (m *managerImpl) CreateRecord(ctx context.Context, accountID, userID, zoneI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) UpdateRecord(ctx context.Context, accountID, userID, zoneID string, updatedRecord *records.Record) (*records.Record, error) {
|
func (m *managerImpl) UpdateRecord(ctx context.Context, accountID, userID, zoneID string, updatedRecord *records.Record) (*records.Record, error) {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ func (m *managerImpl) UpdateRecord(ctx context.Context, accountID, userID, zoneI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) DeleteRecord(ctx context.Context, accountID, userID, zoneID, recordID string) error {
|
func (m *managerImpl) DeleteRecord(ctx context.Context, accountID, userID, zoneID, recordID string) error {
|
||||||
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Delete)
|
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func TestManagerImpl_GetAllRecords(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.GetAllRecords(ctx, testAccountID, testUserID, zone.ID)
|
result, err := manager.GetAllRecords(ctx, testAccountID, testUserID, zone.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -96,7 +96,7 @@ func TestManagerImpl_GetAllRecords(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.GetAllRecords(ctx, testAccountID, testUserID, zone.ID)
|
result, err := manager.GetAllRecords(ctx, testAccountID, testUserID, zone.ID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -113,7 +113,7 @@ func TestManagerImpl_GetAllRecords(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(false, ctx, status.Errorf(status.Internal, "permission check failed"))
|
Return(false, status.Errorf(status.Internal, "permission check failed"))
|
||||||
|
|
||||||
result, err := manager.GetAllRecords(ctx, testAccountID, testUserID, zone.ID)
|
result, err := manager.GetAllRecords(ctx, testAccountID, testUserID, zone.ID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -135,7 +135,7 @@ func TestManagerImpl_GetRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.GetRecord(ctx, testAccountID, testUserID, zone.ID, record.ID)
|
result, err := manager.GetRecord(ctx, testAccountID, testUserID, zone.ID, record.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -153,7 +153,7 @@ func TestManagerImpl_GetRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Read).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.GetRecord(ctx, testAccountID, testUserID, zone.ID, testRecordID)
|
result, err := manager.GetRecord(ctx, testAccountID, testUserID, zone.ID, testRecordID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -181,7 +181,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
assert.Equal(t, testUserID, initiatorID)
|
assert.Equal(t, testUserID, initiatorID)
|
||||||
@@ -215,7 +215,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
assert.Equal(t, testUserID, initiatorID)
|
assert.Equal(t, testUserID, initiatorID)
|
||||||
@@ -244,7 +244,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
assert.Equal(t, testUserID, initiatorID)
|
assert.Equal(t, testUserID, initiatorID)
|
||||||
@@ -273,7 +273,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -297,7 +297,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -323,7 +323,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -349,7 +349,7 @@ func TestManagerImpl_CreateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Create).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
result, err := manager.CreateRecord(ctx, testAccountID, testUserID, zone.ID, inputRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -380,7 +380,7 @@ func TestManagerImpl_UpdateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
storeEventCalled := false
|
storeEventCalled := false
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
@@ -418,7 +418,7 @@ func TestManagerImpl_UpdateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
// Event should be stored
|
// Event should be stored
|
||||||
@@ -445,7 +445,7 @@ func TestManagerImpl_UpdateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
result, err := manager.UpdateRecord(ctx, testAccountID, testUserID, zone.ID, updatedRecord)
|
result, err := manager.UpdateRecord(ctx, testAccountID, testUserID, zone.ID, updatedRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -470,7 +470,7 @@ func TestManagerImpl_UpdateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.UpdateRecord(ctx, testAccountID, testUserID, zone.ID, updatedRecord)
|
result, err := manager.UpdateRecord(ctx, testAccountID, testUserID, zone.ID, updatedRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -500,7 +500,7 @@ func TestManagerImpl_UpdateRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Update).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
result, err := manager.UpdateRecord(ctx, testAccountID, testUserID, zone.ID, updatedRecord)
|
result, err := manager.UpdateRecord(ctx, testAccountID, testUserID, zone.ID, updatedRecord)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -523,7 +523,7 @@ func TestManagerImpl_DeleteRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
storeEventCalled := false
|
storeEventCalled := false
|
||||||
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
mockAccountManager.StoreEventFunc = func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) {
|
||||||
@@ -549,7 +549,7 @@ func TestManagerImpl_DeleteRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(false, ctx, nil)
|
Return(false, nil)
|
||||||
|
|
||||||
err := manager.DeleteRecord(ctx, testAccountID, testUserID, zone.ID, testRecordID)
|
err := manager.DeleteRecord(ctx, testAccountID, testUserID, zone.ID, testRecordID)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -565,7 +565,7 @@ func TestManagerImpl_DeleteRecord(t *testing.T) {
|
|||||||
|
|
||||||
mockPermissionsManager.EXPECT().
|
mockPermissionsManager.EXPECT().
|
||||||
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
ValidateUserPermissions(ctx, testAccountID, testUserID, modules.Dns, operations.Delete).
|
||||||
Return(true, ctx, nil)
|
Return(true, nil)
|
||||||
|
|
||||||
err := manager.DeleteRecord(ctx, testAccountID, testUserID, zone.ID, "non-existent-record")
|
err := manager.DeleteRecord(ctx, testAccountID, testUserID, zone.ID, "non-existent-record")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
|
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
|
||||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
||||||
"github.com/rs/cors"
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -21,6 +19,7 @@ import (
|
|||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
cachestore "github.com/eko/gocache/lib/v4/store"
|
cachestore "github.com/eko/gocache/lib/v4/store"
|
||||||
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
"github.com/netbirdio/netbird/formatter/hook"
|
"github.com/netbirdio/netbird/formatter/hook"
|
||||||
@@ -28,20 +27,16 @@ import (
|
|||||||
accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager"
|
accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager"
|
||||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
|
|
||||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
nbContext "github.com/netbirdio/netbird/management/server/context"
|
nbContext "github.com/netbirdio/netbird/management/server/context"
|
||||||
nbhttp "github.com/netbirdio/netbird/management/server/http"
|
nbhttp "github.com/netbirdio/netbird/management/server/http"
|
||||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/util/crypt"
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiPrefix = "/api"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
kaep = keepalive.EnforcementPolicy{
|
kaep = keepalive.EnforcementPolicy{
|
||||||
MinTime: 15 * time.Second,
|
MinTime: 15 * time.Second,
|
||||||
@@ -99,17 +94,12 @@ func (s *BaseServer) Store() store.Store {
|
|||||||
|
|
||||||
func (s *BaseServer) EventStore() activity.Store {
|
func (s *BaseServer) EventStore() activity.Store {
|
||||||
return Create(s, func() activity.Store {
|
return Create(s, func() activity.Store {
|
||||||
var err error
|
integrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())
|
||||||
key := s.Config.DataStoreEncryptionKey
|
if err != nil {
|
||||||
if key == "" {
|
log.Fatalf("failed to initialize integration metrics: %v", err)
|
||||||
log.Debugf("generate new activity store encryption key")
|
|
||||||
key, err = crypt.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to generate event store encryption key: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventStore, err := activitystore.NewSqlStore(context.Background(), s.Config.Datadir, key)
|
eventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to initialize event store: %v", err)
|
log.Fatalf("failed to initialize event store: %v", err)
|
||||||
}
|
}
|
||||||
@@ -120,7 +110,7 @@ func (s *BaseServer) EventStore() activity.Store {
|
|||||||
|
|
||||||
func (s *BaseServer) APIHandler() http.Handler {
|
func (s *BaseServer) APIHandler() http.Handler {
|
||||||
return Create(s, func() http.Handler {
|
return Create(s, func() http.Handler {
|
||||||
httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.Router(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.PermissionsManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies, s.RateLimiter(), s.IsValidChildAccount)
|
httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies, s.RateLimiter())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create API handler: %v", err)
|
log.Fatalf("failed to create API handler: %v", err)
|
||||||
}
|
}
|
||||||
@@ -128,22 +118,6 @@ func (s *BaseServer) APIHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDPHandler returns the HTTP handler for the embedded IdP (Dex), or nil if
|
|
||||||
// the deployment isn't using the embedded variant.
|
|
||||||
func (s *BaseServer) IDPHandler() http.Handler {
|
|
||||||
embeddedIdP, ok := s.IdpManager().(*idp.EmbeddedIdPManager)
|
|
||||||
if !ok || embeddedIdP == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cors.AllowAll().Handler(embeddedIdP.Handler())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaseServer) Router() *mux.Router {
|
|
||||||
return Create(s, func() *mux.Router {
|
|
||||||
return mux.NewRouter().PathPrefix(apiPrefix).Subrouter()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaseServer) RateLimiter() *middleware.APIRateLimiter {
|
func (s *BaseServer) RateLimiter() *middleware.APIRateLimiter {
|
||||||
return Create(s, func() *middleware.APIRateLimiter {
|
return Create(s, func() *middleware.APIRateLimiter {
|
||||||
cfg, enabled := middleware.RateLimiterConfigFromEnv()
|
cfg, enabled := middleware.RateLimiterConfigFromEnv()
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/auth"
|
"github.com/netbirdio/netbird/management/server/auth"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
||||||
"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/integrations/port_forwarding"
|
||||||
"github.com/netbirdio/netbird/management/server/job"
|
"github.com/netbirdio/netbird/management/server/job"
|
||||||
nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
|
nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
|
||||||
@@ -39,7 +38,7 @@ func (s *BaseServer) JobManager() *job.Manager {
|
|||||||
|
|
||||||
func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
|
func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
|
||||||
return Create(s, func() integrated_validator.IntegratedValidator {
|
return Create(s, func() integrated_validator.IntegratedValidator {
|
||||||
integratedPeerValidator, err := validator.NewIntegratedValidator(
|
integratedPeerValidator, err := integrations.NewIntegratedValidator(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
s.PeersManager(),
|
s.PeersManager(),
|
||||||
s.SettingsManager(),
|
s.SettingsManager(),
|
||||||
|
|||||||
@@ -57,7 +57,13 @@ func (s *BaseServer) GeoLocationManager() geolocation.Geolocation {
|
|||||||
|
|
||||||
func (s *BaseServer) PermissionsManager() permissions.Manager {
|
func (s *BaseServer) PermissionsManager() permissions.Manager {
|
||||||
return Create(s, func() permissions.Manager {
|
return Create(s, func() permissions.Manager {
|
||||||
return permissions.NewManager(s.Store())
|
manager := integrations.InitPermissionsManager(s.Store(), s.Metrics().GetMeter())
|
||||||
|
|
||||||
|
s.AfterInit(func(s *BaseServer) {
|
||||||
|
manager.SetAccountManager(s.AccountManager())
|
||||||
|
})
|
||||||
|
|
||||||
|
return manager
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +153,7 @@ func (s *BaseServer) IdpManager() idp.Manager {
|
|||||||
return idpManager
|
return idpManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -228,7 +235,3 @@ func (s *BaseServer) ReverseProxyDomainManager() *manager.Manager {
|
|||||||
return &m
|
return &m
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaseServer) IsValidChildAccount(_ context.Context, _, _, _ string) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ const (
|
|||||||
ManagementLegacyPort = 33073
|
ManagementLegacyPort = 33073
|
||||||
// DefaultSelfHostedDomain is the default domain used for self-hosted fresh installs.
|
// DefaultSelfHostedDomain is the default domain used for self-hosted fresh installs.
|
||||||
DefaultSelfHostedDomain = "netbird.selfhosted"
|
DefaultSelfHostedDomain = "netbird.selfhosted"
|
||||||
|
|
||||||
ContainerKeyBaseServer = "baseServer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server interface {
|
type Server interface {
|
||||||
@@ -93,7 +91,7 @@ type Config struct {
|
|||||||
|
|
||||||
// NewServer initializes and configures a new Server instance
|
// NewServer initializes and configures a new Server instance
|
||||||
func NewServer(cfg *Config) *BaseServer {
|
func NewServer(cfg *Config) *BaseServer {
|
||||||
s := &BaseServer{
|
return &BaseServer{
|
||||||
Config: cfg.NbConfig,
|
Config: cfg.NbConfig,
|
||||||
container: make(map[string]any),
|
container: make(map[string]any),
|
||||||
dnsDomain: cfg.DNSDomain,
|
dnsDomain: cfg.DNSDomain,
|
||||||
@@ -106,9 +104,6 @@ func NewServer(cfg *Config) *BaseServer {
|
|||||||
mgmtMetricsPort: cfg.MgmtMetricsPort,
|
mgmtMetricsPort: cfg.MgmtMetricsPort,
|
||||||
autoResolveDomains: cfg.AutoResolveDomains,
|
autoResolveDomains: cfg.AutoResolveDomains,
|
||||||
}
|
}
|
||||||
s.container[ContainerKeyBaseServer] = s
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaseServer) AfterInit(fn func(s *BaseServer)) {
|
func (s *BaseServer) AfterInit(fn func(s *BaseServer)) {
|
||||||
@@ -193,7 +188,7 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
|||||||
log.WithContext(srvCtx).Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String())
|
log.WithContext(srvCtx).Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
rootHandler := s.handlerFunc(srvCtx, s.GRPCServer(), s.APIHandler(), s.IDPHandler(), s.Metrics().GetMeter())
|
rootHandler := s.handlerFunc(srvCtx, s.GRPCServer(), s.APIHandler(), s.Metrics().GetMeter())
|
||||||
switch {
|
switch {
|
||||||
case s.certManager != nil:
|
case s.certManager != nil:
|
||||||
// a call to certManager.Listener() always creates a new listener so we do it once
|
// a call to certManager.Listener() always creates a new listener so we do it once
|
||||||
@@ -304,7 +299,7 @@ func (s *BaseServer) SetHandlerFunc(handler http.Handler) {
|
|||||||
log.Tracef("custom handler set successfully")
|
log.Tracef("custom handler set successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, idpHandler http.Handler, meter metric.Meter) http.Handler {
|
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler {
|
||||||
// Check if a custom handler was set (for multiplexing additional services)
|
// Check if a custom handler was set (for multiplexing additional services)
|
||||||
if customHandler, ok := s.GetContainer("customHandler"); ok {
|
if customHandler, ok := s.GetContainer("customHandler"); ok {
|
||||||
if handler, ok := customHandler.(http.Handler); ok {
|
if handler, ok := customHandler.(http.Handler); ok {
|
||||||
@@ -323,8 +318,6 @@ func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, ht
|
|||||||
gRPCHandler.ServeHTTP(writer, request)
|
gRPCHandler.ServeHTTP(writer, request)
|
||||||
case request.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:
|
case request.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:
|
||||||
wsProxy.Handler().ServeHTTP(writer, request)
|
wsProxy.Handler().ServeHTTP(writer, request)
|
||||||
case idpHandler != nil && strings.HasPrefix(request.URL.Path, "/oauth2"):
|
|
||||||
idpHandler.ServeHTTP(writer, request)
|
|
||||||
default:
|
default:
|
||||||
httpHandler.ServeHTTP(writer, request)
|
httpHandler.ServeHTTP(writer, request)
|
||||||
}
|
}
|
||||||
|
|||||||
815
management/internals/shared/grpc/components_encoder.go
Normal file
815
management/internals/shared/grpc/components_encoder.go
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
|
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wgKeyRawLen is the raw byte length of a WireGuard public key.
|
||||||
|
const wgKeyRawLen = 32
|
||||||
|
|
||||||
|
// ComponentsEnvelopeInput bundles the data the component-format encoder needs.
|
||||||
|
// In Step 2 the envelope is fully self-contained — every field needed by the
|
||||||
|
// client's local Calculate() comes from the components struct itself. The
|
||||||
|
// only externally-supplied data is the receiving peer's PeerConfig (which is
|
||||||
|
// computed alongside the components in the network_map controller and reused
|
||||||
|
// from the legacy proto path) and the dns_domain string.
|
||||||
|
type ComponentsEnvelopeInput struct {
|
||||||
|
Components *types.NetworkMapComponents
|
||||||
|
PeerConfig *proto.PeerConfig
|
||||||
|
DNSDomain string
|
||||||
|
DNSForwarderPort int64
|
||||||
|
// UserIDClaim is the OIDC claim name the client should embed in
|
||||||
|
// SshAuth.UserIDClaim when reconstructing the NetworkMap. Empty value
|
||||||
|
// is OK — client treats empty as "no SshAuth to build".
|
||||||
|
UserIDClaim string
|
||||||
|
// ProxyPatch carries pre-expanded NetworkMap fragments injected by
|
||||||
|
// external controllers (BYOP/port-forwarding). Nil when no proxy data
|
||||||
|
// is present; encoder skips the field in that case.
|
||||||
|
ProxyPatch *proto.ProxyPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeNetworkMapEnvelope converts NetworkMapComponents into the component
|
||||||
|
// wire envelope. The encoder is intentionally non-deterministic: it iterates
|
||||||
|
// Go maps in their native (random) order. Indexes inside the envelope
|
||||||
|
// (peer_indexes, source_group_ids, agent_version_idx, router_peer_indexes)
|
||||||
|
// are self-consistent within a single encode, so the decoder reconstructs
|
||||||
|
// the same typed objects regardless of emit order. Tests that need to
|
||||||
|
// compare envelopes do so semantically via proto round-trip + canonicalize,
|
||||||
|
// not byte-equal.
|
||||||
|
//
|
||||||
|
// Callers must NOT concatenate or merge envelopes from different encodes —
|
||||||
|
// index spaces are local to a single envelope. Delta sync (Step 3+) will
|
||||||
|
// use a different shape for the same reason.
|
||||||
|
func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvelope {
|
||||||
|
c := in.Components
|
||||||
|
|
||||||
|
// Graceful degrade when components is nil — matches the legacy path's
|
||||||
|
// account_components.go:43 behaviour for missing/unvalidated peers
|
||||||
|
// (return a NetworkMap with only Network populated). The receiver gets
|
||||||
|
// an envelope it can decode without crashing; AccountSettings stays
|
||||||
|
// non-nil so client-side dereferences are safe.
|
||||||
|
if c == nil {
|
||||||
|
// Match legacy missing-peer minimum: a NetworkMap with only Network
|
||||||
|
// populated (account_components.go:43). The receiver gets enough to
|
||||||
|
// bootstrap (Network identifier, dns_domain, account_settings) and
|
||||||
|
// nothing else.
|
||||||
|
return &proto.NetworkMapEnvelope{
|
||||||
|
Payload: &proto.NetworkMapEnvelope_Full{
|
||||||
|
Full: &proto.NetworkMapComponentsFull{
|
||||||
|
PeerConfig: in.PeerConfig,
|
||||||
|
DnsDomain: in.DNSDomain,
|
||||||
|
DnsForwarderPort: in.DNSForwarderPort,
|
||||||
|
UserIdClaim: in.UserIDClaim,
|
||||||
|
AccountSettings: &proto.AccountSettingsCompact{},
|
||||||
|
ProxyPatch: in.ProxyPatch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: build dedup tables. Every routing peer (in c.RouterPeers) and
|
||||||
|
// every regular peer (in c.Peers) must be indexed before any encoder
|
||||||
|
// looks up indexes via e.peerOrder — otherwise routes / routers_map for
|
||||||
|
// peers that exist only in c.RouterPeers would silently lose their
|
||||||
|
// peer_index reference.
|
||||||
|
enc := newComponentEncoder(c)
|
||||||
|
enc.indexAllPeers()
|
||||||
|
routerIdxs := enc.indexRouterPeers(c.RouterPeers)
|
||||||
|
|
||||||
|
// Phase 2: gather every policy that any consumer references (peer-pair
|
||||||
|
// policies + resource-only policies) so encodeResourcePoliciesMap can
|
||||||
|
// translate every *Policy pointer to a wire index.
|
||||||
|
allPolicies := unionPolicies(c.Policies, c.ResourcePoliciesMap)
|
||||||
|
policies, policyToIdxs := enc.encodePolicies(allPolicies)
|
||||||
|
|
||||||
|
// Phase 3: emit. Order of struct field expressions no longer matters:
|
||||||
|
// every encoder either reads from the dedup tables or works on
|
||||||
|
// independent input.
|
||||||
|
full := &proto.NetworkMapComponentsFull{
|
||||||
|
Serial: networkSerial(c.Network),
|
||||||
|
PeerConfig: in.PeerConfig,
|
||||||
|
Network: toAccountNetwork(c.Network),
|
||||||
|
AccountSettings: toAccountSettingsCompact(c.AccountSettings),
|
||||||
|
DnsForwarderPort: in.DNSForwarderPort,
|
||||||
|
UserIdClaim: in.UserIDClaim,
|
||||||
|
ProxyPatch: in.ProxyPatch,
|
||||||
|
DnsSettings: enc.encodeDNSSettings(c.DNSSettings),
|
||||||
|
DnsDomain: in.DNSDomain,
|
||||||
|
CustomZoneDomain: c.CustomZoneDomain,
|
||||||
|
AgentVersions: enc.agentVersions,
|
||||||
|
Peers: enc.peers,
|
||||||
|
RouterPeerIndexes: routerIdxs,
|
||||||
|
Policies: policies,
|
||||||
|
Groups: enc.encodeGroups(),
|
||||||
|
Routes: enc.encodeRoutes(c.Routes),
|
||||||
|
NameserverGroups: enc.encodeNameServerGroups(c.NameServerGroups),
|
||||||
|
AllDnsRecords: encodeSimpleRecords(c.AllDNSRecords),
|
||||||
|
AccountZones: encodeCustomZones(c.AccountZones),
|
||||||
|
NetworkResources: enc.encodeNetworkResources(c.NetworkResources),
|
||||||
|
RoutersMap: enc.encodeRoutersMap(c.RoutersMap),
|
||||||
|
ResourcePoliciesMap: enc.encodeResourcePoliciesMap(c.ResourcePoliciesMap, policyToIdxs),
|
||||||
|
GroupIdToUserIds: enc.encodeGroupIDToUserIDs(c.GroupIDToUserIDs),
|
||||||
|
AllowedUserIds: stringSetToSlice(c.AllowedUserIDs),
|
||||||
|
PostureFailedPeers: enc.encodePostureFailedPeers(c.PostureFailedPeers),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.NetworkMapEnvelope{
|
||||||
|
Payload: &proto.NetworkMapEnvelope_Full{Full: full},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkSerial returns c.Network.CurrentSerial() with a nil guard. The
|
||||||
|
// production path always populates c.Network (account_components.go:86), but
|
||||||
|
// the encoder is exported and a hand-built components struct may omit it.
|
||||||
|
func networkSerial(n *types.Network) uint64 {
|
||||||
|
if n == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return n.CurrentSerial()
|
||||||
|
}
|
||||||
|
|
||||||
|
type componentEncoder struct {
|
||||||
|
components *types.NetworkMapComponents
|
||||||
|
|
||||||
|
peerOrder map[string]uint32
|
||||||
|
peers []*proto.PeerCompact
|
||||||
|
|
||||||
|
agentVersionOrder map[string]uint32
|
||||||
|
agentVersions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newComponentEncoder(c *types.NetworkMapComponents) *componentEncoder {
|
||||||
|
return &componentEncoder{
|
||||||
|
components: c,
|
||||||
|
peerOrder: make(map[string]uint32, len(c.Peers)),
|
||||||
|
peers: make([]*proto.PeerCompact, 0, len(c.Peers)),
|
||||||
|
agentVersionOrder: make(map[string]uint32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) indexAllPeers() {
|
||||||
|
for _, p := range e.components.Peers {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.appendPeer(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) appendPeer(p *nbpeer.Peer) uint32 {
|
||||||
|
if idx, ok := e.peerOrder[p.ID]; ok {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
idx := uint32(len(e.peers))
|
||||||
|
e.peerOrder[p.ID] = idx
|
||||||
|
e.peers = append(e.peers, toPeerCompact(p, e.agentVersionIndex(p.Meta.WtVersion)))
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) agentVersionIndex(v string) uint32 {
|
||||||
|
if idx, ok := e.agentVersionOrder[v]; ok {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
// Lazy-initialise the table with "" at index 0 so the empty string
|
||||||
|
// stays interchangeable with proto3's default uint32=0 — peers without
|
||||||
|
// a WtVersion don't force the table to materialise.
|
||||||
|
if v == "" {
|
||||||
|
idx := uint32(len(e.agentVersions))
|
||||||
|
if idx == 0 {
|
||||||
|
e.agentVersions = append(e.agentVersions, "")
|
||||||
|
}
|
||||||
|
e.agentVersionOrder[""] = idx
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
if len(e.agentVersions) == 0 {
|
||||||
|
e.agentVersions = append(e.agentVersions, "")
|
||||||
|
e.agentVersionOrder[""] = 0
|
||||||
|
}
|
||||||
|
idx := uint32(len(e.agentVersions))
|
||||||
|
e.agentVersionOrder[v] = idx
|
||||||
|
e.agentVersions = append(e.agentVersions, v)
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexRouterPeers ensures every router peer is in the peer dedup table
|
||||||
|
// (c.RouterPeers may contain peers not in c.Peers when validation rules drop
|
||||||
|
// them) and returns their wire indexes for the RouterPeerIndexes field. Must
|
||||||
|
// run before any encoder that resolves peer ids via e.peerOrder.
|
||||||
|
func (e *componentEncoder) indexRouterPeers(routers map[string]*nbpeer.Peer) []uint32 {
|
||||||
|
if len(routers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(routers))
|
||||||
|
for _, p := range routers {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, e.appendPeer(p))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeGroups() []*proto.GroupCompact {
|
||||||
|
if len(e.components.Groups) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*proto.GroupCompact, 0, len(e.components.Groups))
|
||||||
|
for _, g := range e.components.Groups {
|
||||||
|
if !g.HasSeqID() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerIdxs := make([]uint32, 0, len(g.Peers))
|
||||||
|
for _, peerID := range g.Peers {
|
||||||
|
if idx, ok := e.peerOrder[peerID]; ok {
|
||||||
|
peerIdxs = append(peerIdxs, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, &proto.GroupCompact{
|
||||||
|
Id: g.AccountSeqID,
|
||||||
|
Name: g.Name,
|
||||||
|
PeerIndexes: peerIdxs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodePolicies flattens Policy{Rules} → []PolicyCompact. Returns the wire
|
||||||
|
// list and a map from policy pointer to the indexes of its emitted rules in
|
||||||
|
// that list — used by encodeResourcePoliciesMap to translate
|
||||||
|
// ResourcePoliciesMap[resourceID][]*Policy into wire-side indexes.
|
||||||
|
func (e *componentEncoder) encodePolicies(policies []*types.Policy) ([]*proto.PolicyCompact, map[*types.Policy][]uint32) {
|
||||||
|
if len(policies) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*proto.PolicyCompact, 0, len(policies))
|
||||||
|
idxByPolicy := make(map[*types.Policy][]uint32, len(policies))
|
||||||
|
|
||||||
|
for _, pol := range policies {
|
||||||
|
if !pol.HasSeqID() || !pol.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, r := range pol.Rules {
|
||||||
|
if r == nil || !r.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idxByPolicy[pol] = append(idxByPolicy[pol], uint32(len(out)))
|
||||||
|
out = append(out, e.encodePolicyRule(pol, r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, idxByPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodePolicyRule maps a single PolicyRule under pol to a PolicyCompact entry.
|
||||||
|
func (e *componentEncoder) encodePolicyRule(pol *types.Policy, r *types.PolicyRule) *proto.PolicyCompact {
|
||||||
|
return &proto.PolicyCompact{
|
||||||
|
Id: pol.AccountSeqID,
|
||||||
|
Action: networkmap.GetProtoAction(string(r.Action)),
|
||||||
|
Protocol: networkmap.GetProtoProtocol(string(r.Protocol)),
|
||||||
|
Bidirectional: r.Bidirectional,
|
||||||
|
Ports: portsToUint32(r.Ports),
|
||||||
|
PortRanges: portRangesToProto(r.PortRanges),
|
||||||
|
SourceGroupIds: e.groupSeqIDs(r.Sources),
|
||||||
|
DestinationGroupIds: e.groupSeqIDs(r.Destinations),
|
||||||
|
AuthorizedUser: r.AuthorizedUser,
|
||||||
|
AuthorizedGroups: e.encodeAuthorizedGroups(r.AuthorizedGroups),
|
||||||
|
SourceResource: e.resourceToProto(r.SourceResource),
|
||||||
|
DestinationResource: e.resourceToProto(r.DestinationResource),
|
||||||
|
SourcePostureCheckSeqIds: e.postureCheckSeqs(pol.SourcePostureChecks),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupSeqIDs maps the xid group IDs in src to their per-account seq ids,
|
||||||
|
// dropping any group that has no seq id assigned.
|
||||||
|
func (e *componentEncoder) groupSeqIDs(src []string) []uint32 {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(src))
|
||||||
|
for _, gid := range src {
|
||||||
|
if seq, ok := e.groupSeq(gid); ok {
|
||||||
|
out = append(out, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// unionPolicies merges c.Policies with every policy referenced by
|
||||||
|
// c.ResourcePoliciesMap, deduplicating by pointer identity. Resource-only
|
||||||
|
// policies (relevant to a NetworkResource but not to peer-pair traffic)
|
||||||
|
// only live in ResourcePoliciesMap; without this union step they'd be lost
|
||||||
|
// from the wire and the client's resource-policy lookup would come back
|
||||||
|
// empty.
|
||||||
|
func unionPolicies(policies []*types.Policy, resourcePolicies map[string][]*types.Policy) []*types.Policy {
|
||||||
|
// Fast path: non-router peers have no resource-only policies, so the
|
||||||
|
// "union" is identical to `policies`. Skip the dedup map allocation.
|
||||||
|
if len(resourcePolicies) == 0 {
|
||||||
|
return policies
|
||||||
|
}
|
||||||
|
seen := make(map[*types.Policy]struct{}, len(policies))
|
||||||
|
out := make([]*types.Policy, 0, len(policies))
|
||||||
|
for _, p := range policies {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[p]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p] = struct{}{}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
for _, list := range resourcePolicies {
|
||||||
|
for _, p := range list {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[p]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p] = struct{}{}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeAuthorizedGroups translates rule.AuthorizedGroups (map keyed by
|
||||||
|
// group xid → local-user names) to the wire form (map keyed by group
|
||||||
|
// account_seq_id → UserNameList). Groups without a seq id are dropped —
|
||||||
|
// matches how source/destination group references handle the same case.
|
||||||
|
func (e *componentEncoder) encodeAuthorizedGroups(m map[string][]string) map[uint32]*proto.UserNameList {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[uint32]*proto.UserNameList, len(m))
|
||||||
|
for groupID, names := range m {
|
||||||
|
seq, ok := e.groupSeq(groupID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[seq] = &proto.UserNameList{Names: append([]string(nil), names...)}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) groupSeq(groupID string) (uint32, bool) {
|
||||||
|
g, ok := e.components.Groups[groupID]
|
||||||
|
if !ok || !g.HasSeqID() {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return g.AccountSeqID, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// resourceToProto translates types.Resource for the wire. For peer-typed
|
||||||
|
// resources the peer id is converted to a peer index into the envelope's
|
||||||
|
// peers array. For other resource types only the type string is shipped
|
||||||
|
// today (Calculate's resource-typed rule path consults SourceResource only
|
||||||
|
// for "peer" — other types fall through to group-based lookup).
|
||||||
|
func (e *componentEncoder) resourceToProto(r types.Resource) *proto.ResourceCompact {
|
||||||
|
if r.ID == "" && r.Type == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &proto.ResourceCompact{Type: string(r.Type)}
|
||||||
|
if r.Type == types.ResourceTypePeer && r.ID != "" {
|
||||||
|
if idx, ok := e.peerOrder[r.ID]; ok {
|
||||||
|
out.PeerIndexSet = true
|
||||||
|
out.PeerIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// postureCheckSeqs translates a slice of posture-check xids to their
|
||||||
|
// per-account integer ids using the NetworkMapComponents.PostureCheckXIDToSeq
|
||||||
|
// lookup. Unresolvable xids are silently dropped — matches how group/peer
|
||||||
|
// references handle the same case.
|
||||||
|
func (e *componentEncoder) postureCheckSeqs(xids []string) []uint32 {
|
||||||
|
if len(xids) == 0 || len(e.components.PostureCheckXIDToSeq) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(xids))
|
||||||
|
for _, xid := range xids {
|
||||||
|
if seq, ok := e.components.PostureCheckXIDToSeq[xid]; ok {
|
||||||
|
out = append(out, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkSeq translates a Network xid to its per-account integer id using
|
||||||
|
// the NetworkMapComponents.NetworkXIDToSeq lookup. Returns (0,false) when
|
||||||
|
// the xid isn't known — callers decide whether to skip the parent record.
|
||||||
|
func (e *componentEncoder) networkSeq(xid string) (uint32, bool) {
|
||||||
|
if xid == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
seq, ok := e.components.NetworkXIDToSeq[xid]
|
||||||
|
if !ok || seq == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return seq, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeDNSSettings(s *types.DNSSettings) *proto.DNSSettingsCompact {
|
||||||
|
if s == nil || len(s.DisabledManagementGroups) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &proto.DNSSettingsCompact{
|
||||||
|
DisabledManagementGroupIds: make([]uint32, 0, len(s.DisabledManagementGroups)),
|
||||||
|
}
|
||||||
|
for _, gid := range s.DisabledManagementGroups {
|
||||||
|
if seq, ok := e.groupSeq(gid); ok {
|
||||||
|
out.DisabledManagementGroupIds = append(out.DisabledManagementGroupIds, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeRoutes(routes []*nbroute.Route) []*proto.RouteRaw {
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.RouteRaw, 0, len(routes))
|
||||||
|
for _, r := range routes {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rr := &proto.RouteRaw{
|
||||||
|
Id: r.AccountSeqID,
|
||||||
|
NetId: string(r.NetID),
|
||||||
|
Description: r.Description,
|
||||||
|
KeepRoute: r.KeepRoute,
|
||||||
|
NetworkType: int32(r.NetworkType),
|
||||||
|
Masquerade: r.Masquerade,
|
||||||
|
Metric: int32(r.Metric),
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
SkipAutoApply: r.SkipAutoApply,
|
||||||
|
Domains: r.Domains.ToPunycodeList(),
|
||||||
|
GroupIds: e.groupIDsToSeq(r.Groups),
|
||||||
|
AccessControlGroupIds: e.groupIDsToSeq(r.AccessControlGroups),
|
||||||
|
PeerGroupIds: e.groupIDsToSeq(r.PeerGroups),
|
||||||
|
}
|
||||||
|
if r.Network.IsValid() {
|
||||||
|
rr.NetworkCidr = r.Network.String()
|
||||||
|
}
|
||||||
|
if r.Peer != "" {
|
||||||
|
if idx, ok := e.peerOrder[r.Peer]; ok {
|
||||||
|
rr.PeerIndexSet = true
|
||||||
|
rr.PeerIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, rr)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) groupIDsToSeq(groupIDs []string) []uint32 {
|
||||||
|
if len(groupIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(groupIDs))
|
||||||
|
for _, gid := range groupIDs {
|
||||||
|
if seq, ok := e.groupSeq(gid); ok {
|
||||||
|
out = append(out, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeNameServerGroups(nsgs []*nbdns.NameServerGroup) []*proto.NameServerGroupRaw {
|
||||||
|
if len(nsgs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.NameServerGroupRaw, 0, len(nsgs))
|
||||||
|
for _, nsg := range nsgs {
|
||||||
|
if nsg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := &proto.NameServerGroupRaw{
|
||||||
|
Id: nsg.AccountSeqID,
|
||||||
|
Name: nsg.Name,
|
||||||
|
Description: nsg.Description,
|
||||||
|
Nameservers: encodeNameServers(nsg.NameServers),
|
||||||
|
GroupIds: e.groupIDsToSeq(nsg.Groups),
|
||||||
|
Primary: nsg.Primary,
|
||||||
|
Domains: nsg.Domains,
|
||||||
|
Enabled: nsg.Enabled,
|
||||||
|
SearchDomainsEnabled: nsg.SearchDomainsEnabled,
|
||||||
|
}
|
||||||
|
out = append(out, entry)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNameServers(servers []nbdns.NameServer) []*proto.NameServer {
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.NameServer, 0, len(servers))
|
||||||
|
for _, s := range servers {
|
||||||
|
out = append(out, &proto.NameServer{
|
||||||
|
IP: s.IP.String(),
|
||||||
|
NSType: int64(s.NSType),
|
||||||
|
Port: int64(s.Port),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeSimpleRecords(records []nbdns.SimpleRecord) []*proto.SimpleRecord {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.SimpleRecord, 0, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
out = append(out, &proto.SimpleRecord{
|
||||||
|
Name: r.Name,
|
||||||
|
Type: int64(r.Type),
|
||||||
|
Class: r.Class,
|
||||||
|
TTL: int64(r.TTL),
|
||||||
|
RData: r.RData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeCustomZones(zones []nbdns.CustomZone) []*proto.CustomZone {
|
||||||
|
if len(zones) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.CustomZone, 0, len(zones))
|
||||||
|
for _, z := range zones {
|
||||||
|
out = append(out, &proto.CustomZone{
|
||||||
|
Domain: z.Domain,
|
||||||
|
Records: encodeSimpleRecords(z.Records),
|
||||||
|
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||||
|
NonAuthoritative: z.NonAuthoritative,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeNetworkResources(resources []*resourceTypes.NetworkResource) []*proto.NetworkResourceRaw {
|
||||||
|
if len(resources) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.NetworkResourceRaw, 0, len(resources))
|
||||||
|
for _, r := range resources {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := &proto.NetworkResourceRaw{
|
||||||
|
Id: r.AccountSeqID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Type: string(r.Type),
|
||||||
|
Address: r.Address,
|
||||||
|
DomainValue: r.Domain,
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
}
|
||||||
|
if seq, ok := e.networkSeq(r.NetworkID); ok {
|
||||||
|
entry.NetworkSeq = seq
|
||||||
|
}
|
||||||
|
if r.Prefix.IsValid() {
|
||||||
|
entry.PrefixCidr = r.Prefix.String()
|
||||||
|
}
|
||||||
|
out = append(out, entry)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeRoutersMap(routersMap map[string]map[string]*routerTypes.NetworkRouter) map[uint32]*proto.NetworkRouterList {
|
||||||
|
if len(routersMap) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[uint32]*proto.NetworkRouterList, len(routersMap))
|
||||||
|
for networkXID, routers := range routersMap {
|
||||||
|
if len(routers) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
netSeq, ok := e.networkSeq(networkXID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries := make([]*proto.NetworkRouterEntry, 0, len(routers))
|
||||||
|
for peerID, r := range routers {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := &proto.NetworkRouterEntry{
|
||||||
|
Id: r.AccountSeqID,
|
||||||
|
PeerGroupIds: e.groupIDsToSeq(r.PeerGroups),
|
||||||
|
Masquerade: r.Masquerade,
|
||||||
|
Metric: int32(r.Metric),
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
}
|
||||||
|
if idx, ok := e.peerOrder[peerID]; ok {
|
||||||
|
entry.PeerIndexSet = true
|
||||||
|
entry.PeerIndex = idx
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
out[netSeq] = &proto.NetworkRouterList{Entries: entries}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeResourcePoliciesMap(rpm map[string][]*types.Policy, policyToIdxs map[*types.Policy][]uint32) map[uint32]*proto.PolicyIndexes {
|
||||||
|
if len(rpm) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// resourceXIDToSeq is local to one encode — built from components.NetworkResources
|
||||||
|
// (small slice). Network resources without seq id are dropped, matching how
|
||||||
|
// other components-without-seq are silently filtered.
|
||||||
|
resourceXIDToSeq := make(map[string]uint32, len(e.components.NetworkResources))
|
||||||
|
for _, r := range e.components.NetworkResources {
|
||||||
|
if r != nil && r.AccountSeqID != 0 {
|
||||||
|
resourceXIDToSeq[r.ID] = r.AccountSeqID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make(map[uint32]*proto.PolicyIndexes, len(rpm))
|
||||||
|
for resourceXID, policies := range rpm {
|
||||||
|
seq, ok := resourceXIDToSeq[resourceXID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idxs := make([]uint32, 0, len(policies)*2)
|
||||||
|
for _, pol := range policies {
|
||||||
|
idxs = append(idxs, policyToIdxs[pol]...)
|
||||||
|
}
|
||||||
|
if len(idxs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[seq] = &proto.PolicyIndexes{Indexes: idxs}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeGroupIDToUserIDs(m map[string][]string) map[uint32]*proto.UserIDList {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[uint32]*proto.UserIDList, len(m))
|
||||||
|
for groupID, userIDs := range m {
|
||||||
|
seq, ok := e.groupSeq(groupID)
|
||||||
|
if !ok || len(userIDs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[seq] = &proto.UserIDList{UserIds: userIDs}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSetToSlice(s map[string]struct{}) []string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(s))
|
||||||
|
for k := range s {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodePostureFailedPeers(m map[string]map[string]struct{}) map[uint32]*proto.PeerIndexSet {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[uint32]*proto.PeerIndexSet, len(m))
|
||||||
|
for checkXID, failedPeerIDs := range m {
|
||||||
|
seq, ok := e.components.PostureCheckXIDToSeq[checkXID]
|
||||||
|
if !ok || seq == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idxs := make([]uint32, 0, len(failedPeerIDs))
|
||||||
|
for peerID := range failedPeerIDs {
|
||||||
|
if idx, ok := e.peerOrder[peerID]; ok {
|
||||||
|
idxs = append(idxs, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(idxs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[seq] = &proto.PeerIndexSet{PeerIndexes: idxs}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// toAccountSettingsCompact always returns a non-nil message — the client
|
||||||
|
// dereferences it unconditionally during Calculate(), so a nil here would
|
||||||
|
// crash the receiver. A missing types.AccountSettingsInfo on the server
|
||||||
|
// (which shouldn't happen in production but the encoder is exported)
|
||||||
|
// degrades to login_expiration_enabled = false, which makes
|
||||||
|
// LoginExpired() return false for every peer.
|
||||||
|
func toAccountSettingsCompact(s *types.AccountSettingsInfo) *proto.AccountSettingsCompact {
|
||||||
|
if s == nil {
|
||||||
|
return &proto.AccountSettingsCompact{}
|
||||||
|
}
|
||||||
|
return &proto.AccountSettingsCompact{
|
||||||
|
PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled,
|
||||||
|
PeerLoginExpirationNs: int64(s.PeerLoginExpiration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAccountNetwork(n *types.Network) *proto.AccountNetwork {
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &proto.AccountNetwork{
|
||||||
|
Identifier: n.Identifier,
|
||||||
|
NetCidr: n.Net.String(),
|
||||||
|
Dns: n.Dns,
|
||||||
|
Serial: n.CurrentSerial(),
|
||||||
|
}
|
||||||
|
if len(n.NetV6.IP) > 0 {
|
||||||
|
out.NetV6Cidr = n.NetV6.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPeerCompact(p *nbpeer.Peer, agentVersionIdx uint32) *proto.PeerCompact {
|
||||||
|
pc := &proto.PeerCompact{
|
||||||
|
WgPubKey: decodeWgKey(p.Key),
|
||||||
|
SshPubKey: []byte(p.SSHKey),
|
||||||
|
DnsLabel: p.DNSLabel,
|
||||||
|
AgentVersionIdx: agentVersionIdx,
|
||||||
|
AddedWithSsoLogin: p.UserID != "",
|
||||||
|
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||||
|
SshEnabled: p.SSHEnabled,
|
||||||
|
SupportsIpv6: p.SupportsIPv6(),
|
||||||
|
SupportsSourcePrefixes: p.SupportsSourcePrefixes(),
|
||||||
|
ServerSshAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||||
|
}
|
||||||
|
if p.LastLogin != nil {
|
||||||
|
pc.LastLoginUnixNano = p.LastLogin.UnixNano()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !p.IP.IsValid():
|
||||||
|
// leave Ip nil
|
||||||
|
case p.IP.Is4() || p.IP.Is4In6():
|
||||||
|
ip := p.IP.Unmap().As4()
|
||||||
|
pc.Ip = ip[:]
|
||||||
|
default:
|
||||||
|
ip := p.IP.As16()
|
||||||
|
pc.Ip = ip[:]
|
||||||
|
}
|
||||||
|
if p.IPv6.IsValid() {
|
||||||
|
ip := p.IPv6.As16()
|
||||||
|
pc.Ipv6 = ip[:]
|
||||||
|
}
|
||||||
|
return pc
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeWgKey returns the raw 32 bytes of a base64-encoded WireGuard public
|
||||||
|
// key, or nil for an empty / malformed key.
|
||||||
|
func decodeWgKey(s string) []byte {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]byte, wgKeyRawLen)
|
||||||
|
n, err := base64.StdEncoding.Decode(out, []byte(s))
|
||||||
|
if err != nil || n != wgKeyRawLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func portsToUint32(ports []string) []uint32 {
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(ports))
|
||||||
|
for _, p := range ports {
|
||||||
|
v, err := strconv.ParseUint(p, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, uint32(v))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func portRangesToProto(ranges []types.RulePortRange) []*proto.PortInfo_Range {
|
||||||
|
if len(ranges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.PortInfo_Range, 0, len(ranges))
|
||||||
|
for _, r := range ranges {
|
||||||
|
out = append(out, &proto.PortInfo_Range{
|
||||||
|
Start: uint32(r.Start),
|
||||||
|
End: uint32(r.End),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
879
management/internals/shared/grpc/components_encoder_test.go
Normal file
879
management/internals/shared/grpc/components_encoder_test.go
Normal file
@@ -0,0 +1,879 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"cmp"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
|
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testWgKeyA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq="
|
||||||
|
const testWgKeyB = "BBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq="
|
||||||
|
const testWgKeyC = "CBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq="
|
||||||
|
|
||||||
|
// canonicalize rewrites a NetworkMapComponentsFull in place into a canonical
|
||||||
|
// form: peers reordered by wg_pub_key, with the rest of the message rewritten
|
||||||
|
// to reference the new peer indexes. Groups, policies, and router indexes are
|
||||||
|
// also sorted. After canonicalize, two envelopes built from the same logical
|
||||||
|
// input compare byte-equal via proto.Equal.
|
||||||
|
//
|
||||||
|
// This lives on the test side — the encoder itself emits in map-iteration
|
||||||
|
// order. Test-side normalization is the contract for "two encodes are
|
||||||
|
// equivalent".
|
||||||
|
func canonicalize(full *proto.NetworkMapComponentsFull) {
|
||||||
|
if full == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize agent_versions first: sort the slice and rewrite each
|
||||||
|
// peer's AgentVersionIdx accordingly. The empty placeholder stays at
|
||||||
|
// index 0 by convention.
|
||||||
|
avRemap := make(map[uint32]uint32, len(full.AgentVersions))
|
||||||
|
if len(full.AgentVersions) > 0 {
|
||||||
|
// Pair version → original index, sort, rebuild.
|
||||||
|
type avEntry struct {
|
||||||
|
version string
|
||||||
|
oldIdx uint32
|
||||||
|
}
|
||||||
|
entries := make([]avEntry, len(full.AgentVersions))
|
||||||
|
for i, v := range full.AgentVersions {
|
||||||
|
entries[i] = avEntry{version: v, oldIdx: uint32(i)}
|
||||||
|
}
|
||||||
|
// Empty stays at 0; sort the rest by string. Tiebreaker on oldIdx
|
||||||
|
// keeps the canonicalize output stable when two entries compare
|
||||||
|
// equal (the encoder dedups, but defending against future inputs).
|
||||||
|
slices.SortFunc(entries, func(a, b avEntry) int {
|
||||||
|
if a.version == "" && b.version != "" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if b.version == "" && a.version != "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c := cmp.Compare(a.version, b.version); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.oldIdx, b.oldIdx)
|
||||||
|
})
|
||||||
|
newVersions := make([]string, len(entries))
|
||||||
|
for newIdx, e := range entries {
|
||||||
|
avRemap[e.oldIdx] = uint32(newIdx)
|
||||||
|
newVersions[newIdx] = e.version
|
||||||
|
}
|
||||||
|
full.AgentVersions = newVersions
|
||||||
|
}
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if newIdx, ok := avRemap[p.AgentVersionIdx]; ok {
|
||||||
|
p.AgentVersionIdx = newIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerEntry struct {
|
||||||
|
peer *proto.PeerCompact
|
||||||
|
oldIdx uint32
|
||||||
|
}
|
||||||
|
entries := make([]peerEntry, len(full.Peers))
|
||||||
|
for i, p := range full.Peers {
|
||||||
|
entries[i] = peerEntry{peer: p, oldIdx: uint32(i)}
|
||||||
|
}
|
||||||
|
// DnsLabel is unique per peer; it tiebreaks on equal WgPubKey (e.g. both
|
||||||
|
// nil from malformed keys, or both empty for placeholders).
|
||||||
|
slices.SortFunc(entries, func(a, b peerEntry) int {
|
||||||
|
if c := bytes.Compare(a.peer.WgPubKey, b.peer.WgPubKey); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.peer.DnsLabel, b.peer.DnsLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
remap := make(map[uint32]uint32, len(entries))
|
||||||
|
newPeers := make([]*proto.PeerCompact, len(entries))
|
||||||
|
for newIdx, e := range entries {
|
||||||
|
remap[e.oldIdx] = uint32(newIdx)
|
||||||
|
newPeers[newIdx] = e.peer
|
||||||
|
}
|
||||||
|
full.Peers = newPeers
|
||||||
|
|
||||||
|
full.RouterPeerIndexes = remapAndSort(full.RouterPeerIndexes, remap)
|
||||||
|
for _, g := range full.Groups {
|
||||||
|
g.PeerIndexes = remapAndSort(g.PeerIndexes, remap)
|
||||||
|
}
|
||||||
|
slices.SortFunc(full.Groups, func(a, b *proto.GroupCompact) int { return cmp.Compare(a.Id, b.Id) })
|
||||||
|
|
||||||
|
for _, r := range full.Routes {
|
||||||
|
if r.PeerIndexSet {
|
||||||
|
if newIdx, ok := remap[r.PeerIndex]; ok {
|
||||||
|
r.PeerIndex = newIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(r.GroupIds)
|
||||||
|
slices.Sort(r.AccessControlGroupIds)
|
||||||
|
slices.Sort(r.PeerGroupIds)
|
||||||
|
}
|
||||||
|
slices.SortFunc(full.Routes, func(a, b *proto.RouteRaw) int { return cmp.Compare(a.Id, b.Id) })
|
||||||
|
|
||||||
|
for _, list := range full.RoutersMap {
|
||||||
|
for _, entry := range list.Entries {
|
||||||
|
if entry.PeerIndexSet {
|
||||||
|
if newIdx, ok := remap[entry.PeerIndex]; ok {
|
||||||
|
entry.PeerIndex = newIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(entry.PeerGroupIds)
|
||||||
|
}
|
||||||
|
slices.SortFunc(list.Entries, func(a, b *proto.NetworkRouterEntry) int { return cmp.Compare(a.Id, b.Id) })
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, set := range full.PostureFailedPeers {
|
||||||
|
set.PeerIndexes = remapAndSort(set.PeerIndexes, remap)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range full.Policies {
|
||||||
|
slices.Sort(p.SourceGroupIds)
|
||||||
|
slices.Sort(p.DestinationGroupIds)
|
||||||
|
}
|
||||||
|
// Sort policies by (Id, source_group_ids, destination_group_ids) so that
|
||||||
|
// multiple PolicyCompact entries sharing the same Id (one per rule, when
|
||||||
|
// a Policy has multiple rules) still get a deterministic order. After
|
||||||
|
// sorting we remap indexes in ResourcePoliciesMap.
|
||||||
|
policyOldOrder := make(map[*proto.PolicyCompact]uint32, len(full.Policies))
|
||||||
|
for i, p := range full.Policies {
|
||||||
|
policyOldOrder[p] = uint32(i)
|
||||||
|
}
|
||||||
|
slices.SortFunc(full.Policies, func(a, b *proto.PolicyCompact) int {
|
||||||
|
if c := cmp.Compare(a.Id, b.Id); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if c := slices.Compare(a.SourceGroupIds, b.SourceGroupIds); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return slices.Compare(a.DestinationGroupIds, b.DestinationGroupIds)
|
||||||
|
})
|
||||||
|
policyRemap := make(map[uint32]uint32, len(full.Policies))
|
||||||
|
for newIdx, p := range full.Policies {
|
||||||
|
policyRemap[policyOldOrder[p]] = uint32(newIdx)
|
||||||
|
}
|
||||||
|
for _, idxs := range full.ResourcePoliciesMap {
|
||||||
|
idxs.Indexes = remapAndSort(idxs.Indexes, policyRemap)
|
||||||
|
}
|
||||||
|
for _, list := range full.GroupIdToUserIds {
|
||||||
|
slices.Sort(list.UserIds)
|
||||||
|
}
|
||||||
|
slices.Sort(full.AllowedUserIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remapAndSort(idxs []uint32, remap map[uint32]uint32) []uint32 {
|
||||||
|
out := make([]uint32, 0, len(idxs))
|
||||||
|
for _, i := range idxs {
|
||||||
|
if newIdx, ok := remap[i]; ok {
|
||||||
|
out = append(out, newIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// envelopesEquivalent decodes both envelopes, canonicalizes them, and reports
|
||||||
|
// whether they're proto.Equal. Use instead of byte-comparing marshaled output:
|
||||||
|
// the encoder is intentionally non-deterministic.
|
||||||
|
func envelopesEquivalent(a, b *proto.NetworkMapEnvelope) bool {
|
||||||
|
canonicalize(a.GetFull())
|
||||||
|
canonicalize(b.GetFull())
|
||||||
|
return goproto.Equal(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestComponents() *types.NetworkMapComponents {
|
||||||
|
peerA := &nbpeer.Peer{
|
||||||
|
ID: "peer-a",
|
||||||
|
Key: testWgKeyA,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
|
||||||
|
DNSLabel: "peera",
|
||||||
|
SSHKey: "ssh-a",
|
||||||
|
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
peerB := &nbpeer.Peer{
|
||||||
|
ID: "peer-b",
|
||||||
|
Key: testWgKeyB,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
|
||||||
|
IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}),
|
||||||
|
DNSLabel: "peerb",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.25.0"},
|
||||||
|
}
|
||||||
|
peerC := &nbpeer.Peer{
|
||||||
|
ID: "peer-c",
|
||||||
|
Key: testWgKeyC,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}),
|
||||||
|
DNSLabel: "peerc",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.NetworkMapComponents{
|
||||||
|
PeerID: "peer-a",
|
||||||
|
Network: &types.Network{
|
||||||
|
Identifier: "net-test",
|
||||||
|
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||||
|
Serial: 7,
|
||||||
|
},
|
||||||
|
AccountSettings: &types.AccountSettingsInfo{
|
||||||
|
PeerLoginExpirationEnabled: true,
|
||||||
|
PeerLoginExpiration: 2 * time.Hour,
|
||||||
|
},
|
||||||
|
Peers: map[string]*nbpeer.Peer{
|
||||||
|
"peer-a": peerA,
|
||||||
|
"peer-b": peerB,
|
||||||
|
"peer-c": peerC,
|
||||||
|
},
|
||||||
|
Groups: map[string]*types.Group{
|
||||||
|
"group-src": {ID: "group-src", AccountSeqID: 1, Name: "Src", Peers: []string{"peer-a"}},
|
||||||
|
"group-dst": {ID: "group-dst", AccountSeqID: 2, Name: "Dst", Peers: []string{"peer-b", "peer-c"}},
|
||||||
|
},
|
||||||
|
Policies: []*types.Policy{
|
||||||
|
{
|
||||||
|
ID: "pol-1",
|
||||||
|
AccountSeqID: 10,
|
||||||
|
Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-1", Enabled: true, Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true,
|
||||||
|
Ports: []string{"22", "80"},
|
||||||
|
PortRanges: []types.RulePortRange{{Start: 8000, End: 8100}},
|
||||||
|
Sources: []string{"group-src"},
|
||||||
|
Destinations: []string{"group-dst"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RouterPeers: map[string]*nbpeer.Peer{"peer-c": peerC},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_Basic(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotNil(t, env)
|
||||||
|
full := env.GetFull()
|
||||||
|
require.NotNil(t, full, "envelope must contain Full payload")
|
||||||
|
|
||||||
|
assert.EqualValues(t, 7, full.Serial)
|
||||||
|
assert.Equal(t, "netbird.cloud", full.DnsDomain)
|
||||||
|
|
||||||
|
require.NotNil(t, full.Network)
|
||||||
|
assert.Equal(t, "net-test", full.Network.Identifier)
|
||||||
|
assert.Equal(t, "100.64.0.0/10", full.Network.NetCidr)
|
||||||
|
|
||||||
|
require.NotNil(t, full.AccountSettings)
|
||||||
|
assert.True(t, full.AccountSettings.PeerLoginExpirationEnabled)
|
||||||
|
assert.EqualValues(t, (2 * time.Hour).Nanoseconds(), full.AccountSettings.PeerLoginExpirationNs)
|
||||||
|
|
||||||
|
require.Len(t, full.Peers, 3)
|
||||||
|
byLabel := map[string]*proto.PeerCompact{}
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
assert.Len(t, p.WgPubKey, 32, "wg key must be raw 32 bytes")
|
||||||
|
assert.Len(t, p.Ip, 4, "ipv4 must be raw 4 bytes")
|
||||||
|
byLabel[p.DnsLabel] = p
|
||||||
|
}
|
||||||
|
assert.Len(t, byLabel["peerb"].Ipv6, 16, "peer-b has ipv6 → 16 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RepeatEncodesEquivalent(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
expected := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
|
||||||
|
// Hammer it 100 times — Go map iteration is randomized per call, so each
|
||||||
|
// run produces different wire bytes, but the canonicalized form must
|
||||||
|
// match.
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
got := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
require.True(t, envelopesEquivalent(expected, got),
|
||||||
|
"encode #%d must be semantically equivalent to first encode", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_ConcurrentEncodesEquivalent(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
expected := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
|
||||||
|
const goroutines = 50
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines)
|
||||||
|
results := make([]*proto.NetworkMapEnvelope, goroutines)
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
i := i
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
results[i] = EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for i, got := range results {
|
||||||
|
require.NotNil(t, got, "goroutine %d returned nil", i)
|
||||||
|
require.True(t, envelopesEquivalent(expected, got),
|
||||||
|
"goroutine %d produced inequivalent envelope", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_GroupsByAccountSeqID(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Groups, 2)
|
||||||
|
|
||||||
|
groupByID := map[uint32]*proto.GroupCompact{}
|
||||||
|
for _, g := range full.Groups {
|
||||||
|
groupByID[g.Id] = g
|
||||||
|
}
|
||||||
|
require.Contains(t, groupByID, uint32(1))
|
||||||
|
require.Contains(t, groupByID, uint32(2))
|
||||||
|
assert.Equal(t, "Src", groupByID[1].Name)
|
||||||
|
assert.Equal(t, "Dst", groupByID[2].Name)
|
||||||
|
assert.Len(t, groupByID[1].PeerIndexes, 1)
|
||||||
|
assert.Len(t, groupByID[2].PeerIndexes, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PolicyExpansion(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Policies, 1)
|
||||||
|
pc := full.Policies[0]
|
||||||
|
assert.EqualValues(t, 10, pc.Id)
|
||||||
|
assert.Equal(t, proto.RuleAction_ACCEPT, pc.Action)
|
||||||
|
assert.Equal(t, proto.RuleProtocol_TCP, pc.Protocol)
|
||||||
|
assert.True(t, pc.Bidirectional)
|
||||||
|
assert.Equal(t, []uint32{22, 80}, pc.Ports)
|
||||||
|
require.Len(t, pc.PortRanges, 1)
|
||||||
|
assert.EqualValues(t, 8000, pc.PortRanges[0].Start)
|
||||||
|
assert.EqualValues(t, 8100, pc.PortRanges[0].End)
|
||||||
|
assert.Equal(t, []uint32{1}, pc.SourceGroupIds)
|
||||||
|
assert.Equal(t, []uint32{2}, pc.DestinationGroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RouterIndexes(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.RouterPeerIndexes, 1)
|
||||||
|
idx := full.RouterPeerIndexes[0]
|
||||||
|
require.Less(t, int(idx), len(full.Peers))
|
||||||
|
assert.Equal(t, "peerc", full.Peers[idx].DnsLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_AgentVersionDedup(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.AgentVersions, 3, "empty placeholder + 2 distinct versions")
|
||||||
|
assert.Equal(t, "", full.AgentVersions[0], "index 0 reserved for empty version")
|
||||||
|
assert.ElementsMatch(t, []string{"0.40.0", "0.25.0"}, full.AgentVersions[1:],
|
||||||
|
"two distinct versions, order depends on map iteration")
|
||||||
|
|
||||||
|
idxByLabel := map[string]uint32{}
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
idxByLabel[p.DnsLabel] = p.AgentVersionIdx
|
||||||
|
}
|
||||||
|
assert.Equal(t, idxByLabel["peera"], idxByLabel["peerc"], "peers with the same agent version share an index")
|
||||||
|
assert.NotEqual(t, idxByLabel["peera"], idxByLabel["peerb"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_DisabledPolicySkipped(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Policies[0].Enabled = false
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
assert.Empty(t, full.Policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_GroupZeroSeqIDSkipped(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Groups["group-src"].AccountSeqID = 0
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Groups, 1, "groups with AccountSeqID=0 are not yet persisted and must be skipped")
|
||||||
|
assert.EqualValues(t, 2, full.Groups[0].Id)
|
||||||
|
|
||||||
|
require.Len(t, full.Policies, 1)
|
||||||
|
pc := full.Policies[0]
|
||||||
|
assert.Empty(t, pc.SourceGroupIds, "rule references a group that was filtered out → no group id on wire")
|
||||||
|
assert.Equal(t, []uint32{2}, pc.DestinationGroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_TwoPeersSameMalformedKey(t *testing.T) {
|
||||||
|
// Both peers have nil WgPubKey after decode; canonicalize must still
|
||||||
|
// produce a stable order using DnsLabel as a tiebreaker, so 100 encodes
|
||||||
|
// canonicalize identically.
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Peers["peer-a"].Key = "garbage-a-!!!"
|
||||||
|
c.Peers["peer-b"].Key = "garbage-b-!!!"
|
||||||
|
|
||||||
|
expected := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
got := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
require.True(t, envelopesEquivalent(expected, got),
|
||||||
|
"encode #%d with two same-key peers must canonicalize equivalently", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_MalformedWgKey(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Peers["peer-a"].Key = "not-base64-!!!"
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Peers, 3)
|
||||||
|
|
||||||
|
var byLabel = map[string]*proto.PeerCompact{}
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
byLabel[p.DnsLabel] = p
|
||||||
|
}
|
||||||
|
assert.Nil(t, byLabel["peera"].WgPubKey, "peer with malformed key encodes nil WgPubKey")
|
||||||
|
assert.Len(t, byLabel["peerb"].WgPubKey, 32, "other peers retain their key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_IPv6OnlyPeer(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
v6Only := &nbpeer.Peer{
|
||||||
|
ID: "peer-v6",
|
||||||
|
Key: testWgKeyA,
|
||||||
|
IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}),
|
||||||
|
DNSLabel: "peerv6",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
c.Peers["peer-v6"] = v6Only
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
var found *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peerv6" {
|
||||||
|
found = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, found, "ipv6-only peer must be present")
|
||||||
|
assert.Empty(t, found.Ip, "no IPv4 address → empty Ip")
|
||||||
|
assert.Len(t, found.Ipv6, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PeerWithoutIP(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Peers["peer-noip"] = &nbpeer.Peer{
|
||||||
|
ID: "peer-noip",
|
||||||
|
Key: testWgKeyA,
|
||||||
|
DNSLabel: "peernoip",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
var found *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peernoip" {
|
||||||
|
found = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, found)
|
||||||
|
assert.Empty(t, found.Ip)
|
||||||
|
assert.Empty(t, found.Ipv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_EmptyInput(t *testing.T) {
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
Network: &types.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
|
||||||
|
full := env.GetFull()
|
||||||
|
require.NotNil(t, full)
|
||||||
|
assert.Empty(t, full.Peers)
|
||||||
|
assert.Empty(t, full.Groups)
|
||||||
|
assert.Empty(t, full.Policies)
|
||||||
|
assert.Empty(t, full.RouterPeerIndexes)
|
||||||
|
require.NotNil(t, full.AccountSettings, "AccountSettingsCompact must always be emitted (client dereferences it unconditionally)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PeerLoginExpirationFields(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
c.Peers["peer-a"].UserID = "user-1"
|
||||||
|
c.Peers["peer-a"].LoginExpirationEnabled = true
|
||||||
|
c.Peers["peer-a"].LastLogin = &now
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
var pa *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peera" {
|
||||||
|
pa = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, pa)
|
||||||
|
assert.True(t, pa.AddedWithSsoLogin)
|
||||||
|
assert.True(t, pa.LoginExpirationEnabled)
|
||||||
|
assert.Equal(t, now.UnixNano(), pa.LastLoginUnixNano)
|
||||||
|
|
||||||
|
// peer-b has no UserID and no LastLogin → all fields zero-value.
|
||||||
|
var pb *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peerb" {
|
||||||
|
pb = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, pb)
|
||||||
|
assert.False(t, pb.AddedWithSsoLogin)
|
||||||
|
assert.False(t, pb.LoginExpirationEnabled)
|
||||||
|
assert.Zero(t, pb.LastLoginUnixNano)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RoutesRoundTrip(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Routes = []*nbroute.Route{
|
||||||
|
{
|
||||||
|
ID: "route-peer",
|
||||||
|
AccountSeqID: 100,
|
||||||
|
NetID: "net-A",
|
||||||
|
Description: "via peer-c",
|
||||||
|
Network: netip.MustParsePrefix("10.0.0.0/16"),
|
||||||
|
Peer: "peer-c", // peer ID, not WG key
|
||||||
|
Groups: []string{"group-src"},
|
||||||
|
AccessControlGroups: []string{"group-dst"},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "route-peergroup",
|
||||||
|
AccountSeqID: 101,
|
||||||
|
NetID: "net-B",
|
||||||
|
Network: netip.MustParsePrefix("10.1.0.0/16"),
|
||||||
|
PeerGroups: []string{"group-src", "group-dst"},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "route-no-seq",
|
||||||
|
AccountSeqID: 0, // unset — should still ship (no group seq filter on routes)
|
||||||
|
Network: netip.MustParsePrefix("10.2.0.0/16"),
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Routes, 3)
|
||||||
|
byNetID := map[string]*proto.RouteRaw{}
|
||||||
|
for _, r := range full.Routes {
|
||||||
|
byNetID[r.NetId] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
r1 := byNetID["net-A"]
|
||||||
|
require.NotNil(t, r1)
|
||||||
|
assert.True(t, r1.PeerIndexSet, "route with peer must set peer_index_set")
|
||||||
|
require.Less(t, int(r1.PeerIndex), len(full.Peers))
|
||||||
|
assert.Equal(t, "peerc", full.Peers[r1.PeerIndex].DnsLabel)
|
||||||
|
assert.Equal(t, []uint32{1}, r1.GroupIds, "group-src has AccountSeqID 1")
|
||||||
|
assert.Equal(t, []uint32{2}, r1.AccessControlGroupIds, "group-dst has AccountSeqID 2")
|
||||||
|
assert.Empty(t, r1.PeerGroupIds)
|
||||||
|
|
||||||
|
r2 := byNetID["net-B"]
|
||||||
|
require.NotNil(t, r2)
|
||||||
|
assert.False(t, r2.PeerIndexSet, "route with peer_groups must NOT set peer_index_set")
|
||||||
|
assert.ElementsMatch(t, []uint32{1, 2}, r2.PeerGroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RouteWithMissingPeerLeavesIndexUnset(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Routes = []*nbroute.Route{{
|
||||||
|
ID: "route-x",
|
||||||
|
AccountSeqID: 100,
|
||||||
|
Peer: "peer-not-in-components",
|
||||||
|
Network: netip.MustParsePrefix("10.0.0.0/16"),
|
||||||
|
Enabled: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Routes, 1)
|
||||||
|
assert.False(t, full.Routes[0].PeerIndexSet,
|
||||||
|
"missing peer reference must not pretend to point at peer index 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_ResourceOnlyPolicyShippedAndIndexed(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
// Policy that exists ONLY in ResourcePoliciesMap, not in c.Policies. This
|
||||||
|
// is the I1 case — without unionPolicies the encoder would silently
|
||||||
|
// drop it from the wire.
|
||||||
|
resourceOnlyPolicy := &types.Policy{
|
||||||
|
ID: "pol-resource", AccountSeqID: 99, Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-r", Enabled: true, Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolTCP,
|
||||||
|
Sources: []string{"group-src"},
|
||||||
|
Destinations: []string{"group-dst"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
c.ResourcePoliciesMap = map[string][]*types.Policy{
|
||||||
|
"resource-x": {c.Policies[0], resourceOnlyPolicy}, // shared + resource-only
|
||||||
|
}
|
||||||
|
// Resource must appear in components.NetworkResources with a seq id —
|
||||||
|
// encoder uses that to translate the xid map key to uint32.
|
||||||
|
c.NetworkResources = []*resourceTypes.NetworkResource{
|
||||||
|
{ID: "resource-x", AccountSeqID: 77, Name: "res-x", Enabled: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Policies, 2, "encoded policies must include both peer-traffic and resource-only")
|
||||||
|
|
||||||
|
policyByID := map[uint32]*proto.PolicyCompact{}
|
||||||
|
policyIdxByID := map[uint32]uint32{}
|
||||||
|
for i, p := range full.Policies {
|
||||||
|
policyByID[p.Id] = p
|
||||||
|
policyIdxByID[p.Id] = uint32(i)
|
||||||
|
}
|
||||||
|
require.Contains(t, policyByID, uint32(10), "original peer-traffic policy id 10")
|
||||||
|
require.Contains(t, policyByID, uint32(99), "resource-only policy id 99")
|
||||||
|
|
||||||
|
require.Contains(t, full.ResourcePoliciesMap, uint32(77))
|
||||||
|
idxs := full.ResourcePoliciesMap[77].Indexes
|
||||||
|
require.Len(t, idxs, 2)
|
||||||
|
assert.ElementsMatch(t, []uint32{policyIdxByID[10], policyIdxByID[99]}, idxs,
|
||||||
|
"resource policies map must reference both wire policy indexes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_NameServerGroups(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.NameServerGroups = []*nbdns.NameServerGroup{{
|
||||||
|
ID: "nsg-1", AccountSeqID: 50, Name: "Main", Description: "primary",
|
||||||
|
NameServers: []nbdns.NameServer{{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53,
|
||||||
|
}},
|
||||||
|
Groups: []string{"group-src", "group-not-persisted"},
|
||||||
|
Primary: true, Enabled: true,
|
||||||
|
Domains: []string{"corp.example"},
|
||||||
|
}}
|
||||||
|
c.Groups["group-not-persisted"] = &types.Group{ID: "group-not-persisted", AccountSeqID: 0, Peers: []string{}}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.NameserverGroups, 1)
|
||||||
|
nsg := full.NameserverGroups[0]
|
||||||
|
assert.EqualValues(t, 50, nsg.Id)
|
||||||
|
assert.Equal(t, "Main", nsg.Name)
|
||||||
|
assert.True(t, nsg.Primary)
|
||||||
|
require.Len(t, nsg.Nameservers, 1)
|
||||||
|
assert.Equal(t, "8.8.8.8", nsg.Nameservers[0].IP)
|
||||||
|
assert.Equal(t, []uint32{1}, nsg.GroupIds, "group-not-persisted is filtered out (AccountSeqID=0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PostureFailedPeers(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.PostureCheckXIDToSeq = map[string]uint32{"check-1": 33}
|
||||||
|
c.PostureFailedPeers = map[string]map[string]struct{}{
|
||||||
|
"check-1": {
|
||||||
|
"peer-a": {},
|
||||||
|
"peer-b": {},
|
||||||
|
"peer-not-in-account": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Contains(t, full.PostureFailedPeers, uint32(33))
|
||||||
|
idxs := full.PostureFailedPeers[33].PeerIndexes
|
||||||
|
assert.Len(t, idxs, 2, "missing peer is silently dropped (filterPostureFailedPeers guarantees presence in real data)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RoutersMap(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.NetworkXIDToSeq = map[string]uint32{"net-1": 5}
|
||||||
|
c.RoutersMap = map[string]map[string]*routerTypes.NetworkRouter{
|
||||||
|
"net-1": {
|
||||||
|
"peer-c": {
|
||||||
|
ID: "router-1", AccountSeqID: 200,
|
||||||
|
Peer: "peer-c", Masquerade: true, Metric: 10, Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Contains(t, full.RoutersMap, uint32(5))
|
||||||
|
entries := full.RoutersMap[5].Entries
|
||||||
|
require.Len(t, entries, 1)
|
||||||
|
e := entries[0]
|
||||||
|
assert.EqualValues(t, 200, e.Id)
|
||||||
|
assert.True(t, e.PeerIndexSet)
|
||||||
|
require.Less(t, int(e.PeerIndex), len(full.Peers))
|
||||||
|
assert.Equal(t, "peerc", full.Peers[e.PeerIndex].DnsLabel)
|
||||||
|
assert.True(t, e.Masquerade)
|
||||||
|
assert.EqualValues(t, 10, e.Metric)
|
||||||
|
assert.True(t, e.Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RouterPeerNotInComponentsPeers(t *testing.T) {
|
||||||
|
// Router peer in c.RouterPeers but NOT in c.Peers (validation may have
|
||||||
|
// filtered it). indexRouterPeers runs before encodeRoutersMap, so the
|
||||||
|
// peer_index reference must still resolve.
|
||||||
|
c := newTestComponents()
|
||||||
|
delete(c.Peers, "peer-c")
|
||||||
|
routerPeer := &nbpeer.Peer{
|
||||||
|
ID: "peer-c", Key: testWgKeyC, IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}),
|
||||||
|
DNSLabel: "peerc", Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
c.RouterPeers = map[string]*nbpeer.Peer{"peer-c": routerPeer}
|
||||||
|
c.NetworkXIDToSeq = map[string]uint32{"net-1": 5}
|
||||||
|
c.RoutersMap = map[string]map[string]*routerTypes.NetworkRouter{
|
||||||
|
"net-1": {"peer-c": {ID: "r-1", AccountSeqID: 1, Peer: "peer-c", Enabled: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Contains(t, full.RoutersMap, uint32(5))
|
||||||
|
require.Len(t, full.RoutersMap[5].Entries, 1)
|
||||||
|
e := full.RoutersMap[5].Entries[0]
|
||||||
|
assert.True(t, e.PeerIndexSet, "router peer must be indexed even when not in c.Peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_DNSSettingsFiltersUnpersistedGroups(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.DNSSettings = &types.DNSSettings{
|
||||||
|
DisabledManagementGroups: []string{"group-src", "group-missing", "group-no-seq"},
|
||||||
|
}
|
||||||
|
c.Groups["group-no-seq"] = &types.Group{ID: "group-no-seq", AccountSeqID: 0}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.DnsSettings)
|
||||||
|
assert.Equal(t, []uint32{1}, full.DnsSettings.DisabledManagementGroupIds,
|
||||||
|
"only group-src (AccountSeqID=1) survives — missing and unpersisted are dropped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_GroupIDToUserIDs(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.GroupIDToUserIDs = map[string][]string{
|
||||||
|
"group-src": {"user-1", "user-2"},
|
||||||
|
"group-no-seq": {"user-3"}, // group not persisted → drop
|
||||||
|
"group-missing": {"user-4"}, // group not in components → drop
|
||||||
|
}
|
||||||
|
c.Groups["group-no-seq"] = &types.Group{ID: "group-no-seq", AccountSeqID: 0}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.GroupIdToUserIds, 1, "only persisted+present groups survive")
|
||||||
|
require.Contains(t, full.GroupIdToUserIds, uint32(1))
|
||||||
|
assert.ElementsMatch(t, []string{"user-1", "user-2"}, full.GroupIdToUserIds[1].UserIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToProxyPatch_EmptyInputReturnsNil(t *testing.T) {
|
||||||
|
assert.Nil(t, toProxyPatch(nil, "netbird.cloud", false, false))
|
||||||
|
assert.Nil(t, toProxyPatch(&types.NetworkMap{}, "netbird.cloud", false, false),
|
||||||
|
"empty NetworkMap (no peers, rules, routes etc) → nil patch so proto3 omits the field")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToProxyPatch_PopulatesAllFields(t *testing.T) {
|
||||||
|
nm := &types.NetworkMap{
|
||||||
|
Peers: []*nbpeer.Peer{{
|
||||||
|
ID: "ext-peer", Key: testWgKeyA, IP: netip.AddrFrom4([4]byte{100, 64, 0, 9}),
|
||||||
|
DNSLabel: "extpeer", Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}},
|
||||||
|
FirewallRules: []*types.FirewallRule{{
|
||||||
|
PeerIP: "100.64.0.9", Action: "accept", Direction: 0, Protocol: "tcp",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := toProxyPatch(nm, "netbird.cloud", false, false)
|
||||||
|
|
||||||
|
require.NotNil(t, patch)
|
||||||
|
assert.Len(t, patch.Peers, 1)
|
||||||
|
assert.Len(t, patch.FirewallRules, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncodeNetworkMapEnvelope_ProxyPatchPropagated covers the ProxyPatch
|
||||||
|
// pass-through in both encoder branches (normal path + nil-Components
|
||||||
|
// graceful-degrade). Without this test a regression that drops `ProxyPatch:`
|
||||||
|
// from one of the struct literals in components_encoder.go would slip past CI.
|
||||||
|
func TestEncodeNetworkMapEnvelope_ProxyPatchPropagated(t *testing.T) {
|
||||||
|
patch := &proto.ProxyPatch{
|
||||||
|
ForwardingRules: []*proto.ForwardingRule{{
|
||||||
|
Protocol: proto.RuleProtocol_TCP,
|
||||||
|
DestinationPort: &proto.PortInfo{PortSelection: &proto.PortInfo_Port{Port: 80}},
|
||||||
|
TranslatedAddress: net.IPv4(10, 0, 0, 1).To4(),
|
||||||
|
TranslatedPort: &proto.PortInfo{PortSelection: &proto.PortInfo_Port{Port: 8080}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("normal_path", func(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
ProxyPatch: patch,
|
||||||
|
}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.ProxyPatch, "ProxyPatch must propagate through the normal encode path")
|
||||||
|
assert.Len(t, full.ProxyPatch.ForwardingRules, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil_components_graceful_degrade", func(t *testing.T) {
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: nil,
|
||||||
|
ProxyPatch: patch,
|
||||||
|
}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.ProxyPatch, "ProxyPatch must propagate through the nil-Components branch too")
|
||||||
|
assert.Len(t, full.ProxyPatch.ForwardingRules, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) {
|
||||||
|
// nil Components → minimal envelope, no crash. Matches the legacy
|
||||||
|
// account_components.go:43 behaviour for missing/unvalidated peers.
|
||||||
|
env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: nil,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotNil(t, env)
|
||||||
|
full := env.GetFull()
|
||||||
|
require.NotNil(t, full)
|
||||||
|
require.NotNil(t, full.AccountSettings, "AccountSettings must always be non-nil")
|
||||||
|
assert.Equal(t, "netbird.cloud", full.DnsDomain)
|
||||||
|
assert.Empty(t, full.Peers)
|
||||||
|
assert.Empty(t, full.Policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) {
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
Network: &types.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}},
|
||||||
|
// AccountSettings deliberately nil
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.AccountSettings, "client dereferences AccountSettings unconditionally during Calculate(); a nil here would crash the receiver")
|
||||||
|
assert.False(t, full.AccountSettings.PeerLoginExpirationEnabled)
|
||||||
|
assert.Zero(t, full.AccountSettings.PeerLoginExpirationNs)
|
||||||
|
}
|
||||||
193
management/internals/shared/grpc/components_envelope_response.go
Normal file
193
management/internals/shared/grpc/components_envelope_response.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToComponentSyncResponse builds a SyncResponse carrying the compact
|
||||||
|
// NetworkMapEnvelope for capability-aware peers. The legacy proto.NetworkMap
|
||||||
|
// field is intentionally left empty — capable peers ignore it and the
|
||||||
|
// envelope alone is the authoritative wire shape.
|
||||||
|
//
|
||||||
|
// PeerConfig is computed once server-side using the receiving peer's own
|
||||||
|
// account-level network metadata. EnableSSH inside PeerConfig is left at
|
||||||
|
// peer.SSHEnabled (the peer's local setting); account-policy-driven SSH is
|
||||||
|
// computed by the client from the envelope's GroupIDToUserIDs / AllowedUserIDs
|
||||||
|
// inside Calculate(), so the SshConfig.SshEnabled bit may flip true on the
|
||||||
|
// client even though the server-side PeerConfig reports false.
|
||||||
|
func ToComponentSyncResponse(
|
||||||
|
ctx context.Context,
|
||||||
|
config *nbconfig.Config,
|
||||||
|
httpConfig *nbconfig.HttpServerConfig,
|
||||||
|
deviceFlowConfig *nbconfig.DeviceAuthorizationFlow,
|
||||||
|
peer *nbpeer.Peer,
|
||||||
|
turnCredentials *Token,
|
||||||
|
relayCredentials *Token,
|
||||||
|
components *types.NetworkMapComponents,
|
||||||
|
proxyPatch *types.NetworkMap,
|
||||||
|
dnsName string,
|
||||||
|
checks []*posture.Checks,
|
||||||
|
settings *types.Settings,
|
||||||
|
extraSettings *types.ExtraSettings,
|
||||||
|
peerGroups []string,
|
||||||
|
dnsFwdPort int64,
|
||||||
|
) *proto.SyncResponse {
|
||||||
|
network := networkOrZero(components)
|
||||||
|
enableSSH := computeSSHEnabledForPeer(components, peer)
|
||||||
|
peerConfig := toPeerConfig(peer, network, dnsName, settings, httpConfig, deviceFlowConfig, enableSSH)
|
||||||
|
|
||||||
|
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
|
||||||
|
useSourcePrefixes := peer.SupportsSourcePrefixes()
|
||||||
|
|
||||||
|
userIDClaim := auth.DefaultUserIDClaim
|
||||||
|
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
||||||
|
userIDClaim = httpConfig.AuthUserIDClaim
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: components,
|
||||||
|
PeerConfig: peerConfig,
|
||||||
|
DNSDomain: dnsName,
|
||||||
|
DNSForwarderPort: dnsFwdPort,
|
||||||
|
UserIDClaim: userIDClaim,
|
||||||
|
ProxyPatch: toProxyPatch(proxyPatch, dnsName, includeIPv6, useSourcePrefixes),
|
||||||
|
})
|
||||||
|
|
||||||
|
resp := &proto.SyncResponse{
|
||||||
|
PeerConfig: peerConfig,
|
||||||
|
NetworkMapEnvelope: envelope,
|
||||||
|
Checks: toProtocolChecks(ctx, checks),
|
||||||
|
}
|
||||||
|
|
||||||
|
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings)
|
||||||
|
resp.NetbirdConfig = integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkOrZero returns components.Network or a zero Network — toPeerConfig
|
||||||
|
// dereferences network.Net which would panic on nil.
|
||||||
|
func networkOrZero(c *types.NetworkMapComponents) *types.Network {
|
||||||
|
if c == nil || c.Network == nil {
|
||||||
|
return &types.Network{}
|
||||||
|
}
|
||||||
|
return c.Network
|
||||||
|
}
|
||||||
|
|
||||||
|
// toProxyPatch converts a proxy-injected *types.NetworkMap into the wire
|
||||||
|
// patch the components envelope ships alongside. Returns nil when there are
|
||||||
|
// no fragments to merge — proto3 omits a nil message field, so the receiver
|
||||||
|
// sees no patch and skips the merge step entirely.
|
||||||
|
//
|
||||||
|
// We reuse the legacy proto-conversion helpers (toProtocolRoutes,
|
||||||
|
// toProtocolFirewallRules, toProtocolRoutesFirewallRules,
|
||||||
|
// appendRemotePeerConfig, ForwardingRule.ToProto) because the proxy
|
||||||
|
// delivers fragments pre-expanded — there's no raw component shape to
|
||||||
|
// derive them from. Components purity isn't violated: proxy data isn't
|
||||||
|
// policy-graph-derived, it's externally injected post-Calculate, so the
|
||||||
|
// client merges it on top of its locally-computed NetworkMap.
|
||||||
|
func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePrefixes bool) *proto.ProxyPatch {
|
||||||
|
if nm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(nm.Peers) == 0 && len(nm.OfflinePeers) == 0 && len(nm.FirewallRules) == 0 &&
|
||||||
|
len(nm.Routes) == 0 && len(nm.RoutesFirewallRules) == 0 && len(nm.ForwardingRules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := &proto.ProxyPatch{
|
||||||
|
Peers: networkmap.AppendRemotePeerConfig(nil, nm.Peers, dnsName, includeIPv6),
|
||||||
|
OfflinePeers: networkmap.AppendRemotePeerConfig(nil, nm.OfflinePeers, dnsName, includeIPv6),
|
||||||
|
FirewallRules: networkmap.ToProtocolFirewallRules(nm.FirewallRules, includeIPv6, useSourcePrefixes),
|
||||||
|
Routes: networkmap.ToProtocolRoutes(nm.Routes),
|
||||||
|
RouteFirewallRules: networkmap.ToProtocolRoutesFirewallRules(nm.RoutesFirewallRules),
|
||||||
|
}
|
||||||
|
if len(nm.ForwardingRules) > 0 {
|
||||||
|
patch.ForwardingRules = make([]*proto.ForwardingRule, 0, len(nm.ForwardingRules))
|
||||||
|
for _, r := range nm.ForwardingRules {
|
||||||
|
patch.ForwardingRules = append(patch.ForwardingRules, r.ToProto())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeSSHEnabledForPeer mirrors the SSH-server-activation bit that
|
||||||
|
// Calculate() folds into NetworkMap.EnableSSH. Components-format peers
|
||||||
|
// receive a freshly-computed PeerConfig.SshConfig.SshEnabled at sync time;
|
||||||
|
// without this helper the field would be incorrectly false for any peer
|
||||||
|
// that's the destination of an SSH-enabling policy without having
|
||||||
|
// peer.SSHEnabled set locally.
|
||||||
|
//
|
||||||
|
// Mirrors the two activation paths in Calculate() (`networkmap_components.go`
|
||||||
|
// `getPeerConnectionResources`):
|
||||||
|
// 1. Explicit: rule.Protocol == NetbirdSSH and peer is in the rule's
|
||||||
|
// destinations.
|
||||||
|
// 2. Legacy implicit: rule covers TCP/22 or TCP/22022 (or ALL), peer is in
|
||||||
|
// destinations, AND the peer has SSHEnabled set locally — this is the
|
||||||
|
// "allow-all/TCP-22 implies SSH activation for SSH-capable peers" path.
|
||||||
|
//
|
||||||
|
// The full SSH AuthorizedUsers map is still produced by the client when it
|
||||||
|
// runs Calculate() over the envelope.
|
||||||
|
func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer) bool {
|
||||||
|
if c == nil || peer == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Mirror Calculate's `getAllPeersFromGroups` invariant: target peer must
|
||||||
|
// exist in c.Peers, otherwise no rule applies to it.
|
||||||
|
if _, ok := c.Peers[peer.ID]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, policy := range c.Policies {
|
||||||
|
if policy == nil || !policy.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, rule := range policy.Rules {
|
||||||
|
if ruleEnablesSSHForPeer(c, rule, peer) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ruleEnablesSSHForPeer returns true when rule is active, targets peer, and
|
||||||
|
// either explicitly authorises SSH or covers the legacy TCP/22 path while the
|
||||||
|
// peer itself has SSH enabled locally.
|
||||||
|
func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *types.PolicyRule, peer *nbpeer.Peer) bool {
|
||||||
|
if rule == nil || !rule.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !peerInDestinations(c, rule, peer.ID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if rule.Protocol == types.PolicyRuleProtocolNetbirdSSH {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return peer.SSHEnabled && types.PolicyRuleImpliesLegacySSH(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// peerInDestinations reports whether peerID is in any of rule.Destinations'
|
||||||
|
// groups (or matches DestinationResource if it's a peer-typed resource —
|
||||||
|
// for non-peer types Calculate falls through to group lookup, so we mirror
|
||||||
|
// that exactly to avoid silent divergence).
|
||||||
|
func peerInDestinations(c *types.NetworkMapComponents, rule *types.PolicyRule, peerID string) bool {
|
||||||
|
if rule.DestinationResource.Type == types.ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||||
|
return rule.DestinationResource.ID == peerID
|
||||||
|
}
|
||||||
|
for _, groupID := range rule.Destinations {
|
||||||
|
if c.IsPeerInGroup(peerID, groupID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestComputeSSHEnabledForPeer covers both Calculate-mirroring branches:
|
||||||
|
// explicit NetbirdSSH protocol, and the legacy implicit case where a
|
||||||
|
// TCP/22 (or 22022 / ALL / port-range-covering-22) rule activates SSH when
|
||||||
|
// the destination peer has SSHEnabled=true locally. Belt-and-suspenders for
|
||||||
|
// the B1 fix that the prod-DB equivalence test alone wouldn't have caught
|
||||||
|
// if no account had this combination.
|
||||||
|
func TestComputeSSHEnabledForPeer(t *testing.T) {
|
||||||
|
const targetPeerID = "target"
|
||||||
|
const targetGroupID = "g_dst"
|
||||||
|
|
||||||
|
mkComponents := func(rule *types.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) {
|
||||||
|
peer := &nbpeer.Peer{ID: targetPeerID, SSHEnabled: sshEnabled}
|
||||||
|
group := &types.Group{ID: targetGroupID, Name: "dst", Peers: []string{targetPeerID}}
|
||||||
|
return &types.NetworkMapComponents{
|
||||||
|
Peers: map[string]*nbpeer.Peer{targetPeerID: peer},
|
||||||
|
Groups: map[string]*types.Group{targetGroupID: group},
|
||||||
|
Policies: []*types.Policy{{
|
||||||
|
ID: "p",
|
||||||
|
Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{rule},
|
||||||
|
}},
|
||||||
|
}, peer
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
peerSSH bool
|
||||||
|
rule types.PolicyRule
|
||||||
|
wantEnabled bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "explicit-netbird-ssh-activates-regardless-of-peer-ssh",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-tcp-22-with-peer-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-tcp-22-without-peer-ssh-disabled",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-tcp-22022-with-peer-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22022"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-all-protocol-with-peer-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolALL,
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-port-range-covers-22",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true,
|
||||||
|
Protocol: types.PolicyRuleProtocolTCP,
|
||||||
|
PortRanges: []types.RulePortRange{{Start: 20, End: 30}},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tcp-80-no-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"80"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled-rule-skipped",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: false, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "peer-not-in-destinations",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{"g_other"}, // target not in this group
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "peer-typed-destination-resource-matches",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true,
|
||||||
|
Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
DestinationResource: types.Resource{ID: targetPeerID, Type: types.ResourceTypePeer},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-peer-destination-resource-falls-through-to-groups",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true,
|
||||||
|
Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
DestinationResource: types.Resource{ID: targetPeerID, Type: "host"}, // wrong type
|
||||||
|
Destinations: []string{targetGroupID}, // saved by group fallback
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c, peer := mkComponents(&tc.rule, tc.peerSSH)
|
||||||
|
got := computeSSHEnabledForPeer(c, peer)
|
||||||
|
assert.Equal(t, tc.wantEnabled, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComputeSSHEnabledForPeer_TargetMissingFromComponents covers the
|
||||||
|
// belt-and-suspenders presence guard mirroring Calculate's
|
||||||
|
// getAllPeersFromGroups invariant.
|
||||||
|
func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) {
|
||||||
|
peer := &nbpeer.Peer{ID: "missing", SSHEnabled: true}
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
Peers: map[string]*nbpeer.Peer{}, // target peer NOT present
|
||||||
|
Groups: map[string]*types.Group{
|
||||||
|
"g": {ID: "g", Peers: []string{"missing"}},
|
||||||
|
},
|
||||||
|
Policies: []*types.Policy{{
|
||||||
|
ID: "p", Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{"g"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
assert.False(t, computeSSHEnabledForPeer(c, peer),
|
||||||
|
"missing target peer must short-circuit to false, not consult policies")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComputeSSHEnabledForPeer_NilInputs guards the cheap nil-checks at
|
||||||
|
// function entry — Calculate doesn't accept nil either, but the helper is
|
||||||
|
// exported indirectly via ToComponentSyncResponse and may receive nil
|
||||||
|
// components on graceful-degrade paths.
|
||||||
|
func TestComputeSSHEnabledForPeer_NilInputs(t *testing.T) {
|
||||||
|
assert.False(t, computeSSHEnabledForPeer(nil, &nbpeer.Peer{ID: "x"}))
|
||||||
|
assert.False(t, computeSSHEnabledForPeer(&types.NetworkMapComponents{}, nil))
|
||||||
|
}
|
||||||
@@ -6,26 +6,19 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
goproto "google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
|
|
||||||
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/ssh/auth"
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
"github.com/netbirdio/netbird/management/server/posture"
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
nbroute "github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
"github.com/netbirdio/netbird/shared/sshauth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings) *proto.NetbirdConfig {
|
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings) *proto.NetbirdConfig {
|
||||||
@@ -140,8 +133,8 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
||||||
NetworkMap: &proto.NetworkMap{
|
NetworkMap: &proto.NetworkMap{
|
||||||
Serial: networkMap.Network.CurrentSerial(),
|
Serial: networkMap.Network.CurrentSerial(),
|
||||||
Routes: toProtocolRoutes(networkMap.Routes),
|
Routes: networkmap.ToProtocolRoutes(networkMap.Routes),
|
||||||
DNSConfig: toProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort),
|
DNSConfig: networkmap.ToProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort),
|
||||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
||||||
},
|
},
|
||||||
Checks: toProtocolChecks(ctx, checks),
|
Checks: toProtocolChecks(ctx, checks),
|
||||||
@@ -154,19 +147,19 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
response.NetworkMap.PeerConfig = response.PeerConfig
|
response.NetworkMap.PeerConfig = response.PeerConfig
|
||||||
|
|
||||||
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
|
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
|
||||||
remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6)
|
remotePeers = networkmap.AppendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6)
|
||||||
response.RemotePeers = remotePeers
|
response.RemotePeers = remotePeers
|
||||||
response.NetworkMap.RemotePeers = remotePeers
|
response.NetworkMap.RemotePeers = remotePeers
|
||||||
response.RemotePeersIsEmpty = len(remotePeers) == 0
|
response.RemotePeersIsEmpty = len(remotePeers) == 0
|
||||||
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
|
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
|
||||||
|
|
||||||
response.NetworkMap.OfflinePeers = appendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6)
|
response.NetworkMap.OfflinePeers = networkmap.AppendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6)
|
||||||
|
|
||||||
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes)
|
firewallRules := networkmap.ToProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes)
|
||||||
response.NetworkMap.FirewallRules = firewallRules
|
response.NetworkMap.FirewallRules = firewallRules
|
||||||
response.NetworkMap.FirewallRulesIsEmpty = len(firewallRules) == 0
|
response.NetworkMap.FirewallRulesIsEmpty = len(firewallRules) == 0
|
||||||
|
|
||||||
routesFirewallRules := toProtocolRoutesFirewallRules(networkMap.RoutesFirewallRules)
|
routesFirewallRules := networkmap.ToProtocolRoutesFirewallRules(networkMap.RoutesFirewallRules)
|
||||||
response.NetworkMap.RoutesFirewallRules = routesFirewallRules
|
response.NetworkMap.RoutesFirewallRules = routesFirewallRules
|
||||||
response.NetworkMap.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
response.NetworkMap.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
||||||
|
|
||||||
@@ -179,7 +172,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
}
|
}
|
||||||
|
|
||||||
if networkMap.AuthorizedUsers != nil {
|
if networkMap.AuthorizedUsers != nil {
|
||||||
hashedUsers, machineUsers := buildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers)
|
hashedUsers, machineUsers := networkmap.BuildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers)
|
||||||
userIDClaim := auth.DefaultUserIDClaim
|
userIDClaim := auth.DefaultUserIDClaim
|
||||||
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
||||||
userIDClaim = httpConfig.AuthUserIDClaim
|
userIDClaim = httpConfig.AuthUserIDClaim
|
||||||
@@ -187,110 +180,9 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
response.NetworkMap.SshAuth = &proto.SSHAuth{AuthorizedUsers: hashedUsers, MachineUsers: machineUsers, UserIDClaim: userIDClaim}
|
response.NetworkMap.SshAuth = &proto.SSHAuth{AuthorizedUsers: hashedUsers, MachineUsers: machineUsers, UserIDClaim: userIDClaim}
|
||||||
}
|
}
|
||||||
|
|
||||||
// settings == nil → field stays nil → "no info in this snapshot", client
|
|
||||||
// preserves the deadline it already had. settings non-nil → emit either a
|
|
||||||
// valid deadline or the explicit-zero "disabled" sentinel via
|
|
||||||
// encodeSessionExpiresAt.
|
|
||||||
if settings != nil {
|
|
||||||
response.SessionExpiresAt = encodeSessionExpiresAt(
|
|
||||||
peer.SessionExpiresAt(settings.PeerLoginExpirationEnabled, settings.PeerLoginExpiration),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeSessionExpiresAt encodes a server-side deadline into the 3-state wire
|
|
||||||
// representation used on LoginResponse, SyncResponse and
|
|
||||||
// ExtendAuthSessionResponse. See the proto comments on those messages.
|
|
||||||
//
|
|
||||||
// - deadline.IsZero() → returns &Timestamp{} (seconds=0, nanos=0): the
|
|
||||||
// "expiry disabled or peer is not SSO-tracked" sentinel; the client clears
|
|
||||||
// its anchor.
|
|
||||||
// - deadline non-zero → returns timestamppb.New(deadline): the new absolute
|
|
||||||
// UTC deadline.
|
|
||||||
//
|
|
||||||
// Returning nil ("no info, preserve client's anchor") is the caller's job —
|
|
||||||
// only meaningful on Sync builds where settings were not resolved.
|
|
||||||
func encodeSessionExpiresAt(deadline time.Time) *timestamppb.Timestamp {
|
|
||||||
if deadline.IsZero() {
|
|
||||||
return ×tamppb.Timestamp{}
|
|
||||||
}
|
|
||||||
return timestamppb.New(deadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]map[string]struct{}) ([][]byte, map[string]*proto.MachineUserIndexes) {
|
|
||||||
userIDToIndex := make(map[string]uint32)
|
|
||||||
var hashedUsers [][]byte
|
|
||||||
machineUsers := make(map[string]*proto.MachineUserIndexes, len(authorizedUsers))
|
|
||||||
|
|
||||||
for machineUser, users := range authorizedUsers {
|
|
||||||
indexes := make([]uint32, 0, len(users))
|
|
||||||
for userID := range users {
|
|
||||||
idx, exists := userIDToIndex[userID]
|
|
||||||
if !exists {
|
|
||||||
hash, err := sshauth.HashUserID(userID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithContext(ctx).Errorf("failed to hash user id %s: %v", userID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
idx = uint32(len(hashedUsers))
|
|
||||||
userIDToIndex[userID] = idx
|
|
||||||
hashedUsers = append(hashedUsers, hash[:])
|
|
||||||
}
|
|
||||||
indexes = append(indexes, idx)
|
|
||||||
}
|
|
||||||
machineUsers[machineUser] = &proto.MachineUserIndexes{Indexes: indexes}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashedUsers, machineUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig {
|
|
||||||
for _, rPeer := range peers {
|
|
||||||
allowedIPs := []string{rPeer.IP.String() + "/32"}
|
|
||||||
if includeIPv6 && rPeer.IPv6.IsValid() {
|
|
||||||
allowedIPs = append(allowedIPs, rPeer.IPv6.String()+"/128")
|
|
||||||
}
|
|
||||||
dst = append(dst, &proto.RemotePeerConfig{
|
|
||||||
WgPubKey: rPeer.Key,
|
|
||||||
AllowedIps: allowedIPs,
|
|
||||||
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
|
||||||
Fqdn: rPeer.FQDN(dnsName),
|
|
||||||
AgentVersion: rPeer.Meta.WtVersion,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// toProtocolDNSConfig converts nbdns.Config to proto.DNSConfig using the cache
|
|
||||||
func toProtocolDNSConfig(update nbdns.Config, cache *cache.DNSConfigCache, forwardPort int64) *proto.DNSConfig {
|
|
||||||
protoUpdate := &proto.DNSConfig{
|
|
||||||
ServiceEnable: update.ServiceEnable,
|
|
||||||
CustomZones: make([]*proto.CustomZone, 0, len(update.CustomZones)),
|
|
||||||
NameServerGroups: make([]*proto.NameServerGroup, 0, len(update.NameServerGroups)),
|
|
||||||
ForwarderPort: forwardPort,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, zone := range update.CustomZones {
|
|
||||||
protoZone := convertToProtoCustomZone(zone)
|
|
||||||
protoUpdate.CustomZones = append(protoUpdate.CustomZones, protoZone)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nsGroup := range update.NameServerGroups {
|
|
||||||
cacheKey := nsGroup.ID
|
|
||||||
if cachedGroup, exists := cache.GetNameServerGroup(cacheKey); exists {
|
|
||||||
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, cachedGroup)
|
|
||||||
} else {
|
|
||||||
protoGroup := convertToProtoNameServerGroup(nsGroup)
|
|
||||||
cache.SetNameServerGroup(cacheKey, protoGroup)
|
|
||||||
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, protoGroup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return protoUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
||||||
switch configProto {
|
switch configProto {
|
||||||
case nbconfig.UDP:
|
case nbconfig.UDP:
|
||||||
@@ -308,204 +200,6 @@ func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
|
||||||
protoRoutes := make([]*proto.Route, 0, len(routes))
|
|
||||||
for _, r := range routes {
|
|
||||||
protoRoutes = append(protoRoutes, toProtocolRoute(r))
|
|
||||||
}
|
|
||||||
return protoRoutes
|
|
||||||
}
|
|
||||||
|
|
||||||
func toProtocolRoute(route *nbroute.Route) *proto.Route {
|
|
||||||
return &proto.Route{
|
|
||||||
ID: string(route.ID),
|
|
||||||
NetID: string(route.NetID),
|
|
||||||
Network: route.Network.String(),
|
|
||||||
Domains: route.Domains.ToPunycodeList(),
|
|
||||||
NetworkType: int64(route.NetworkType),
|
|
||||||
Peer: route.Peer,
|
|
||||||
Metric: int64(route.Metric),
|
|
||||||
Masquerade: route.Masquerade,
|
|
||||||
KeepRoute: route.KeepRoute,
|
|
||||||
SkipAutoApply: route.SkipAutoApply,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toProtocolFirewallRules converts the firewall rules to the protocol firewall rules.
|
|
||||||
// When useSourcePrefixes is true, the compact SourcePrefixes field is populated
|
|
||||||
// alongside the deprecated PeerIP for forward compatibility.
|
|
||||||
// Wildcard rules ("0.0.0.0") are expanded into separate v4 and v6 SourcePrefixes
|
|
||||||
// when includeIPv6 is true.
|
|
||||||
func toProtocolFirewallRules(rules []*types.FirewallRule, includeIPv6, useSourcePrefixes bool) []*proto.FirewallRule {
|
|
||||||
result := make([]*proto.FirewallRule, 0, len(rules))
|
|
||||||
for i := range rules {
|
|
||||||
rule := rules[i]
|
|
||||||
|
|
||||||
fwRule := &proto.FirewallRule{
|
|
||||||
PolicyID: []byte(rule.PolicyID),
|
|
||||||
PeerIP: rule.PeerIP, //nolint:staticcheck // populated for backward compatibility
|
|
||||||
Direction: getProtoDirection(rule.Direction),
|
|
||||||
Action: getProtoAction(rule.Action),
|
|
||||||
Protocol: getProtoProtocol(rule.Protocol),
|
|
||||||
Port: rule.Port,
|
|
||||||
}
|
|
||||||
|
|
||||||
if useSourcePrefixes && rule.PeerIP != "" {
|
|
||||||
result = append(result, populateSourcePrefixes(fwRule, rule, includeIPv6)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldUsePortRange(fwRule) {
|
|
||||||
fwRule.PortInfo = rule.PortRange.ToProto()
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, fwRule)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// populateSourcePrefixes sets SourcePrefixes on fwRule and returns any
|
|
||||||
// additional rules needed (e.g. a v6 wildcard clone when the peer IP is unspecified).
|
|
||||||
func populateSourcePrefixes(fwRule *proto.FirewallRule, rule *types.FirewallRule, includeIPv6 bool) []*proto.FirewallRule {
|
|
||||||
addr, err := netip.ParseAddr(rule.PeerIP)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addr.IsUnspecified() {
|
|
||||||
fwRule.SourcePrefixes = [][]byte{netiputil.EncodeAddr(addr.Unmap())}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv4Unspecified/0 is always valid, error is impossible.
|
|
||||||
v4Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
|
|
||||||
fwRule.SourcePrefixes = [][]byte{v4Wildcard}
|
|
||||||
|
|
||||||
if !includeIPv6 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v6Rule := goproto.Clone(fwRule).(*proto.FirewallRule)
|
|
||||||
v6Rule.PeerIP = "::" //nolint:staticcheck // populated for backward compatibility
|
|
||||||
// IPv6Unspecified/0 is always valid, error is impossible.
|
|
||||||
v6Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
|
|
||||||
v6Rule.SourcePrefixes = [][]byte{v6Wildcard}
|
|
||||||
if shouldUsePortRange(v6Rule) {
|
|
||||||
v6Rule.PortInfo = rule.PortRange.ToProto()
|
|
||||||
}
|
|
||||||
return []*proto.FirewallRule{v6Rule}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoDirection converts the direction to proto.RuleDirection.
|
|
||||||
func getProtoDirection(direction int) proto.RuleDirection {
|
|
||||||
if direction == types.FirewallRuleDirectionOUT {
|
|
||||||
return proto.RuleDirection_OUT
|
|
||||||
}
|
|
||||||
return proto.RuleDirection_IN
|
|
||||||
}
|
|
||||||
|
|
||||||
func toProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.RouteFirewallRule {
|
|
||||||
result := make([]*proto.RouteFirewallRule, len(rules))
|
|
||||||
for i := range rules {
|
|
||||||
rule := rules[i]
|
|
||||||
result[i] = &proto.RouteFirewallRule{
|
|
||||||
SourceRanges: rule.SourceRanges,
|
|
||||||
Action: getProtoAction(rule.Action),
|
|
||||||
Destination: rule.Destination,
|
|
||||||
Protocol: getProtoProtocol(rule.Protocol),
|
|
||||||
PortInfo: getProtoPortInfo(rule),
|
|
||||||
IsDynamic: rule.IsDynamic,
|
|
||||||
Domains: rule.Domains.ToPunycodeList(),
|
|
||||||
PolicyID: []byte(rule.PolicyID),
|
|
||||||
RouteID: string(rule.RouteID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoAction converts the action to proto.RuleAction.
|
|
||||||
func getProtoAction(action string) proto.RuleAction {
|
|
||||||
if action == string(types.PolicyTrafficActionDrop) {
|
|
||||||
return proto.RuleAction_DROP
|
|
||||||
}
|
|
||||||
return proto.RuleAction_ACCEPT
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoProtocol converts the protocol to proto.RuleProtocol.
|
|
||||||
func getProtoProtocol(protocol string) proto.RuleProtocol {
|
|
||||||
switch types.PolicyRuleProtocolType(protocol) {
|
|
||||||
case types.PolicyRuleProtocolALL:
|
|
||||||
return proto.RuleProtocol_ALL
|
|
||||||
case types.PolicyRuleProtocolTCP:
|
|
||||||
return proto.RuleProtocol_TCP
|
|
||||||
case types.PolicyRuleProtocolUDP:
|
|
||||||
return proto.RuleProtocol_UDP
|
|
||||||
case types.PolicyRuleProtocolICMP:
|
|
||||||
return proto.RuleProtocol_ICMP
|
|
||||||
default:
|
|
||||||
return proto.RuleProtocol_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoPortInfo converts the port info to proto.PortInfo.
|
|
||||||
func getProtoPortInfo(rule *types.RouteFirewallRule) *proto.PortInfo {
|
|
||||||
var portInfo proto.PortInfo
|
|
||||||
if rule.Port != 0 {
|
|
||||||
portInfo.PortSelection = &proto.PortInfo_Port{Port: uint32(rule.Port)}
|
|
||||||
} else if portRange := rule.PortRange; portRange.Start != 0 && portRange.End != 0 {
|
|
||||||
portInfo.PortSelection = &proto.PortInfo_Range_{
|
|
||||||
Range: &proto.PortInfo_Range{
|
|
||||||
Start: uint32(portRange.Start),
|
|
||||||
End: uint32(portRange.End),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &portInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldUsePortRange(rule *proto.FirewallRule) bool {
|
|
||||||
return rule.Port == "" && (rule.Protocol == proto.RuleProtocol_UDP || rule.Protocol == proto.RuleProtocol_TCP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert nbdns.CustomZone to proto.CustomZone
|
|
||||||
func convertToProtoCustomZone(zone nbdns.CustomZone) *proto.CustomZone {
|
|
||||||
protoZone := &proto.CustomZone{
|
|
||||||
Domain: zone.Domain,
|
|
||||||
Records: make([]*proto.SimpleRecord, 0, len(zone.Records)),
|
|
||||||
SearchDomainDisabled: zone.SearchDomainDisabled,
|
|
||||||
NonAuthoritative: zone.NonAuthoritative,
|
|
||||||
}
|
|
||||||
for _, record := range zone.Records {
|
|
||||||
protoZone.Records = append(protoZone.Records, &proto.SimpleRecord{
|
|
||||||
Name: record.Name,
|
|
||||||
Type: int64(record.Type),
|
|
||||||
Class: record.Class,
|
|
||||||
TTL: int64(record.TTL),
|
|
||||||
RData: record.RData,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return protoZone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert nbdns.NameServerGroup to proto.NameServerGroup
|
|
||||||
func convertToProtoNameServerGroup(nsGroup *nbdns.NameServerGroup) *proto.NameServerGroup {
|
|
||||||
protoGroup := &proto.NameServerGroup{
|
|
||||||
Primary: nsGroup.Primary,
|
|
||||||
Domains: nsGroup.Domains,
|
|
||||||
SearchDomainsEnabled: nsGroup.SearchDomainsEnabled,
|
|
||||||
NameServers: make([]*proto.NameServer, 0, len(nsGroup.NameServers)),
|
|
||||||
}
|
|
||||||
for _, ns := range nsGroup.NameServers {
|
|
||||||
protoGroup.NameServers = append(protoGroup.NameServers, &proto.NameServer{
|
|
||||||
IP: ns.IP.String(),
|
|
||||||
Port: int64(ns.Port),
|
|
||||||
NSType: int64(ns.NSType),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return protoGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildJWTConfig constructs JWT configuration for SSH servers from management server config
|
// buildJWTConfig constructs JWT configuration for SSH servers from management server config
|
||||||
func buildJWTConfig(config *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow) *proto.JWTConfig {
|
func buildJWTConfig(config *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow) *proto.JWTConfig {
|
||||||
if config == nil || config.AuthAudience == "" {
|
if config == nil || config.AuthAudience == "" {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
||||||
@@ -62,13 +62,13 @@ func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First run with config1
|
// First run with config1
|
||||||
result1 := toProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
result1 := networkmap.ToProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
||||||
|
|
||||||
// Second run with config2
|
// Second run with config2
|
||||||
result2 := toProtocolDNSConfig(config2, &cache, int64(network_map.DnsForwarderPort))
|
result2 := networkmap.ToProtocolDNSConfig(config2, &cache, int64(network_map.DnsForwarderPort))
|
||||||
|
|
||||||
// Third run with config1 again
|
// Third run with config1 again
|
||||||
result3 := toProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
result3 := networkmap.ToProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
||||||
|
|
||||||
// Verify that result1 and result3 are identical
|
// Verify that result1 and result3 are identical
|
||||||
if !reflect.DeepEqual(result1, result3) {
|
if !reflect.DeepEqual(result1, result3) {
|
||||||
@@ -100,7 +100,7 @@ func BenchmarkToProtocolDNSConfig(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
toProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ func BenchmarkToProtocolDNSConfig(b *testing.B) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
cache := &cache.DNSConfigCache{}
|
cache := &cache.DNSConfigCache{}
|
||||||
toProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -201,29 +201,3 @@ func TestBuildJWTConfig_Audiences(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEncodeSessionExpiresAt pins the wire encoding the client's
|
|
||||||
// applySessionDeadline depends on:
|
|
||||||
//
|
|
||||||
// - zero deadline → &Timestamp{} (seconds=0, nanos=0): the explicit
|
|
||||||
// "expiry disabled or peer is not SSO-tracked" sentinel.
|
|
||||||
// - non-zero → timestamppb.New(deadline): the absolute UTC deadline.
|
|
||||||
//
|
|
||||||
// The third state (nil pointer = "no info in this snapshot") is the caller's
|
|
||||||
// responsibility on the Sync path when settings could not be resolved; the
|
|
||||||
// helper itself never returns nil.
|
|
||||||
func TestEncodeSessionExpiresAt(t *testing.T) {
|
|
||||||
t.Run("zero deadline encodes as explicit-zero sentinel", func(t *testing.T) {
|
|
||||||
got := encodeSessionExpiresAt(time.Time{})
|
|
||||||
assert.NotNil(t, got, "must not return nil; nil means 'no info', not 'disabled'")
|
|
||||||
assert.Equal(t, int64(0), got.GetSeconds())
|
|
||||||
assert.Equal(t, int32(0), got.GetNanos())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non-zero deadline round-trips", func(t *testing.T) {
|
|
||||||
deadline := time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
||||||
got := encodeSessionExpiresAt(deadline)
|
|
||||||
assert.NotNil(t, got)
|
|
||||||
assert.True(t, got.AsTime().Equal(deadline))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -351,7 +351,6 @@ func (s *ProxyServiceServer) registerProxyConnection(ctx context.Context, params
|
|||||||
SupportsCustomPorts: c.SupportsCustomPorts,
|
SupportsCustomPorts: c.SupportsCustomPorts,
|
||||||
RequireSubdomain: c.RequireSubdomain,
|
RequireSubdomain: c.RequireSubdomain,
|
||||||
SupportsCrowdsec: c.SupportsCrowdsec,
|
SupportsCrowdsec: c.SupportsCrowdsec,
|
||||||
Private: c.Private,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,11 +754,6 @@ func (s *ProxyServiceServer) SendServiceUpdate(update *proto.GetMappingUpdateRes
|
|||||||
InitialSyncComplete: update.InitialSyncComplete,
|
InitialSyncComplete: update.InitialSyncComplete,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Drop mappings the proxy lacks capability for (e.g. private without SupportsPrivateService).
|
|
||||||
connUpdate = filterMappingsForProxy(conn, connUpdate)
|
|
||||||
if connUpdate == nil || len(connUpdate.Mapping) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
resp := s.perProxyMessage(connUpdate, conn.proxyID)
|
resp := s.perProxyMessage(connUpdate, conn.proxyID)
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
log.Warnf("Token generation failed for proxy %s, disconnecting to force resync", conn.proxyID)
|
log.Warnf("Token generation failed for proxy %s, disconnecting to force resync", conn.proxyID)
|
||||||
@@ -888,20 +882,16 @@ func (s *ProxyServiceServer) SendServiceUpdateToCluster(ctx context.Context, upd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxyAcceptsMapping returns whether the proxy can receive this mapping.
|
// proxyAcceptsMapping returns whether the proxy should receive this mapping.
|
||||||
// Private mappings require SupportsPrivateService; custom-port L4 mappings
|
// Old proxies that never reported capabilities are skipped for non-TLS L4
|
||||||
// require SupportsCustomPorts. Remove operations always pass so proxies can
|
// mappings with a custom listen port, since they don't understand the
|
||||||
// clean up.
|
// protocol. Proxies that report capabilities (even SupportsCustomPorts=false)
|
||||||
|
// are new enough to handle the mapping. TLS uses SNI routing and works on
|
||||||
|
// any proxy. Delete operations are always sent so proxies can clean up.
|
||||||
func proxyAcceptsMapping(conn *proxyConnection, mapping *proto.ProxyMapping) bool {
|
func proxyAcceptsMapping(conn *proxyConnection, mapping *proto.ProxyMapping) bool {
|
||||||
if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
|
if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if mapping.GetPrivate() {
|
|
||||||
caps := conn.capabilities
|
|
||||||
if caps == nil || caps.SupportsPrivateService == nil || !*caps.SupportsPrivateService {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mapping.ListenPort == 0 || mapping.Mode == "tls" {
|
if mapping.ListenPort == 0 || mapping.Mode == "tls" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -910,29 +900,6 @@ func proxyAcceptsMapping(conn *proxyConnection, mapping *proto.ProxyMapping) boo
|
|||||||
return conn.capabilities != nil && conn.capabilities.SupportsCustomPorts != nil
|
return conn.capabilities != nil && conn.capabilities.SupportsCustomPorts != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterMappingsForProxy drops mappings the proxy cannot safely receive
|
|
||||||
// (e.g. private mappings to a proxy without SupportsPrivateService).
|
|
||||||
// Returns the input unchanged when no filtering is needed.
|
|
||||||
func filterMappingsForProxy(conn *proxyConnection, update *proto.GetMappingUpdateResponse) *proto.GetMappingUpdateResponse {
|
|
||||||
if update == nil || len(update.Mapping) == 0 {
|
|
||||||
return update
|
|
||||||
}
|
|
||||||
kept := make([]*proto.ProxyMapping, 0, len(update.Mapping))
|
|
||||||
for _, m := range update.Mapping {
|
|
||||||
if !proxyAcceptsMapping(conn, m) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kept = append(kept, m)
|
|
||||||
}
|
|
||||||
if len(kept) == len(update.Mapping) {
|
|
||||||
return update
|
|
||||||
}
|
|
||||||
return &proto.GetMappingUpdateResponse{
|
|
||||||
Mapping: kept,
|
|
||||||
InitialSyncComplete: update.InitialSyncComplete,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// perProxyMessage returns a copy of update with a fresh one-time token for
|
// perProxyMessage returns a copy of update with a fresh one-time token for
|
||||||
// create/update operations. For delete operations the original mapping is
|
// create/update operations. For delete operations the original mapping is
|
||||||
// used unchanged because proxies do not need to authenticate for removal.
|
// used unchanged because proxies do not need to authenticate for removal.
|
||||||
@@ -994,10 +961,7 @@ func (s *ProxyServiceServer) Authenticate(ctx context.Context, req *proto.Authen
|
|||||||
|
|
||||||
authenticated, userId, method := s.authenticateRequest(ctx, req, service)
|
authenticated, userId, method := s.authenticateRequest(ctx, req, service)
|
||||||
|
|
||||||
// Non-OIDC schemes (PIN/Password/Header) authenticate against per-service
|
token, err := s.generateSessionToken(ctx, authenticated, service, userId, method)
|
||||||
// secrets and have no user-level group context, so groups stay nil. Email
|
|
||||||
// is also empty — these schemes don't resolve a user record at sign time.
|
|
||||||
token, err := s.generateSessionToken(ctx, authenticated, service, userId, "", method, nil, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1086,7 +1050,7 @@ func (s *ProxyServiceServer) logAuthenticationError(ctx context.Context, err err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProxyServiceServer) generateSessionToken(ctx context.Context, authenticated bool, service *rpservice.Service, userId, userEmail string, method proxyauth.Method, groupIDs, groupNames []string) (string, error) {
|
func (s *ProxyServiceServer) generateSessionToken(ctx context.Context, authenticated bool, service *rpservice.Service, userId string, method proxyauth.Method) (string, error) {
|
||||||
if !authenticated || service.SessionPrivateKey == "" {
|
if !authenticated || service.SessionPrivateKey == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@@ -1094,11 +1058,8 @@ func (s *ProxyServiceServer) generateSessionToken(ctx context.Context, authentic
|
|||||||
token, err := sessionkey.SignToken(
|
token, err := sessionkey.SignToken(
|
||||||
service.SessionPrivateKey,
|
service.SessionPrivateKey,
|
||||||
userId,
|
userId,
|
||||||
userEmail,
|
|
||||||
service.Domain,
|
service.Domain,
|
||||||
method,
|
method,
|
||||||
groupIDs,
|
|
||||||
groupNames,
|
|
||||||
proxyauth.DefaultSessionExpiry,
|
proxyauth.DefaultSessionExpiry,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1109,26 +1070,6 @@ func (s *ProxyServiceServer) generateSessionToken(ctx context.Context, authentic
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pairGroupIDsAndNames splits a slice of resolved *types.Group records
|
|
||||||
// into parallel id and name slices. ids[i] and names[i] always pair to
|
|
||||||
// the same group. nil entries (orphan ids the manager couldn't resolve)
|
|
||||||
// are skipped so the consumer can rely on positional pairing.
|
|
||||||
func pairGroupIDsAndNames(groups []*types.Group) (ids, names []string) {
|
|
||||||
if len(groups) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
ids = make([]string, 0, len(groups))
|
|
||||||
names = make([]string, 0, len(groups))
|
|
||||||
for _, g := range groups {
|
|
||||||
if g == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ids = append(ids, g.ID)
|
|
||||||
names = append(names, g.Name)
|
|
||||||
}
|
|
||||||
return ids, names
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendStatusUpdate handles status updates from proxy clients.
|
// SendStatusUpdate handles status updates from proxy clients.
|
||||||
func (s *ProxyServiceServer) SendStatusUpdate(ctx context.Context, req *proto.SendStatusUpdateRequest) (*proto.SendStatusUpdateResponse, error) {
|
func (s *ProxyServiceServer) SendStatusUpdate(ctx context.Context, req *proto.SendStatusUpdateRequest) (*proto.SendStatusUpdateResponse, error) {
|
||||||
if err := enforceAccountScope(ctx, req.GetAccountId()); err != nil {
|
if err := enforceAccountScope(ctx, req.GetAccountId()); err != nil {
|
||||||
@@ -1393,9 +1334,7 @@ func (s *ProxyServiceServer) ValidateState(state string) (verifier, redirectURL
|
|||||||
return verifier, redirectURL, nil
|
return verifier, redirectURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSessionToken creates a signed session JWT for the given domain and
|
// GenerateSessionToken creates a signed session JWT for the given domain and user.
|
||||||
// user. The user's group memberships are embedded in the token so policy-aware
|
|
||||||
// middlewares on the proxy can authorise without an extra management round-trip.
|
|
||||||
func (s *ProxyServiceServer) GenerateSessionToken(ctx context.Context, domain, userID string, method proxyauth.Method) (string, error) {
|
func (s *ProxyServiceServer) GenerateSessionToken(ctx context.Context, domain, userID string, method proxyauth.Method) (string, error) {
|
||||||
service, err := s.getServiceByDomain(ctx, domain)
|
service, err := s.getServiceByDomain(ctx, domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1406,29 +1345,11 @@ func (s *ProxyServiceServer) GenerateSessionToken(ctx context.Context, domain, u
|
|||||||
return "", fmt.Errorf("no session key configured for domain: %s", domain)
|
return "", fmt.Errorf("no session key configured for domain: %s", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
email string
|
|
||||||
groupIDs []string
|
|
||||||
groupNames []string
|
|
||||||
)
|
|
||||||
if s.usersManager != nil {
|
|
||||||
user, userGroups, uerr := s.usersManager.GetUserWithGroups(ctx, userID)
|
|
||||||
if uerr != nil {
|
|
||||||
log.WithContext(ctx).Debugf("session token mint: lookup user %s: %v", userID, uerr)
|
|
||||||
} else if user != nil {
|
|
||||||
email = user.Email
|
|
||||||
groupIDs, groupNames = pairGroupIDsAndNames(userGroups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionkey.SignToken(
|
return sessionkey.SignToken(
|
||||||
service.SessionPrivateKey,
|
service.SessionPrivateKey,
|
||||||
userID,
|
userID,
|
||||||
email,
|
|
||||||
domain,
|
domain,
|
||||||
method,
|
method,
|
||||||
groupIDs,
|
|
||||||
groupNames,
|
|
||||||
proxyauth.DefaultSessionExpiry,
|
proxyauth.DefaultSessionExpiry,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1532,7 +1453,7 @@ func (s *ProxyServiceServer) ValidateSession(ctx context.Context, req *proto.Val
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, _, _, _, _, err := proxyauth.ValidateSessionJWT(sessionToken, domain, pubKeyBytes)
|
userID, _, err := proxyauth.ValidateSessionJWT(sessionToken, domain, pubKeyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
@@ -1545,7 +1466,7 @@ func (s *ProxyServiceServer) ValidateSession(ctx context.Context, req *proto.Val
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user, userGroups, err := s.usersManager.GetUserWithGroups(ctx, userID)
|
user, err := s.usersManager.GetUser(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
@@ -1579,15 +1500,12 @@ func (s *ProxyServiceServer) ValidateSession(ctx context.Context, req *proto.Val
|
|||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
}).Debug("ValidateSession: access denied")
|
}).Debug("ValidateSession: access denied")
|
||||||
groupIDs, groupNames := pairGroupIDsAndNames(userGroups)
|
|
||||||
//nolint:nilerr
|
//nolint:nilerr
|
||||||
return &proto.ValidateSessionResponse{
|
return &proto.ValidateSessionResponse{
|
||||||
Valid: false,
|
Valid: false,
|
||||||
UserId: user.Id,
|
UserId: user.Id,
|
||||||
UserEmail: user.Email,
|
UserEmail: user.Email,
|
||||||
DeniedReason: "not_in_group",
|
DeniedReason: "not_in_group",
|
||||||
PeerGroupIds: groupIDs,
|
|
||||||
PeerGroupNames: groupNames,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1597,13 +1515,10 @@ func (s *ProxyServiceServer) ValidateSession(ctx context.Context, req *proto.Val
|
|||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
}).Debug("ValidateSession: access granted")
|
}).Debug("ValidateSession: access granted")
|
||||||
|
|
||||||
groupIDs, groupNames := pairGroupIDsAndNames(userGroups)
|
|
||||||
return &proto.ValidateSessionResponse{
|
return &proto.ValidateSessionResponse{
|
||||||
Valid: true,
|
Valid: true,
|
||||||
UserId: user.Id,
|
UserId: user.Id,
|
||||||
UserEmail: user.Email,
|
UserEmail: user.Email,
|
||||||
PeerGroupIds: groupIDs,
|
|
||||||
PeerGroupNames: groupNames,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1636,154 +1551,3 @@ func (s *ProxyServiceServer) checkGroupAccess(service *rpservice.Service, user *
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ptr[T any](v T) *T { return &v }
|
func ptr[T any](v T) *T { return &v }
|
||||||
|
|
||||||
// ValidateTunnelPeer resolves an inbound peer by its WireGuard tunnel IP and
|
|
||||||
// checks the peer's group membership against the service's access groups.
|
|
||||||
// Peers without a user (machine agents, automation workloads) are first-class
|
|
||||||
// callers; authorisation runs off peer-group memberships rather than the
|
|
||||||
// optional owning user's auto-groups. On success a session JWT is minted so
|
|
||||||
// the proxy can install a cookie and skip subsequent management round-trips.
|
|
||||||
func (s *ProxyServiceServer) ValidateTunnelPeer(ctx context.Context, req *proto.ValidateTunnelPeerRequest) (*proto.ValidateTunnelPeerResponse, error) {
|
|
||||||
domain := req.GetDomain()
|
|
||||||
tunnelIPStr := req.GetTunnelIp()
|
|
||||||
|
|
||||||
if domain == "" || tunnelIPStr == "" {
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: false,
|
|
||||||
DeniedReason: "missing domain or tunnel_ip",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelIP := net.ParseIP(tunnelIPStr)
|
|
||||||
if tunnelIP == nil {
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: false,
|
|
||||||
DeniedReason: "invalid_tunnel_ip",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
service, err := s.getServiceByDomain(ctx, domain)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"domain": domain, "error": err.Error()}).Debug("ValidateTunnelPeer: service not found")
|
|
||||||
//nolint:nilerr
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: false,
|
|
||||||
DeniedReason: "service_not_found",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror ValidateSession: account-scoped (BYOP) proxy tokens may only
|
|
||||||
// validate and mint session cookies for their own account's domains.
|
|
||||||
if err := enforceAccountScope(ctx, service.AccountID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
peer, err := s.peersManager.GetPeerByTunnelIP(ctx, service.AccountID, tunnelIP)
|
|
||||||
if err != nil || peer == nil {
|
|
||||||
log.WithFields(log.Fields{"domain": domain, "tunnel_ip": tunnelIPStr}).Debug("ValidateTunnelPeer: peer not found")
|
|
||||||
//nolint:nilerr
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: false,
|
|
||||||
DeniedReason: "peer_not_found",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, peerGroups, err := s.peersManager.GetPeerWithGroups(ctx, service.AccountID, peer.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"domain": domain, "peer_id": peer.ID, "error": err.Error()}).Debug("ValidateTunnelPeer: peer groups lookup failed")
|
|
||||||
//nolint:nilerr
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: false,
|
|
||||||
DeniedReason: "peer_not_found",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
groupIDs, groupNames := pairGroupIDsAndNames(peerGroups)
|
|
||||||
|
|
||||||
// Resolve the principal: when the peer is linked to a user, the human
|
|
||||||
// is the principal so multiple peers owned by the same user share a
|
|
||||||
// single identity. Unlinked peers (machine agents) are their own
|
|
||||||
// principal keyed on peer.ID. displayIdentity is what upstream gateways
|
|
||||||
// tag spend with — user.Email when linked, peer.Name when not.
|
|
||||||
principalID := peer.ID
|
|
||||||
displayIdentity := peer.Name
|
|
||||||
if peer.UserID != "" {
|
|
||||||
if user, uerr := s.usersManager.GetUser(ctx, peer.UserID); uerr == nil && user != nil {
|
|
||||||
principalID = user.Id
|
|
||||||
if user.Email != "" {
|
|
||||||
displayIdentity = user.Email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkPeerGroupAccess(service, groupIDs); err != nil {
|
|
||||||
log.WithFields(log.Fields{"domain": domain, "peer_id": peer.ID, "error": err.Error()}).Debug("ValidateTunnelPeer: access denied")
|
|
||||||
//nolint:nilerr
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: false,
|
|
||||||
UserId: principalID,
|
|
||||||
UserEmail: displayIdentity,
|
|
||||||
DeniedReason: "not_in_group",
|
|
||||||
PeerGroupIds: groupIDs,
|
|
||||||
PeerGroupNames: groupNames,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := s.generateSessionToken(ctx, true, service, principalID, displayIdentity, proxyauth.MethodOIDC, groupIDs, groupNames)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"domain": domain,
|
|
||||||
"tunnel_ip": tunnelIPStr,
|
|
||||||
"peer_id": peer.ID,
|
|
||||||
"principal_id": principalID,
|
|
||||||
}).Debug("ValidateTunnelPeer: access granted")
|
|
||||||
|
|
||||||
return &proto.ValidateTunnelPeerResponse{
|
|
||||||
Valid: true,
|
|
||||||
UserId: principalID,
|
|
||||||
UserEmail: displayIdentity,
|
|
||||||
SessionToken: token,
|
|
||||||
PeerGroupIds: groupIDs,
|
|
||||||
PeerGroupNames: groupNames,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPeerGroupAccess gates ValidateTunnelPeer by the service's required
|
|
||||||
// groups. Private services authorise against AccessGroups (empty list fails
|
|
||||||
// closed — Validate() rejects that at save time but the RPC is the security
|
|
||||||
// boundary and must not trust upstream state). Bearer-auth services authorise
|
|
||||||
// against DistributionGroups when populated. Non-private non-bearer services
|
|
||||||
// are open.
|
|
||||||
func checkPeerGroupAccess(service *rpservice.Service, peerGroupIDs []string) error {
|
|
||||||
if service.Private {
|
|
||||||
if len(service.AccessGroups) == 0 {
|
|
||||||
return fmt.Errorf("private service has no access groups")
|
|
||||||
}
|
|
||||||
return matchAnyGroup(service.AccessGroups, peerGroupIDs)
|
|
||||||
}
|
|
||||||
if service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled && len(service.Auth.BearerAuth.DistributionGroups) > 0 {
|
|
||||||
return matchAnyGroup(service.Auth.BearerAuth.DistributionGroups, peerGroupIDs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchAnyGroup returns nil when peerGroupIDs intersects allowedGroups,
|
|
||||||
// else a non-nil error.
|
|
||||||
func matchAnyGroup(allowedGroups, peerGroupIDs []string) error {
|
|
||||||
if len(allowedGroups) == 0 {
|
|
||||||
return fmt.Errorf("no allowed groups configured")
|
|
||||||
}
|
|
||||||
allowed := make(map[string]struct{}, len(allowedGroups))
|
|
||||||
for _, g := range allowedGroups {
|
|
||||||
allowed[g] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, g := range peerGroupIDs {
|
|
||||||
if _, ok := allowed[g]; ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("peer not in allowed groups")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -129,14 +129,6 @@ func (m *mockUsersManager) GetUser(ctx context.Context, userID string) (*types.U
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockUsersManager) GetUserWithGroups(ctx context.Context, userID string) (*types.User, []*types.Group, error) {
|
|
||||||
user, err := m.GetUser(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return user, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateUserGroupAccess(t *testing.T) {
|
func TestValidateUserGroupAccess(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -428,46 +420,3 @@ func TestGetAccountProxyByDomain(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckPeerGroupAccess(t *testing.T) {
|
|
||||||
t.Run("private with empty AccessGroups denies", func(t *testing.T) {
|
|
||||||
svc := &service.Service{Private: true, AccessGroups: nil}
|
|
||||||
err := checkPeerGroupAccess(svc, []string{"grp-admins"})
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no access groups")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("private with peer in AccessGroups allows", func(t *testing.T) {
|
|
||||||
svc := &service.Service{Private: true, AccessGroups: []string{"grp-admins", "grp-ops"}}
|
|
||||||
assert.NoError(t, checkPeerGroupAccess(svc, []string{"grp-other", "grp-ops"}))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("private with peer outside AccessGroups denies", func(t *testing.T) {
|
|
||||||
svc := &service.Service{Private: true, AccessGroups: []string{"grp-admins"}}
|
|
||||||
assert.Error(t, checkPeerGroupAccess(svc, []string{"grp-other"}))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("bearer enabled with empty DistributionGroups allows", func(t *testing.T) {
|
|
||||||
svc := &service.Service{
|
|
||||||
Auth: service.AuthConfig{BearerAuth: &service.BearerAuthConfig{Enabled: true}},
|
|
||||||
}
|
|
||||||
assert.NoError(t, checkPeerGroupAccess(svc, []string{"grp-anyone"}))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("bearer enabled gates on DistributionGroups", func(t *testing.T) {
|
|
||||||
svc := &service.Service{
|
|
||||||
Auth: service.AuthConfig{
|
|
||||||
BearerAuth: &service.BearerAuthConfig{
|
|
||||||
Enabled: true,
|
|
||||||
DistributionGroups: []string{"grp-allowed"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.NoError(t, checkPeerGroupAccess(svc, []string{"grp-allowed"}))
|
|
||||||
assert.Error(t, checkPeerGroupAccess(svc, []string{"grp-other"}))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non-private non-bearer is open", func(t *testing.T) {
|
|
||||||
assert.NoError(t, checkPeerGroupAccess(&service.Service{}, nil))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ func (s *Server) handleUpdates(ctx context.Context, accountID string, peerKey wg
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithContext(ctx).Tracef("received an update for peer %s", peerKey.String())
|
log.WithContext(ctx).Debugf("received an update for peer %s", peerKey.String())
|
||||||
if debouncer.ProcessUpdate(update) {
|
if debouncer.ProcessUpdate(update) {
|
||||||
// Send immediately (first update or after quiet period)
|
// Send immediately (first update or after quiet period)
|
||||||
if err := s.sendUpdate(ctx, accountID, peerKey, peer, update, srv, streamStartTime); err != nil {
|
if err := s.sendUpdate(ctx, accountID, peerKey, peer, update, srv, streamStartTime); err != nil {
|
||||||
@@ -492,7 +492,7 @@ func (s *Server) sendUpdate(ctx context.Context, accountID string, peerKey wgtyp
|
|||||||
s.cancelPeerRoutines(ctx, accountID, peer, streamStartTime)
|
s.cancelPeerRoutines(ctx, accountID, peer, streamStartTime)
|
||||||
return status.Errorf(codes.Internal, "failed sending update message")
|
return status.Errorf(codes.Internal, "failed sending update message")
|
||||||
}
|
}
|
||||||
log.WithContext(ctx).Tracef("sent an update to peer %s", peerKey.String())
|
log.WithContext(ctx).Debugf("sent an update to peer %s", peerKey.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,80 +821,6 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtendAuthSession refreshes the peer's SSO session expiry deadline using a
|
|
||||||
// fresh JWT. The same JWT validation pipeline as Login is used. The tunnel
|
|
||||||
// stays up; no network map sync is performed. The new deadline is returned
|
|
||||||
// in ExtendAuthSessionResponse.SessionExpiresAt.
|
|
||||||
func (s *Server) ExtendAuthSession(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
|
||||||
extendReq := &proto.ExtendAuthSessionRequest{}
|
|
||||||
peerKey, err := s.parseRequest(ctx, req, extendReq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint
|
|
||||||
ctx = context.WithValue(ctx, nbContext.PeerIDKey, peerKey.String())
|
|
||||||
if accountID, accErr := s.accountManager.GetAccountIDForPeerKey(ctx, peerKey.String()); accErr == nil {
|
|
||||||
//nolint
|
|
||||||
ctx = context.WithValue(ctx, nbContext.AccountIDKey, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt := extendReq.GetJwtToken()
|
|
||||||
if jwt == "" {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "jwt token is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var userID string
|
|
||||||
const attempts = 3
|
|
||||||
for i := 0; i < attempts; i++ {
|
|
||||||
userID, err = s.validateToken(ctx, peerKey.String(), jwt)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if i == attempts-1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.WithContext(ctx).Warnf("failed validating JWT token while extending session for peer %s: %v. Retrying (idP cache).", peerKey.String(), err)
|
|
||||||
select {
|
|
||||||
case <-time.After(200 * time.Millisecond):
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if userID == "" {
|
|
||||||
return nil, status.Errorf(codes.Unauthenticated, "jwt token did not yield a user id")
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline, err := s.accountManager.ExtendPeerSession(ctx, peerKey.String(), userID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithContext(ctx).Warnf("failed extending session for peer %s: %v", peerKey.String(), err)
|
|
||||||
return nil, mapError(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success path normally returns a non-zero deadline. A defensive zero
|
|
||||||
// would still encode as the explicit "disabled" sentinel rather than nil,
|
|
||||||
// so the client clears any stale anchor instead of preserving it.
|
|
||||||
resp := &proto.ExtendAuthSessionResponse{
|
|
||||||
SessionExpiresAt: encodeSessionExpiresAt(deadline),
|
|
||||||
}
|
|
||||||
|
|
||||||
wgKey, err := s.secretsManager.GetWGKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed processing request")
|
|
||||||
}
|
|
||||||
encrypted, err := encryption.EncryptMessage(peerKey, wgKey, resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed encrypting response")
|
|
||||||
}
|
|
||||||
return &proto.EncryptedMessage{
|
|
||||||
WgPubKey: wgKey.PublicKey().String(),
|
|
||||||
Body: encrypted,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, netMap *types.NetworkMap, postureChecks []*posture.Checks) (*proto.LoginResponse, error) {
|
func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, netMap *types.NetworkMap, postureChecks []*posture.Checks) (*proto.LoginResponse, error) {
|
||||||
var relayToken *Token
|
var relayToken *Token
|
||||||
var err error
|
var err error
|
||||||
@@ -918,12 +844,6 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
|
|||||||
Checks: toProtocolChecks(ctx, postureChecks),
|
Checks: toProtocolChecks(ctx, postureChecks),
|
||||||
}
|
}
|
||||||
|
|
||||||
// settings is always non-nil here, so we never emit nil — encoder returns
|
|
||||||
// either a valid deadline or the explicit-zero "disabled" sentinel.
|
|
||||||
loginResp.SessionExpiresAt = encodeSessionExpiresAt(
|
|
||||||
peer.SessionExpiresAt(settings.PeerLoginExpirationEnabled, settings.PeerLoginExpiration),
|
|
||||||
)
|
|
||||||
|
|
||||||
return loginResp, nil
|
return loginResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,7 +932,31 @@ func (s *Server) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, peer
|
|||||||
return status.Errorf(codes.Internal, "failed to get peer groups %s", err)
|
return status.Errorf(codes.Internal, "failed to get peer groups %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plainResp := ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, s.networkMapController.GetDNSDomain(settings), postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort)
|
dnsName := s.networkMapController.GetDNSDomain(settings)
|
||||||
|
|
||||||
|
var plainResp *proto.SyncResponse
|
||||||
|
if s.networkMapController.PeerNeedsComponents(peer) {
|
||||||
|
// Capable peer: discard the legacy NetworkMap that SyncAndMarkPeer
|
||||||
|
// computed and recompute the raw components instead. This wastes one
|
||||||
|
// Calculate() call per initial-sync — the component-based wire
|
||||||
|
// format is what the peer actually consumes. The streaming path
|
||||||
|
// (network_map.Controller.UpdateAccountPeers) skips this duplication
|
||||||
|
// because it dispatches by capability before computing.
|
||||||
|
//
|
||||||
|
// TODO(step-4-sync): refactor SyncPeer / SyncAndMarkPeer / their
|
||||||
|
// mocks + manager interfaces to return PeerNetworkMapResult so the
|
||||||
|
// initial-sync path stops doing duplicate work. ~13 files of churn,
|
||||||
|
// deferred until the client-side decoder lands and there's a real
|
||||||
|
// deployment of capability=3 peers worth optimizing for.
|
||||||
|
_, components, proxyPatch, _, _, err := s.networkMapController.GetValidatedPeerWithComponents(ctx, false, peer.AccountID, peer)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to build components for peer %s on initial sync: %v", peer.ID, err)
|
||||||
|
return status.Errorf(codes.Internal, "failed to build initial sync envelope")
|
||||||
|
}
|
||||||
|
plainResp = ToComponentSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, components, proxyPatch, dnsName, postureChecks, settings, settings.Extra, peerGroups, dnsFwdPort)
|
||||||
|
} else {
|
||||||
|
plainResp = ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, dnsName, postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort)
|
||||||
|
}
|
||||||
|
|
||||||
key, err := s.secretsManager.GetWGKey()
|
key, err := s.secretsManager.GetWGKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func generateSessionKeyPair(t *testing.T) (string, string) {
|
|||||||
|
|
||||||
func createSessionToken(t *testing.T, privKeyB64, userID, domain string) string {
|
func createSessionToken(t *testing.T, privKeyB64, userID, domain string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
token, err := sessionkey.SignToken(privKeyB64, userID, domain, auth.MethodOIDC, nil, time.Hour)
|
token, err := sessionkey.SignToken(privKeyB64, userID, domain, auth.MethodOIDC, time.Hour)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,6 @@ func TestValidateSession_UserAllowed(t *testing.T) {
|
|||||||
assert.True(t, resp.Valid, "User should be allowed access")
|
assert.True(t, resp.Valid, "User should be allowed access")
|
||||||
assert.Equal(t, "allowedUserId", resp.UserId)
|
assert.Equal(t, "allowedUserId", resp.UserId)
|
||||||
assert.Empty(t, resp.DeniedReason)
|
assert.Empty(t, resp.DeniedReason)
|
||||||
assert.Equal(t, []string{"allowedGroupId"}, resp.GetPeerGroupIds(), "PeerGroupIds must mirror the resolved user's group memberships")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateSession_UserNotInAllowedGroup(t *testing.T) {
|
func TestValidateSession_UserNotInAllowedGroup(t *testing.T) {
|
||||||
@@ -146,7 +145,6 @@ func TestValidateSession_UserNotInAllowedGroup(t *testing.T) {
|
|||||||
assert.False(t, resp.Valid, "User not in group should be denied")
|
assert.False(t, resp.Valid, "User not in group should be denied")
|
||||||
assert.Equal(t, "not_in_group", resp.DeniedReason)
|
assert.Equal(t, "not_in_group", resp.DeniedReason)
|
||||||
assert.Equal(t, "nonGroupUserId", resp.UserId)
|
assert.Equal(t, "nonGroupUserId", resp.UserId)
|
||||||
assert.Empty(t, resp.GetPeerGroupIds(), "PeerGroupIds must mirror the resolved user's actual (empty) memberships on denial")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateSession_UserInDifferentAccount(t *testing.T) {
|
func TestValidateSession_UserInDifferentAccount(t *testing.T) {
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ func (am *DefaultAccountManager) GetIdpManager() idp.Manager {
|
|||||||
// User that performs the update has to belong to the account.
|
// User that performs the update has to belong to the account.
|
||||||
// Returns an updated Settings
|
// Returns an updated Settings
|
||||||
func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) {
|
func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -355,17 +355,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
|||||||
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
|
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
|
||||||
oldSettings.DNSDomain != newSettings.DNSDomain ||
|
oldSettings.DNSDomain != newSettings.DNSDomain ||
|
||||||
oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion ||
|
oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion ||
|
||||||
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways ||
|
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways {
|
||||||
oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled ||
|
|
||||||
oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration {
|
|
||||||
// Session deadline is derived from LastLogin + PeerLoginExpiration
|
|
||||||
// on every Login/Sync response. Without a fan-out push, connected
|
|
||||||
// peers keep the deadline they received at login time and only see
|
|
||||||
// the new value after the next unrelated NetworkMap change. Add
|
|
||||||
// these two fields to the trigger list so admin-side expiry tweaks
|
|
||||||
// (e.g. shortening from 24h to 1h) reach every connected peer
|
|
||||||
// within seconds, which is what the proactive-warning feature
|
|
||||||
// relies on (see client/internal/auth/sessionwatch).
|
|
||||||
updateAccountPeers = true
|
updateAccountPeers = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,7 +845,7 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Delete)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to validate user permissions: %w", err)
|
return fmt.Errorf("failed to validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1422,7 +1412,7 @@ func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID strin
|
|||||||
|
|
||||||
// GetAccountByID returns an account associated with this account ID.
|
// GetAccountByID returns an account associated with this account ID.
|
||||||
func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) {
|
func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -1435,7 +1425,7 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s
|
|||||||
|
|
||||||
// GetAccountMeta returns the account metadata associated with this account ID.
|
// GetAccountMeta returns the account metadata associated with this account ID.
|
||||||
func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
|
func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -1448,7 +1438,7 @@ func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID s
|
|||||||
|
|
||||||
// GetAccountOnboarding retrieves the onboarding information for a specific account.
|
// GetAccountOnboarding retrieves the onboarding information for a specific account.
|
||||||
func (am *DefaultAccountManager) GetAccountOnboarding(ctx context.Context, accountID string, userID string) (*types.AccountOnboarding, error) {
|
func (am *DefaultAccountManager) GetAccountOnboarding(ctx context.Context, accountID string, userID string) (*types.AccountOnboarding, error) {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -1473,7 +1463,7 @@ func (am *DefaultAccountManager) GetAccountOnboarding(ctx context.Context, accou
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (am *DefaultAccountManager) UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) {
|
func (am *DefaultAccountManager) UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error) {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1540,8 +1530,7 @@ func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, u
|
|||||||
return accountID, user.Id, nil
|
return accountID, user.Id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, err = am.permissionsManager.ValidateAccountAccess(ctx, accountID, user, false)
|
if err := am.permissionsManager.ValidateAccountAccess(ctx, accountID, user, false); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1632,6 +1621,14 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, g := range newGroupsToCreate {
|
||||||
|
seq, err := transaction.AllocateAccountSeqID(ctx, userAuth.AccountId, types.AccountSeqEntityGroup)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error allocating group seq id: %w", err)
|
||||||
|
}
|
||||||
|
g.AccountSeqID = seq
|
||||||
|
}
|
||||||
|
|
||||||
if err = transaction.CreateGroups(ctx, userAuth.AccountId, newGroupsToCreate); err != nil {
|
if err = transaction.CreateGroups(ctx, userAuth.AccountId, newGroupsToCreate); err != nil {
|
||||||
return fmt.Errorf("error saving groups: %w", err)
|
return fmt.Errorf("error saving groups: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1987,7 +1984,7 @@ func (am *DefaultAccountManager) handleUserPeer(ctx context.Context, transaction
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Read)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -2555,7 +2552,7 @@ func (am *DefaultAccountManager) validateIPForUpdate(account *types.Account, pee
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (am *DefaultAccountManager) UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error {
|
func (am *DefaultAccountManager) UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate user permissions: %w", err)
|
return fmt.Errorf("validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -2573,7 +2570,9 @@ func (am *DefaultAccountManager) UpdatePeerIP(ctx context.Context, accountID, us
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = am.networkMapController.OnPeersUpdated(ctx, peer.AccountID, []string{peerID})
|
changedPeerIDs := []string{peerID}
|
||||||
|
affectedPeerIDs := am.resolveAffectedPeersForPeerChanges(ctx, am.Store, accountID, changedPeerIDs)
|
||||||
|
err = am.networkMapController.OnPeersUpdated(ctx, peer.AccountID, changedPeerIDs, affectedPeerIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("notify network map controller of peer update: %w", err)
|
return fmt.Errorf("notify network map controller of peer update: %w", err)
|
||||||
}
|
}
|
||||||
@@ -2645,7 +2644,7 @@ func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transacti
|
|||||||
// UpdatePeerIPv6 updates the IPv6 overlay address of a peer, validating it's
|
// UpdatePeerIPv6 updates the IPv6 overlay address of a peer, validating it's
|
||||||
// within the account's v6 network range and not already taken.
|
// within the account's v6 network range and not already taken.
|
||||||
func (am *DefaultAccountManager) UpdatePeerIPv6(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error {
|
func (am *DefaultAccountManager) UpdatePeerIPv6(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error {
|
||||||
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate user permissions: %w", err)
|
return fmt.Errorf("validate user permissions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -2664,7 +2663,9 @@ func (am *DefaultAccountManager) UpdatePeerIPv6(ctx context.Context, accountID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updateNetworkMap {
|
if updateNetworkMap {
|
||||||
if err := am.networkMapController.OnPeersUpdated(ctx, accountID, []string{peerID}); err != nil {
|
changedPeerIDs := []string{peerID}
|
||||||
|
affectedPeerIDs := am.resolveAffectedPeersForPeerChanges(ctx, am.Store, accountID, changedPeerIDs)
|
||||||
|
if err := am.networkMapController.OnPeersUpdated(ctx, accountID, changedPeerIDs, affectedPeerIDs); err != nil {
|
||||||
return fmt.Errorf("notify network map controller: %w", err)
|
return fmt.Errorf("notify network map controller: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ type Manager interface {
|
|||||||
UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error)
|
UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error)
|
||||||
UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error)
|
UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error)
|
||||||
LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API
|
LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API
|
||||||
ExtendPeerSession(ctx context.Context, peerPubKey, userID string) (time.Time, error) // used by peer gRPC API for ExtendAuthSession
|
|
||||||
SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) // used by peer gRPC API
|
SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) // used by peer gRPC API
|
||||||
GetExternalCacheManager() ExternalCacheManager
|
GetExternalCacheManager() ExternalCacheManager
|
||||||
GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error)
|
GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error)
|
||||||
@@ -128,6 +127,8 @@ type Manager interface {
|
|||||||
GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error)
|
GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error)
|
||||||
DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error
|
DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error
|
||||||
UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason)
|
UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason)
|
||||||
|
UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string)
|
||||||
|
BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason)
|
||||||
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason)
|
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason)
|
||||||
BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
|
BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
|
||||||
SyncUserJWTGroups(ctx context.Context, userAuth auth.UserAuth) error
|
SyncUserJWTGroups(ctx context.Context, userAuth auth.UserAuth) error
|
||||||
|
|||||||
@@ -122,6 +122,18 @@ func (mr *MockManagerMockRecorder) BufferUpdateAccountPeers(ctx, accountID, reas
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAccountPeers", reflect.TypeOf((*MockManager)(nil).BufferUpdateAccountPeers), ctx, accountID, reason)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAccountPeers", reflect.TypeOf((*MockManager)(nil).BufferUpdateAccountPeers), ctx, accountID, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BufferUpdateAffectedPeers mocks base method.
|
||||||
|
func (m *MockManager) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "BufferUpdateAffectedPeers", ctx, accountID, peerIDs, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferUpdateAffectedPeers indicates an expected call of BufferUpdateAffectedPeers.
|
||||||
|
func (mr *MockManagerMockRecorder) BufferUpdateAffectedPeers(ctx, accountID, peerIDs, reason interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAffectedPeers", reflect.TypeOf((*MockManager)(nil).BufferUpdateAffectedPeers), ctx, accountID, peerIDs, reason)
|
||||||
|
}
|
||||||
|
|
||||||
// BuildUserInfosForAccount mocks base method.
|
// BuildUserInfosForAccount mocks base method.
|
||||||
func (m *MockManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
|
func (m *MockManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -1304,21 +1316,6 @@ func (mr *MockManagerMockRecorder) LoginPeer(ctx, login interface{}) *gomock.Cal
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginPeer", reflect.TypeOf((*MockManager)(nil).LoginPeer), ctx, login)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginPeer", reflect.TypeOf((*MockManager)(nil).LoginPeer), ctx, login)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtendPeerSession mocks base method.
|
|
||||||
func (m *MockManager) ExtendPeerSession(ctx context.Context, peerPubKey, userID string) (time.Time, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ExtendPeerSession", ctx, peerPubKey, userID)
|
|
||||||
ret0, _ := ret[0].(time.Time)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtendPeerSession indicates an expected call of ExtendPeerSession.
|
|
||||||
func (mr *MockManagerMockRecorder) ExtendPeerSession(ctx, peerPubKey, userID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendPeerSession", reflect.TypeOf((*MockManager)(nil).ExtendPeerSession), ctx, peerPubKey, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkPeerConnected mocks base method.
|
// MarkPeerConnected mocks base method.
|
||||||
func (m *MockManager) MarkPeerConnected(ctx context.Context, peerKey string, realIP net.IP, accountID string, sessionStartedAt int64) error {
|
func (m *MockManager) MarkPeerConnected(ctx context.Context, peerKey string, realIP net.IP, accountID string, sessionStartedAt int64) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -1637,6 +1634,18 @@ func (mr *MockManagerMockRecorder) UpdateAccountPeers(ctx, accountID, reason int
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountPeers", reflect.TypeOf((*MockManager)(nil).UpdateAccountPeers), ctx, accountID, reason)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountPeers", reflect.TypeOf((*MockManager)(nil).UpdateAccountPeers), ctx, accountID, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAffectedPeers mocks base method.
|
||||||
|
func (m *MockManager) UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "UpdateAffectedPeers", ctx, accountID, peerIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAffectedPeers indicates an expected call of UpdateAffectedPeers.
|
||||||
|
func (mr *MockManagerMockRecorder) UpdateAffectedPeers(ctx, accountID, peerIDs interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAffectedPeers", reflect.TypeOf((*MockManager)(nil).UpdateAffectedPeers), ctx, accountID, peerIDs)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateAccountSettings mocks base method.
|
// UpdateAccountSettings mocks base method.
|
||||||
func (m *MockManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) {
|
func (m *MockManager) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -3036,6 +3036,16 @@ func TestAccount_SetJWTGroups(t *testing.T) {
|
|||||||
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user2")
|
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user2")
|
||||||
assert.NoError(t, err, "unable to get user")
|
assert.NoError(t, err, "unable to get user")
|
||||||
assert.Len(t, user.AutoGroups, 1, "new group should be added")
|
assert.Len(t, user.AutoGroups, 1, "new group should be added")
|
||||||
|
|
||||||
|
var newJWTGroup *types.Group
|
||||||
|
for _, g := range groups {
|
||||||
|
if g.Name == "group3" {
|
||||||
|
newJWTGroup = g
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, newJWTGroup, "JIT-created JWT group not found")
|
||||||
|
assert.NotZero(t, newJWTGroup.AccountSeqID, "JIT-created JWT group must have a non-zero AccountSeqID")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
|
t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
|
||||||
@@ -3282,6 +3292,16 @@ func setupNetworkMapTest(t *testing.T) (*DefaultAccountManager, *update_channel.
|
|||||||
// when the channel delivers.
|
// when the channel delivers.
|
||||||
const peerUpdateTimeout = 5 * time.Second
|
const peerUpdateTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
func drainPeerUpdates(ch <-chan *network_map.UpdateMessage) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func peerShouldNotReceiveUpdate(t *testing.T, updateMessage <-chan *network_map.UpdateMessage) {
|
func peerShouldNotReceiveUpdate(t *testing.T, updateMessage <-chan *network_map.UpdateMessage) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
select {
|
select {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user