mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-30 12:39:54 +00:00
Compare commits
19 Commits
nmap/combi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9189625487 | ||
|
|
e9dbf9db6f | ||
|
|
5a9e9e7bc9 | ||
|
|
43e041cf9f | ||
|
|
77e5693200 | ||
|
|
174dc24867 | ||
|
|
7ea5e37dd4 | ||
|
|
9d7ef9b255 | ||
|
|
944a258459 | ||
|
|
1f9a829f2c | ||
|
|
14af179556 | ||
|
|
1fbb5e6d5d | ||
|
|
6771e35d57 | ||
|
|
e89b1e0596 | ||
|
|
d542c60e21 | ||
|
|
4983b5cf17 | ||
|
|
b3b0feb3b8 | ||
|
|
7aebdd69dd | ||
|
|
0358be2313 |
45
.github/dependabot.yml
vendored
Normal file
45
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
ignore:
|
||||||
|
# git-town/action v1.3.x crashes on cyclic PR graphs (self-loop main->main
|
||||||
|
# fork PRs) via its topological-sort visualization. Pinned to v1.2.1 in
|
||||||
|
# git-town.yml; block v1.3.x until upstream tolerates cyclic edges.
|
||||||
|
- dependency-name: "git-town/action"
|
||||||
|
update-types:
|
||||||
|
- "version-update:semver-minor"
|
||||||
|
- "version-update:semver-major"
|
||||||
|
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directories:
|
||||||
|
- "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
groups:
|
||||||
|
aws-sdk:
|
||||||
|
patterns:
|
||||||
|
- "github.com/aws/aws-sdk-go-v2/*"
|
||||||
|
pion:
|
||||||
|
patterns:
|
||||||
|
- "github.com/pion/*"
|
||||||
|
gorm:
|
||||||
|
patterns:
|
||||||
|
- "gorm.io/*"
|
||||||
|
otel:
|
||||||
|
patterns:
|
||||||
|
- "go.opentelemetry.io/*"
|
||||||
|
testcontainers:
|
||||||
|
patterns:
|
||||||
|
- "github.com/testcontainers/testcontainers-go/*"
|
||||||
|
wireguard:
|
||||||
|
patterns:
|
||||||
|
- "golang.zx2c4.com/wireguard*"
|
||||||
109
.github/workflows/check-license-dependencies.yml
vendored
109
.github/workflows/check-license-dependencies.yml
vendored
@@ -2,16 +2,16 @@ name: Check License Dependencies
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'go.mod'
|
- "go.mod"
|
||||||
- 'go.sum'
|
- "go.sum"
|
||||||
- '.github/workflows/check-license-dependencies.yml'
|
- ".github/workflows/check-license-dependencies.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'go.mod'
|
- "go.mod"
|
||||||
- 'go.sum'
|
- "go.sum"
|
||||||
- '.github/workflows/check-license-dependencies.yml'
|
- ".github/workflows/check-license-dependencies.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-internal-dependencies:
|
check-internal-dependencies:
|
||||||
@@ -19,7 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for problematic license dependencies
|
- name: Check for problematic license dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -56,55 +59,57 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: "go.mod"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install go-licenses
|
- name: Install go-licenses
|
||||||
run: go install github.com/google/go-licenses@v1.6.0
|
run: go install github.com/google/go-licenses@v1.6.0
|
||||||
|
|
||||||
- name: Check for GPL/AGPL licensed dependencies
|
- name: Check for GPL/AGPL licensed dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
|
||||||
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
|
||||||
|
|
||||||
if [ -n "$COPYLEFT_DEPS" ]; then
|
|
||||||
echo "Found copyleft licensed dependencies:"
|
|
||||||
echo "$COPYLEFT_DEPS"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Filter out dependencies that are only pulled in by internal AGPL packages
|
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
||||||
INCOMPATIBLE=""
|
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
||||||
while IFS=',' read -r package url license; do
|
|
||||||
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
|
||||||
# Find ALL packages that import this GPL package using go list
|
|
||||||
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
|
||||||
|
|
||||||
# Check if any importer is NOT in management/signal/relay
|
if [ -n "$COPYLEFT_DEPS" ]; then
|
||||||
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
echo "Found copyleft licensed dependencies:"
|
||||||
|
echo "$COPYLEFT_DEPS"
|
||||||
if [ -n "$BSD_IMPORTER" ]; then
|
|
||||||
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
|
||||||
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
|
||||||
else
|
|
||||||
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$COPYLEFT_DEPS"
|
|
||||||
|
|
||||||
if [ -n "$INCOMPATIBLE" ]; then
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
|
||||||
echo -e "$INCOMPATIBLE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
# Filter out dependencies that are only pulled in by internal AGPL packages
|
||||||
|
INCOMPATIBLE=""
|
||||||
|
while IFS=',' read -r package url license; do
|
||||||
|
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
||||||
|
# Find ALL packages that import this GPL package using go list
|
||||||
|
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
||||||
|
|
||||||
|
# Check if any importer is NOT in management/signal/relay
|
||||||
|
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
||||||
|
|
||||||
|
if [ -n "$BSD_IMPORTER" ]; then
|
||||||
|
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
||||||
|
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
||||||
|
else
|
||||||
|
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$COPYLEFT_DEPS"
|
||||||
|
|
||||||
|
if [ -n "$INCOMPATIBLE" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
||||||
|
echo -e "$INCOMPATIBLE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
||||||
|
|||||||
2
.github/workflows/docs-ack.yml
vendored
2
.github/workflows/docs-ack.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify docs PR exists (and is open or merged)
|
- name: Verify docs PR exists (and is open or merged)
|
||||||
if: steps.validate.outputs.mode == 'added'
|
if: steps.validate.outputs.mode == 'added'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
id: verify
|
id: verify
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.extract.outputs.pr_number }}
|
pr_number: ${{ steps.extract.outputs.pr_number }}
|
||||||
|
|||||||
5
.github/workflows/forum.yml
vendored
5
.github/workflows/forum.yml
vendored
@@ -8,11 +8,10 @@ jobs:
|
|||||||
post:
|
post:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: roots/discourse-topic-github-release-action@main
|
- uses: roots/discourse-topic-github-release-action@557d74ea05b6cc0c47f555c1d5d28a89d904005b # v1.1.0
|
||||||
with:
|
with:
|
||||||
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
|
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
|
||||||
discourse-base-url: https://forum.netbird.io
|
discourse-base-url: https://forum.netbird.io
|
||||||
discourse-author-username: NetBird
|
discourse-author-username: NetBird
|
||||||
discourse-category: 17
|
discourse-category: 17
|
||||||
discourse-tags:
|
discourse-tags: releases
|
||||||
releases
|
|
||||||
|
|||||||
8
.github/workflows/git-town.yml
vendored
8
.github/workflows/git-town.yml
vendored
@@ -3,7 +3,7 @@ name: Git Town
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
git-town:
|
git-town:
|
||||||
@@ -15,7 +15,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: git-town/action@v1.2.1
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: git-town/action@3d8b878379abb1ee393fb49865a28b4a6c2cd3b0 # v1.2.1
|
||||||
with:
|
with:
|
||||||
skip-single-stacks: true
|
skip-single-stacks: true
|
||||||
|
|||||||
9
.github/workflows/golang-test-darwin.yml
vendored
9
.github/workflows/golang-test-darwin.yml
vendored
@@ -16,16 +16,18 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -44,4 +46,3 @@ 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)
|
||||||
|
|
||||||
|
|||||||
21
.github/workflows/golang-test-freebsd.yml
vendored
21
.github/workflows/golang-test-freebsd.yml
vendored
@@ -15,20 +15,31 @@ jobs:
|
|||||||
name: "Client / Unit"
|
name: "Client / Unit"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Read Go version from go.mod
|
||||||
|
id: goversion
|
||||||
|
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Test in FreeBSD
|
- name: Test in FreeBSD
|
||||||
id: test
|
id: test
|
||||||
uses: vmactions/freebsd-vm@v1
|
env:
|
||||||
|
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
||||||
|
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "14.2"
|
release: "15.0"
|
||||||
|
envs: "GO_VERSION"
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y curl pkgconf xorg
|
pkg install -y curl pkgconf xorg
|
||||||
GO_TARBALL="go1.25.3.freebsd-amd64.tar.gz"
|
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -vLO "$GO_URL"
|
curl -vLO "$GO_URL"
|
||||||
tar -C /usr/local -vxzf "$GO_TARBALL"
|
tar -C /usr/local -vxzf "$GO_TARBALL"
|
||||||
|
|
||||||
# -x - to print all executed commands
|
# -x - to print all executed commands
|
||||||
# -e - to faile on first error
|
# -e - to faile on first error
|
||||||
|
|||||||
138
.github/workflows/golang-test-linux.yml
vendored
138
.github/workflows/golang-test-linux.yml
vendored
@@ -18,9 +18,11 @@ jobs:
|
|||||||
management: ${{ steps.filter.outputs.management }}
|
management: ${{ steps.filter.outputs.management }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
@@ -28,7 +30,7 @@ jobs:
|
|||||||
- 'management/**'
|
- 'management/**'
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -36,10 +38,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -113,14 +115,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: ["386", "amd64"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -128,10 +132,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -158,14 +162,16 @@ 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@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -177,7 +183,7 @@ jobs:
|
|||||||
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
id: cache-restore
|
id: cache-restore
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -231,10 +237,12 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -246,10 +254,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -277,14 +285,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: ["386", "amd64"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -298,7 +308,7 @@ jobs:
|
|||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -324,14 +334,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: ["386", "amd64"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -343,10 +355,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -370,19 +382,21 @@ 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@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -390,10 +404,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -410,7 +424,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -427,7 +441,7 @@ jobs:
|
|||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
run: docker pull mlsmaycon/warmed-mysql:8
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
@@ -437,13 +451,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
|
||||||
@@ -474,10 +488,12 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -485,10 +501,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -505,7 +521,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -529,13 +545,13 @@ jobs:
|
|||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'amd64' ]
|
arch: ["amd64"]
|
||||||
store: [ 'sqlite', 'postgres' ]
|
store: ["sqlite", "postgres"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -566,10 +582,12 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -577,10 +595,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -597,7 +615,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -623,20 +641,22 @@ jobs:
|
|||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
needs: [ build-cache ]
|
needs: [build-cache]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'amd64' ]
|
arch: ["amd64"]
|
||||||
store: [ 'sqlite', 'postgres']
|
store: ["sqlite", "postgres"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -644,10 +664,10 @@ jobs:
|
|||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
|
|||||||
19
.github/workflows/golang-test-windows.yml
vendored
19
.github/workflows/golang-test-windows.yml
vendored
@@ -18,10 +18,12 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
@@ -33,7 +35,7 @@ jobs:
|
|||||||
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -44,16 +46,15 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
file-name: wintun.zip
|
destination: ${{ env.downloadPath }}\wintun.zip
|
||||||
location: ${{ env.downloadPath }}
|
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
||||||
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
|
||||||
|
|
||||||
- name: Decompressing wintun files
|
- name: Decompressing wintun files
|
||||||
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
run: tar -xvf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
||||||
|
|
||||||
|
|||||||
14
.github/workflows/golangci-lint.yml
vendored
14
.github/workflows/golangci-lint.yml
vendored
@@ -15,9 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals
|
||||||
skip: go.mod,go.sum,**/proxy/web/**
|
skip: go.mod,go.sum,**/proxy/web/**
|
||||||
@@ -38,13 +40,15 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Check for duplicate constants
|
- name: Check for duplicate constants
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -52,7 +56,7 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
skip-cache: true
|
skip-cache: true
|
||||||
|
|||||||
4
.github/workflows/install-script-test.yml
vendored
4
.github/workflows/install-script-test.yml
vendored
@@ -22,7 +22,9 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: run install script
|
- name: run install script
|
||||||
env:
|
env:
|
||||||
|
|||||||
18
.github/workflows/mobile-build-validation.yml
vendored
18
.github/workflows/mobile-build-validation.yml
vendored
@@ -16,23 +16,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
||||||
with:
|
with:
|
||||||
cmdline-tools-version: 8512546
|
cmdline-tools-version: 8512546
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
|
||||||
with:
|
with:
|
||||||
java-version: "11"
|
java-version: "11"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
- name: NDK Cache
|
- name: NDK Cache
|
||||||
id: ndk-cache
|
id: ndk-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: /usr/local/lib/android/sdk/ndk
|
path: /usr/local/lib/android/sdk/ndk
|
||||||
key: ndk-cache-23.1.7779620
|
key: ndk-cache-23.1.7779620
|
||||||
@@ -52,9 +54,11 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
|
|||||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Validate PR title prefix
|
- name: Validate PR title prefix
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const title = context.payload.pull_request.title;
|
const title = context.payload.pull_request.title;
|
||||||
|
|||||||
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@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||||
@@ -20,34 +20,66 @@ jobs:
|
|||||||
per_page: 100,
|
per_page: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pbFiles = files.filter(f => f.filename.endsWith('.pb.go'));
|
const modifiedPbFiles = files.filter(
|
||||||
const missingPatch = pbFiles.filter(f => !f.patch).map(f => f.filename);
|
f => f.filename.endsWith('.pb.go') && f.status === 'modified'
|
||||||
if (missingPatch.length > 0) {
|
);
|
||||||
core.setFailed(
|
if (modifiedPbFiles.length === 0) {
|
||||||
`Cannot inspect patch data for:\n` +
|
console.log('No modified .pb.go files to check');
|
||||||
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 violations = [];
|
|
||||||
|
|
||||||
for (const file of pbFiles) {
|
const versionPattern = /^\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
|
||||||
const changed = file.patch
|
const baseSha = context.payload.pull_request.base.sha;
|
||||||
.split('\n')
|
const headSha = context.payload.pull_request.head.sha;
|
||||||
.filter(line => versionPattern.test(line));
|
|
||||||
if (changed.length > 0) {
|
async function getVersionHeader(path, ref) {
|
||||||
|
try {
|
||||||
|
const res = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path,
|
||||||
|
ref,
|
||||||
|
});
|
||||||
|
if (!res.data.content) {
|
||||||
|
return { ok: false, reason: 'no inline content (file too large)' };
|
||||||
|
}
|
||||||
|
const content = Buffer.from(res.data.content, 'base64').toString('utf8');
|
||||||
|
const lines = content
|
||||||
|
.split('\n')
|
||||||
|
.slice(0, 20)
|
||||||
|
.filter(line => versionPattern.test(line));
|
||||||
|
return { ok: true, lines };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, reason: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
for (const file of modifiedPbFiles) {
|
||||||
|
const [base, head] = await Promise.all([
|
||||||
|
getVersionHeader(file.filename, baseSha),
|
||||||
|
getVersionHeader(file.filename, headSha),
|
||||||
|
]);
|
||||||
|
if (!base.ok || !head.ok) {
|
||||||
|
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,
|
||||||
lines: changed,
|
base: base.lines,
|
||||||
|
head: head.lines,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
const details = violations.map(v =>
|
const details = violations.map(v =>
|
||||||
`${v.file}:\n${v.lines.map(l => ' ' + l).join('\n')}`
|
`${v.file}:\n` +
|
||||||
|
` base:\n${v.base.map(l => ' ' + l).join('\n') || ' (none)'}\n` +
|
||||||
|
` head:\n${v.head.map(l => ' ' + l).join('\n') || ' (none)'}`
|
||||||
).join('\n\n');
|
).join('\n\n');
|
||||||
|
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
|
|||||||
168
.github/workflows/release.yml
vendored
168
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.4"
|
SIGN_PIPE_VER: "v0.1.5"
|
||||||
GORELEASER_VER: "v2.14.3"
|
GORELEASER_VER: "v2.14.3"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
@@ -24,7 +24,9 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Generate FreeBSD port diff
|
- name: Generate FreeBSD port diff
|
||||||
run: bash release_files/freebsd-port-diff.sh
|
run: bash release_files/freebsd-port-diff.sh
|
||||||
@@ -51,19 +53,26 @@ jobs:
|
|||||||
echo "Generated files for version: $VERSION"
|
echo "Generated files for version: $VERSION"
|
||||||
cat netbird-*.diff
|
cat netbird-*.diff
|
||||||
|
|
||||||
|
- name: Read Go version from go.mod
|
||||||
|
id: goversion
|
||||||
|
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Test FreeBSD port
|
- name: Test FreeBSD port
|
||||||
if: steps.check_diff.outputs.diff_exists == 'true'
|
if: steps.check_diff.outputs.diff_exists == 'true'
|
||||||
uses: vmactions/freebsd-vm@v1
|
env:
|
||||||
|
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
||||||
|
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "15.0"
|
release: "15.0"
|
||||||
|
envs: "GO_VERSION"
|
||||||
prepare: |
|
prepare: |
|
||||||
# Install required packages
|
# Install required packages
|
||||||
pkg install -y git curl portlint go
|
pkg install -y git curl portlint
|
||||||
|
|
||||||
# Install Go for building
|
# Install Go for building
|
||||||
GO_TARBALL="go1.25.5.freebsd-amd64.tar.gz"
|
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -LO "$GO_URL"
|
curl -LO "$GO_URL"
|
||||||
tar -C /usr/local -xzf "$GO_TARBALL"
|
tar -C /usr/local -xzf "$GO_TARBALL"
|
||||||
@@ -93,19 +102,19 @@ jobs:
|
|||||||
|
|
||||||
# Show patched Makefile
|
# Show patched Makefile
|
||||||
version=$(cat security/netbird/Makefile | grep -E '^DISTVERSION=' | awk '{print $NF}')
|
version=$(cat security/netbird/Makefile | grep -E '^DISTVERSION=' | awk '{print $NF}')
|
||||||
|
|
||||||
cd /usr/ports/security/netbird
|
cd /usr/ports/security/netbird
|
||||||
export BATCH=yes
|
export BATCH=yes
|
||||||
make package
|
make package
|
||||||
pkg add ./work/pkg/netbird-*.pkg
|
pkg add ./work/pkg/netbird-*.pkg
|
||||||
|
|
||||||
netbird version | grep "$version"
|
netbird version | grep "$version"
|
||||||
|
|
||||||
echo "FreeBSD port test completed successfully!"
|
echo "FreeBSD port test completed successfully!"
|
||||||
|
|
||||||
- name: Upload FreeBSD port files
|
- name: Upload FreeBSD port files
|
||||||
if: steps.check_diff.outputs.diff_exists == 'true'
|
if: steps.check_diff.outputs.diff_exists == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: freebsd-port-files
|
name: freebsd-port-files
|
||||||
path: |
|
path: |
|
||||||
@@ -124,26 +133,25 @@ 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: booxmedialtd/ws-action-parse-semver@v1
|
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
|
||||||
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
|
||||||
version_extractor_regex: '\/v(.*)$'
|
|
||||||
|
|
||||||
- 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@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -156,18 +164,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@v2
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a #v4.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -191,7 +199,7 @@ jobs:
|
|||||||
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso
|
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --clean ${{ env.flags }}
|
args: release --clean ${{ env.flags }}
|
||||||
@@ -282,28 +290,28 @@ jobs:
|
|||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release
|
id: upload_release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload linux packages
|
- name: upload linux packages
|
||||||
id: upload_linux_packages
|
id: upload_linux_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: linux-packages
|
name: linux-packages
|
||||||
path: dist/netbird_linux**
|
path: dist/netbird_linux**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload windows packages
|
- name: upload windows packages
|
||||||
id: upload_windows_packages
|
id: upload_windows_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: windows-packages
|
name: windows-packages
|
||||||
path: dist/netbird_windows**
|
path: dist/netbird_windows**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload macos packages
|
- name: upload macos packages
|
||||||
id: upload_macos_packages
|
id: upload_macos_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: macos-packages
|
name: macos-packages
|
||||||
path: dist/netbird_darwin**
|
path: dist/netbird_darwin**
|
||||||
@@ -314,27 +322,26 @@ 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: booxmedialtd/ws-action-parse-semver@v1
|
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
|
||||||
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
|
||||||
version_extractor_regex: '\/v(.*)$'
|
|
||||||
|
|
||||||
- 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@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -375,7 +382,7 @@ jobs:
|
|||||||
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso
|
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
||||||
@@ -404,7 +411,7 @@ jobs:
|
|||||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release_ui
|
id: upload_release_ui
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -418,16 +425,17 @@ jobs:
|
|||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
persist-credentials: false
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -441,7 +449,7 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@4c6ab561adb47e50c45ef534e2155934e91c40c1 # v7.2.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
||||||
@@ -449,7 +457,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release_ui_darwin
|
id: upload_release_ui_darwin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -474,27 +482,26 @@ jobs:
|
|||||||
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
||||||
downloadPath: '${{ github.workspace }}\temp'
|
downloadPath: '${{ github.workspace }}\temp'
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: booxmedialtd/ws-action-parse-semver@v1
|
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
|
||||||
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
|
||||||
version_extractor_regex: '\/v(.*)$'
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Add 7-Zip to PATH
|
- name: Add 7-Zip to PATH
|
||||||
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: Download release artifacts
|
- name: Download release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Download UI release artifacts
|
- name: Download UI release artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: release-ui
|
path: release-ui
|
||||||
@@ -514,29 +521,27 @@ jobs:
|
|||||||
Get-ChildItem $workdir
|
Get-ChildItem $workdir
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
file-name: wintun.zip
|
destination: ${{ env.downloadPath }}\wintun.zip
|
||||||
location: ${{ env.downloadPath }}
|
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
||||||
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
|
||||||
|
|
||||||
- name: Decompress wintun files
|
- name: Decompress wintun files
|
||||||
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
run: tar -xvf "${{ env.downloadPath }}\wintun.zip" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- name: Move wintun.dll into dist
|
- name: Move wintun.dll into dist
|
||||||
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
- name: Download Mesa3D (amd64 only)
|
- name: Download Mesa3D (amd64 only)
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
id: download-mesa3d
|
id: download-mesa3d
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
|
url: https://pkgs.netbird.io/mesa3d/MesaForWindows-x64-20.1.8.7z
|
||||||
file-name: mesa3d.7z
|
destination: ${{ env.downloadPath }}\mesa3d.7z
|
||||||
location: ${{ env.downloadPath }}
|
sha256: 71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9
|
||||||
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
|
|
||||||
|
|
||||||
- name: Extract Mesa3D driver (amd64 only)
|
- name: Extract Mesa3D driver (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
@@ -547,35 +552,38 @@ jobs:
|
|||||||
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
- name: Download EnVar plugin for NSIS
|
- name: Download EnVar plugin for NSIS
|
||||||
uses: carlosperate/download-file-action@v2
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
|
url: https://pkgs.netbird.io/nsis/EnVar_plugin.zip
|
||||||
file-name: envar_plugin.zip
|
destination: ${{ github.workspace }}\envar_plugin.zip
|
||||||
location: ${{ github.workspace }}
|
sha256: e9aa92de351345ed82795251d838f1ae9041ba35af9d381a5780c7843b01f56a
|
||||||
|
|
||||||
- name: Extract EnVar plugin
|
- name: Extract EnVar plugin
|
||||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
||||||
|
|
||||||
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
||||||
uses: carlosperate/download-file-action@v2
|
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
|
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||||
with:
|
with:
|
||||||
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
|
url: https://pkgs.netbird.io/nsis/ShellExecAsUser_amd64-Unicode.7z
|
||||||
file-name: ShellExecAsUser_amd64-Unicode.7z
|
destination: ${{ github.workspace }}\ShellExecAsUser_amd64-Unicode.7z
|
||||||
location: ${{ github.workspace }}
|
sha256: 0a55ea25c7330a92cec028eda8afcaf1b1a7092e0dfb77c21c8f654564b4ff9d
|
||||||
|
|
||||||
- name: Extract ShellExecAsUser plugin (amd64 only)
|
- name: Extract ShellExecAsUser plugin (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||||
|
|
||||||
- name: Build NSIS installer
|
- name: Build NSIS installer
|
||||||
uses: joncloud/makensis-action@v3.3
|
shell: pwsh
|
||||||
with:
|
|
||||||
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
|
|
||||||
script-file: client/installer.nsis
|
|
||||||
arguments: "/V4 /DARCH=${{ matrix.arch }}"
|
|
||||||
env:
|
env:
|
||||||
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||||
|
$srcPlugins = "${{ github.workspace }}\NSIS_Plugins\Plugins"
|
||||||
|
Get-ChildItem -Path $srcPlugins -Recurse -Filter *.dll |
|
||||||
|
Copy-Item -Destination $nsisPluginDir -Force
|
||||||
|
& "C:\Program Files (x86)\NSIS\makensis.exe" /V4 "/DARCH=${{ matrix.arch }}" client\installer.nsis
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" }
|
||||||
|
|
||||||
- name: Rename NSIS installer
|
- name: Rename NSIS installer
|
||||||
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||||
@@ -592,7 +600,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload installer artifacts
|
- name: Upload installer artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||||
with:
|
with:
|
||||||
name: windows-installer-test-${{ matrix.arch }}
|
name: windows-installer-test-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -611,7 +619,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Create or update PR comment
|
- name: Create or update PR comment
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
env:
|
env:
|
||||||
RELEASE_RESULT: ${{ needs.release.result }}
|
RELEASE_RESULT: ${{ needs.release.result }}
|
||||||
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||||
@@ -703,7 +711,7 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger binaries sign pipelines
|
- name: Trigger binaries sign pipelines
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: Sign bin and installer
|
workflow: Sign bin and installer
|
||||||
repo: netbirdio/sign-pipelines
|
repo: netbirdio/sign-pipelines
|
||||||
|
|||||||
4
.github/workflows/sync-main.yml
vendored
4
.github/workflows/sync-main.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger main branch sync
|
- name: Trigger main branch sync
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: sync-main.yml
|
workflow: sync-main.yml
|
||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "sha": "${{ github.sha }}" }'
|
inputs: '{ "sha": "${{ github.sha }}" }'
|
||||||
|
|||||||
10
.github/workflows/sync-tag.yml
vendored
10
.github/workflows/sync-tag.yml
vendored
@@ -3,7 +3,7 @@ name: sync tag
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger release tag sync
|
- name: Trigger release tag sync
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: sync-tag.yml
|
workflow: sync-tag.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger android-client submodule bump
|
- name: Trigger android-client submodule bump
|
||||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -42,10 +42,10 @@ jobs:
|
|||||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger ios-client submodule bump
|
- name: Trigger ios-client submodule bump
|
||||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
repo: netbirdio/ios-client
|
repo: netbirdio/ios-client
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
|
|||||||
26
.github/workflows/test-infrastructure-files.yml
vendored
26
.github/workflows/test-infrastructure-files.yml
vendored
@@ -6,10 +6,10 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'infrastructure_files/**'
|
- "infrastructure_files/**"
|
||||||
- '.github/workflows/test-infrastructure-files.yml'
|
- ".github/workflows/test-infrastructure-files.yml"
|
||||||
- 'management/cmd/**'
|
- "management/cmd/**"
|
||||||
- 'signal/cmd/**'
|
- "signal/cmd/**"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
store: [ 'sqlite', 'postgres', 'mysql' ]
|
store: ["sqlite", "postgres", "mysql"]
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
||||||
@@ -68,15 +68,17 @@ jobs:
|
|||||||
run: sudo apt-get install -y curl
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -139,8 +141,8 @@ jobs:
|
|||||||
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
||||||
CI_NETBIRD_SIGNAL_PORT: 12345
|
CI_NETBIRD_SIGNAL_PORT: 12345
|
||||||
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
|
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
|
||||||
NETBIRD_STORE_ENGINE_POSTGRES_DSN: '${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$'
|
NETBIRD_STORE_ENGINE_POSTGRES_DSN: "${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$"
|
||||||
NETBIRD_STORE_ENGINE_MYSQL_DSN: '${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$'
|
NETBIRD_STORE_ENGINE_MYSQL_DSN: "${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$"
|
||||||
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
||||||
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
||||||
CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false
|
CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false
|
||||||
@@ -254,7 +256,9 @@ jobs:
|
|||||||
run: sudo apt-get install -y jq
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: run script with Zitadel PostgreSQL
|
- name: run script with Zitadel PostgreSQL
|
||||||
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|||||||
8
.github/workflows/update-docs.yml
vendored
8
.github/workflows/update-docs.yml
vendored
@@ -3,9 +3,9 @@ name: update docs
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
paths:
|
paths:
|
||||||
- 'shared/management/http/api/openapi.yml'
|
- "shared/management/http/api/openapi.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger_docs_api_update:
|
trigger_docs_api_update:
|
||||||
@@ -13,10 +13,10 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger API pages generation
|
- name: Trigger API pages generation
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||||
with:
|
with:
|
||||||
workflow: generate api pages
|
workflow: generate api pages
|
||||||
repo: netbirdio/docs
|
repo: netbirdio/docs
|
||||||
ref: "refs/heads/main"
|
ref: "refs/heads/main"
|
||||||
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref }}" }'
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
|
|||||||
15
.github/workflows/wasm-build-validation.yml
vendored
15
.github/workflows/wasm-build-validation.yml
vendored
@@ -19,15 +19,17 @@ jobs:
|
|||||||
GOARCH: wasm
|
GOARCH: wasm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
- name: Install golangci-lint
|
- name: Install golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
@@ -42,9 +44,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Build Wasm client
|
- name: Build Wasm client
|
||||||
@@ -65,4 +69,3 @@ 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/management-integrations/integrations"
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||||
|
|
||||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
iv, _ := integrations.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
iv, _ := validator.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
@@ -84,6 +85,12 @@ type Options struct {
|
|||||||
DisableIPv6 bool
|
DisableIPv6 bool
|
||||||
// BlockInbound blocks all inbound connections from peers
|
// BlockInbound blocks all inbound connections from peers
|
||||||
BlockInbound bool
|
BlockInbound bool
|
||||||
|
// BlockLANAccess blocks the embedded peer from reaching the host's
|
||||||
|
// LAN (RFC 1918, link-local, loopback) when it's used as a routing
|
||||||
|
// peer. Mirrors profilemanager.ConfigInput.BlockLANAccess. Useful
|
||||||
|
// when the embedded client must never act as a stepping stone into
|
||||||
|
// the host's local network (e.g. the proxy's overlay peer).
|
||||||
|
BlockLANAccess bool
|
||||||
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
||||||
WireguardPort *int
|
WireguardPort *int
|
||||||
// MTU is the MTU for the tunnel interface.
|
// MTU is the MTU for the tunnel interface.
|
||||||
@@ -94,6 +101,26 @@ type Options struct {
|
|||||||
MTU *uint16
|
MTU *uint16
|
||||||
// DNSLabels defines additional DNS labels configured in the peer.
|
// DNSLabels defines additional DNS labels configured in the peer.
|
||||||
DNSLabels []string
|
DNSLabels []string
|
||||||
|
// Performance configures the tunnel's buffer pool cap and batch size.
|
||||||
|
Performance Performance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance configures the embedded client's tunnel memory/throughput knobs.
|
||||||
|
//
|
||||||
|
// These settings are process-global: any non-nil field also becomes the
|
||||||
|
// default for Clients constructed by later embed.New calls in the same
|
||||||
|
// process. Nil fields are ignored.
|
||||||
|
type Performance struct {
|
||||||
|
// PreallocatedBuffersPerPool caps the per-tunnel buffer pool. Zero
|
||||||
|
// leaves the pool unbounded. Lower values trade throughput for a
|
||||||
|
// tighter memory ceiling. May also be changed on a running Client via
|
||||||
|
// Client.SetPerformance, provided this field was nonzero at construction.
|
||||||
|
PreallocatedBuffersPerPool *uint32
|
||||||
|
// MaxBatchSize overrides the number of packets the tunnel reads or
|
||||||
|
// writes per syscall, which also bounds eager buffer allocation per
|
||||||
|
// worker. Zero uses the platform default. Applied at construction
|
||||||
|
// only; ignored by Client.SetPerformance.
|
||||||
|
MaxBatchSize *uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCredentials checks that exactly one credential type is provided
|
// validateCredentials checks that exactly one credential type is provided
|
||||||
@@ -175,6 +202,7 @@ func New(opts Options) (*Client, error) {
|
|||||||
DisableClientRoutes: &opts.DisableClientRoutes,
|
DisableClientRoutes: &opts.DisableClientRoutes,
|
||||||
DisableIPv6: &opts.DisableIPv6,
|
DisableIPv6: &opts.DisableIPv6,
|
||||||
BlockInbound: &opts.BlockInbound,
|
BlockInbound: &opts.BlockInbound,
|
||||||
|
BlockLANAccess: &opts.BlockLANAccess,
|
||||||
WireguardPort: opts.WireguardPort,
|
WireguardPort: opts.WireguardPort,
|
||||||
MTU: opts.MTU,
|
MTU: opts.MTU,
|
||||||
DNSLabels: parsedLabels,
|
DNSLabels: parsedLabels,
|
||||||
@@ -192,6 +220,13 @@ func New(opts Options) (*Client, error) {
|
|||||||
config.PrivateKey = opts.PrivateKey
|
config.PrivateKey = opts.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Performance.PreallocatedBuffersPerPool != nil {
|
||||||
|
wgdevice.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool)
|
||||||
|
}
|
||||||
|
if opts.Performance.MaxBatchSize != nil {
|
||||||
|
wgdevice.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
deviceName: opts.DeviceName,
|
deviceName: opts.DeviceName,
|
||||||
setupKey: opts.SetupKey,
|
setupKey: opts.SetupKey,
|
||||||
@@ -405,6 +440,21 @@ func (c *Client) Expose(ctx context.Context, req ExposeRequest) (*ExposeSession,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IdentityForIP looks up a remote peer by its tunnel IP using the
|
||||||
|
// embedded client's status recorder. Returns the peer's WireGuard public
|
||||||
|
// key and FQDN. ok=false means the IP 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()
|
||||||
@@ -473,6 +523,25 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
|||||||
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPerformance retunes a running Client. Only PreallocatedBuffersPerPool
|
||||||
|
// takes effect, and only when it was nonzero at construction;
|
||||||
|
// MaxBatchSize is construction-only and returns an error if set here.
|
||||||
|
//
|
||||||
|
// Returns ErrClientNotStarted / ErrEngineNotStarted if the Client is not
|
||||||
|
// running yet.
|
||||||
|
func (c *Client) SetPerformance(t Performance) error {
|
||||||
|
if t.MaxBatchSize != nil {
|
||||||
|
return errors.New("MaxBatchSize is construction-only and cannot be changed at runtime")
|
||||||
|
}
|
||||||
|
engine, err := c.getEngine()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return engine.SetPerformance(internal.Performance{
|
||||||
|
PreallocatedBuffersPerPool: t.PreallocatedBuffersPerPool,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// StartCapture begins capturing packets on this client's tunnel device.
|
// StartCapture begins capturing packets on this client's tunnel device.
|
||||||
// Only one capture can be active at a time; starting a new one stops the previous.
|
// Only one capture can be active at a time; starting a new one stops the previous.
|
||||||
// Call StopCapture (or CaptureSession.Stop) to end it.
|
// Call StopCapture (or CaptureSession.Stop) to end it.
|
||||||
|
|||||||
@@ -260,23 +260,15 @@ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
|
|||||||
|
|
||||||
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
|
||||||
; Drop Run, App Paths and Uninstall entries left in the 32-bit registry view
|
; Create autostart registry entry based on checkbox
|
||||||
; or HKCU by legacy installers.
|
|
||||||
DetailPrint "Cleaning legacy 32-bit / HKCU entries..."
|
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
|
||||||
SetRegView 32
|
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
|
||||||
DeleteRegKey HKLM "${REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UI_REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UNINSTALL_PATH}"
|
|
||||||
SetRegView 64
|
|
||||||
|
|
||||||
DetailPrint "Autostart enabled: $AutostartEnabled"
|
DetailPrint "Autostart enabled: $AutostartEnabled"
|
||||||
${If} $AutostartEnabled == "1"
|
${If} $AutostartEnabled == "1"
|
||||||
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
||||||
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
||||||
${Else}
|
${Else}
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
DetailPrint "Autostart not enabled by user"
|
DetailPrint "Autostart not enabled by user"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
@@ -307,16 +299,11 @@ ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
|||||||
DetailPrint "Terminating Netbird UI process..."
|
DetailPrint "Terminating Netbird UI process..."
|
||||||
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
||||||
|
|
||||||
; Remove autostart entries from every view a previous installer may have used.
|
; Remove autostart registry entry
|
||||||
DetailPrint "Removing autostart registry entry if exists..."
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
SetRegView 32
|
|
||||||
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
|
||||||
DeleteRegKey HKLM "${REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UI_REG_APP_PATH}"
|
|
||||||
DeleteRegKey HKLM "${UNINSTALL_PATH}"
|
|
||||||
SetRegView 64
|
|
||||||
|
|
||||||
; Handle data deletion based on checkbox
|
; Handle data deletion based on checkbox
|
||||||
DetailPrint "Checking if user requested data deletion..."
|
DetailPrint "Checking if user requested data deletion..."
|
||||||
|
|||||||
@@ -360,7 +360,13 @@ func isRedirectURLPortUsed(redirectURL string, excludedRanges []excludedPortRang
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%s", port)
|
// FreeBSD 15 disables connecting to INADDR_ANY (0.0.0.0) as a localhost
|
||||||
|
// alias by default, ensure explicit ip for localhost.
|
||||||
|
host := parsedURL.Hostname()
|
||||||
|
if host == "" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(host, port)
|
||||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -339,8 +339,7 @@ func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
|
|||||||
case entry.Pattern == ".":
|
case entry.Pattern == ".":
|
||||||
return true
|
return true
|
||||||
case entry.IsWildcard:
|
case entry.IsWildcard:
|
||||||
parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
|
return strings.HasSuffix(qname, "."+entry.Pattern)
|
||||||
return len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
|
|
||||||
default:
|
default:
|
||||||
// For non-wildcard patterns:
|
// For non-wildcard patterns:
|
||||||
// If handler wants subdomain matching, allow suffix match
|
// If handler wants subdomain matching, allow suffix match
|
||||||
|
|||||||
@@ -164,6 +164,54 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
|||||||
matchSubdomains: true,
|
matchSubdomains: true,
|
||||||
shouldMatch: true,
|
shouldMatch: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard label-boundary mismatch (suffix overlap)",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "x.ab.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard label-boundary match",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "x.b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard multi-label match",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "x.y.b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard no match on multi-label apex",
|
||||||
|
handlerDomain: "*.b.test.",
|
||||||
|
queryDomain: "b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard no match on unrelated suffix containment",
|
||||||
|
handlerDomain: "*.example.com.",
|
||||||
|
queryDomain: "notexample.com.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard accepts pattern registered without trailing dot",
|
||||||
|
handlerDomain: "*.b.test",
|
||||||
|
queryDomain: "x.b.test.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -273,6 +321,19 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
expectedCalls: 1,
|
expectedCalls: 1,
|
||||||
expectedHandler: 2, // highest priority matching handler should be called
|
expectedHandler: 2, // highest priority matching handler should be called
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "overlapping wildcard suffixes route to correct handler",
|
||||||
|
handlers: []struct {
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
}{
|
||||||
|
{pattern: "*.b.test.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
{pattern: "*.ab.test.", priority: nbdns.PriorityDNSRoute},
|
||||||
|
},
|
||||||
|
queryDomain: "app.ab.test.",
|
||||||
|
expectedCalls: 1,
|
||||||
|
expectedHandler: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "root zone with specific domain",
|
name: "root zone with specific domain",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ type resolver interface {
|
|||||||
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerConnectivity reports whether a tunnel IP belongs to a peer the
|
||||||
|
// client knows about and whether that peer is currently connected. The
|
||||||
|
// local resolver uses this to suppress A/AAAA answers whose RDATA points
|
||||||
|
// at a disconnected peer (typical case: a synthesized private-service
|
||||||
|
// record pointing at an embedded proxy peer that just went offline).
|
||||||
|
//
|
||||||
|
// known=false means the IP isn't in the local peerstore at all — the
|
||||||
|
// record is left alone (it points at something outside our mesh, e.g.
|
||||||
|
// a non-peer upstream).
|
||||||
|
type PeerConnectivity interface {
|
||||||
|
IsConnectedByIP(ip string) (known, connected bool)
|
||||||
|
}
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
records map[dns.Question][]dns.RR
|
records map[dns.Question][]dns.RR
|
||||||
@@ -33,6 +46,11 @@ type Resolver struct {
|
|||||||
// zones maps zone domain -> NonAuthoritative (true = non-authoritative, user-created zone)
|
// zones maps zone domain -> NonAuthoritative (true = non-authoritative, user-created zone)
|
||||||
zones map[domain.Domain]bool
|
zones map[domain.Domain]bool
|
||||||
resolver resolver
|
resolver resolver
|
||||||
|
// peerConn, when non-nil, is consulted on every A/AAAA answer to
|
||||||
|
// drop records pointing at disconnected peers. nil disables the
|
||||||
|
// filter and preserves the legacy "return whatever is registered"
|
||||||
|
// behaviour for callers that never wire a status source.
|
||||||
|
peerConn PeerConnectivity
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -49,6 +67,15 @@ func NewResolver() *Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPeerConnectivity wires the per-IP connectivity check used to filter
|
||||||
|
// out A/AAAA answers pointing at disconnected peers. Pass nil to disable.
|
||||||
|
// Safe to call multiple times; the latest value wins.
|
||||||
|
func (d *Resolver) SetPeerConnectivity(p PeerConnectivity) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.peerConn = p
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Resolver) MatchSubdomains() bool {
|
func (d *Resolver) MatchSubdomains() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -95,6 +122,7 @@ func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
replyMessage.RecursionAvailable = true
|
replyMessage.RecursionAvailable = true
|
||||||
|
|
||||||
result := d.lookupRecords(logger, question)
|
result := d.lookupRecords(logger, question)
|
||||||
|
result.records = d.filterDisconnectedPeerAnswers(logger, question, result.records)
|
||||||
replyMessage.Authoritative = !result.hasExternalData
|
replyMessage.Authoritative = !result.hasExternalData
|
||||||
replyMessage.Answer = result.records
|
replyMessage.Answer = result.records
|
||||||
replyMessage.Rcode = d.determineRcode(question, result)
|
replyMessage.Rcode = d.determineRcode(question, result)
|
||||||
@@ -436,6 +464,78 @@ func (d *Resolver) logDNSError(logger *log.Entry, hostname string, qtype uint16,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterDisconnectedPeerAnswers drops A/AAAA records whose RDATA matches
|
||||||
|
// a known but disconnected peer. The synthesized private-service zones
|
||||||
|
// emit one A record per connected proxy peer in a cluster; when a peer
|
||||||
|
// goes offline, the server-side refresh removes the record from the
|
||||||
|
// next netmap, but the client may still hold the previous netmap for a
|
||||||
|
// short window. This filter is the local belt to that braces — even on
|
||||||
|
// the stale netmap, the resolver hides the offline target.
|
||||||
|
//
|
||||||
|
// Records pointing at unknown IPs (outside the local peerstore, e.g.
|
||||||
|
// non-mesh upstreams) are never dropped. Non-A/AAAA records pass
|
||||||
|
// through untouched.
|
||||||
|
//
|
||||||
|
// Escape hatch: if filtering would leave the answer empty AND at least
|
||||||
|
// one record was filtered, the original list is returned. Better to
|
||||||
|
// hand the client a record that may not respond than NXDOMAIN it
|
||||||
|
// completely when every proxy peer is offline (the upstream may still
|
||||||
|
// be reachable some other way, or the peerstore may be stale).
|
||||||
|
func (d *Resolver) filterDisconnectedPeerAnswers(logger *log.Entry, question dns.Question, records []dns.RR) []dns.RR {
|
||||||
|
if len(records) == 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,6 +30,21 @@ func (m *mockResolver) LookupNetIP(ctx context.Context, network, host string) ([
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockPeerConnectivity returns canned (known, connected) results per IP.
|
||||||
|
// Used by the disconnected-peer filter tests below. IPs not in the map
|
||||||
|
// are reported as unknown so the filter leaves them alone.
|
||||||
|
type mockPeerConnectivity struct {
|
||||||
|
byIP map[string]struct{ known, connected bool }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockPeerConnectivity) IsConnectedByIP(ip string) (known, connected bool) {
|
||||||
|
v, ok := m.byIP[ip]
|
||||||
|
if !ok {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
return v.known, v.connected
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocalResolver_ServeDNS(t *testing.T) {
|
func TestLocalResolver_ServeDNS(t *testing.T) {
|
||||||
recordA := nbdns.SimpleRecord{
|
recordA := nbdns.SimpleRecord{
|
||||||
Name: "peera.netbird.cloud.",
|
Name: "peera.netbird.cloud.",
|
||||||
@@ -2652,3 +2667,114 @@ 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,6 +301,11 @@ 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)
|
||||||
@@ -1386,3 +1391,25 @@ func (s *DefaultServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// localPeerConnectivity adapts *peer.Status to local.PeerConnectivity so
|
||||||
|
// the local resolver can ask "is this IP a known peer and is it
|
||||||
|
// connected?" without taking on the peer package as a dependency.
|
||||||
|
// A nil status recorder always reports known=false so the resolver
|
||||||
|
// short-circuits to the legacy "return everything" path.
|
||||||
|
type localPeerConnectivity struct {
|
||||||
|
status *peer.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnectedByIP looks the IP up in the peerstore and surfaces both
|
||||||
|
// the known and connected bits. Used by Resolver.filterDisconnectedPeerAnswers.
|
||||||
|
func (l localPeerConnectivity) IsConnectedByIP(ip string) (known, connected bool) {
|
||||||
|
if l.status == nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
state, ok := l.status.PeerStateByIP(ip)
|
||||||
|
if !ok {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
return true, state.ConnStatus == peer.StatusConnected
|
||||||
|
}
|
||||||
|
|||||||
@@ -1967,6 +1967,29 @@ func (e *Engine) GetClientMetrics() *metrics.ClientMetrics {
|
|||||||
return e.clientMetrics
|
return e.clientMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performance bundles runtime-adjustable tunnel pool knobs.
|
||||||
|
// See Engine.SetPerformance. Nil fields are ignored.
|
||||||
|
type Performance struct {
|
||||||
|
PreallocatedBuffersPerPool *uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPerformance applies the given tuning to this engine's live Device.
|
||||||
|
func (e *Engine) SetPerformance(t Performance) error {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
if e.wgInterface == nil {
|
||||||
|
return fmt.Errorf("wg interface not initialized")
|
||||||
|
}
|
||||||
|
dev := e.wgInterface.GetWGDevice()
|
||||||
|
if dev == nil {
|
||||||
|
return fmt.Errorf("wg device not initialized")
|
||||||
|
}
|
||||||
|
if t.PreallocatedBuffersPerPool != nil {
|
||||||
|
dev.SetPreallocatedBuffersPerPool(*t.PreallocatedBuffersPerPool)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
||||||
iface, err := net.InterfaceByName(ifaceName)
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/management/server/job"
|
"github.com/netbirdio/netbird/management/server/job"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||||
@@ -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"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
signal "github.com/netbirdio/netbird/shared/signal/client"
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
"github.com/netbirdio/netbird/shared/signal/proto"
|
"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, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
// handle route changes
|
// handle route changes
|
||||||
case unix.RTM_ADD, syscall.RTM_DELETE:
|
case unix.RTM_ADD, syscall.RTM_DELETE:
|
||||||
route, err := parseRouteMessage(buf[:n])
|
route, flags, err := parseRouteMessage(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
log.Debugf("Network monitor: error parsing routing message: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -66,6 +66,10 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
}
|
}
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case unix.RTM_ADD:
|
case unix.RTM_ADD:
|
||||||
|
if systemops.IgnoreAddedDefaultRoute(flags) {
|
||||||
|
log.Debugf("Network monitor: ignoring added default route via %s, interface %s, flags %#x", route.Gw, intf, flags)
|
||||||
|
continue
|
||||||
|
}
|
||||||
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
|
||||||
return nil
|
return nil
|
||||||
case unix.RTM_DELETE:
|
case unix.RTM_DELETE:
|
||||||
@@ -78,22 +82,26 @@ func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Next
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRouteMessage(buf []byte) (*systemops.Route, error) {
|
func parseRouteMessage(buf []byte) (*systemops.Route, int, error) {
|
||||||
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse RIB: %v", err)
|
return nil, 0, fmt.Errorf("parse RIB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msgs) != 1 {
|
if len(msgs) != 1 {
|
||||||
return nil, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
return nil, 0, fmt.Errorf("unexpected RIB message msgs: %v", msgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, ok := msgs[0].(*route.RouteMessage)
|
msg, ok := msgs[0].(*route.RouteMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
return nil, 0, fmt.Errorf("unexpected RIB message type: %T", msgs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemops.MsgToRoute(msg)
|
r, err := systemops.MsgToRoute(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return r, msg.Flags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
// waitReadable blocks until fd has data to read, or ctx is cancelled.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer/id"
|
"github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
||||||
"github.com/netbirdio/netbird/client/internal/portforward"
|
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
@@ -899,7 +900,7 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to deterministic key if no NetBird PSK is configured
|
// Fallback to deterministic key if no NetBird PSK is configured
|
||||||
determKey, err := conn.rosenpassDetermKey()
|
determKey, err := rosenpass.DeterministicSeedKey(conn.config.LocalKey, conn.config.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
|
conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -908,26 +909,6 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
|||||||
return determKey
|
return determKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: move this logic into Rosenpass package
|
|
||||||
func (conn *Conn) rosenpassDetermKey() (*wgtypes.Key, error) {
|
|
||||||
lk := []byte(conn.config.LocalKey)
|
|
||||||
rk := []byte(conn.config.Key) // remote key
|
|
||||||
var keyInput []byte
|
|
||||||
if string(lk) > string(rk) {
|
|
||||||
//nolint:gocritic
|
|
||||||
keyInput = append(lk[:16], rk[:16]...)
|
|
||||||
} else {
|
|
||||||
//nolint:gocritic
|
|
||||||
keyInput = append(rk[:16], lk[:16]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := wgtypes.NewKey(keyInput)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isController(config ConnConfig) bool {
|
func isController(config ConnConfig) bool {
|
||||||
return config.LocalKey > config.Key
|
return config.LocalKey > config.Key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,9 +185,12 @@ func (s *StatusChangeSubscription) Events() chan map[string]RouterState {
|
|||||||
return s.eventsChan
|
return s.eventsChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status holds a state of peers, signal, management connections and relays
|
// Status holds a state of peers, signal, management connections and relays.
|
||||||
|
// mux is an RWMutex so hot read paths (notably PeerStateByIP, called for
|
||||||
|
// every private-service request) don't contend against each other.
|
||||||
|
// Pure read methods take RLock; anything that mutates state takes Lock.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
mux sync.Mutex
|
mux sync.RWMutex
|
||||||
peers map[string]State
|
peers map[string]State
|
||||||
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
|
||||||
@@ -283,8 +286,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.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
state, ok := d.peers[peerPubKey]
|
state, ok := d.peers[peerPubKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -294,8 +297,8 @@ func (d *Status) GetPeer(peerPubKey string) (State, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) PeerByIP(ip string) (string, bool) {
|
func (d *Status) PeerByIP(ip string) (string, bool) {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
for _, state := range d.peers {
|
for _, state := range d.peers {
|
||||||
if state.IP == ip {
|
if state.IP == ip {
|
||||||
@@ -305,6 +308,25 @@ 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()
|
||||||
@@ -702,8 +724,8 @@ func (d *Status) UnsubscribePeerStateChanges(subscription *StatusChangeSubscript
|
|||||||
|
|
||||||
// GetLocalPeerState returns the local peer state
|
// GetLocalPeerState returns the local peer state
|
||||||
func (d *Status) GetLocalPeerState() LocalPeerState {
|
func (d *Status) GetLocalPeerState() LocalPeerState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return d.localPeer.Clone()
|
return d.localPeer.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,8 +931,8 @@ func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetRosenpassState() RosenpassState {
|
func (d *Status) GetRosenpassState() RosenpassState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return RosenpassState{
|
return RosenpassState{
|
||||||
d.rosenpassEnabled,
|
d.rosenpassEnabled,
|
||||||
d.rosenpassPermissive,
|
d.rosenpassPermissive,
|
||||||
@@ -918,14 +940,14 @@ func (d *Status) GetRosenpassState() RosenpassState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetLazyConnection() bool {
|
func (d *Status) GetLazyConnection() bool {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return d.lazyConnectionEnabled
|
return d.lazyConnectionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetManagementState() ManagementState {
|
func (d *Status) GetManagementState() ManagementState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return ManagementState{
|
return ManagementState{
|
||||||
d.mgmAddress,
|
d.mgmAddress,
|
||||||
d.managementState,
|
d.managementState,
|
||||||
@@ -951,8 +973,8 @@ func (d *Status) UpdateLatency(pubKey string, latency time.Duration) error {
|
|||||||
|
|
||||||
// IsLoginRequired determines if a peer's login has expired.
|
// IsLoginRequired determines if a peer's login has expired.
|
||||||
func (d *Status) IsLoginRequired() bool {
|
func (d *Status) IsLoginRequired() bool {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
// if peer is connected to the management then login is not expired
|
// if peer is connected to the management then login is not expired
|
||||||
if d.managementState {
|
if d.managementState {
|
||||||
@@ -967,8 +989,8 @@ func (d *Status) IsLoginRequired() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetSignalState() SignalState {
|
func (d *Status) GetSignalState() SignalState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return SignalState{
|
return SignalState{
|
||||||
d.signalAddress,
|
d.signalAddress,
|
||||||
d.signalState,
|
d.signalState,
|
||||||
@@ -978,8 +1000,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.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
if d.relayMgr == nil {
|
if d.relayMgr == nil {
|
||||||
return d.relayStates
|
return d.relayStates
|
||||||
}
|
}
|
||||||
@@ -1008,8 +1030,8 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
if d.ingressGwMgr == nil {
|
if d.ingressGwMgr == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1018,16 +1040,16 @@ func (d *Status) ForwardingRules() []firewall.ForwardRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetDNSStates() []NSGroupState {
|
func (d *Status) GetDNSStates() []NSGroupState {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
// shallow copy is good enough, as slices fields are currently not updated
|
// shallow copy is good enough, as slices fields are currently not updated
|
||||||
return slices.Clone(d.nsGroupStates)
|
return slices.Clone(d.nsGroupStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
return maps.Clone(d.resolvedDomainsStates)
|
return maps.Clone(d.resolvedDomainsStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,8 +1065,8 @@ func (d *Status) GetFullStatus() FullStatus {
|
|||||||
LazyConnectionEnabled: d.GetLazyConnection(),
|
LazyConnectionEnabled: d.GetLazyConnection(),
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
|
|
||||||
fullStatus.LocalPeerState = d.localPeer
|
fullStatus.LocalPeerState = d.localPeer
|
||||||
|
|
||||||
@@ -1219,8 +1241,8 @@ func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) PeersStatus() (*configurer.Stats, error) {
|
func (d *Status) PeersStatus() (*configurer.Stats, error) {
|
||||||
d.mux.Lock()
|
d.mux.RLock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.RUnlock()
|
||||||
if d.wgIface == nil {
|
if d.wgIface == nil {
|
||||||
return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
|
return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,33 @@ 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,8 +179,10 @@ func getDefaultGateway() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dst := net.IPv4zero
|
dst := net.IPv4zero
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux.
|
// go-netroute v0.4.0 rejects unspecified destinations client-side on Linux/Android.
|
||||||
|
// TODO: on android/ios, use platform APIs (ConnectivityManager.getLinkProperties /
|
||||||
|
// NWPathMonitor) when netlink-based lookup is restricted or unavailable.
|
||||||
dst = net.IPv4(0, 0, 0, 1)
|
dst = net.IPv4(0, 0, 0, 1)
|
||||||
}
|
}
|
||||||
_, gateway, localIP, err = router.Route(dst)
|
_, gateway, localIP, err = router.Route(dst)
|
||||||
@@ -203,7 +205,7 @@ func getDefaultGateway6() (gateway net.IP, localIP net.IP, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dst := net.IPv6zero
|
dst := net.IPv6zero
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
// ::2
|
// ::2
|
||||||
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
dst = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ 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
|
||||||
@@ -36,7 +45,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 *rp.Server
|
server rpServer
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
port int
|
port int
|
||||||
wgIface PresharedKeySetter
|
wgIface PresharedKeySetter
|
||||||
@@ -51,7 +60,22 @@ 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{ifaceName: wgIfaceName, rpKeyHash: rpKeyHash, spk: public, ssk: secret, preSharedKey: (*[32]byte)(preSharedKey), rpPeerIDs: make(map[string]*rp.PeerID), lock: sync.Mutex{}}, nil
|
return &Manager{
|
||||||
|
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 {
|
||||||
@@ -65,6 +89,16 @@ 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 {
|
||||||
@@ -79,6 +113,16 @@ 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 {
|
||||||
@@ -182,24 +226,31 @@ func (m *Manager) Run() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.server, err = rp.NewUDPServer(conf)
|
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 m.server.Run()
|
return server.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the Rosenpass server
|
// Close closes the Rosenpass server
|
||||||
func (m *Manager) Close() error {
|
func (m *Manager) Close() error {
|
||||||
if m.server != nil {
|
m.lock.Lock()
|
||||||
err := m.server.Close()
|
server := m.server
|
||||||
if err != nil {
|
m.server = nil
|
||||||
log.Errorf("failed closing local rosenpass server")
|
m.lock.Unlock()
|
||||||
}
|
if server == nil {
|
||||||
m.server = nil
|
return nil
|
||||||
|
}
|
||||||
|
if err := server.Close(); err != nil {
|
||||||
|
log.Errorf("failed closing local rosenpass server: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,412 @@
|
|||||||
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)
|
||||||
|
}
|
||||||
|
|||||||
42
client/internal/rosenpass/seed.go
Normal file
42
client/internal/rosenpass/seed.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
44
client/internal/rosenpass/seed_test.go
Normal file
44
client/internal/rosenpass/seed_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//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,17 +96,19 @@ func (m *Manager) Stop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
cancel := m.cancel
|
||||||
|
done := m.done
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
if m.cancel == nil {
|
if cancel == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.cancel()
|
cancel()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-m.done:
|
case <-done:
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -64,13 +64,6 @@
|
|||||||
<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">
|
||||||
@@ -83,28 +76,10 @@
|
|||||||
</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/management-integrations/integrations"
|
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
||||||
@@ -315,7 +315,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, settingsManagerMock, eventStore, cacheStore)
|
ia, _ := validator.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,15 +3,14 @@
|
|||||||
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"
|
||||||
|
|
||||||
@@ -29,19 +28,11 @@ 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 {
|
||||||
info := _getInfo()
|
kernelName, kernelVersion, kernelPlatform := kernelInfo()
|
||||||
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 = osInfo[3]
|
osName = kernelName
|
||||||
}
|
}
|
||||||
|
|
||||||
systemHostname, _ := os.Hostname()
|
systemHostname, _ := os.Hostname()
|
||||||
@@ -58,8 +49,8 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gio := &Info{
|
gio := &Info{
|
||||||
Kernel: osInfo[0],
|
Kernel: kernelName,
|
||||||
Platform: osInfo[2],
|
Platform: kernelPlatform,
|
||||||
OS: osName,
|
OS: osName,
|
||||||
OSVersion: osVersion,
|
OSVersion: osVersion,
|
||||||
Hostname: extractDeviceName(ctx, systemHostname),
|
Hostname: extractDeviceName(ctx, systemHostname),
|
||||||
@@ -67,7 +58,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: osInfo[1],
|
KernelVersion: kernelVersion,
|
||||||
NetworkAddresses: addrs,
|
NetworkAddresses: addrs,
|
||||||
SystemSerialNumber: si.SystemSerialNumber,
|
SystemSerialNumber: si.SystemSerialNumber,
|
||||||
SystemProductName: si.SystemProductName,
|
SystemProductName: si.SystemProductName,
|
||||||
@@ -78,18 +69,12 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
func _getInfo() string {
|
func kernelInfo() (string, string, string) {
|
||||||
cmd := exec.Command("uname", "-srio")
|
var uts unix.Utsname
|
||||||
cmd.Stdin = strings.NewReader("some")
|
if err := unix.Uname(&uts); err != nil {
|
||||||
var out bytes.Buffer
|
return "", "", ""
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("getInfo: %s", err)
|
|
||||||
}
|
}
|
||||||
return out.String()
|
return unix.ByteSliceToString(uts.Sysname[:]), unix.ByteSliceToString(uts.Release[:]), unix.ByteSliceToString(uts.Machine[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func sysInfo() (string, string, string) {
|
func sysInfo() (string, string, string) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
certValidationTimeout = 60 * time.Second
|
certValidationTimeout = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) {
|
func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) {
|
||||||
@@ -46,17 +47,31 @@ func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, cert
|
|||||||
|
|
||||||
promise := conn.wsHandlers.Call("onCertificateRequest", certInfo)
|
promise := conn.wsHandlers.Call("onCertificateRequest", certInfo)
|
||||||
|
|
||||||
resultChan := make(chan bool)
|
resultChan := make(chan bool, 1)
|
||||||
errorChan := make(chan error)
|
errorChan := make(chan error, 1)
|
||||||
|
|
||||||
promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
// Release from inside the callbacks so a post-timeout promise resolution
|
||||||
result := args[0].Bool()
|
// does not invoke an already-released func.
|
||||||
resultChan <- result
|
var thenFn, catchFn js.Func
|
||||||
|
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,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +69,15 @@ type proxyConnection struct {
|
|||||||
rdpConn net.Conn
|
rdpConn net.Conn
|
||||||
tlsConn *tls.Conn
|
tlsConn *tls.Conn
|
||||||
wsHandlers js.Value
|
wsHandlers js.Value
|
||||||
ctx context.Context
|
// Go-side callbacks exposed to JS. js.FuncOf pins the Go closure in a
|
||||||
cancel context.CancelFunc
|
// global handle map and MUST be released, otherwise every connection
|
||||||
|
// 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
|
||||||
@@ -80,7 +90,11 @@ 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)
|
||||||
|
|
||||||
@@ -88,7 +102,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", len(p.activeConnections))
|
proxyID := fmt.Sprintf("proxy_%d", p.nextID.Add(1))
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
if p.destinations == nil {
|
if p.destinations == nil {
|
||||||
@@ -100,7 +114,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
|
||||||
js.Global().Set(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID), js.FuncOf(func(_ js.Value, args []js.Value) any {
|
handlerFn := 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")
|
||||||
}
|
}
|
||||||
@@ -108,7 +122,14 @@ 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)
|
||||||
@@ -142,6 +163,10 @@ 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)
|
||||||
@@ -150,7 +175,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) {
|
||||||
ws.Set("onGoMessage", js.FuncOf(func(this js.Value, args []js.Value) any {
|
conn.onMessageFn = js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -158,13 +183,15 @@ 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)
|
||||||
|
|
||||||
ws.Set("onGoClose", js.FuncOf(func(_ js.Value, args []js.Value) any {
|
conn.onCloseFn = 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) {
|
||||||
@@ -261,25 +288,49 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) {
|
func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) {
|
||||||
log.Debugf("Cleaning up connection %s", conn.id)
|
conn.cleanupOnce.Do(func() {
|
||||||
conn.cancel()
|
log.Debugf("Cleaning up connection %s", conn.id)
|
||||||
if conn.tlsConn != nil {
|
conn.cancel()
|
||||||
log.Debug("Closing TLS connection")
|
if conn.tlsConn != nil {
|
||||||
if err := conn.tlsConn.Close(); err != nil {
|
log.Debug("Closing TLS connection")
|
||||||
log.Debugf("Error closing TLS connection: %v", err)
|
if err := conn.tlsConn.Close(); err != nil {
|
||||||
|
log.Debugf("Error closing TLS connection: %v", err)
|
||||||
|
}
|
||||||
|
conn.tlsConn = nil
|
||||||
}
|
}
|
||||||
conn.tlsConn = nil
|
if conn.rdpConn != nil {
|
||||||
}
|
log.Debug("Closing TCP connection")
|
||||||
if conn.rdpConn != nil {
|
if err := conn.rdpConn.Close(); err != nil {
|
||||||
log.Debug("Closing TCP connection")
|
log.Debugf("Error closing TCP connection: %v", err)
|
||||||
if err := conn.rdpConn.Close(); err != nil {
|
}
|
||||||
log.Debugf("Error closing TCP connection: %v", err)
|
conn.rdpConn = nil
|
||||||
}
|
}
|
||||||
conn.rdpConn = nil
|
js.Global().Delete(fmt.Sprintf("handleRDCleanPathWebSocket_%s", conn.id))
|
||||||
}
|
|
||||||
p.mu.Lock()
|
// Detach before releasing so late JS calls surface as TypeError instead
|
||||||
delete(p.activeConnections, conn.id)
|
// of silent "call to released function".
|
||||||
p.mu.Unlock()
|
if conn.wsHandlers.Truthy() {
|
||||||
|
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())
|
||||||
|
|
||||||
jsInterface.Set("write", js.FuncOf(func(this js.Value, args []js.Value) any {
|
writeFunc := 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,9 +32,10 @@ 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)
|
||||||
|
|
||||||
jsInterface.Set("resize", js.FuncOf(func(this js.Value, args []js.Value) any {
|
resizeFunc := 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)
|
||||||
}
|
}
|
||||||
@@ -42,14 +43,26 @@ 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)
|
||||||
|
|
||||||
jsInterface.Set("close", js.FuncOf(func(this js.Value, args []js.Value) any {
|
closeFunc := 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 readLoop(client, jsInterface)
|
go func() {
|
||||||
|
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,6 +67,10 @@ 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()
|
||||||
}
|
}
|
||||||
@@ -168,7 +172,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.BaseServer
|
mgmtSrv mgmtServer.Server
|
||||||
signalSrv *signalServer.Server
|
signalSrv *signalServer.Server
|
||||||
healthcheck *healthcheck.Server
|
healthcheck *healthcheck.Server
|
||||||
stunServer *stun.Server
|
stunServer *stun.Server
|
||||||
@@ -324,19 +328,24 @@ func setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
servers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {
|
if s, ok := servers.mgmtSrv.GetContainer(mgmtServer.ContainerKeyBaseServer); ok {
|
||||||
grpcSrv := s.GRPCServer()
|
if baseServer, ok := s.(*mgmtServer.BaseServer); ok {
|
||||||
|
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(), servers.relaySrv, servers.metricsServer.Meter, cfg))
|
s.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), s.IDPHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))
|
||||||
if servers.relaySrv != nil {
|
if servers.relaySrv != nil {
|
||||||
log.Infof("Relay WebSocket handler added (path: /relay)")
|
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) {
|
||||||
@@ -346,38 +355,32 @@ 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.Add(1)
|
wg.Go(func() {
|
||||||
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.Add(1)
|
wg.Go(func() {
|
||||||
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.Add(1)
|
wg.Go(func() {
|
||||||
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.BaseServer, metricsServer *sharedMetrics.Metrics) error {
|
func shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv mgmtServer.Server, metricsServer *sharedMetrics.Metrics) error {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
if err := httpHealthcheck.Shutdown(ctx); err != nil {
|
if err := httpHealthcheck.Shutdown(ctx); err != nil {
|
||||||
@@ -491,7 +494,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.BaseServer, error) {
|
func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (mgmtServer.Server, error) {
|
||||||
mgmt := cfg.Management
|
mgmt := cfg.Management
|
||||||
|
|
||||||
// Extract port from listen address
|
// Extract port from listen address
|
||||||
@@ -502,7 +505,7 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*
|
|||||||
}
|
}
|
||||||
mgmtPort, _ := strconv.Atoi(portStr)
|
mgmtPort, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
mgmtSrv := mgmtServer.NewServer(
|
mgmtSrv := newServer(
|
||||||
&mgmtServer.Config{
|
&mgmtServer.Config{
|
||||||
NbConfig: mgmtConfig,
|
NbConfig: mgmtConfig,
|
||||||
DNSDomain: "",
|
DNSDomain: "",
|
||||||
@@ -521,7 +524,7 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
|
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, idpHandler 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)
|
||||||
@@ -556,6 +559,10 @@ func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, re
|
|||||||
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)
|
||||||
|
|||||||
13
combined/cmd/server.go
Normal file
13
combined/cmd/server.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
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.4.0
|
cunicu.li/go-rosenpass v0.5.42
|
||||||
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-20230704135630-469159ecf7d1
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
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.15.0
|
github.com/cilium/ebpf v0.19.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.1.1
|
github.com/gopacket/gopacket v1.4.0
|
||||||
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-20260107100953-33b7c9d03db0
|
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f
|
||||||
|
|
||||||
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.4.0 h1:LtPtBgFWY/9emfgC4glKLEqS0MJTylzV6+ChRhiZERw=
|
cunicu.li/go-rosenpass v0.5.42 h1:fRDsGwCxd7DhDgZI1Pxeo8GtNyq8BESZJ7w2/BGGJtU=
|
||||||
cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJplf/M=
|
cunicu.li/go-rosenpass v0.5.42/go.mod h1:YRBeyKOe/gWpSX2kpDUec5p9t0XOLsshTguId5gTGVg=
|
||||||
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.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
|
||||||
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
|
||||||
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.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
|
||||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||||
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.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw=
|
github.com/gopacket/gopacket v1.4.0 h1:cr1OlFpzksCkZHNO0eLjaSSOrMQnpPXg0j6qHIY3y2U=
|
||||||
github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs=
|
github.com/gopacket/gopacket v1.4.0/go.mod h1:EpvsxINeehp5qj4YMKMLf2/dekdhKn2IIAO/ZOifS7o=
|
||||||
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,6 +390,8 @@ 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=
|
||||||
@@ -499,8 +501,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-20260107100953-33b7c9d03db0 h1:h/QnNzm7xzHPm+gajcblYUOclrW2FeNeDlUNj6tTWKQ=
|
github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f h1:ff2D57RBjWtyQ2wVwJOxOgXAXOe/J2lJWtSX0Bz/BRk=
|
||||||
github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f/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=
|
||||||
@@ -900,8 +902,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-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||||
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=
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ func (c *Controller) CountStreams() int {
|
|||||||
return c.peersUpdateManager.CountStreams()
|
return c.peersUpdateManager.CountStreams()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string) error {
|
func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) 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,6 +175,10 @@ 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) {
|
||||||
@@ -242,14 +246,14 @@ func (c *Controller) bufferSendUpdateAccountPeers(ctx context.Context, accountID
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
_ = c.sendUpdateAccountPeers(ctx, accountID)
|
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
||||||
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)
|
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -265,7 +269,7 @@ func (c *Controller) UpdateAccountPeers(ctx context.Context, accountID string, r
|
|||||||
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)
|
return c.sendUpdateAccountPeers(ctx, accountID, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error {
|
func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error {
|
||||||
@@ -359,14 +363,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)
|
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
||||||
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)
|
_ = c.sendUpdateAccountPeers(ctx, accountID, reason)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).Debugf("update was sent to channel for peer %s", peerID)
|
log.WithContext(ctx).Tracef("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,6 +5,7 @@ package peers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
@@ -35,6 +36,14 @@ 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 {
|
||||||
@@ -66,7 +75,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -79,7 +88,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -99,6 +108,26 @@ 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,6 +6,7 @@ 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"
|
||||||
@@ -13,6 +14,7 @@ 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.
|
||||||
@@ -38,6 +40,20 @@ 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()
|
||||||
@@ -97,6 +113,21 @@ 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()
|
||||||
@@ -112,6 +143,22 @@ 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()
|
||||||
@@ -162,17 +209,3 @@ 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, ctx, 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,6 +23,8 @@ 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,6 +49,7 @@ 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,6 +35,7 @@ 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 {
|
||||||
@@ -56,7 +57,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -93,6 +94,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +111,7 @@ 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.
|
||||||
@@ -119,7 +122,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -160,7 +163,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -184,7 +187,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,6 +40,10 @@ 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) {
|
||||||
@@ -151,4 +155,3 @@ 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,6 +19,7 @@ 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,6 +21,7 @@ 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)
|
||||||
@@ -137,6 +138,11 @@ 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 {
|
||||||
@@ -178,4 +184,3 @@ 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,6 +99,9 @@ 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,6 +92,20 @@ 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,6 +20,9 @@ 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
|
||||||
@@ -67,10 +70,9 @@ type Cluster struct {
|
|||||||
Type ClusterType
|
Type ClusterType
|
||||||
Online bool
|
Online bool
|
||||||
ConnectedProxies int
|
ConnectedProxies int
|
||||||
// Capability flags. *bool because nil means "no proxy reported a
|
// *bool: nil = no proxy reported the capability; the dashboard renders that as unknown.
|
||||||
// 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, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Create)
|
ok, ctx, 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(r.Context(), &generated.ProxyAccessToken); err != nil {
|
if err := h.store.SaveProxyAccessToken(ctx, &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(r.Context(), w, resp)
|
util.WriteJSONObject(ctx, 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, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Read)
|
ok, ctx, 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(r.Context(), store.LockingStrengthNone, userAuth.AccountId)
|
tokens, err := h.store.GetProxyAccessTokensByAccountID(ctx, 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(r.Context(), w, resp)
|
util.WriteJSONObject(ctx, 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, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Delete)
|
ok, ctx, 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(r.Context(), store.LockingStrengthNone, tokenID)
|
token, err := h.store.GetProxyAccessTokenByID(ctx, 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(r.Context(), tokenID); err != nil {
|
if err := h.store.RevokeProxyAccessToken(ctx, 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(r.Context(), w, util.EmptyObject{})
|
util.WriteJSONObject(ctx, 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Create).Return(true, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(true, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(true, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Create).Return(false, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Read).Return(true, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Delete).Return(true, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(true, context.Background(), 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, nil)
|
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(true, context.Background(), nil)
|
||||||
|
|
||||||
h := &handler{
|
h := &handler{
|
||||||
store: mockStore,
|
store: mockStore,
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ 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,6 +82,7 @@ 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 {
|
||||||
@@ -119,7 +120,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -136,6 +137,7 @@ 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
|
||||||
@@ -144,7 +146,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -156,7 +158,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -208,6 +210,9 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -217,7 +222,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -238,7 +243,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -523,7 +528,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Update)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -779,6 +784,10 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -786,6 +795,13 @@ 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 {
|
||||||
@@ -820,7 +836,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -860,7 +876,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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
ok, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -962,12 +978,14 @@ 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, "", m.proxyController.GetOIDCValidationConfig()), s.ProxyCluster)
|
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, s.ToProtoMapping(service.Update, "", oidcCfg), 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, nil)
|
Return(true, ctx, 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,3 +1344,66 @@ 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,10 +45,11 @@ 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"
|
||||||
@@ -60,6 +61,11 @@ 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 {
|
||||||
@@ -67,7 +73,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"` // the Host field is only used for subnet targets, otherwise ignored
|
Host string `json:"host"`
|
||||||
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"`
|
||||||
@@ -200,6 +206,10 @@ 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
|
||||||
@@ -299,6 +309,12 @@ 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 != "" {
|
||||||
@@ -308,6 +324,7 @@ 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()
|
||||||
|
|
||||||
@@ -349,6 +366,7 @@ 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 {
|
||||||
@@ -455,7 +473,8 @@ 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 && opts.PathRewrite == "" && len(opts.CustomHeaders) == 0 {
|
if !opts.SkipTLSVerify && opts.RequestTimeout == 0 && opts.SessionIdleTimeout == 0 &&
|
||||||
|
opts.PathRewrite == "" && len(opts.CustomHeaders) == 0 && !opts.DirectUpstream {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
apiOpts := &api.ServiceTargetOptions{}
|
apiOpts := &api.ServiceTargetOptions{}
|
||||||
@@ -477,17 +496,22 @@ 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 && len(opts.CustomHeaders) == 0 {
|
if !opts.SkipTLSVerify && opts.PathRewrite == "" && opts.RequestTimeout == 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)
|
||||||
@@ -537,6 +561,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +578,14 @@ 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 {
|
||||||
@@ -740,6 +775,9 @@ 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:
|
||||||
@@ -753,6 +791,23 @@ 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")
|
||||||
@@ -799,11 +854,21 @@ 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 field will be ignored
|
// Host is normally overwritten by replaceHostByLookup with the
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
@@ -821,25 +886,67 @@ 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:
|
||||||
// OK
|
if err := validateDirectUpstreamHost(0, target); err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -1174,6 +1281,11 @@ 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,
|
||||||
@@ -1195,6 +1307,8 @@ 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,6 +12,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1116,3 +1117,191 @@ 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,6 +20,20 @@ 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) {
|
||||||
@@ -34,7 +48,13 @@ func GenerateKeyPair() (*KeyPair, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SignToken(privKeyB64, userID, domain string, method auth.Method, expiration time.Duration) (string, error) {
|
// SignToken mints a session JWT for the given user and domain. email,
|
||||||
|
// 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)
|
||||||
@@ -56,7 +76,10 @@ func SignToken(privKeyB64, userID, domain string, method auth.Method, expiration
|
|||||||
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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Create)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Delete)
|
ok, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, status.Errorf(status.Internal, "permission check failed"))
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Create)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
ok, ctx, 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, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Delete)
|
ok, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, status.Errorf(status.Internal, "permission check failed"))
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(true, ctx, 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, nil)
|
Return(false, ctx, 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, nil)
|
Return(true, ctx, 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,8 +10,10 @@ 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"
|
||||||
@@ -19,7 +21,6 @@ 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"
|
||||||
@@ -27,16 +28,20 @@ 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,
|
||||||
@@ -94,12 +99,17 @@ 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 {
|
||||||
integrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())
|
var err error
|
||||||
if err != nil {
|
key := s.Config.DataStoreEncryptionKey
|
||||||
log.Fatalf("failed to initialize integration metrics: %v", err)
|
if key == "" {
|
||||||
|
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 := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)
|
eventStore, err := activitystore.NewSqlStore(context.Background(), s.Config.Datadir, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to initialize event store: %v", err)
|
log.Fatalf("failed to initialize event store: %v", err)
|
||||||
}
|
}
|
||||||
@@ -110,7 +120,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.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())
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create API handler: %v", err)
|
log.Fatalf("failed to create API handler: %v", err)
|
||||||
}
|
}
|
||||||
@@ -118,6 +128,22 @@ 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,6 +19,7 @@ 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"
|
||||||
@@ -38,7 +39,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 := integrations.NewIntegratedValidator(
|
integratedPeerValidator, err := validator.NewIntegratedValidator(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
s.PeersManager(),
|
s.PeersManager(),
|
||||||
s.SettingsManager(),
|
s.SettingsManager(),
|
||||||
|
|||||||
@@ -57,13 +57,7 @@ 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 {
|
||||||
manager := integrations.InitPermissionsManager(s.Store(), s.Metrics().GetMeter())
|
return permissions.NewManager(s.Store())
|
||||||
|
|
||||||
s.AfterInit(func(s *BaseServer) {
|
|
||||||
manager.SetAccountManager(s.AccountManager())
|
|
||||||
})
|
|
||||||
|
|
||||||
return manager
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +147,6 @@ func (s *BaseServer) IdpManager() idp.Manager {
|
|||||||
return idpManager
|
return idpManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -235,3 +228,7 @@ func (s *BaseServer) ReverseProxyDomainManager() *manager.Manager {
|
|||||||
return &m
|
return &m
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BaseServer) IsValidChildAccount(_ context.Context, _, _, _ string) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ 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 {
|
||||||
@@ -91,7 +93,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 {
|
||||||
return &BaseServer{
|
s := &BaseServer{
|
||||||
Config: cfg.NbConfig,
|
Config: cfg.NbConfig,
|
||||||
container: make(map[string]any),
|
container: make(map[string]any),
|
||||||
dnsDomain: cfg.DNSDomain,
|
dnsDomain: cfg.DNSDomain,
|
||||||
@@ -104,6 +106,9 @@ 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)) {
|
||||||
@@ -188,7 +193,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.Metrics().GetMeter())
|
rootHandler := s.handlerFunc(srvCtx, s.GRPCServer(), s.APIHandler(), s.IDPHandler(), 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
|
||||||
@@ -299,7 +304,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, meter metric.Meter) http.Handler {
|
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, idpHandler 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 {
|
||||||
@@ -318,6 +323,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
goproto "google.golang.org/protobuf/proto"
|
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"
|
||||||
|
|
||||||
@@ -185,9 +187,38 @@ 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) {
|
func buildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]map[string]struct{}) ([][]byte, map[string]*proto.MachineUserIndexes) {
|
||||||
userIDToIndex := make(map[string]uint32)
|
userIDToIndex := make(map[string]uint32)
|
||||||
var hashedUsers [][]byte
|
var hashedUsers [][]byte
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
@@ -200,3 +201,29 @@ 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,6 +351,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,6 +755,11 @@ 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)
|
||||||
@@ -882,16 +888,20 @@ func (s *ProxyServiceServer) SendServiceUpdateToCluster(ctx context.Context, upd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxyAcceptsMapping returns whether the proxy should receive this mapping.
|
// proxyAcceptsMapping returns whether the proxy can receive this mapping.
|
||||||
// Old proxies that never reported capabilities are skipped for non-TLS L4
|
// Private mappings require SupportsPrivateService; custom-port L4 mappings
|
||||||
// mappings with a custom listen port, since they don't understand the
|
// require SupportsCustomPorts. Remove operations always pass so proxies can
|
||||||
// protocol. Proxies that report capabilities (even SupportsCustomPorts=false)
|
// clean up.
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -900,6 +910,29 @@ 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.
|
||||||
@@ -961,7 +994,10 @@ 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)
|
||||||
|
|
||||||
token, err := s.generateSessionToken(ctx, authenticated, service, userId, method)
|
// Non-OIDC schemes (PIN/Password/Header) authenticate against per-service
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
@@ -1050,7 +1086,7 @@ func (s *ProxyServiceServer) logAuthenticationError(ctx context.Context, err err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProxyServiceServer) generateSessionToken(ctx context.Context, authenticated bool, service *rpservice.Service, userId string, method proxyauth.Method) (string, error) {
|
func (s *ProxyServiceServer) generateSessionToken(ctx context.Context, authenticated bool, service *rpservice.Service, userId, userEmail string, method proxyauth.Method, groupIDs, groupNames []string) (string, error) {
|
||||||
if !authenticated || service.SessionPrivateKey == "" {
|
if !authenticated || service.SessionPrivateKey == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@@ -1058,8 +1094,11 @@ 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 {
|
||||||
@@ -1070,6 +1109,26 @@ 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 {
|
||||||
@@ -1334,7 +1393,9 @@ 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 user.
|
// GenerateSessionToken creates a signed session JWT for the given domain and
|
||||||
|
// 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 {
|
||||||
@@ -1345,11 +1406,29 @@ 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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1453,7 +1532,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,
|
||||||
@@ -1466,7 +1545,7 @@ func (s *ProxyServiceServer) ValidateSession(ctx context.Context, req *proto.Val
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.usersManager.GetUser(ctx, userID)
|
user, userGroups, err := s.usersManager.GetUserWithGroups(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
@@ -1500,12 +1579,15 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1515,10 +1597,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1551,3 +1636,154 @@ 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,6 +129,14 @@ 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
|
||||||
@@ -420,3 +428,46 @@ 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).Debugf("received an update for peer %s", peerKey.String())
|
log.WithContext(ctx).Tracef("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).Debugf("sent an update to peer %s", peerKey.String())
|
log.WithContext(ctx).Tracef("sent an update to peer %s", peerKey.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,6 +821,80 @@ 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
|
||||||
@@ -844,6 +918,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, time.Hour)
|
token, err := sessionkey.SignToken(privKeyB64, userID, domain, auth.MethodOIDC, nil, time.Hour)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,7 @@ 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) {
|
||||||
@@ -145,6 +146,7 @@ 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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
allowed, ctx, 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,7 +355,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,7 +855,7 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Delete)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -1412,7 +1422,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -1425,7 +1435,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -1438,7 +1448,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -1463,7 +1473,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -1530,7 +1540,8 @@ func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, u
|
|||||||
return accountID, user.Id, nil
|
return accountID, user.Id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := am.permissionsManager.ValidateAccountAccess(ctx, accountID, user, false); err != nil {
|
ctx, err = am.permissionsManager.ValidateAccountAccess(ctx, accountID, user, false)
|
||||||
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1976,7 +1987,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Read)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -2544,7 +2555,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
@@ -2634,7 +2645,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, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
|
allowed, ctx, 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ 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)
|
||||||
|
|||||||
@@ -1304,6 +1304,21 @@ 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()
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ const (
|
|||||||
AccountLocalMfaEnabled Activity = 123
|
AccountLocalMfaEnabled Activity = 123
|
||||||
// AccountLocalMfaDisabled indicates that a user disabled TOTP MFA for local users
|
// AccountLocalMfaDisabled indicates that a user disabled TOTP MFA for local users
|
||||||
AccountLocalMfaDisabled Activity = 124
|
AccountLocalMfaDisabled Activity = 124
|
||||||
|
// UserExtendedPeerSession indicates that a user refreshed their peer's
|
||||||
|
// SSO session deadline via ExtendAuthSession without re-establishing the
|
||||||
|
// tunnel. Distinct from UserLoggedInPeer (full interactive login).
|
||||||
|
UserExtendedPeerSession Activity = 125
|
||||||
|
|
||||||
AccountDeleted Activity = 99999
|
AccountDeleted Activity = 99999
|
||||||
)
|
)
|
||||||
@@ -394,6 +398,8 @@ var activityMap = map[Activity]Code{
|
|||||||
AccountLocalMfaEnabled: {"Account local MFA enabled", "account.setting.local.mfa.enable"},
|
AccountLocalMfaEnabled: {"Account local MFA enabled", "account.setting.local.mfa.enable"},
|
||||||
AccountLocalMfaDisabled: {"Account local MFA disabled", "account.setting.local.mfa.disable"},
|
AccountLocalMfaDisabled: {"Account local MFA disabled", "account.setting.local.mfa.disable"},
|
||||||
|
|
||||||
|
UserExtendedPeerSession: {"User extended peer session", "user.peer.session.extend"},
|
||||||
|
|
||||||
DomainAdded: {"Domain added", "domain.add"},
|
DomainAdded: {"Domain added", "domain.add"},
|
||||||
DomainDeleted: {"Domain deleted", "domain.delete"},
|
DomainDeleted: {"Domain deleted", "domain.delete"},
|
||||||
DomainValidated: {"Domain validated", "domain.validate"},
|
DomainValidated: {"Domain validated", "domain.validate"},
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
package context
|
package context
|
||||||
|
|
||||||
import "github.com/netbirdio/netbird/shared/context"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
nbcontext "github.com/netbirdio/netbird/shared/context"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RequestIDKey = context.RequestIDKey
|
RequestIDKey = nbcontext.RequestIDKey
|
||||||
AccountIDKey = context.AccountIDKey
|
AccountIDKey = nbcontext.AccountIDKey
|
||||||
UserIDKey = context.UserIDKey
|
RoleKey = nbcontext.RoleKey
|
||||||
PeerIDKey = context.PeerIDKey
|
UserIDKey = nbcontext.UserIDKey
|
||||||
|
PeerIDKey = nbcontext.PeerIDKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RoleFromContext returns the role stored in ctx, or empty string and false if absent.
|
||||||
|
func RoleFromContext(ctx context.Context) (string, bool) {
|
||||||
|
role, ok := ctx.Value(RoleKey).(string)
|
||||||
|
return role, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRole returns a new context carrying the given role.
|
||||||
|
func WithRole(ctx context.Context, role string) context.Context {
|
||||||
|
//nolint
|
||||||
|
return context.WithValue(ctx, RoleKey, role)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
|
|
||||||
// GetDNSSettings validates a user role and returns the DNS settings for the provided account ID
|
// GetDNSSettings validates a user role and returns the DNS settings for the provided account ID
|
||||||
func (am *DefaultAccountManager) GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error) {
|
func (am *DefaultAccountManager) GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error) {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Read)
|
allowed, ctx, err := am.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)
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ func (am *DefaultAccountManager) SaveDNSSettings(ctx context.Context, accountID
|
|||||||
return status.Errorf(status.InvalidArgument, "the dns settings provided are nil")
|
return status.Errorf(status.InvalidArgument, "the dns settings provided are nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Dns, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func isEnabled() bool {
|
|||||||
|
|
||||||
// GetEvents returns a list of activity events of an account
|
// GetEvents returns a list of activity events of an account
|
||||||
func (am *DefaultAccountManager) GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error) {
|
func (am *DefaultAccountManager) GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error) {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Events, operations.Read)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Events, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (e *GroupLinkError) Error() string {
|
|||||||
|
|
||||||
// CheckGroupPermissions validates if a user has the necessary permissions to view groups
|
// CheckGroupPermissions validates if a user has the necessary permissions to view groups
|
||||||
func (am *DefaultAccountManager) CheckGroupPermissions(ctx context.Context, accountID, userID string) error {
|
func (am *DefaultAccountManager) CheckGroupPermissions(ctx context.Context, accountID, userID string) error {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Read)
|
allowed, _, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ func (am *DefaultAccountManager) GetGroupByName(ctx context.Context, groupName,
|
|||||||
|
|
||||||
// CreateGroup object of the peers
|
// CreateGroup object of the peers
|
||||||
func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error {
|
func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Create)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, use
|
|||||||
|
|
||||||
// UpdateGroup object of the peers
|
// UpdateGroup object of the peers
|
||||||
func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error {
|
func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, userID string, newGroup *types.Group) error {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, use
|
|||||||
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
|
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
|
||||||
// This method will not create group peer membership relations. Use AddPeerToGroup or RemovePeerFromGroup methods for that.
|
// This method will not create group peer membership relations. Use AddPeerToGroup or RemovePeerFromGroup methods for that.
|
||||||
func (am *DefaultAccountManager) CreateGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error {
|
func (am *DefaultAccountManager) CreateGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Create)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ func (am *DefaultAccountManager) CreateGroups(ctx context.Context, accountID, us
|
|||||||
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
|
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
|
||||||
// This method will not create group peer membership relations. Use AddPeerToGroup or RemovePeerFromGroup methods for that.
|
// This method will not create group peer membership relations. Use AddPeerToGroup or RemovePeerFromGroup methods for that.
|
||||||
func (am *DefaultAccountManager) UpdateGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error {
|
func (am *DefaultAccountManager) UpdateGroups(ctx context.Context, accountID, userID string, groups []*types.Group) error {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
@@ -427,7 +427,7 @@ func (am *DefaultAccountManager) DeleteGroup(ctx context.Context, accountID, use
|
|||||||
// If an error occurs while deleting a group, the function skips it and continues deleting other groups.
|
// If an error occurs while deleting a group, the function skips it and continues deleting other groups.
|
||||||
// Errors are collected and returned at the end.
|
// Errors are collected and returned at the end.
|
||||||
func (am *DefaultAccountManager) DeleteGroups(ctx context.Context, accountID, userID string, groupIDs []string) error {
|
func (am *DefaultAccountManager) DeleteGroups(ctx context.Context, accountID, userID string, groupIDs []string) error {
|
||||||
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Delete)
|
allowed, ctx, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Delete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.NewPermissionValidationError(err)
|
return status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func NewManager(store store.Store, permissionsManager permissions.Manager, accou
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) {
|
func (m *managerImpl) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) {
|
||||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Read)
|
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func (m *managerImpl) GetAllGroupsMap(ctx context.Context, accountID, userID str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) AddResourceToGroup(ctx context.Context, accountID, userID, groupID string, resource *types.Resource) error {
|
func (m *managerImpl) AddResourceToGroup(ctx context.Context, accountID, userID, groupID string, resource *types.Resource) error {
|
||||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
|
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Groups, operations.Update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxytoken"
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxytoken"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||||
reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager"
|
reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager"
|
||||||
|
|
||||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||||
idpmanager "github.com/netbirdio/netbird/management/server/idp"
|
idpmanager "github.com/netbirdio/netbird/management/server/idp"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||||
zonesManager "github.com/netbirdio/netbird/management/internals/modules/zones/manager"
|
zonesManager "github.com/netbirdio/netbird/management/internals/modules/zones/manager"
|
||||||
@@ -32,12 +30,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/account"
|
"github.com/netbirdio/netbird/management/server/account"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/http/handlers/proxy"
|
"github.com/netbirdio/netbird/management/server/http/handlers/proxy"
|
||||||
|
|
||||||
nbpeers "github.com/netbirdio/netbird/management/internals/modules/peers"
|
|
||||||
"github.com/netbirdio/netbird/management/server/auth"
|
"github.com/netbirdio/netbird/management/server/auth"
|
||||||
"github.com/netbirdio/netbird/management/server/geolocation"
|
"github.com/netbirdio/netbird/management/server/geolocation"
|
||||||
nbgroups "github.com/netbirdio/netbird/management/server/groups"
|
nbgroups "github.com/netbirdio/netbird/management/server/groups"
|
||||||
@@ -56,17 +52,14 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||||
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
||||||
nbinstance "github.com/netbirdio/netbird/management/server/instance"
|
nbinstance "github.com/netbirdio/netbird/management/server/instance"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
|
||||||
nbnetworks "github.com/netbirdio/netbird/management/server/networks"
|
nbnetworks "github.com/netbirdio/netbird/management/server/networks"
|
||||||
"github.com/netbirdio/netbird/management/server/networks/resources"
|
"github.com/netbirdio/netbird/management/server/networks/resources"
|
||||||
"github.com/netbirdio/netbird/management/server/networks/routers"
|
"github.com/netbirdio/netbird/management/server/networks/routers"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiPrefix = "/api"
|
|
||||||
|
|
||||||
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
||||||
func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, rateLimiter *middleware.APIRateLimiter) (http.Handler, error) {
|
func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, permissionsManager permissions.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, rateLimiter *middleware.APIRateLimiter, isValidChildAccount middleware.IsValidChildAccountFunc) (http.Handler, error) {
|
||||||
|
|
||||||
// Register bypass paths for unauthenticated endpoints
|
// Register bypass paths for unauthenticated endpoints
|
||||||
if err := bypass.AddBypassPath("/api/instance"); err != nil {
|
if err := bypass.AddBypassPath("/api/instance"); err != nil {
|
||||||
@@ -100,25 +93,16 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
|
|||||||
accountManager.GetUserFromUserAuth,
|
accountManager.GetUserFromUserAuth,
|
||||||
rateLimiter,
|
rateLimiter,
|
||||||
appMetrics.GetMeter(),
|
appMetrics.GetMeter(),
|
||||||
|
isValidChildAccount,
|
||||||
)
|
)
|
||||||
|
|
||||||
corsMiddleware := cors.AllowAll()
|
corsMiddleware := cors.AllowAll()
|
||||||
|
|
||||||
rootRouter := mux.NewRouter()
|
|
||||||
metricsMiddleware := appMetrics.HTTPMiddleware()
|
metricsMiddleware := appMetrics.HTTPMiddleware()
|
||||||
|
|
||||||
prefix := apiPrefix
|
|
||||||
router := rootRouter.PathPrefix(prefix).Subrouter()
|
|
||||||
|
|
||||||
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler)
|
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler)
|
||||||
|
|
||||||
if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter(), permissionsManager, peersManager, proxyController, settingsManager); err != nil {
|
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), idpManager)
|
||||||
return nil, fmt.Errorf("register integrations endpoints: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if embedded IdP is enabled for instance manager
|
|
||||||
embeddedIdP, embeddedIdpEnabled := idpManager.(*idpmanager.EmbeddedIdPManager)
|
|
||||||
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
||||||
}
|
}
|
||||||
@@ -154,10 +138,5 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
|
|||||||
oauthHandler.RegisterEndpoints(router)
|
oauthHandler.RegisterEndpoints(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mount embedded IdP handler at /oauth2 path if configured
|
return router, nil
|
||||||
if embeddedIdpEnabled {
|
|
||||||
rootRouter.PathPrefix("/oauth2").Handler(corsMiddleware.Handler(embeddedIdP.Handler()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootRouter, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,48 +405,48 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed, err := h.permissionsManager.ValidateUserPermissions(r.Context(), accountID, userID, modules.Peers, operations.Read)
|
allowed, ctx, err := h.permissionsManager.ValidateUserPermissions(r.Context(), accountID, userID, modules.Peers, operations.Read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(r.Context(), status.NewPermissionValidationError(err), w)
|
util.WriteError(ctx, status.NewPermissionValidationError(err), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := h.accountManager.GetAccountByID(r.Context(), accountID, activity.SystemInitiator)
|
account, err := h.accountManager.GetAccountByID(ctx, accountID, activity.SystemInitiator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(r.Context(), err, w)
|
util.WriteError(ctx, err, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowed && !userAuth.IsChild {
|
if !allowed && !userAuth.IsChild {
|
||||||
if account.Settings.RegularUsersViewBlocked {
|
if account.Settings.RegularUsersViewBlocked {
|
||||||
util.WriteJSONObject(r.Context(), w, []api.AccessiblePeer{})
|
util.WriteJSONObject(ctx, w, []api.AccessiblePeer{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peer, ok := account.Peers[peerID]
|
peer, ok := account.Peers[peerID]
|
||||||
if !ok {
|
if !ok {
|
||||||
util.WriteError(r.Context(), status.Errorf(status.NotFound, "peer not found"), w)
|
util.WriteError(ctx, status.Errorf(status.NotFound, "peer not found"), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if peer.UserID != user.Id {
|
if peer.UserID != user.Id {
|
||||||
util.WriteJSONObject(r.Context(), w, []api.AccessiblePeer{})
|
util.WriteJSONObject(ctx, w, []api.AccessiblePeer{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validPeers, _, err := h.accountManager.GetValidatedPeers(r.Context(), accountID)
|
validPeers, _, err := h.accountManager.GetValidatedPeers(ctx, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(r.Context()).Errorf("failed to list approved peers: %v", err)
|
log.WithContext(ctx).Errorf("failed to list approved peers: %v", err)
|
||||||
util.WriteError(r.Context(), fmt.Errorf("internal error"), w)
|
util.WriteError(ctx, fmt.Errorf("internal error"), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsDomain := h.networkMapController.GetDNSDomain(account.Settings)
|
dnsDomain := h.networkMapController.GetDNSDomain(account.Settings)
|
||||||
|
|
||||||
netMap := account.GetPeerNetworkMapFromComponents(r.Context(), peerID, dns.CustomZone{}, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers())
|
netMap := account.GetPeerNetworkMapFromComponents(ctx, peerID, dns.CustomZone{}, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers())
|
||||||
|
|
||||||
util.WriteJSONObject(r.Context(), w, toAccessiblePeers(netMap, dnsDomain))
|
util.WriteJSONObject(ctx, w, toAccessiblePeers(netMap, dnsDomain))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user