mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-06 06:39:57 +00:00
Compare commits
6 Commits
netmap_pro
...
worktree-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b8a5c15cc | ||
|
|
ddc9f8199a | ||
|
|
6c6ad8d14f | ||
|
|
7461d4cef4 | ||
|
|
c761d0d1cd | ||
|
|
c46dee4e6b |
45
.github/dependabot.yml
vendored
45
.github/dependabot.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
open-pull-requests-limit: 15
|
|
||||||
groups:
|
|
||||||
actions:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
ignore:
|
|
||||||
# git-town/action v1.3.x crashes on cyclic PR graphs (self-loop main->main
|
|
||||||
# fork PRs) via its topological-sort visualization. Pinned to v1.2.1 in
|
|
||||||
# git-town.yml; block v1.3.x until upstream tolerates cyclic edges.
|
|
||||||
- dependency-name: "git-town/action"
|
|
||||||
update-types:
|
|
||||||
- "version-update:semver-minor"
|
|
||||||
- "version-update:semver-major"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directories:
|
|
||||||
- "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
open-pull-requests-limit: 15
|
|
||||||
groups:
|
|
||||||
aws-sdk:
|
|
||||||
patterns:
|
|
||||||
- "github.com/aws/aws-sdk-go-v2/*"
|
|
||||||
pion:
|
|
||||||
patterns:
|
|
||||||
- "github.com/pion/*"
|
|
||||||
gorm:
|
|
||||||
patterns:
|
|
||||||
- "gorm.io/*"
|
|
||||||
otel:
|
|
||||||
patterns:
|
|
||||||
- "go.opentelemetry.io/*"
|
|
||||||
testcontainers:
|
|
||||||
patterns:
|
|
||||||
- "github.com/testcontainers/testcontainers-go/*"
|
|
||||||
wireguard:
|
|
||||||
patterns:
|
|
||||||
- "golang.zx2c4.com/wireguard*"
|
|
||||||
68
.github/workflows/agent-network-e2e.yml
vendored
68
.github/workflows/agent-network-e2e.yml
vendored
@@ -1,68 +0,0 @@
|
|||||||
name: Agent Network E2E
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Nightly at 03:00 UTC, plus on demand from the Actions tab.
|
|
||||||
schedule:
|
|
||||||
- cron: "0 3 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
e2e:
|
|
||||||
name: Agent Network E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 45
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
||||||
with:
|
|
||||||
go-version-file: "go.mod"
|
|
||||||
|
|
||||||
# Container-driver builder so the harness can build the combined/proxy/
|
|
||||||
# client images from source with a local layer cache.
|
|
||||||
- name: Set up Buildx
|
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
|
||||||
|
|
||||||
# Persist the Docker layer cache across runs. This caches the base, apt,
|
|
||||||
# and go-mod-download layers; the Go compile still re-runs, as BuildKit
|
|
||||||
# mount caches cannot be exported to the GitHub cache.
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-anet-e2e-buildx-${{ hashFiles('go.sum', 'combined/Dockerfile.multistage', 'proxy/Dockerfile.multistage', 'e2e/harness/Dockerfile.client') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-anet-e2e-buildx-
|
|
||||||
|
|
||||||
- name: Run agent-network e2e
|
|
||||||
env:
|
|
||||||
# Build the images from source (this branch's code) with the shared
|
|
||||||
# local layer cache.
|
|
||||||
NB_E2E_BUILDX_CACHE: /tmp/.buildx-cache
|
|
||||||
# Provider credentials. Each provider scenario skips if its
|
|
||||||
# token (and URL, for gateways) is unset, so partial coverage is fine.
|
|
||||||
OPENAI_TOKEN: ${{ secrets.E2E_OPENAI_TOKEN }}
|
|
||||||
ANTHROPIC_TOKEN: ${{ secrets.E2E_ANTHROPIC_TOKEN }}
|
|
||||||
VERCEL_URL: ${{ secrets.E2E_VERCEL_URL }}
|
|
||||||
VERCEL_TOKEN: ${{ secrets.E2E_VERCEL_TOKEN }}
|
|
||||||
OPENROUTER_URL: ${{ secrets.E2E_OPENROUTER_URL }}
|
|
||||||
OPENROUTER_TOKEN: ${{ secrets.E2E_OPENROUTER_TOKEN }}
|
|
||||||
CLOUDFLARE_URL: ${{ secrets.E2E_CLOUDFLARE_URL }}
|
|
||||||
CLOUDFLARE_TOKEN: ${{ secrets.E2E_CLOUDFLARE_TOKEN }}
|
|
||||||
AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.E2E_AWS_BEARER_TOKEN_BEDROCK }}
|
|
||||||
AWS_REGION: ${{ secrets.E2E_AWS_REGION }}
|
|
||||||
# Vertex (Anthropic-on-Vertex): SA + project required; region defaults
|
|
||||||
# to "global", model to a pinned claude snapshot.
|
|
||||||
GOOGLE_VERTEX_SA_BASE64: ${{ secrets.E2E_GOOGLE_VERTEX_SA_BASE64 }}
|
|
||||||
GOOGLE_VERTEX_PROJECT: ${{ secrets.E2E_GOOGLE_VERTEX_PROJECT }}
|
|
||||||
GOOGLE_VERTEX_REGION: ${{ secrets.E2E_GOOGLE_VERTEX_REGION }}
|
|
||||||
GOOGLE_VERTEX_MODEL: ${{ secrets.E2E_GOOGLE_VERTEX_MODEL }}
|
|
||||||
run: go test -tags e2e -timeout 40m -v ./e2e/...
|
|
||||||
105
.github/workflows/check-license-dependencies.yml
vendored
105
.github/workflows/check-license-dependencies.yml
vendored
@@ -2,16 +2,16 @@ name: Check License Dependencies
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- "go.mod"
|
- 'go.mod'
|
||||||
- "go.sum"
|
- 'go.sum'
|
||||||
- ".github/workflows/check-license-dependencies.yml"
|
- '.github/workflows/check-license-dependencies.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "go.mod"
|
- 'go.mod'
|
||||||
- "go.sum"
|
- 'go.sum'
|
||||||
- ".github/workflows/check-license-dependencies.yml"
|
- '.github/workflows/check-license-dependencies.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-internal-dependencies:
|
check-internal-dependencies:
|
||||||
@@ -19,10 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Check for problematic license dependencies
|
- name: Check for problematic license dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -59,57 +56,55 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: 'go.mod'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install go-licenses
|
- name: Install go-licenses
|
||||||
run: go install github.com/google/go-licenses@v1.6.0
|
run: go install github.com/google/go-licenses@v1.6.0
|
||||||
|
|
||||||
- name: Check for GPL/AGPL licensed dependencies
|
- name: Check for GPL/AGPL licensed dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
||||||
|
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
||||||
|
|
||||||
|
if [ -n "$COPYLEFT_DEPS" ]; then
|
||||||
|
echo "Found copyleft licensed dependencies:"
|
||||||
|
echo "$COPYLEFT_DEPS"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
# Filter out dependencies that are only pulled in by internal AGPL packages
|
||||||
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
INCOMPATIBLE=""
|
||||||
|
while IFS=',' read -r package url license; do
|
||||||
|
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
||||||
|
# Find ALL packages that import this GPL package using go list
|
||||||
|
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
||||||
|
|
||||||
if [ -n "$COPYLEFT_DEPS" ]; then
|
# Check if any importer is NOT in management/signal/relay
|
||||||
echo "Found copyleft licensed dependencies:"
|
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
||||||
echo "$COPYLEFT_DEPS"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Filter out dependencies that are only pulled in by internal AGPL packages
|
if [ -n "$BSD_IMPORTER" ]; then
|
||||||
INCOMPATIBLE=""
|
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
||||||
while IFS=',' read -r package url license; do
|
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
||||||
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
else
|
||||||
# Find ALL packages that import this GPL package using go list
|
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
||||||
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
|
||||||
|
|
||||||
# Check if any importer is NOT in management/signal/relay
|
|
||||||
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\|proxy\|combined\|tools/idp-migrate\)" | head -1)
|
|
||||||
|
|
||||||
if [ -n "$BSD_IMPORTER" ]; then
|
|
||||||
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
|
||||||
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
|
||||||
else
|
|
||||||
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done <<< "$COPYLEFT_DEPS"
|
|
||||||
|
|
||||||
if [ -n "$INCOMPATIBLE" ]; then
|
|
||||||
echo ""
|
|
||||||
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
|
||||||
echo -e "$INCOMPATIBLE"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
fi
|
done <<< "$COPYLEFT_DEPS"
|
||||||
|
|
||||||
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
if [ -n "$INCOMPATIBLE" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
||||||
|
echo -e "$INCOMPATIBLE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
||||||
|
|||||||
2
.github/workflows/docs-ack.yml
vendored
2
.github/workflows/docs-ack.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify docs PR exists (and is open or merged)
|
- name: Verify docs PR exists (and is open or merged)
|
||||||
if: steps.validate.outputs.mode == 'added'
|
if: steps.validate.outputs.mode == 'added'
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
id: verify
|
id: verify
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.extract.outputs.pr_number }}
|
pr_number: ${{ steps.extract.outputs.pr_number }}
|
||||||
|
|||||||
5
.github/workflows/forum.yml
vendored
5
.github/workflows/forum.yml
vendored
@@ -8,10 +8,11 @@ jobs:
|
|||||||
post:
|
post:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: roots/discourse-topic-github-release-action@557d74ea05b6cc0c47f555c1d5d28a89d904005b # v1.1.0
|
- uses: roots/discourse-topic-github-release-action@main
|
||||||
with:
|
with:
|
||||||
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
|
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
|
||||||
discourse-base-url: https://forum.netbird.io
|
discourse-base-url: https://forum.netbird.io
|
||||||
discourse-author-username: NetBird
|
discourse-author-username: NetBird
|
||||||
discourse-category: 17
|
discourse-category: 17
|
||||||
discourse-tags: releases
|
discourse-tags:
|
||||||
|
releases
|
||||||
|
|||||||
8
.github/workflows/git-town.yml
vendored
8
.github/workflows/git-town.yml
vendored
@@ -3,7 +3,7 @@ name: Git Town
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- '**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
git-town:
|
git-town:
|
||||||
@@ -15,9 +15,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
- uses: git-town/action@v1.2.1
|
||||||
persist-credentials: false
|
|
||||||
- uses: git-town/action@3d8b878379abb1ee393fb49865a28b4a6c2cd3b0 # v1.2.1
|
|
||||||
with:
|
with:
|
||||||
skip-single-stacks: true
|
skip-single-stacks: true
|
||||||
|
|||||||
16
.github/workflows/golang-test-darwin.yml
vendored
16
.github/workflows/golang-test-darwin.yml
vendored
@@ -16,18 +16,16 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
key: macos-gotest-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -45,11 +43,5 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -coverprofile=coverage.txt -tags 'devcert privileged' -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/testutil/privileged)
|
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)
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: unit,client
|
|
||||||
|
|||||||
41
.github/workflows/golang-test-freebsd.yml
vendored
41
.github/workflows/golang-test-freebsd.yml
vendored
@@ -15,31 +15,20 @@ jobs:
|
|||||||
name: "Client / Unit"
|
name: "Client / Unit"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Read Go version from go.mod
|
|
||||||
id: goversion
|
|
||||||
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Test in FreeBSD
|
- name: Test in FreeBSD
|
||||||
id: test
|
id: test
|
||||||
env:
|
uses: vmactions/freebsd-vm@v1
|
||||||
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
|
||||||
uses: vmactions/freebsd-vm@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8
|
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "15.0"
|
release: "14.2"
|
||||||
envs: "GO_VERSION"
|
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y curl pkgconf xorg
|
pkg install -y curl pkgconf xorg
|
||||||
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
GO_TARBALL="go1.25.3.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -vLO "$GO_URL"
|
curl -vLO "$GO_URL"
|
||||||
tar -C /usr/local -vxzf "$GO_TARBALL"
|
tar -C /usr/local -vxzf "$GO_TARBALL"
|
||||||
|
|
||||||
# -x - to print all executed commands
|
# -x - to print all executed commands
|
||||||
# -e - to faile on first error
|
# -e - to faile on first error
|
||||||
@@ -48,14 +37,14 @@ jobs:
|
|||||||
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
|
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
|
||||||
time go build -o netbird client/main.go
|
time go build -o netbird client/main.go
|
||||||
# check all component except management, since we do not support management server on freebsd
|
# check all component except management, since we do not support management server on freebsd
|
||||||
time go test -tags privileged -timeout 1m -failfast ./base62/...
|
time go test -timeout 1m -failfast ./base62/...
|
||||||
# NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use`
|
# NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use`
|
||||||
time go test -tags privileged -timeout 8m -failfast -v -p 1 ./client/...
|
time go test -timeout 8m -failfast -v -p 1 ./client/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./dns/...
|
time go test -timeout 1m -failfast ./dns/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./encryption/...
|
time go test -timeout 1m -failfast ./encryption/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./formatter/...
|
time go test -timeout 1m -failfast ./formatter/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./client/iface/...
|
time go test -timeout 1m -failfast ./client/iface/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./route/...
|
time go test -timeout 1m -failfast ./route/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./sharedsock/...
|
time go test -timeout 1m -failfast ./sharedsock/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./util/...
|
time go test -timeout 1m -failfast ./util/...
|
||||||
time go test -tags privileged -timeout 1m -failfast ./version/...
|
time go test -timeout 1m -failfast ./version/...
|
||||||
|
|||||||
207
.github/workflows/golang-test-linux.yml
vendored
207
.github/workflows/golang-test-linux.yml
vendored
@@ -18,11 +18,9 @@ jobs:
|
|||||||
management: ${{ steps.filter.outputs.management }}
|
management: ${{ steps.filter.outputs.management }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
@@ -30,7 +28,7 @@ jobs:
|
|||||||
- 'management/**'
|
- 'management/**'
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -38,10 +36,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -115,16 +113,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["386", "amd64"]
|
arch: [ '386','amd64' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -132,10 +128,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -158,29 +154,18 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -coverprofile=coverage.txt -tags 'devcert privileged' -exec 'sudo --preserve-env=CI,CGO_ENABLED' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/testutil/privileged)
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined)
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: unit,client
|
|
||||||
|
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
name: "Client (Docker) / Unit"
|
name: "Client (Docker) / Unit"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -192,7 +177,7 @@ jobs:
|
|||||||
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
id: cache-restore
|
id: cache-restore
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -229,7 +214,7 @@ jobs:
|
|||||||
sh -c ' \
|
sh -c ' \
|
||||||
apk update; apk add --no-cache \
|
apk update; apk add --no-cache \
|
||||||
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
||||||
go test -buildvcs=false -tags "devcert privileged" -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server -e /client/testutil/privileged)
|
go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /proxy -e /combined -e /client/ui -e /upload-server)
|
||||||
'
|
'
|
||||||
|
|
||||||
test_relay:
|
test_relay:
|
||||||
@@ -246,12 +231,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -263,10 +246,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -285,33 +268,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
go test ${{ matrix.raceFlag }} \
|
go test ${{ matrix.raceFlag }} \
|
||||||
-exec 'sudo' -coverprofile=coverage.txt \
|
-exec 'sudo' \
|
||||||
-timeout 10m -p 1 ./relay/... ./shared/relay/...
|
-timeout 10m -p 1 ./relay/... ./shared/relay/...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: unit,relay
|
|
||||||
|
|
||||||
test_proxy:
|
test_proxy:
|
||||||
name: "Proxy / Unit"
|
name: "Proxy / Unit"
|
||||||
needs: [build-cache]
|
needs: [build-cache]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["386", "amd64"]
|
arch: [ '386','amd64' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -325,7 +298,7 @@ jobs:
|
|||||||
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -343,15 +316,7 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
go test -timeout 10m -p 1 -coverprofile=coverage.txt ./proxy/...
|
go test -timeout 10m -p 1 ./proxy/...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: unit,proxy
|
|
||||||
|
|
||||||
test_signal:
|
test_signal:
|
||||||
name: "Signal / Unit"
|
name: "Signal / Unit"
|
||||||
@@ -359,16 +324,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["386", "amd64"]
|
arch: [ '386','amd64' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -380,10 +343,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -402,34 +365,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
go test \
|
go test \
|
||||||
-exec 'sudo' -coverprofile=coverage.txt \
|
-exec 'sudo' \
|
||||||
-timeout 10m ./signal/... ./shared/signal/...
|
-timeout 10m ./signal/... ./shared/signal/...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: unit,signal
|
|
||||||
|
|
||||||
test_management:
|
test_management:
|
||||||
name: "Management / Unit"
|
name: "Management / Unit"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["amd64"]
|
arch: [ 'amd64' ]
|
||||||
store: ["sqlite", "postgres", "mysql"]
|
store: [ 'sqlite', 'postgres', 'mysql' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -437,10 +390,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -457,7 +410,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -474,31 +427,23 @@ jobs:
|
|||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
run: docker pull mlsmaycon/warmed-mysql:8
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
go test -tags=devcert -coverprofile=coverage.txt \
|
go test -tags=devcert \
|
||||||
-exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \
|
-exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/... ./shared/management/...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: unit,management
|
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
name: "Management / Benchmark"
|
name: "Management / Benchmark"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["amd64"]
|
arch: [ 'amd64' ]
|
||||||
store: ["sqlite", "postgres"]
|
store: [ 'sqlite', 'postgres' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -529,12 +474,10 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -542,10 +485,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -562,7 +505,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -579,21 +522,20 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
|
GIT_BRANCH=${{ github.ref_name }} \
|
||||||
go test -tags devcert -run=^$ -bench=. \
|
go test -tags devcert -run=^$ -bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
||||||
env:
|
|
||||||
GIT_BRANCH: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["amd64"]
|
arch: [ 'amd64' ]
|
||||||
store: ["sqlite", "postgres"]
|
store: [ 'sqlite', 'postgres' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -624,12 +566,10 @@ jobs:
|
|||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -637,10 +577,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -657,7 +597,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
if: github.event.pull_request && github.event.pull_request.head.repo && github.event.pull_request.head.repo.full_name == '' || github.repository == github.event.pull_request.head.repo.full_name || !github.head_ref
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -674,32 +614,29 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
|
GIT_BRANCH=${{ github.ref_name }} \
|
||||||
go test -tags=benchmark \
|
go test -tags=benchmark \
|
||||||
-run=^$ \
|
-run=^$ \
|
||||||
-bench=. \
|
-bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/server/http/...
|
-timeout 20m ./management/server/http/...
|
||||||
env:
|
|
||||||
GIT_BRANCH: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
needs: [build-cache]
|
needs: [ build-cache ]
|
||||||
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ["amd64"]
|
arch: [ 'amd64' ]
|
||||||
store: ["sqlite", "postgres"]
|
store: [ 'sqlite', 'postgres']
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -707,10 +644,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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -730,14 +667,6 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
go test -tags=integration -coverprofile=coverage.txt \
|
go test -tags=integration \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
||||||
-timeout 20m ./management/server/http/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
if: matrix.arch == 'amd64'
|
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f #v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
slug: netbirdio/netbird
|
|
||||||
flags: integration,management
|
|
||||||
|
|||||||
21
.github/workflows/golang-test-windows.yml
vendored
21
.github/workflows/golang-test-windows.yml
vendored
@@ -18,12 +18,10 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
@@ -35,7 +33,7 @@ jobs:
|
|||||||
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
echo "modcache=$(go env GOMODCACHE)" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.cache }}
|
${{ env.cache }}
|
||||||
@@ -46,15 +44,16 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
|
||||||
with:
|
with:
|
||||||
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
destination: ${{ env.downloadPath }}\wintun.zip
|
file-name: wintun.zip
|
||||||
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||||
|
|
||||||
- name: Decompressing wintun files
|
- name: Decompressing wintun files
|
||||||
run: tar -xvf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' }
|
$packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' }
|
||||||
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
|
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
|
||||||
$cmd = "$goExe test -tags `"devcert privileged`" -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
|
$cmd = "$goExe test -tags=devcert -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
|
||||||
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
|
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
|
|||||||
16
.github/workflows/golangci-lint.yml
vendored
16
.github/workflows/golangci-lint.yml
vendored
@@ -15,13 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
uses: codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans,deriver,te,userA,ede,additionals,flate,recordin,unparseable
|
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/**
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -40,15 +38,13 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Check for duplicate constants
|
- name: Check for duplicate constants
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
@@ -56,7 +52,7 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
skip-cache: true
|
skip-cache: true
|
||||||
|
|||||||
4
.github/workflows/install-script-test.yml
vendored
4
.github/workflows/install-script-test.yml
vendored
@@ -22,9 +22,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: run install script
|
- name: run install script
|
||||||
env:
|
env:
|
||||||
|
|||||||
18
.github/workflows/mobile-build-validation.yml
vendored
18
.github/workflows/mobile-build-validation.yml
vendored
@@ -16,25 +16,23 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
uses: android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
cmdline-tools-version: 8512546
|
cmdline-tools-version: 8512546
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: "11"
|
java-version: "11"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
- name: NDK Cache
|
- name: NDK Cache
|
||||||
id: ndk-cache
|
id: ndk-cache
|
||||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: /usr/local/lib/android/sdk/ndk
|
path: /usr/local/lib/android/sdk/ndk
|
||||||
key: ndk-cache-23.1.7779620
|
key: ndk-cache-23.1.7779620
|
||||||
@@ -54,11 +52,9 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
|
|||||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Validate PR title prefix
|
- name: Validate PR title prefix
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const title = context.payload.pull_request.title;
|
const title = context.payload.pull_request.title;
|
||||||
|
|||||||
87
.github/workflows/proto-version-check.yml
vendored
87
.github/workflows/proto-version-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check for proto tool version changes
|
- name: Check for proto tool version changes
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||||
@@ -20,83 +20,34 @@ jobs:
|
|||||||
per_page: 100,
|
per_page: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cover renamed .pb.go files in addition to plain edits.
|
const pbFiles = files.filter(f => f.filename.endsWith('.pb.go'));
|
||||||
// Renamed entries land under the new path with previous_filename
|
const missingPatch = pbFiles.filter(f => !f.patch).map(f => f.filename);
|
||||||
// pointing at the base-side name, so we read the base content
|
if (missingPatch.length > 0) {
|
||||||
// from the old path when present.
|
core.setFailed(
|
||||||
const changedPbFiles = files
|
`Cannot inspect patch data for:\n` +
|
||||||
.filter(f => (f.status === 'modified' || f.status === 'renamed')
|
missingPatch.map(f => `- ${f}`).join('\n') +
|
||||||
&& f.filename.endsWith('.pb.go'))
|
`\nThis can happen with very large PRs. Verify proto versions manually.`
|
||||||
.map(f => ({
|
);
|
||||||
headPath: f.filename,
|
|
||||||
basePath: f.previous_filename || f.filename,
|
|
||||||
}));
|
|
||||||
if (changedPbFiles.length === 0) {
|
|
||||||
console.log('No modified or renamed .pb.go files to check');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const versionPattern = /^[+-]\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
|
||||||
// Matches the generator version headers protoc writes at the top
|
|
||||||
// of generated files:
|
|
||||||
// // protoc v3.21.12
|
|
||||||
// // protoc-gen-go v1.26.0
|
|
||||||
// // - protoc-gen-go-grpc v1.6.1 (grpc files prefix with "- ")
|
|
||||||
// The optional "- " prefix and the optional -gen-go / -gen-go-grpc
|
|
||||||
// suffixes keep the *_grpc.pb.go headers in scope.
|
|
||||||
const versionPattern = /^\s*\/\/\s+(?:-\s+)?protoc(?:-gen-go(?:-grpc)?)?\s+v[\d.]+/;
|
|
||||||
const baseSha = context.payload.pull_request.base.sha;
|
|
||||||
const headSha = context.payload.pull_request.head.sha;
|
|
||||||
|
|
||||||
async function getVersionHeader(path, ref) {
|
|
||||||
try {
|
|
||||||
const res = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path,
|
|
||||||
ref,
|
|
||||||
});
|
|
||||||
if (!res.data.content) {
|
|
||||||
return { ok: false, reason: 'no inline content (file too large)' };
|
|
||||||
}
|
|
||||||
const content = Buffer.from(res.data.content, 'base64').toString('utf8');
|
|
||||||
const lines = content
|
|
||||||
.split('\n')
|
|
||||||
.slice(0, 20)
|
|
||||||
.filter(line => versionPattern.test(line));
|
|
||||||
return { ok: true, lines };
|
|
||||||
} catch (e) {
|
|
||||||
return { ok: false, reason: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const violations = [];
|
const violations = [];
|
||||||
for (const file of changedPbFiles) {
|
|
||||||
const [base, head] = await Promise.all([
|
for (const file of pbFiles) {
|
||||||
getVersionHeader(file.basePath, baseSha),
|
const changed = file.patch
|
||||||
getVersionHeader(file.headPath, headSha),
|
.split('\n')
|
||||||
]);
|
.filter(line => versionPattern.test(line));
|
||||||
if (!base.ok || !head.ok) {
|
if (changed.length > 0) {
|
||||||
core.warning(
|
|
||||||
`Skipping ${file.headPath}: base=${base.ok ? 'ok' : base.reason}, head=${head.ok ? 'ok' : head.reason}`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (base.lines.join('\n') !== head.lines.join('\n')) {
|
|
||||||
violations.push({
|
violations.push({
|
||||||
file: file.basePath === file.headPath
|
file: file.filename,
|
||||||
? file.headPath
|
lines: changed,
|
||||||
: `${file.basePath} → ${file.headPath}`,
|
|
||||||
base: base.lines,
|
|
||||||
head: head.lines,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
const details = violations.map(v =>
|
const details = violations.map(v =>
|
||||||
`${v.file}:\n` +
|
`${v.file}:\n${v.lines.map(l => ' ' + l).join('\n')}`
|
||||||
` base:\n${v.base.map(l => ' ' + l).join('\n') || ' (none)'}\n` +
|
|
||||||
` head:\n${v.head.map(l => ' ' + l).join('\n') || ' (none)'}`
|
|
||||||
).join('\n\n');
|
).join('\n\n');
|
||||||
|
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
|
|||||||
230
.github/workflows/release.yml
vendored
230
.github/workflows/release.yml
vendored
@@ -9,13 +9,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.6"
|
SIGN_PIPE_VER: "v0.1.4"
|
||||||
GORELEASER_VER: "v2.16.0"
|
GORELEASER_VER: "v2.14.3"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
flags: ""
|
|
||||||
SKIP_PUBLISH: "true"
|
|
||||||
SKIP_DOCKER_PUSH: "false"
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -27,15 +24,13 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Generate FreeBSD port diff
|
- name: Generate FreeBSD port diff
|
||||||
run: bash -x release_files/freebsd-port-diff.sh
|
run: bash release_files/freebsd-port-diff.sh
|
||||||
|
|
||||||
- name: Generate FreeBSD port issue body
|
- name: Generate FreeBSD port issue body
|
||||||
run: bash -x release_files/freebsd-port-issue-body.sh
|
run: bash release_files/freebsd-port-issue-body.sh
|
||||||
|
|
||||||
- name: Check if diff was generated
|
- name: Check if diff was generated
|
||||||
id: check_diff
|
id: check_diff
|
||||||
@@ -56,26 +51,19 @@ jobs:
|
|||||||
echo "Generated files for version: $VERSION"
|
echo "Generated files for version: $VERSION"
|
||||||
cat netbird-*.diff
|
cat netbird-*.diff
|
||||||
|
|
||||||
- name: Read Go version from go.mod
|
|
||||||
id: goversion
|
|
||||||
run: echo "version=$(awk '/^go / {print $2}' go.mod)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Test FreeBSD port
|
- name: Test FreeBSD port
|
||||||
if: steps.check_diff.outputs.diff_exists == 'true'
|
if: steps.check_diff.outputs.diff_exists == 'true'
|
||||||
env:
|
uses: vmactions/freebsd-vm@v1
|
||||||
GO_VERSION: ${{ steps.goversion.outputs.version }}
|
|
||||||
uses: vmactions/freebsd-vm@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8
|
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
copyback: false
|
copyback: false
|
||||||
release: "15.0"
|
release: "15.0"
|
||||||
envs: "GO_VERSION"
|
|
||||||
prepare: |
|
prepare: |
|
||||||
# Install required packages
|
# Install required packages
|
||||||
pkg install -y git curl portlint
|
pkg install -y git curl portlint go
|
||||||
|
|
||||||
# Install Go for building
|
# Install Go for building
|
||||||
GO_TARBALL="go${GO_VERSION}.freebsd-amd64.tar.gz"
|
GO_TARBALL="go1.25.5.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -LO "$GO_URL"
|
curl -LO "$GO_URL"
|
||||||
tar -C /usr/local -xzf "$GO_TARBALL"
|
tar -C /usr/local -xzf "$GO_TARBALL"
|
||||||
@@ -105,19 +93,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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: freebsd-port-files
|
name: freebsd-port-files
|
||||||
path: |
|
path: |
|
||||||
@@ -133,45 +121,29 @@ jobs:
|
|||||||
windows_packages_artifact_url: ${{ steps.upload_windows_packages.outputs.artifact-url }}
|
windows_packages_artifact_url: ${{ steps.upload_windows_packages.outputs.artifact-url }}
|
||||||
macos_packages_artifact_url: ${{ steps.upload_macos_packages.outputs.artifact-url }}
|
macos_packages_artifact_url: ${{ steps.upload_macos_packages.outputs.artifact-url }}
|
||||||
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
||||||
|
env:
|
||||||
|
flags: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
uses: booxmedialtd/ws-action-parse-semver@v1
|
||||||
|
with:
|
||||||
- name: Set snapshot flag
|
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
version_extractor_regex: '\/v(.*)$'
|
||||||
run: |
|
|
||||||
echo "flags=--snapshot" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set build vars
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: |
|
|
||||||
if [[ "x-${{ steps.semver_parser.outputs.prerelease }}" == "x-" && "x-${{ github.repository }}" == "x-netbirdio/netbird" ]]; then
|
|
||||||
echo "x-${{ github.repository }}"
|
|
||||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
|
||||||
echo "SKIP_PUBLISH=false" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "x-${{ github.repository }}"
|
|
||||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "x-${{ github.repository }}" != "x-netbirdio/netbird" ]]; then
|
|
||||||
echo "SKIP_DOCKER_PUSH=true" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -181,23 +153,21 @@ jobs:
|
|||||||
${{ runner.os }}-go-releaser-
|
${{ runner.os }}-go-releaser-
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
- name: run openapi generator
|
|
||||||
run: bash shared/management/http/api/generate.sh
|
|
||||||
- name: check git status
|
- name: check git status
|
||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 #v4.1.0
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 #v4.1.0
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to Docker hub
|
- name: Login to Docker hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -221,7 +191,7 @@ jobs:
|
|||||||
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso
|
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --clean ${{ env.flags }}
|
args: release --clean ${{ env.flags }}
|
||||||
@@ -232,8 +202,6 @@ jobs:
|
|||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||||
NFPM_NETBIRD_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
NFPM_NETBIRD_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||||
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
|
||||||
SKIP_DOCKER_PUSH: ${{ env.SKIP_DOCKER_PUSH }}
|
|
||||||
- name: Verify RPM signatures
|
- name: Verify RPM signatures
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
||||||
@@ -293,11 +261,8 @@ jobs:
|
|||||||
${{ steps.goreleaser.outputs.artifacts }}
|
${{ steps.goreleaser.outputs.artifacts }}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
# dockers_v2 artifacts have no top-level goarch field, so match the
|
|
||||||
# per-platform -amd64 tag suffix instead; it works for both the old
|
|
||||||
# dockers and the new dockers_v2 image naming.
|
|
||||||
mapfile -t src_images < <(
|
mapfile -t src_images < <(
|
||||||
jq -r '.[] | select(.type == "Docker Image") | .name | select(startswith("ghcr.io/") and endswith("-amd64"))' /tmp/goreleaser-artifacts.json
|
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name | select(startswith("ghcr.io/"))' /tmp/goreleaser-artifacts.json
|
||||||
)
|
)
|
||||||
|
|
||||||
for src in "${src_images[@]}"; do
|
for src in "${src_images[@]}"; do
|
||||||
@@ -317,28 +282,28 @@ jobs:
|
|||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release
|
id: upload_release
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload linux packages
|
- name: upload linux packages
|
||||||
id: upload_linux_packages
|
id: upload_linux_packages
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-packages
|
name: linux-packages
|
||||||
path: dist/netbird_linux**
|
path: dist/netbird_linux**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload windows packages
|
- name: upload windows packages
|
||||||
id: upload_windows_packages
|
id: upload_windows_packages
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-packages
|
name: windows-packages
|
||||||
path: dist/netbird_windows**
|
path: dist/netbird_windows**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload macos packages
|
- name: upload macos packages
|
||||||
id: upload_macos_packages
|
id: upload_macos_packages
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-packages
|
name: macos-packages
|
||||||
path: dist/netbird_darwin**
|
path: dist/netbird_darwin**
|
||||||
@@ -349,40 +314,27 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
uses: booxmedialtd/ws-action-parse-semver@v1
|
||||||
|
with:
|
||||||
|
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
||||||
|
version_extractor_regex: '\/v(.*)$'
|
||||||
|
|
||||||
- name: Set snapshot flag
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
run: |
|
- name: Checkout
|
||||||
echo "flags=--snapshot" >> $GITHUB_ENV
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
- name: Set build vars
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: |
|
|
||||||
if [[ "x-${{ steps.semver_parser.outputs.prerelease }}" == "x-" && "x-${{ github.repository }}" == "x-netbirdio/netbird" ]]; then
|
|
||||||
echo "x-${{ github.repository }}"
|
|
||||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
|
||||||
echo "SKIP_PUBLISH=false" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "x-${{ github.repository }}"
|
|
||||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -423,7 +375,7 @@ jobs:
|
|||||||
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso
|
run: goversioninfo -arm -64 -icon client/ui/assets/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_arm64.syso
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
args: release --config .goreleaser_ui.yaml --clean ${{ env.flags }}
|
||||||
@@ -434,7 +386,6 @@ jobs:
|
|||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||||
NFPM_NETBIRD_UI_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
NFPM_NETBIRD_UI_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||||
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
|
||||||
- name: Verify RPM signatures
|
- name: Verify RPM signatures
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
||||||
@@ -453,7 +404,7 @@ jobs:
|
|||||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release_ui
|
id: upload_release_ui
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -467,17 +418,16 @@ jobs:
|
|||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
persist-credentials: false
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -491,7 +441,7 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
args: release --config .goreleaser_ui_darwin.yaml --clean ${{ env.flags }}
|
||||||
@@ -499,7 +449,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
id: upload_release_ui_darwin
|
id: upload_release_ui_darwin
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -524,26 +474,27 @@ jobs:
|
|||||||
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
||||||
downloadPath: '${{ github.workspace }}\temp'
|
downloadPath: '${{ github.workspace }}\temp'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
uses: booxmedialtd/ws-action-parse-semver@v1
|
||||||
|
with:
|
||||||
|
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
||||||
|
version_extractor_regex: '\/v(.*)$'
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Add 7-Zip to PATH
|
- name: Add 7-Zip to PATH
|
||||||
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: Download release artifacts
|
- name: Download release artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Download UI release artifacts
|
- name: Download UI release artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: release-ui
|
path: release-ui
|
||||||
@@ -563,27 +514,29 @@ jobs:
|
|||||||
Get-ChildItem $workdir
|
Get-ChildItem $workdir
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
|
||||||
with:
|
with:
|
||||||
url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
destination: ${{ env.downloadPath }}\wintun.zip
|
file-name: wintun.zip
|
||||||
sha256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||||
|
|
||||||
- name: Decompress wintun files
|
- name: Decompress wintun files
|
||||||
run: tar -xvf "${{ env.downloadPath }}\wintun.zip" -C ${{ env.downloadPath }}
|
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
- name: Move wintun.dll into dist
|
- name: Move wintun.dll into dist
|
||||||
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
- name: Download Mesa3D (amd64 only)
|
- name: Download Mesa3D (amd64 only)
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
id: download-mesa3d
|
id: download-mesa3d
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
|
||||||
with:
|
with:
|
||||||
url: https://pkgs.netbird.io/mesa3d/MesaForWindows-x64-20.1.8.7z
|
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
|
||||||
destination: ${{ env.downloadPath }}\mesa3d.7z
|
file-name: mesa3d.7z
|
||||||
sha256: 71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
|
||||||
|
|
||||||
- name: Extract Mesa3D driver (amd64 only)
|
- name: Extract Mesa3D driver (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
@@ -594,38 +547,35 @@ jobs:
|
|||||||
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
- name: Download EnVar plugin for NSIS
|
- name: Download EnVar plugin for NSIS
|
||||||
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
uses: carlosperate/download-file-action@v2
|
||||||
with:
|
with:
|
||||||
url: https://pkgs.netbird.io/nsis/EnVar_plugin.zip
|
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
|
||||||
destination: ${{ github.workspace }}\envar_plugin.zip
|
file-name: envar_plugin.zip
|
||||||
sha256: e9aa92de351345ed82795251d838f1ae9041ba35af9d381a5780c7843b01f56a
|
location: ${{ github.workspace }}
|
||||||
|
|
||||||
- name: Extract EnVar plugin
|
- name: Extract EnVar plugin
|
||||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
||||||
|
|
||||||
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: netbirdio/shared-actions/actions/win-download-and-verify@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
|
||||||
with:
|
with:
|
||||||
url: https://pkgs.netbird.io/nsis/ShellExecAsUser_amd64-Unicode.7z
|
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
|
||||||
destination: ${{ github.workspace }}\ShellExecAsUser_amd64-Unicode.7z
|
file-name: ShellExecAsUser_amd64-Unicode.7z
|
||||||
sha256: 0a55ea25c7330a92cec028eda8afcaf1b1a7092e0dfb77c21c8f654564b4ff9d
|
location: ${{ github.workspace }}
|
||||||
|
|
||||||
- name: Extract ShellExecAsUser plugin (amd64 only)
|
- name: Extract ShellExecAsUser plugin (amd64 only)
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||||
|
|
||||||
- name: Build NSIS installer
|
- name: Build NSIS installer
|
||||||
shell: pwsh
|
uses: joncloud/makensis-action@v3.3
|
||||||
|
with:
|
||||||
|
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
|
||||||
|
script-file: client/installer.nsis
|
||||||
|
arguments: "/V4 /DARCH=${{ matrix.arch }}"
|
||||||
env:
|
env:
|
||||||
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
||||||
run: |
|
|
||||||
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
|
||||||
$srcPlugins = "${{ github.workspace }}\NSIS_Plugins\Plugins"
|
|
||||||
Get-ChildItem -Path $srcPlugins -Recurse -Filter *.dll |
|
|
||||||
Copy-Item -Destination $nsisPluginDir -Force
|
|
||||||
& "C:\Program Files (x86)\NSIS\makensis.exe" /V4 "/DARCH=${{ matrix.arch }}" client\installer.nsis
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" }
|
|
||||||
|
|
||||||
- name: Rename NSIS installer
|
- name: Rename NSIS installer
|
||||||
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||||
@@ -642,7 +592,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload installer artifacts
|
- name: Upload installer artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-installer-test-${{ matrix.arch }}
|
name: windows-installer-test-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -661,7 +611,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Create or update PR comment
|
- name: Create or update PR comment
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@v7
|
||||||
env:
|
env:
|
||||||
RELEASE_RESULT: ${{ needs.release.result }}
|
RELEASE_RESULT: ${{ needs.release.result }}
|
||||||
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||||
@@ -753,7 +703,7 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger binaries sign pipelines
|
- name: Trigger binaries sign pipelines
|
||||||
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: Sign bin and installer
|
workflow: Sign bin and installer
|
||||||
repo: netbirdio/sign-pipelines
|
repo: netbirdio/sign-pipelines
|
||||||
|
|||||||
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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: sync-main.yml
|
workflow: sync-main.yml
|
||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: sync-tag.yml
|
workflow: sync-tag.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger android-client submodule bump
|
- name: Trigger android-client submodule bump
|
||||||
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
@@ -42,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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
with:
|
with:
|
||||||
workflow: bump-netbird.yml
|
workflow: bump-netbird.yml
|
||||||
ref: main
|
ref: main
|
||||||
repo: netbirdio/ios-client
|
repo: netbirdio/ios-client
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
32
.github/workflows/test-infrastructure-files.yml
vendored
32
.github/workflows/test-infrastructure-files.yml
vendored
@@ -6,10 +6,10 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "infrastructure_files/**"
|
- 'infrastructure_files/**'
|
||||||
- ".github/workflows/test-infrastructure-files.yml"
|
- '.github/workflows/test-infrastructure-files.yml'
|
||||||
- "management/cmd/**"
|
- 'management/cmd/**'
|
||||||
- "signal/cmd/**"
|
- 'signal/cmd/**'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
store: ["sqlite", "postgres", "mysql"]
|
store: [ 'sqlite', 'postgres', 'mysql' ]
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
image: ${{ (matrix.store == 'postgres') && 'postgres' || '' }}
|
||||||
@@ -68,17 +68,15 @@ jobs:
|
|||||||
run: sudo apt-get install -y curl
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -141,8 +139,8 @@ jobs:
|
|||||||
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
||||||
CI_NETBIRD_SIGNAL_PORT: 12345
|
CI_NETBIRD_SIGNAL_PORT: 12345
|
||||||
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
|
CI_NETBIRD_STORE_CONFIG_ENGINE: ${{ matrix.store }}
|
||||||
NETBIRD_STORE_ENGINE_POSTGRES_DSN: "${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$"
|
NETBIRD_STORE_ENGINE_POSTGRES_DSN: '${{ env.NETBIRD_STORE_ENGINE_POSTGRES_DSN }}$'
|
||||||
NETBIRD_STORE_ENGINE_MYSQL_DSN: "${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$"
|
NETBIRD_STORE_ENGINE_MYSQL_DSN: '${{ env.NETBIRD_STORE_ENGINE_MYSQL_DSN }}$'
|
||||||
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
||||||
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
||||||
CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false
|
CI_NETBIRD_MGMT_DISABLE_DEFAULT_POLICY: false
|
||||||
@@ -207,7 +205,7 @@ jobs:
|
|||||||
- name: Build management docker image
|
- name: Build management docker image
|
||||||
working-directory: management
|
working-directory: management
|
||||||
run: |
|
run: |
|
||||||
docker build -t netbirdio/management:latest --build-arg TARGETPLATFORM=. .
|
docker build -t netbirdio/management:latest .
|
||||||
|
|
||||||
- name: Build signal binary
|
- name: Build signal binary
|
||||||
working-directory: signal
|
working-directory: signal
|
||||||
@@ -216,7 +214,7 @@ jobs:
|
|||||||
- name: Build signal docker image
|
- name: Build signal docker image
|
||||||
working-directory: signal
|
working-directory: signal
|
||||||
run: |
|
run: |
|
||||||
docker build -t netbirdio/signal:latest --build-arg TARGETPLATFORM=. .
|
docker build -t netbirdio/signal:latest .
|
||||||
|
|
||||||
- name: Build relay binary
|
- name: Build relay binary
|
||||||
working-directory: relay
|
working-directory: relay
|
||||||
@@ -225,7 +223,7 @@ jobs:
|
|||||||
- name: Build relay docker image
|
- name: Build relay docker image
|
||||||
working-directory: relay
|
working-directory: relay
|
||||||
run: |
|
run: |
|
||||||
docker build -t netbirdio/relay:latest --build-arg TARGETPLATFORM=. .
|
docker build -t netbirdio/relay:latest .
|
||||||
|
|
||||||
- name: run docker compose up
|
- name: run docker compose up
|
||||||
working-directory: infrastructure_files/artifacts
|
working-directory: infrastructure_files/artifacts
|
||||||
@@ -256,9 +254,7 @@ jobs:
|
|||||||
run: sudo apt-get install -y jq
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: run script with Zitadel PostgreSQL
|
- name: run script with Zitadel PostgreSQL
|
||||||
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|||||||
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@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: generate api pages
|
workflow: generate api pages
|
||||||
repo: netbirdio/docs
|
repo: netbirdio/docs
|
||||||
ref: "refs/heads/main"
|
ref: "refs/heads/main"
|
||||||
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref }}" }'
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
19
.github/workflows/wasm-build-validation.yml
vendored
19
.github/workflows/wasm-build-validation.yml
vendored
@@ -19,17 +19,15 @@ jobs:
|
|||||||
GOARCH: wasm
|
GOARCH: wasm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
- name: Install golangci-lint
|
- name: Install golangci-lint
|
||||||
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #v9.2.1
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
@@ -44,11 +42,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Build Wasm client
|
- name: Build Wasm client
|
||||||
@@ -65,7 +61,8 @@ jobs:
|
|||||||
|
|
||||||
echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"
|
echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"
|
||||||
|
|
||||||
if [ ${SIZE} -gt 62914560 ]; then
|
if [ ${SIZE} -gt 58720256 ]; then
|
||||||
echo "Wasm binary size (${SIZE_MB}MB) exceeds 60MB limit!"
|
echo "Wasm binary size (${SIZE_MB}MB) exceeds 56MB limit!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
.claude
|
|
||||||
.idea
|
.idea
|
||||||
.run
|
.run
|
||||||
*.iml
|
*.iml
|
||||||
|
|||||||
866
.goreleaser.yaml
866
.goreleaser.yaml
@@ -1,7 +1,5 @@
|
|||||||
version: 2
|
version: 2
|
||||||
env:
|
|
||||||
- SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }}
|
|
||||||
- SKIP_DOCKER_PUSH={{ if index .Env "SKIP_DOCKER_PUSH" }}{{ .Env.SKIP_DOCKER_PUSH }}{{ else }}false{{ end }}
|
|
||||||
project_name: netbird
|
project_name: netbird
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-wasm
|
- id: netbird-wasm
|
||||||
@@ -76,8 +74,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -92,8 +88,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -108,8 +102,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -130,8 +122,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -146,8 +136,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -162,8 +150,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.CommitDate}}
|
- -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.CommitDate}}
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -184,8 +170,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
@@ -238,192 +222,670 @@ nfpms:
|
|||||||
rpm:
|
rpm:
|
||||||
signature:
|
signature:
|
||||||
key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}'
|
key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}'
|
||||||
dockers_v2:
|
dockers:
|
||||||
- id: netbird
|
- image_templates:
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
- netbirdio/netbird:{{ .Version }}-amd64
|
||||||
ids:
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-amd64
|
||||||
- netbird
|
ids:
|
||||||
images:
|
- netbird
|
||||||
- netbirdio/netbird
|
goarch: amd64
|
||||||
- ghcr.io/netbirdio/netbird
|
use: buildx
|
||||||
tags:
|
dockerfile: client/Dockerfile
|
||||||
- "{{ .Version }}"
|
extra_files:
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
- client/netbird-entrypoint.sh
|
||||||
dockerfile: client/Dockerfile
|
build_flag_templates:
|
||||||
extra_files:
|
- "--platform=linux/amd64"
|
||||||
- client/netbird-entrypoint.sh
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
platforms:
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
- linux/amd64
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- linux/arm64
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- linux/arm/6
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
annotations:
|
- "--label=maintainer=dev@netbird.io"
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
- image_templates:
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
- netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
ids:
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- netbird
|
||||||
"maintainer": "dev@netbird.io"
|
goarch: arm64
|
||||||
- id: netbird-rootless
|
use: buildx
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
dockerfile: client/Dockerfile
|
||||||
ids:
|
extra_files:
|
||||||
- netbird
|
- client/netbird-entrypoint.sh
|
||||||
images:
|
build_flag_templates:
|
||||||
- netbirdio/netbird
|
- "--platform=linux/arm64"
|
||||||
- ghcr.io/netbirdio/netbird
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
tags:
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
- "v{{ .Version }}-rootless"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
dockerfile: client/Dockerfile-rootless
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
extra_files:
|
- "--label=maintainer=dev@netbird.io"
|
||||||
- client/netbird-entrypoint.sh
|
- image_templates:
|
||||||
platforms:
|
- netbirdio/netbird:{{ .Version }}-arm
|
||||||
- linux/amd64
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm
|
||||||
- linux/arm64
|
ids:
|
||||||
- linux/arm/6
|
- netbird
|
||||||
annotations:
|
goarch: arm
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
goarm: 6
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
use: buildx
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
dockerfile: client/Dockerfile
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
extra_files:
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- client/netbird-entrypoint.sh
|
||||||
"maintainer": "dev@netbird.io"
|
build_flag_templates:
|
||||||
- id: relay
|
- "--platform=linux/arm"
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
ids:
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
- netbird-relay
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
images:
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- netbirdio/relay
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
- ghcr.io/netbirdio/relay
|
- "--label=maintainer=dev@netbird.io"
|
||||||
tags:
|
|
||||||
- "{{ .Version }}"
|
- image_templates:
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
- netbirdio/netbird:{{ .Version }}-rootless-amd64
|
||||||
dockerfile: relay/Dockerfile
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64
|
||||||
platforms:
|
ids:
|
||||||
- linux/amd64
|
- netbird
|
||||||
- linux/arm64
|
goarch: amd64
|
||||||
- linux/arm
|
use: buildx
|
||||||
annotations:
|
dockerfile: client/Dockerfile-rootless
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
extra_files:
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
- client/netbird-entrypoint.sh
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
build_flag_templates:
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
- "--platform=linux/amd64"
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
"maintainer": "dev@netbird.io"
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
- id: signal
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
ids:
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
- netbird-signal
|
- "--label=maintainer=dev@netbird.io"
|
||||||
images:
|
- image_templates:
|
||||||
- netbirdio/signal
|
- netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
||||||
- ghcr.io/netbirdio/signal
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
||||||
tags:
|
ids:
|
||||||
- "{{ .Version }}"
|
- netbird
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
goarch: arm64
|
||||||
dockerfile: signal/Dockerfile
|
use: buildx
|
||||||
platforms:
|
dockerfile: client/Dockerfile-rootless
|
||||||
- linux/amd64
|
extra_files:
|
||||||
- linux/arm64
|
- client/netbird-entrypoint.sh
|
||||||
- linux/arm
|
build_flag_templates:
|
||||||
annotations:
|
- "--platform=linux/arm64"
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
"maintainer": "dev@netbird.io"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
- id: management
|
- image_templates:
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
- netbirdio/netbird:{{ .Version }}-rootless-arm
|
||||||
ids:
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm
|
||||||
- netbird-mgmt
|
ids:
|
||||||
images:
|
- netbird
|
||||||
- netbirdio/management
|
goarch: arm
|
||||||
- ghcr.io/netbirdio/management
|
goarm: 6
|
||||||
tags:
|
use: buildx
|
||||||
- "{{ .Version }}"
|
dockerfile: client/Dockerfile-rootless
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
extra_files:
|
||||||
dockerfile: management/Dockerfile
|
- client/netbird-entrypoint.sh
|
||||||
platforms:
|
build_flag_templates:
|
||||||
- linux/amd64
|
- "--platform=linux/arm"
|
||||||
- linux/arm64
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
- linux/arm
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
annotations:
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- image_templates:
|
||||||
"maintainer": "dev@netbird.io"
|
- netbirdio/relay:{{ .Version }}-amd64
|
||||||
- id: upload
|
- ghcr.io/netbirdio/relay:{{ .Version }}-amd64
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
ids:
|
||||||
ids:
|
- netbird-relay
|
||||||
- netbird-upload
|
goarch: amd64
|
||||||
images:
|
use: buildx
|
||||||
- netbirdio/upload
|
dockerfile: relay/Dockerfile
|
||||||
- ghcr.io/netbirdio/upload
|
build_flag_templates:
|
||||||
tags:
|
- "--platform=linux/amd64"
|
||||||
- "{{ .Version }}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
dockerfile: upload-server/Dockerfile
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
platforms:
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- linux/amd64
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
- linux/arm64
|
- "--label=maintainer=dev@netbird.io"
|
||||||
- linux/arm
|
- image_templates:
|
||||||
annotations:
|
- netbirdio/relay:{{ .Version }}-arm64v8
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
- ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
ids:
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
- netbird-relay
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
goarch: arm64
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
use: buildx
|
||||||
"maintainer": "dev@netbird.io"
|
dockerfile: relay/Dockerfile
|
||||||
- id: netbird-server
|
build_flag_templates:
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
- "--platform=linux/arm64"
|
||||||
ids:
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
- netbird-server
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
images:
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- netbirdio/netbird-server
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- ghcr.io/netbirdio/netbird-server
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
tags:
|
- "--label=maintainer=dev@netbird.io"
|
||||||
- "{{ .Version }}"
|
- image_templates:
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
- netbirdio/relay:{{ .Version }}-arm
|
||||||
dockerfile: combined/Dockerfile
|
- ghcr.io/netbirdio/relay:{{ .Version }}-arm
|
||||||
platforms:
|
ids:
|
||||||
- linux/amd64
|
- netbird-relay
|
||||||
- linux/arm64
|
goarch: arm
|
||||||
- linux/arm
|
goarm: 6
|
||||||
annotations:
|
use: buildx
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
dockerfile: relay/Dockerfile
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
build_flag_templates:
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
- "--platform=linux/arm"
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
"maintainer": "dev@netbird.io"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- id: netbird-proxy
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
disable: "{{ .Env.SKIP_DOCKER_PUSH }}"
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
ids:
|
- "--label=maintainer=dev@netbird.io"
|
||||||
- netbird-proxy
|
- image_templates:
|
||||||
images:
|
- netbirdio/signal:{{ .Version }}-amd64
|
||||||
- netbirdio/reverse-proxy
|
- ghcr.io/netbirdio/signal:{{ .Version }}-amd64
|
||||||
- ghcr.io/netbirdio/reverse-proxy
|
ids:
|
||||||
tags:
|
- netbird-signal
|
||||||
- "{{ .Version }}"
|
goarch: amd64
|
||||||
- "{{ if eq .Env.SKIP_PUBLISH \"false\" }}latest{{ end }}"
|
use: buildx
|
||||||
dockerfile: proxy/Dockerfile
|
dockerfile: signal/Dockerfile
|
||||||
platforms:
|
build_flag_templates:
|
||||||
- linux/amd64
|
- "--platform=linux/amd64"
|
||||||
- linux/arm64
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
- linux/arm
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
annotations:
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
"org.opencontainers.image.created": "{{.Date}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
"org.opencontainers.image.title": "{{.ProjectName}}"
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
"org.opencontainers.image.version": "{{.Version}}"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
- image_templates:
|
||||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
- netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
"maintainer": "dev@netbird.io"
|
- ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-signal
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: signal/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-signal
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: signal/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-amd64
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile.debug
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile.debug
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile.debug
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-upload
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: upload-server/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-upload
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: upload-server/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-upload
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: upload-server/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-server
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: combined/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-server
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: combined/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-server
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: combined/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-amd64
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-proxy
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: proxy/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-proxy
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: proxy/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-proxy
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: proxy/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
docker_manifests:
|
||||||
|
- name_template: netbirdio/netbird:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird:{{ .Version }}-rootless
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
||||||
|
- netbirdio/netbird:{{ .Version }}-rootless-arm
|
||||||
|
- netbirdio/netbird:{{ .Version }}-rootless-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird:rootless-latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
||||||
|
- netbirdio/netbird:{{ .Version }}-rootless-arm
|
||||||
|
- netbirdio/netbird:{{ .Version }}-rootless-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/relay:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/relay:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/relay:{{ .Version }}-arm
|
||||||
|
- netbirdio/relay:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/relay:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/relay:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/relay:{{ .Version }}-arm
|
||||||
|
- netbirdio/relay:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/signal:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/signal:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/management:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/management:{{ .Version }}-arm
|
||||||
|
- netbirdio/management:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/management:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/management:{{ .Version }}-arm
|
||||||
|
- netbirdio/management:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/management:debug-latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
- name_template: netbirdio/upload:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/upload:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird-server:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird-server:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird:{{ .Version }}-rootless
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird:rootless-latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-arm
|
||||||
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-rootless-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/relay:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/relay:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/relay:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/relay:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/relay:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/relay:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/relay:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/signal:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/signal:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/management:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/management:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/management:debug-latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-debug-arm
|
||||||
|
- ghcr.io/netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/upload:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/upload:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird-server:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird-server:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/reverse-proxy:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-arm
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/reverse-proxy:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-arm
|
||||||
|
- netbirdio/reverse-proxy:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/reverse-proxy:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/reverse-proxy:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/reverse-proxy:{{ .Version }}-amd64
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- ids:
|
- ids:
|
||||||
- default
|
- default
|
||||||
skip_upload: "{{ .Env.SKIP_PUBLISH }}"
|
|
||||||
repository:
|
repository:
|
||||||
owner: netbirdio
|
owner: netbirdio
|
||||||
name: homebrew-tap
|
name: homebrew-tap
|
||||||
@@ -440,7 +902,6 @@ brews:
|
|||||||
|
|
||||||
uploads:
|
uploads:
|
||||||
- name: debian
|
- name: debian
|
||||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
|
||||||
ids:
|
ids:
|
||||||
- netbird_deb
|
- netbird_deb
|
||||||
mode: archive
|
mode: archive
|
||||||
@@ -449,7 +910,6 @@ uploads:
|
|||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
- name: yum
|
- name: yum
|
||||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
|
||||||
ids:
|
ids:
|
||||||
- netbird_rpm
|
- netbird_rpm
|
||||||
mode: archive
|
mode: archive
|
||||||
@@ -462,13 +922,9 @@ checksum:
|
|||||||
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
- glob: ./release_files/install.sh
|
- glob: ./release_files/install.sh
|
||||||
- glob: ./infrastructure_files/getting-started.sh
|
- glob: ./infrastructure_files/getting-started.sh
|
||||||
- glob: ./infrastructure_files/getting-started-enterprise.sh
|
|
||||||
- glob: ./infrastructure_files/migrate-to-enterprise.sh
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
extra_files:
|
extra_files:
|
||||||
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
- glob: ./release_files/install.sh
|
- glob: ./release_files/install.sh
|
||||||
- glob: ./infrastructure_files/getting-started.sh
|
- glob: ./infrastructure_files/getting-started.sh
|
||||||
- glob: ./infrastructure_files/getting-started-enterprise.sh
|
|
||||||
- glob: ./infrastructure_files/migrate-to-enterprise.sh
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
version: 2
|
version: 2
|
||||||
env:
|
|
||||||
- SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }}
|
|
||||||
project_name: netbird-ui
|
project_name: netbird-ui
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-ui
|
- id: netbird-ui
|
||||||
@@ -102,7 +101,6 @@ nfpms:
|
|||||||
|
|
||||||
uploads:
|
uploads:
|
||||||
- name: debian
|
- name: debian
|
||||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
|
||||||
ids:
|
ids:
|
||||||
- netbird_ui_deb
|
- netbird_ui_deb
|
||||||
mode: archive
|
mode: archive
|
||||||
@@ -111,7 +109,6 @@ uploads:
|
|||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
- name: yum
|
- name: yum
|
||||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
|
||||||
ids:
|
ids:
|
||||||
- netbird_ui_rpm
|
- netbird_ui_rpm
|
||||||
mode: archive
|
mode: archive
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: lint lint-all lint-install setup-hooks test-unit test-privileged
|
.PHONY: lint lint-all lint-install setup-hooks
|
||||||
GOLANGCI_LINT := $(shell pwd)/bin/golangci-lint
|
GOLANGCI_LINT := $(shell pwd)/bin/golangci-lint
|
||||||
|
|
||||||
# Install golangci-lint locally if needed
|
# Install golangci-lint locally if needed
|
||||||
@@ -25,15 +25,3 @@ setup-hooks:
|
|||||||
@git config core.hooksPath .githooks
|
@git config core.hooksPath .githooks
|
||||||
@chmod +x .githooks/pre-push
|
@chmod +x .githooks/pre-push
|
||||||
@echo "✅ Git hooks configured! Pre-push will now run 'make lint'"
|
@echo "✅ Git hooks configured! Pre-push will now run 'make lint'"
|
||||||
|
|
||||||
# Host-safe unit tests: excludes the privileged-tagged tests (root / system-mutating).
|
|
||||||
# Runs as a normal user with no sudo and leaves host networking untouched.
|
|
||||||
test-unit:
|
|
||||||
@go test -tags devcert -timeout 10m ./...
|
|
||||||
|
|
||||||
# Privileged suite: runs the `privileged`-tagged tests inside a --privileged
|
|
||||||
# --cap-add=NET_ADMIN container via the ory/dockertest harness. Requires Docker.
|
|
||||||
# Narrow the run with env vars, e.g.:
|
|
||||||
# PRIV_RUN=TestNftablesManager PRIV_PKGS=./client/firewall/nftables/... make test-privileged
|
|
||||||
test-privileged:
|
|
||||||
@go test -tags 'devcert privileged' -timeout 30m -run TestRunPrivilegedSuiteInDocker -v ./client/testutil/privileged/...
|
|
||||||
|
|||||||
@@ -33,15 +33,10 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<strong>
|
<strong>
|
||||||
🚀 <a href="https://netbird.io/careers">We are hiring! Join us at https://netbird.io/careers</a>
|
🚀 <a href="https://careers.netbird.io">We are hiring! Join us at careers.netbird.io</a>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> ### 🤖 NetBird Agent Network (Beta)
|
|
||||||
> Identity-aware access control for AI agents — keyless access to LLM APIs and private
|
|
||||||
> resources over the encrypted NetBird tunnel. See [`agent-network/`](agent-network/) or
|
|
||||||
> read the docs at **[netbird.ai](https://netbird.ai)**.
|
|
||||||
|
|
||||||
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
||||||
|
|
||||||
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# NetBird Agent Network
|
|
||||||
|
|
||||||
Agent Network is NetBird's access control layer for AI agents and the people who run
|
|
||||||
them. It gives every agent a real identity, tied to your identity provider (IdP), and
|
|
||||||
governs what it can reach — the LLM APIs and AI gateways it can call, and the internal
|
|
||||||
resources it can access. Traffic flows only over the encrypted NetBird tunnel, scoped by
|
|
||||||
policy, with no API keys to leak.
|
|
||||||
|
|
||||||
> **Beta.** Agent Network is open source and can be self-hosted on your own
|
|
||||||
> infrastructure.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
Agent Network is built on two existing NetBird capabilities:
|
|
||||||
|
|
||||||
- **Overlay network** — the encrypted WireGuard mesh between peers.
|
|
||||||
- **Reverse proxy** — a NetBird peer that terminates LLM requests, establishes the
|
|
||||||
caller's identity, evaluates policies/limits/guardrails, injects the upstream provider
|
|
||||||
key server-side, forwards to the API or gateway, and records usage.
|
|
||||||
|
|
||||||
LLM traffic is routed through the proxy's identity-aware pipeline, while internal
|
|
||||||
resources (databases, internal APIs, self-hosted models) are reached directly over
|
|
||||||
peer-to-peer WireGuard tunnels, governed by the same identities and access policies.
|
|
||||||
|
|
||||||
## Where the code lives
|
|
||||||
|
|
||||||
There is no separate "agent-network" service — it reuses the reverse-proxy and management
|
|
||||||
components:
|
|
||||||
|
|
||||||
- [`proxy/`](../proxy) — the NetBird reverse proxy that serves the agent network endpoint
|
|
||||||
and runs the per-request middleware pipeline.
|
|
||||||
- [`management/internals/modules/reverseproxy/`](../management/internals/modules/reverseproxy)
|
|
||||||
— the management-side control plane: providers, policies, guardrails, limits, routing,
|
|
||||||
and usage/access logs.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Full documentation, architecture, and quickstart:
|
|
||||||
**https://docs.netbird.io/agent-network**
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
||||||
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
||||||
|
|
||||||
FROM alpine:3.24
|
FROM alpine:3.23.3
|
||||||
# iproute2: busybox doesn't display ip rules properly
|
# iproute2: busybox doesn't display ip rules properly
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
bash \
|
bash \
|
||||||
@@ -21,7 +21,7 @@ ENV \
|
|||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
ARG TARGETPLATFORM
|
|
||||||
ARG NETBIRD_BINARY=$TARGETPLATFORM/netbird
|
ARG NETBIRD_BINARY=netbird
|
||||||
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
||||||
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
|
||||||
# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
||||||
|
|
||||||
FROM alpine:3.24
|
FROM alpine:3.22.0
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
bash \
|
bash \
|
||||||
@@ -27,7 +27,7 @@ ENV \
|
|||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
ARG TARGETPLATFORM
|
|
||||||
ARG NETBIRD_BINARY=$TARGETPLATFORM/netbird
|
ARG NETBIRD_BINARY=netbird
|
||||||
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
||||||
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var (
|
|||||||
EnvKeyNBForceRelay = peer.EnvKeyNBForceRelay
|
EnvKeyNBForceRelay = peer.EnvKeyNBForceRelay
|
||||||
|
|
||||||
// EnvKeyNBLazyConn Exported for Android java client to configure lazy connection
|
// EnvKeyNBLazyConn Exported for Android java client to configure lazy connection
|
||||||
EnvKeyNBLazyConn = lazyconn.EnvLazyConn
|
EnvKeyNBLazyConn = lazyconn.EnvEnableLazyConn
|
||||||
|
|
||||||
// EnvKeyNBInactivityThreshold Exported for Android java client to configure connection inactivity threshold
|
// EnvKeyNBInactivityThreshold Exported for Android java client to configure connection inactivity threshold
|
||||||
EnvKeyNBInactivityThreshold = lazyconn.EnvInactivityThreshold
|
EnvKeyNBInactivityThreshold = lazyconn.EnvInactivityThreshold
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ const (
|
|||||||
|
|
||||||
// Profile represents a profile for gomobile
|
// Profile represents a profile for gomobile
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
ID string
|
|
||||||
Name string
|
Name string
|
||||||
IsActive bool
|
IsActive bool
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,10 @@ func (p *ProfileArray) Get(i int) *Profile {
|
|||||||
├── state.json ← Default profile state
|
├── state.json ← Default profile state
|
||||||
├── active_profile.json ← Active profile tracker (JSON with Name + Username)
|
├── active_profile.json ← Active profile tracker (JSON with Name + Username)
|
||||||
└── profiles/ ← Subdirectory for non-default profiles
|
└── profiles/ ← Subdirectory for non-default profiles
|
||||||
├── work.json ← Legacy work profile config
|
├── work.json ← Work profile config
|
||||||
├── work.state.json ← Legacy work profile state
|
├── work.state.json ← Work profile state
|
||||||
├── 4c5f5c8198c3989cffb5b5394f5a7ae0.json ← ID profile config
|
├── personal.json ← Personal profile config
|
||||||
├── 4c5f5c8198c3989cffb5b5394f5a7ae0.state.json ← ID profile state
|
└── personal.state.json ← Personal profile state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ProfileManager manages profiles for Android
|
// ProfileManager manages profiles for Android
|
||||||
@@ -99,7 +99,6 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
|
|||||||
var profiles []*Profile
|
var profiles []*Profile
|
||||||
for _, p := range internalProfiles {
|
for _, p := range internalProfiles {
|
||||||
profiles = append(profiles, &Profile{
|
profiles = append(profiles, &Profile{
|
||||||
ID: p.ID.String(),
|
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsActive: p.IsActive,
|
IsActive: p.IsActive,
|
||||||
})
|
})
|
||||||
@@ -109,65 +108,55 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveProfile returns the currently active profile name
|
// GetActiveProfile returns the currently active profile name
|
||||||
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
|
func (pm *ProfileManager) GetActiveProfile() (string, error) {
|
||||||
// Use ServiceManager to stay consistent with ListProfiles
|
// Use ServiceManager to stay consistent with ListProfiles
|
||||||
// ServiceManager uses active_profile.json
|
// ServiceManager uses active_profile.json
|
||||||
activeState, err := pm.serviceMgr.GetActiveProfileState()
|
activeState, err := pm.serviceMgr.GetActiveProfileState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get active profile: %w", err)
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||||
}
|
}
|
||||||
|
return activeState.Name, nil
|
||||||
// ActiveProfileState only stores the ID (and username), not the display
|
|
||||||
// name. Resolve the ID to the full profile so callers get the real Name.
|
|
||||||
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID.String(), androidUsername)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve active profile %q: %w", activeState.ID, err)
|
|
||||||
}
|
|
||||||
return &Profile{ID: prof.ID.String(), Name: prof.Name, IsActive: true}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwitchProfile switches to a different profile
|
// SwitchProfile switches to a different profile
|
||||||
func (pm *ProfileManager) SwitchProfile(id string) error {
|
func (pm *ProfileManager) SwitchProfile(profileName string) error {
|
||||||
// Use ServiceManager to stay consistent with ListProfiles
|
// Use ServiceManager to stay consistent with ListProfiles
|
||||||
// ServiceManager uses active_profile.json
|
// ServiceManager uses active_profile.json
|
||||||
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||||
ID: profilemanager.ID(id),
|
Name: profileName,
|
||||||
Username: androidUsername,
|
Username: androidUsername,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to switch profile: %w", err)
|
return fmt.Errorf("failed to switch profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("switched to profile: %s", id)
|
log.Infof("switched to profile: %s", profileName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddProfile creates a new profile
|
// AddProfile creates a new profile
|
||||||
func (pm *ProfileManager) AddProfile(profileName string) error {
|
func (pm *ProfileManager) AddProfile(profileName string) error {
|
||||||
// Use ServiceManager (creates profile in profiles/ directory)
|
// Use ServiceManager (creates profile in profiles/ directory)
|
||||||
profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername)
|
if err := pm.serviceMgr.AddProfile(profileName, androidUsername); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add profile: %w", err)
|
return fmt.Errorf("failed to add profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("created new profile: %s", profile.ID)
|
log.Infof("created new profile: %s", profileName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutProfile logs out from a profile (clears authentication)
|
// LogoutProfile logs out from a profile (clears authentication)
|
||||||
func (pm *ProfileManager) LogoutProfile(id string) error {
|
func (pm *ProfileManager) LogoutProfile(profileName string) error {
|
||||||
configPath, err := pm.getProfileConfigPath(id)
|
profileName = sanitizeProfileName(profileName)
|
||||||
|
|
||||||
|
configPath, err := pm.getProfileConfigPath(profileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
|
||||||
return fmt.Errorf("id '%s' is not valid", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if profile exists
|
// Check if profile exists
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("profile '%s' does not exist", id)
|
return fmt.Errorf("profile '%s' does not exist", profileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read current config using internal profilemanager
|
// Read current config using internal profilemanager
|
||||||
@@ -185,57 +174,53 @@ func (pm *ProfileManager) LogoutProfile(id string) error {
|
|||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("logged out from profile: %s", id)
|
log.Infof("logged out from profile: %s", profileName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveProfile deletes a profile
|
// RemoveProfile deletes a profile
|
||||||
func (pm *ProfileManager) RemoveProfile(id string) error {
|
func (pm *ProfileManager) RemoveProfile(profileName string) error {
|
||||||
// Use ServiceManager (removes profile from profiles/ directory)
|
// Use ServiceManager (removes profile from profiles/ directory)
|
||||||
if err := pm.serviceMgr.RemoveProfile(profilemanager.ID(id), androidUsername); err != nil {
|
if err := pm.serviceMgr.RemoveProfile(profileName, androidUsername); err != nil {
|
||||||
return fmt.Errorf("failed to remove profile: %w", err)
|
return fmt.Errorf("failed to remove profile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("removed profile: %s", id)
|
log.Infof("removed profile: %s", profileName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getProfileConfigPath returns the config file path for a profile
|
// getProfileConfigPath returns the config file path for a profile
|
||||||
// This is needed for Android-specific path handling (netbird.cfg for default profile)
|
// This is needed for Android-specific path handling (netbird.cfg for default profile)
|
||||||
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {
|
func (pm *ProfileManager) getProfileConfigPath(profileName string) (string, error) {
|
||||||
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
if profileName == "" || profileName == profilemanager.DefaultProfileName {
|
||||||
return "", fmt.Errorf("id %q is not valid", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id == profilemanager.DefaultProfileName {
|
|
||||||
// Android uses netbird.cfg for default profile instead of default.json
|
// Android uses netbird.cfg for default profile instead of default.json
|
||||||
// Default profile is stored in root configDir, not in profiles/
|
// Default profile is stored in root configDir, not in profiles/
|
||||||
return filepath.Join(pm.configDir, defaultConfigFilename), nil
|
return filepath.Join(pm.configDir, defaultConfigFilename), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-default profiles are stored in profiles subdirectory
|
||||||
|
// This matches the Java Preferences.java expectation
|
||||||
|
profileName = sanitizeProfileName(profileName)
|
||||||
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
||||||
return filepath.Join(profilesDir, id+".json"), nil
|
return filepath.Join(profilesDir, profileName+".json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigPath returns the config file path for a given profile id
|
// GetConfigPath returns the config file path for a given profile
|
||||||
// Java should call this instead of constructing paths with Preferences.configFile()
|
// Java should call this instead of constructing paths with Preferences.configFile()
|
||||||
func (pm *ProfileManager) GetConfigPath(id string) (string, error) {
|
func (pm *ProfileManager) GetConfigPath(profileName string) (string, error) {
|
||||||
return pm.getProfileConfigPath(id)
|
return pm.getProfileConfigPath(profileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStateFilePath returns the state file path for a given profile
|
// GetStateFilePath returns the state file path for a given profile
|
||||||
// Java should call this instead of constructing paths with Preferences.stateFile()
|
// Java should call this instead of constructing paths with Preferences.stateFile()
|
||||||
func (pm *ProfileManager) GetStateFilePath(id string) (string, error) {
|
func (pm *ProfileManager) GetStateFilePath(profileName string) (string, error) {
|
||||||
if id == "" || id == profilemanager.DefaultProfileName {
|
if profileName == "" || profileName == profilemanager.DefaultProfileName {
|
||||||
return filepath.Join(pm.configDir, "state.json"), nil
|
return filepath.Join(pm.configDir, "state.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
profileName = sanitizeProfileName(profileName)
|
||||||
return "", fmt.Errorf("id %q is not valid", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
|
||||||
return filepath.Join(profilesDir, id+".state.json"), nil
|
return filepath.Join(profilesDir, profileName+".state.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveConfigPath returns the config file path for the currently active profile
|
// GetActiveConfigPath returns the config file path for the currently active profile
|
||||||
@@ -245,7 +230,7 @@ func (pm *ProfileManager) GetActiveConfigPath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||||
}
|
}
|
||||||
return pm.GetConfigPath(activeProfile.ID)
|
return pm.GetConfigPath(activeProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveStateFilePath returns the state file path for the currently active profile
|
// GetActiveStateFilePath returns the state file path for the currently active profile
|
||||||
@@ -255,5 +240,18 @@ func (pm *ProfileManager) GetActiveStateFilePath() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||||
}
|
}
|
||||||
return pm.GetStateFilePath(activeProfile.ID)
|
return pm.GetStateFilePath(activeProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeProfileName removes invalid characters from profile name
|
||||||
|
func sanitizeProfileName(name string) string {
|
||||||
|
// Keep only alphanumeric, underscore, and hyphen
|
||||||
|
var result strings.Builder
|
||||||
|
for _, r := range name {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
||||||
|
(r >= '0' && r <= '9') || r == '_' || r == '-' {
|
||||||
|
result.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/user"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
@@ -21,7 +19,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/upload-server/types"
|
"github.com/netbirdio/netbird/upload-server/types"
|
||||||
"github.com/netbirdio/netbird/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const errCloseConnection = "Failed to close connection: %v"
|
const errCloseConnection = "Failed to close connection: %v"
|
||||||
@@ -87,73 +84,6 @@ var persistenceCmd = &cobra.Command{
|
|||||||
RunE: setSyncResponsePersistence,
|
RunE: setSyncResponsePersistence,
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugConfigCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Example: " netbird debug config",
|
|
||||||
Short: "Dump the effective configuration",
|
|
||||||
Long: "Prints the daemon's resolved configuration (after applying defaults, file, env, CLI input, and MDM policy overrides) as JSON. Includes the list of MDM-managed fields.",
|
|
||||||
RunE: debugConfigDump,
|
|
||||||
}
|
|
||||||
|
|
||||||
// debugConfigDump implements `netbird debug config`. It resolves the
|
|
||||||
// active profile, queries the daemon for the effective configuration
|
|
||||||
// via GetConfig, and prints the resulting GetConfigResponse as JSON
|
|
||||||
// (via protojson with EmitUnpopulated=true so the output is stable
|
|
||||||
// across runs and includes zero-valued fields).
|
|
||||||
//
|
|
||||||
// Useful for verifying MDM enforcement end-to-end: the response's
|
|
||||||
// mDMManagedFields array is the single source of truth for "which
|
|
||||||
// fields is the daemon currently enforcing from the MDM source", and
|
|
||||||
// every config field side-by-side with that list confirms the merge
|
|
||||||
// result. Secrets in the response (e.g. PreSharedKey) are already
|
|
||||||
// redacted by the daemon-side handler.
|
|
||||||
func debugConfigDump(cmd *cobra.Command, _ []string) error {
|
|
||||||
pm := profilemanager.NewProfileManager()
|
|
||||||
activeProf, err := pm.GetActiveProfile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get active profile: %v", err)
|
|
||||||
}
|
|
||||||
currUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get current user: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := getClient(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := conn.Close(); err != nil {
|
|
||||||
log.Errorf(errCloseConnection, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
|
||||||
resp, err := client.GetConfig(cmd.Context(), &proto.GetConfigRequest{
|
|
||||||
ProfileName: string(activeProf.ID),
|
|
||||||
Username: currUser.Username,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get config: %v", status.Convert(err).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use protojson so well-known fields render correctly; emit defaults so
|
|
||||||
// the operator sees every field even when zero/empty.
|
|
||||||
m := protojson.MarshalOptions{Multiline: true, Indent: " ", EmitUnpopulated: true}
|
|
||||||
out, err := m.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal config: %w", err)
|
|
||||||
}
|
|
||||||
cmd.Println(string(out))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// debugBundle requests the daemon to create a debug bundle and prints
|
|
||||||
// the resulting local file path and, if uploaded, the uploaded file
|
|
||||||
// key. It uses the package flags (anonymize, system info, log file
|
|
||||||
// count, CLI version, optional upload URL) to configure the bundle
|
|
||||||
// request. Returns an error if the RPC fails or if the daemon reports
|
|
||||||
// an upload failure reason.
|
|
||||||
func debugBundle(cmd *cobra.Command, _ []string) error {
|
func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||||
conn, err := getClient(cmd)
|
conn, err := getClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,7 +100,6 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
|||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
SystemInfo: systemInfoFlag,
|
SystemInfo: systemInfoFlag,
|
||||||
LogFileCount: logFileCount,
|
LogFileCount: logFileCount,
|
||||||
CliVersion: version.NetbirdVersion(),
|
|
||||||
}
|
}
|
||||||
if uploadBundleFlag {
|
if uploadBundleFlag {
|
||||||
request.UploadURL = uploadBundleURLFlag
|
request.UploadURL = uploadBundleURLFlag
|
||||||
@@ -369,7 +298,6 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
SystemInfo: systemInfoFlag,
|
SystemInfo: systemInfoFlag,
|
||||||
LogFileCount: logFileCount,
|
LogFileCount: logFileCount,
|
||||||
CliVersion: version.NetbirdVersion(),
|
|
||||||
}
|
}
|
||||||
if uploadBundleFlag {
|
if uploadBundleFlag {
|
||||||
request.UploadURL = uploadBundleURLFlag
|
request.UploadURL = uploadBundleURLFlag
|
||||||
@@ -504,7 +432,6 @@ func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, c
|
|||||||
SyncResponse: syncResponse,
|
SyncResponse: syncResponse,
|
||||||
LogPath: logFilePath,
|
LogPath: logFilePath,
|
||||||
CPUProfile: nil,
|
CPUProfile: nil,
|
||||||
DaemonVersion: version.NetbirdVersion(), // acting as daemon
|
|
||||||
},
|
},
|
||||||
debug.BundleConfig{
|
debug.BundleConfig{
|
||||||
IncludeSystemInfo: true,
|
IncludeSystemInfo: true,
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
KubernetesDNSSuffix = "netbird-kubeapi-proxy"
|
|
||||||
)
|
|
||||||
|
|
||||||
var kubernetesCmd = &cobra.Command{
|
|
||||||
Use: "kubernetes",
|
|
||||||
Short: "Kubernetes cluster commands.",
|
|
||||||
Long: "Kubernetes cluster commands.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var kubernetesListCmd = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
RunE: kubernetesList,
|
|
||||||
Short: "List Kubernetes clusters.",
|
|
||||||
Long: "List Kubernetes clusters by discovering NetBird peers running netbird-kubeapi-proxy.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var kubernetesWriteKubeconfigCmd = &cobra.Command{
|
|
||||||
Use: "write-kubeconfig",
|
|
||||||
RunE: kubernetesWriteKubeconfig,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Short: "Write kubeconfig for a Kubernetes cluster.",
|
|
||||||
Long: "Updates kubeconfig in place to allow token-less access to the Kubernetes cluster through NetBird.",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
kubernetesWriteKubeconfigCmd.Flags().String("kubeconfig", "", "path to kubeconfig file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func kubernetesList(cmd *cobra.Command, _ []string) error {
|
|
||||||
conn, err := getClient(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
|
||||||
statusResp, err := client.Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
kcs, err := getKubernetesClusters(cmd.Context(), statusResp.FullStatus.Peers, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(kcs) == 0 {
|
|
||||||
cmd.Println("No Kubernetes clusters available.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cmd.Println("Available Kubernetes clusters:")
|
|
||||||
for _, k := range kcs {
|
|
||||||
cmd.Printf("\n - Name: %s\n FQDN: %s\n Version: %s\n", k.name, k.url.Host, k.version)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func kubernetesWriteKubeconfig(cmd *cobra.Command, args []string) error {
|
|
||||||
kubeconfigPath, err := resolveKubeconfigPath(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := getClient(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
|
||||||
statusResp, err := client.Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterName := args[0]
|
|
||||||
kcs, err := getKubernetesClusters(cmd.Context(), statusResp.FullStatus.Peers, clusterName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(kcs) == 0 {
|
|
||||||
return fmt.Errorf("kubernetes cluster named %s not found", clusterName)
|
|
||||||
}
|
|
||||||
if len(kcs) > 1 {
|
|
||||||
return fmt.Errorf("too many Kubernetes clusters returned")
|
|
||||||
}
|
|
||||||
err = writeKubeconfig(kubeconfigPath, kcs[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type kubernetesCluster struct {
|
|
||||||
name string
|
|
||||||
url *url.URL
|
|
||||||
version string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getKubernetesClusters(ctx context.Context, peers []*proto.PeerState, nameFilter string) ([]kubernetesCluster, error) {
|
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
transport.TLSClientConfig = &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
resolver := net.Resolver{
|
|
||||||
// Required so both DNS records are returned.
|
|
||||||
// https://github.com/golang/go/issues/17093
|
|
||||||
PreferGo: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
kcs := []kubernetesCluster{}
|
|
||||||
attempted := map[string]struct{}{}
|
|
||||||
for _, peer := range peers {
|
|
||||||
fqdns, err := resolver.LookupAddr(ctx, peer.IP)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, fqdn := range fqdns {
|
|
||||||
if _, ok := attempted[fqdn]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attempted[fqdn] = struct{}{}
|
|
||||||
comps := strings.Split(fqdn, ".")
|
|
||||||
if len(comps) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if comps[1] != KubernetesDNSSuffix {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if nameFilter != "" && nameFilter != comps[0] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
clusterURL, clusterVersion, err := fingerprintClusters(ctx, httpClient, fqdn)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("could not fingerprint Kubernetes cluster %s %q", fqdn, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kc := kubernetesCluster{
|
|
||||||
name: comps[0],
|
|
||||||
url: clusterURL,
|
|
||||||
version: clusterVersion,
|
|
||||||
}
|
|
||||||
if nameFilter != "" {
|
|
||||||
return []kubernetesCluster{kc}, nil
|
|
||||||
}
|
|
||||||
kcs = append(kcs, kc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return kcs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fingerprintClusters(ctx context.Context, httpClient *http.Client, fqdn string) (*url.URL, string, error) {
|
|
||||||
clusterURL, err := url.Parse("https://" + fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
versionURL, err := clusterURL.Parse("/version")
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionURL.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, "", fmt.Errorf("expected %d response but got %s", http.StatusOK, resp.Status)
|
|
||||||
}
|
|
||||||
b, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
versionData := map[string]string{}
|
|
||||||
err = json.Unmarshal(b, &versionData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
version, ok := versionData["gitVersion"]
|
|
||||||
if !ok {
|
|
||||||
return nil, "", errors.New("no version found in response")
|
|
||||||
}
|
|
||||||
return clusterURL, version, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveKubeconfigPath(cmd *cobra.Command) (string, error) {
|
|
||||||
if cmd.Flags().Changed("kubeconfig") {
|
|
||||||
path, err := cmd.Flags().GetString("kubeconfig")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
if env := os.Getenv("KUBECONFIG"); env != "" {
|
|
||||||
return env, nil
|
|
||||||
}
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine home directory: %w", err)
|
|
||||||
}
|
|
||||||
return filepath.Join(home, ".kube", "config"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeKubeconfig(kubeconfigPath string, kc kubernetesCluster) error {
|
|
||||||
b, err := os.ReadFile(kubeconfigPath)
|
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var cfg map[string]any
|
|
||||||
if err := yaml.Unmarshal(b, &cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
cfg = map[string]any{
|
|
||||||
"apiVersion": "v1",
|
|
||||||
"kind": "Config",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg["clusters"] = appendWithName(cfg["clusters"], map[string]any{
|
|
||||||
"name": kc.name,
|
|
||||||
"cluster": map[string]any{
|
|
||||||
"server": kc.url.String(),
|
|
||||||
"insecure-skip-tls-verify": true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
cfg["users"] = appendWithName(cfg["users"], map[string]any{
|
|
||||||
"name": "netbird",
|
|
||||||
"user": map[string]any{
|
|
||||||
"token": "none",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
cfg["contexts"] = appendWithName(cfg["contexts"], map[string]any{
|
|
||||||
"name": kc.name,
|
|
||||||
"context": map[string]any{
|
|
||||||
"cluster": kc.name,
|
|
||||||
"user": "netbird",
|
|
||||||
"namespace": "default",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
cfg["current-context"] = kc.name
|
|
||||||
|
|
||||||
out, err := yaml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(kubeconfigPath, out, 0o600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendWithName(data any, add map[string]any) any {
|
|
||||||
if data == nil {
|
|
||||||
return []any{add}
|
|
||||||
}
|
|
||||||
v, ok := data.([]any)
|
|
||||||
if !ok {
|
|
||||||
return []any{add}
|
|
||||||
}
|
|
||||||
i := slices.IndexFunc(v, func(item any) bool {
|
|
||||||
m, ok := item.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m["name"] == add["name"]
|
|
||||||
})
|
|
||||||
if i == -1 {
|
|
||||||
return append(v, add)
|
|
||||||
}
|
|
||||||
v[i] = add
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFingerprintClusters(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
//nolint: errcheck
|
|
||||||
w.Write([]byte(`{"gitVersion": "foobar"}`))
|
|
||||||
}))
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
clusterURL, clusterVersion, err := fingerprintClusters(t.Context(), srv.Client(), srv.Listener.Addr().String())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, srv.URL, clusterURL.String())
|
|
||||||
require.Equal(t, "foobar", clusterVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveKubeconfigPath(t *testing.T) {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not determine home directory: %v", err)
|
|
||||||
}
|
|
||||||
defaultPath := filepath.Join(home, ".kube", "config")
|
|
||||||
path, err := resolveKubeconfigPath(&cobra.Command{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, defaultPath, path)
|
|
||||||
|
|
||||||
flagPath := "flag-path"
|
|
||||||
cmd := &cobra.Command{}
|
|
||||||
cmd.Flags().String("kubeconfig", "", "")
|
|
||||||
err = cmd.Flags().Set("kubeconfig", flagPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
path, err = resolveKubeconfigPath(cmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, flagPath, path)
|
|
||||||
|
|
||||||
envPath := "env-path"
|
|
||||||
t.Setenv("KUBECONFIG", envPath)
|
|
||||||
path, err = resolveKubeconfigPath(&cobra.Command{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, envPath, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteKubeconfig(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
existing string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty file",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "existing content",
|
|
||||||
existing: `apiVersion: v1
|
|
||||||
clusters:
|
|
||||||
- cluster:
|
|
||||||
insecure-skip-tls-verify: true
|
|
||||||
server: https://foobar.com
|
|
||||||
name: foo
|
|
||||||
current-context: test
|
|
||||||
kind: Config
|
|
||||||
users: []
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
kubeconfigPath := filepath.Join(t.TempDir(), "config")
|
|
||||||
err := os.WriteFile(kubeconfigPath, []byte(tt.existing), 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
kc := kubernetesCluster{
|
|
||||||
name: "foo",
|
|
||||||
url: &url.URL{Scheme: "https", Host: "example.com"},
|
|
||||||
}
|
|
||||||
err = writeKubeconfig(kubeconfigPath, kc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b, err := os.ReadFile(kubeconfigPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
expected := `apiVersion: v1
|
|
||||||
clusters:
|
|
||||||
- cluster:
|
|
||||||
insecure-skip-tls-verify: true
|
|
||||||
server: https://example.com
|
|
||||||
name: foo
|
|
||||||
contexts:
|
|
||||||
- context:
|
|
||||||
cluster: foo
|
|
||||||
namespace: default
|
|
||||||
user: netbird
|
|
||||||
name: foo
|
|
||||||
current-context: foo
|
|
||||||
kind: Config
|
|
||||||
users:
|
|
||||||
- name: netbird
|
|
||||||
user:
|
|
||||||
token: none
|
|
||||||
`
|
|
||||||
require.Equal(t, expected, string(b))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -96,19 +96,17 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str
|
|||||||
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
|
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
|
||||||
}
|
}
|
||||||
|
|
||||||
handle := activeProf.ID.String()
|
|
||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: providedSetupKey,
|
SetupKey: providedSetupKey,
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
IsUnixDesktopClient: isUnixRunningDesktop(),
|
IsUnixDesktopClient: isUnixRunningDesktop(),
|
||||||
Hostname: hostName,
|
Hostname: hostName,
|
||||||
DnsLabels: dnsLabelsReq,
|
DnsLabels: dnsLabelsReq,
|
||||||
ProfileName: &handle,
|
ProfileName: &activeProf.Name,
|
||||||
Username: &username,
|
Username: &username,
|
||||||
}
|
}
|
||||||
|
|
||||||
profileState, err := pm.GetProfileState(activeProf.ID)
|
profileState, err := pm.GetProfileState(activeProf.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
||||||
} else if profileState.Email != "" {
|
} else if profileState.Email != "" {
|
||||||
@@ -172,13 +170,14 @@ func getActiveProfile(ctx context.Context, pm *profilemanager.ProfileManager, pr
|
|||||||
return activeProf, nil
|
return activeProf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, handle string, username string) error {
|
func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) error {
|
||||||
resolvedID, err := switchProfile(ctx, handle, username)
|
err := switchProfile(context.Background(), profileName, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("switch profile on daemon: %v", err)
|
return fmt.Errorf("switch profile on daemon: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pm.SwitchProfile(resolvedID); err != nil {
|
err = pm.SwitchProfile(profileName)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("switch profile: %v", err)
|
return fmt.Errorf("switch profile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,15 +205,11 @@ func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManage
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// switchProfile asks the daemon to switch to the profile identified by
|
func switchProfile(ctx context.Context, profileName string, username string) error {
|
||||||
// handle (a name, ID, or unique ID prefix). Returns the resolved profile
|
|
||||||
// ID so the caller can update the local active-profile state without
|
|
||||||
// re-resolving the handle.
|
|
||||||
func switchProfile(ctx context.Context, handle string, username string) (profilemanager.ID, error) {
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint
|
//nolint
|
||||||
return "", fmt.Errorf("failed to connect to daemon error: %v\n"+
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
"If the daemon is not running please run: "+
|
"If the daemon is not running please run: "+
|
||||||
"\nnetbird service install \nnetbird service start\n", err)
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
}
|
}
|
||||||
@@ -222,15 +217,15 @@ func switchProfile(ctx context.Context, handle string, username string) (profile
|
|||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
resp, err := client.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
_, err = client.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
||||||
ProfileName: &handle,
|
ProfileName: &profileName,
|
||||||
Username: &username,
|
Username: &username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("switch profile failed: %w", err)
|
return fmt.Errorf("switch profile failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return profilemanager.ID(resp.Id), nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error {
|
func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error {
|
||||||
@@ -254,7 +249,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
|||||||
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.ID)
|
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -282,7 +277,7 @@ func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.Lo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey string, profileID profilemanager.ID) error {
|
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey, profileName string) error {
|
||||||
authClient, err := auth.NewAuth(ctx, config.PrivateKey, config.ManagementURL, config)
|
authClient, err := auth.NewAuth(ctx, config.PrivateKey, config.ManagementURL, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create auth client: %v", err)
|
return fmt.Errorf("failed to create auth client: %v", err)
|
||||||
@@ -296,7 +291,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
|
|||||||
|
|
||||||
jwtToken := ""
|
jwtToken := ""
|
||||||
if setupKey == "" && needsLogin {
|
if setupKey == "" && needsLogin {
|
||||||
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config, profileID)
|
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config, profileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("interactive sso login failed: %v", err)
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -311,10 +306,10 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, profileID profilemanager.ID) (*auth.TokenInfo, error) {
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, profileName string) (*auth.TokenInfo, error) {
|
||||||
hint := ""
|
hint := ""
|
||||||
pm := profilemanager.NewProfileManager()
|
pm := profilemanager.NewProfileManager()
|
||||||
profileState, err := pm.GetProfileState(profileID)
|
profileState, err := pm.GetProfileState(profileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
||||||
} else if profileState.Email != "" {
|
} else if profileState.Email != "" {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestLogin(t *testing.T) {
|
|||||||
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
|
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
|
||||||
sm := profilemanager.ServiceManager{}
|
sm := profilemanager.ServiceManager{}
|
||||||
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||||
ID: "default",
|
Name: "default",
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
gstatus "google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
@@ -19,8 +14,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var profileListShowID bool
|
|
||||||
|
|
||||||
var profileCmd = &cobra.Command{
|
var profileCmd = &cobra.Command{
|
||||||
Use: "profile",
|
Use: "profile",
|
||||||
Short: "Manage NetBird client profiles",
|
Short: "Manage NetBird client profiles",
|
||||||
@@ -38,40 +31,27 @@ var profileListCmd = &cobra.Command{
|
|||||||
var profileAddCmd = &cobra.Command{
|
var profileAddCmd = &cobra.Command{
|
||||||
Use: "add <profile_name>",
|
Use: "add <profile_name>",
|
||||||
Short: "Add a new profile",
|
Short: "Add a new profile",
|
||||||
Long: `Add a new profile. Profile name is free-form, a unique ID is generated for the on-disk config file.`,
|
Long: `Add a new profile to the NetBird client. The profile name must be unique.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: addProfileFunc,
|
RunE: addProfileFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileRenameCmd = &cobra.Command{
|
|
||||||
Use: "rename <profile> <new_profile_name>",
|
|
||||||
Short: "Renames an existing profile",
|
|
||||||
Long: `Renames an existing profile (by a name, ID, or unique ID prefix). Profile name is free-form.`,
|
|
||||||
Args: cobra.ExactArgs(2),
|
|
||||||
RunE: renameProfileFunc,
|
|
||||||
}
|
|
||||||
|
|
||||||
var profileRemoveCmd = &cobra.Command{
|
var profileRemoveCmd = &cobra.Command{
|
||||||
Use: "remove <profile>",
|
Use: "remove <profile_name>",
|
||||||
Short: "Remove a profile",
|
Short: "Remove a profile",
|
||||||
Long: `Remove a profile by name, ID, or unique ID prefix.`,
|
Long: `Remove a profile from the NetBird client. The profile must not be inactive.`,
|
||||||
Aliases: []string{"rm"},
|
Args: cobra.ExactArgs(1),
|
||||||
Args: cobra.ExactArgs(1),
|
RunE: removeProfileFunc,
|
||||||
RunE: removeProfileFunc,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileSelectCmd = &cobra.Command{
|
var profileSelectCmd = &cobra.Command{
|
||||||
Use: "select <profile>",
|
Use: "select <profile_name>",
|
||||||
Short: "Select a profile",
|
Short: "Select a profile",
|
||||||
Long: `Make the specified profile active. Accepts a name, ID, or unique ID prefix.`,
|
Long: `Make the specified profile active. This will switch the client to use the selected profile's configuration.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: selectProfileFunc,
|
RunE: selectProfileFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
profileListCmd.Flags().BoolVar(&profileListShowID, "show-id", false, "show the profile ID column")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupCmd(cmd *cobra.Command) error {
|
func setupCmd(cmd *cobra.Command) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
SetFlagsFromEnvVars(cmd)
|
SetFlagsFromEnvVars(cmd)
|
||||||
@@ -85,7 +65,6 @@ func setupCmd(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listProfilesFunc(cmd *cobra.Command, _ []string) error {
|
func listProfilesFunc(cmd *cobra.Command, _ []string) error {
|
||||||
if err := setupCmd(cmd); err != nil {
|
if err := setupCmd(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -104,33 +83,25 @@ func listProfilesFunc(cmd *cobra.Command, _ []string) error {
|
|||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
resp, err := daemonClient.ListProfiles(cmd.Context(), &proto.ListProfilesRequest{
|
profiles, err := daemonClient.ListProfiles(cmd.Context(), &proto.ListProfilesRequest{
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tw := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
|
// list profiles, add a tick if the profile is active
|
||||||
if profileListShowID {
|
cmd.Println("Found", len(profiles.Profiles), "profiles:")
|
||||||
fmt.Fprintln(tw, "ID\tNAME\tACTIVE")
|
for _, profile := range profiles.Profiles {
|
||||||
} else {
|
// use a cross to indicate the passive profiles
|
||||||
fmt.Fprintln(tw, "NAME\tACTIVE")
|
activeMarker := "✗"
|
||||||
}
|
|
||||||
for _, profile := range resp.Profiles {
|
|
||||||
marker := ""
|
|
||||||
if profile.IsActive {
|
if profile.IsActive {
|
||||||
marker = "✓"
|
activeMarker = "✓"
|
||||||
}
|
|
||||||
name := profilemanager.StripCtrlChars(profile.Name)
|
|
||||||
id := profilemanager.ID(profile.Id)
|
|
||||||
if profileListShowID {
|
|
||||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", id.ShortID(), name, marker)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(tw, "%s\t%s\n", name, marker)
|
|
||||||
}
|
}
|
||||||
|
cmd.Println(activeMarker, profile.Name)
|
||||||
}
|
}
|
||||||
return tw.Flush()
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addProfileFunc(cmd *cobra.Command, args []string) error {
|
func addProfileFunc(cmd *cobra.Command, args []string) error {
|
||||||
@@ -138,90 +109,33 @@ func addProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
currUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get current user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect to service CLI interface: %w", err)
|
return fmt.Errorf("connect to service CLI interface: %w", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
currUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
profileName := args[0]
|
profileName := args[0]
|
||||||
|
|
||||||
id, err := addProfileOnDaemon(cmd.Context(), daemonClient, profileName, currUser.Username)
|
_, err = daemonClient.AddProfile(cmd.Context(), &proto.AddProfileRequest{
|
||||||
if err != nil {
|
ProfileName: profileName,
|
||||||
return err
|
Username: currUser.Username,
|
||||||
}
|
|
||||||
|
|
||||||
dupCount, _ := countProfilesWithName(cmd.Context(), daemonClient, currUser.Username, profileName)
|
|
||||||
if dupCount > 1 {
|
|
||||||
cmd.Printf("Warning: %d other profile(s) already use the name %q.\n", dupCount-1, profileName)
|
|
||||||
cmd.Println("Use `netbird profile list --show-id` to disambiguate later.")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("Profile added: %s %s\n", id.ShortID(), profilemanager.StripCtrlChars(profileName))
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func renameProfileFunc(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := setupCmd(cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connect to service CLI interface: %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
currUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get current user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
|
||||||
handle := args[0]
|
|
||||||
newProfilename := args[1]
|
|
||||||
|
|
||||||
resp, err := daemonClient.RenameProfile(cmd.Context(), &proto.RenameProfileRequest{
|
|
||||||
Handle: handle,
|
|
||||||
Username: currUser.Username,
|
|
||||||
NewProfileName: newProfilename,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapAmbiguityError(err, handle)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dupCount, _ := countProfilesWithName(cmd.Context(), daemonClient, currUser.Username, newProfilename)
|
cmd.Println("Profile added successfully:", profileName)
|
||||||
if dupCount > 1 {
|
|
||||||
cmd.Printf("Warning: %d other profile(s) already use the name %q.\n", dupCount-1, newProfilename)
|
|
||||||
cmd.Println("Use `netbird profile list --show-id` to disambiguate later.")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("Profile renamed from %s to %s\n", profilemanager.StripCtrlChars(resp.OldProfileName), profilemanager.StripCtrlChars(newProfilename))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func countProfilesWithName(ctx context.Context, c proto.DaemonServiceClient, username, name string) (int, error) {
|
|
||||||
resp, err := c.ListProfiles(ctx, &proto.ListProfilesRequest{Username: username})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
n := 0
|
|
||||||
for _, p := range resp.Profiles {
|
|
||||||
if p.Name == name {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeProfileFunc(cmd *cobra.Command, args []string) error {
|
func removeProfileFunc(cmd *cobra.Command, args []string) error {
|
||||||
if err := setupCmd(cmd); err != nil {
|
if err := setupCmd(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -239,17 +153,18 @@ func removeProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
handle := args[0]
|
|
||||||
|
|
||||||
resp, err := daemonClient.RemoveProfile(cmd.Context(), &proto.RemoveProfileRequest{
|
profileName := args[0]
|
||||||
ProfileName: handle,
|
|
||||||
|
_, err = daemonClient.RemoveProfile(cmd.Context(), &proto.RemoveProfileRequest{
|
||||||
|
ProfileName: profileName,
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapAmbiguityError(err, handle)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Profile removed: %s\n", resp.Id)
|
cmd.Println("Profile removed successfully:", profileName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +174,7 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
profileManager := profilemanager.NewProfileManager()
|
profileManager := profilemanager.NewProfileManager()
|
||||||
handle := args[0]
|
profileName := args[0]
|
||||||
|
|
||||||
currUser, err := user.Current()
|
currUser, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -276,15 +191,32 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
daemonClient := proto.NewDaemonServiceClient(conn)
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
switchResp, err := daemonClient.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
profiles, err := daemonClient.ListProfiles(ctx, &proto.ListProfilesRequest{
|
||||||
ProfileName: &handle,
|
Username: currUser.Username,
|
||||||
Username: &currUser.Username,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapAmbiguityError(err, handle)
|
return fmt.Errorf("list profiles: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := profileManager.SwitchProfile(profilemanager.ID(switchResp.Id)); err != nil {
|
var profileExists bool
|
||||||
|
|
||||||
|
for _, profile := range profiles.Profiles {
|
||||||
|
if profile.Name == profileName {
|
||||||
|
profileExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !profileExists {
|
||||||
|
return fmt.Errorf("profile %s does not exist", profileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := switchProfile(cmd.Context(), profileName, currUser.Username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = profileManager.SwitchProfile(profileName)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,46 +231,6 @@ func selectProfileFunc(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id := profilemanager.ID(switchResp.Id)
|
cmd.Println("Profile switched successfully to:", profileName)
|
||||||
cmd.Printf("Profile switched to: %s\n", id.ShortID())
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapAmbiguityError turns the daemon's gRPC InvalidArgument errors
|
|
||||||
// (which carry the resolver's message verbatim) into CLI-friendly text
|
|
||||||
// that points the user at --show-id.
|
|
||||||
func wrapAmbiguityError(err error, handle string) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
st, ok := gstatus.FromError(err)
|
|
||||||
if !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch st.Code() {
|
|
||||||
case codes.InvalidArgument:
|
|
||||||
msg := st.Message()
|
|
||||||
if strings.Contains(msg, "ambiguous") {
|
|
||||||
return errors.New(msg + "\nRun `netbird profile list --show-id` to see IDs, then select by ID prefix:\n netbird profile select|remove <id-prefix>")
|
|
||||||
}
|
|
||||||
case codes.NotFound:
|
|
||||||
return fmt.Errorf("profile %q not found", handle)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// addProfileOnDaemon issues the AddProfile RPC on an existing daemon client
|
|
||||||
// and returns the new profile's ID. It is the single entry point for profile
|
|
||||||
// creation, shared by `netbird profile add` and the `netbird up --profile
|
|
||||||
// <name>` auto-create path.
|
|
||||||
func addProfileOnDaemon(ctx context.Context, client proto.DaemonServiceClient, profileName, username string) (profilemanager.ID, error) {
|
|
||||||
resp, err := client.AddProfile(ctx, &proto.AddProfileRequest{
|
|
||||||
ProfileName: profileName,
|
|
||||||
Username: username,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("add profile failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return profilemanager.ID(resp.Id), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -71,14 +71,12 @@ var (
|
|||||||
extraIFaceBlackList []string
|
extraIFaceBlackList []string
|
||||||
anonymizeFlag bool
|
anonymizeFlag bool
|
||||||
dnsRouteInterval time.Duration
|
dnsRouteInterval time.Duration
|
||||||
// lazyConnEnabled is the parse target for the deprecated --enable-lazy-connection
|
lazyConnEnabled bool
|
||||||
// flag. The flag is inert; the value is no longer read (use NB_LAZY_CONN instead).
|
mtu uint16
|
||||||
lazyConnEnabled bool
|
profilesDisabled bool
|
||||||
mtu uint16
|
updateSettingsDisabled bool
|
||||||
profilesDisabled bool
|
captureEnabled bool
|
||||||
updateSettingsDisabled bool
|
networksDisabled bool
|
||||||
captureEnabled bool
|
|
||||||
networksDisabled bool
|
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird",
|
Use: "netbird",
|
||||||
@@ -97,9 +95,7 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Execute runs the appropriate Cobra command for the CLI.
|
// Execute executes the root command.
|
||||||
// If the process is the update binary it delegates to updateCmd; otherwise it runs the root command.
|
|
||||||
// It returns any error produced during command execution.
|
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
if isUpdateBinary() {
|
if isUpdateBinary() {
|
||||||
return updateCmd.Execute()
|
return updateCmd.Execute()
|
||||||
@@ -107,16 +103,6 @@ func Execute() error {
|
|||||||
return rootCmd.Execute()
|
return rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
// init initialises package-level defaults and configures the root
|
|
||||||
// Cobra command tree. Sets platform-specific config / log directory
|
|
||||||
// paths (including legacy Wiretrustee fallbacks) and a default daemon
|
|
||||||
// address; registers persistent CLI flags (daemon address,
|
|
||||||
// management / admin URLs, logging, setup key (file and inline,
|
|
||||||
// mutually exclusive), preshared key, hostname, anonymise, config
|
|
||||||
// path); attaches top-level and nested subcommands to the root
|
|
||||||
// command; and registers `up`-specific persistent flags (external IP
|
|
||||||
// maps, custom DNS resolver address, Rosenpass options, auto-connect
|
|
||||||
// disabling, lazy connection).
|
|
||||||
func init() {
|
func init() {
|
||||||
defaultConfigPathDir = "/etc/netbird/"
|
defaultConfigPathDir = "/etc/netbird/"
|
||||||
defaultLogFileDir = "/var/log/netbird/"
|
defaultLogFileDir = "/var/log/netbird/"
|
||||||
@@ -182,17 +168,10 @@ func init() {
|
|||||||
logCmd.AddCommand(logLevelCmd)
|
logCmd.AddCommand(logLevelCmd)
|
||||||
debugCmd.AddCommand(forCmd)
|
debugCmd.AddCommand(forCmd)
|
||||||
debugCmd.AddCommand(persistenceCmd)
|
debugCmd.AddCommand(persistenceCmd)
|
||||||
debugCmd.AddCommand(debugConfigCmd)
|
|
||||||
|
|
||||||
// kubernetes commands
|
|
||||||
rootCmd.AddCommand(kubernetesCmd)
|
|
||||||
kubernetesCmd.AddCommand(kubernetesListCmd)
|
|
||||||
kubernetesCmd.AddCommand(kubernetesWriteKubeconfigCmd)
|
|
||||||
|
|
||||||
// profile commands
|
// profile commands
|
||||||
profileCmd.AddCommand(profileListCmd)
|
profileCmd.AddCommand(profileListCmd)
|
||||||
profileCmd.AddCommand(profileAddCmd)
|
profileCmd.AddCommand(profileAddCmd)
|
||||||
profileCmd.AddCommand(profileRenameCmd)
|
|
||||||
profileCmd.AddCommand(profileRemoveCmd)
|
profileCmd.AddCommand(profileRemoveCmd)
|
||||||
profileCmd.AddCommand(profileSelectCmd)
|
profileCmd.AddCommand(profileSelectCmd)
|
||||||
|
|
||||||
@@ -212,8 +191,7 @@ func init() {
|
|||||||
upCmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "[Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.")
|
upCmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "[Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.")
|
||||||
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
|
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
|
||||||
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
|
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
|
||||||
upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "Deprecated: no longer used. Lazy connections are controlled by the server and the NB_LAZY_CONN environment variable.")
|
upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand. Note: this setting may be overridden by management configuration.")
|
||||||
_ = upCmd.PersistentFlags().MarkDeprecated(enableLazyConnectionFlag, "no longer used; lazy connections are controlled by the server and the NB_LAZY_CONN environment variable")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (p *program) Stop(srv service.Service) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Common setup for service control commands
|
// Common setup for service control commands
|
||||||
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc, consoleLog bool) (service.Service, error) {
|
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc) (service.Service, error) {
|
||||||
// rootCmd env vars are already applied by PersistentPreRunE.
|
// rootCmd env vars are already applied by PersistentPreRunE.
|
||||||
SetFlagsFromEnvVars(serviceCmd)
|
SetFlagsFromEnvVars(serviceCmd)
|
||||||
|
|
||||||
@@ -112,14 +112,8 @@ func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if consoleLog {
|
if err := util.InitLog(logLevel, logFiles...); err != nil {
|
||||||
if err := util.InitLog(logLevel, util.LogConsole); err != nil {
|
return nil, fmt.Errorf("init log: %w", err)
|
||||||
return nil, fmt.Errorf("init log: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := util.InitLog(logLevel, logFiles...); err != nil {
|
|
||||||
return nil, fmt.Errorf("init log: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := newSVCConfig()
|
cfg, err := newSVCConfig()
|
||||||
@@ -144,7 +138,7 @@ var runCmd = &cobra.Command{
|
|||||||
SetupCloseHandler(ctx, cancel)
|
SetupCloseHandler(ctx, cancel)
|
||||||
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
|
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
|
||||||
|
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -158,7 +152,7 @@ var startCmd = &cobra.Command{
|
|||||||
Short: "starts NetBird service",
|
Short: "starts NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -176,7 +170,7 @@ var stopCmd = &cobra.Command{
|
|||||||
Short: "stops NetBird service",
|
Short: "stops NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -194,7 +188,7 @@ var restartCmd = &cobra.Command{
|
|||||||
Short: "restarts NetBird service",
|
Short: "restarts NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -212,7 +206,7 @@ var svcStatusCmd = &cobra.Command{
|
|||||||
Short: "shows NetBird service status",
|
Short: "shows NetBird service status",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
s, err := setupServiceControlCommand(cmd, ctx, cancel, true)
|
s, err := setupServiceControlCommand(cmd, ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
//go:build privileged
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
serviceStartTimeout = 10 * time.Second
|
|
||||||
serviceStopTimeout = 5 * time.Second
|
|
||||||
statusPollInterval = 500 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// waitForServiceStatus waits for service to reach expected status with timeout
|
|
||||||
func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) {
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer timeoutCancel()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(statusPollInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus)
|
|
||||||
case <-ticker.C:
|
|
||||||
status, err := s.Status()
|
|
||||||
if err != nil {
|
|
||||||
// Continue polling on transient errors
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if status == expectedStatus {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestServiceLifecycle tests the complete service lifecycle
|
|
||||||
func TestServiceLifecycle(t *testing.T) {
|
|
||||||
// TODO: Add support for Windows and macOS
|
|
||||||
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
|
||||||
t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("CONTAINER") == "true" {
|
|
||||||
t.Skip("Skipping service lifecycle test in container environment")
|
|
||||||
}
|
|
||||||
|
|
||||||
originalServiceName := serviceName
|
|
||||||
serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix())
|
|
||||||
defer func() {
|
|
||||||
serviceName = originalServiceName
|
|
||||||
}()
|
|
||||||
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir)
|
|
||||||
logLevel = "info"
|
|
||||||
daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir)
|
|
||||||
|
|
||||||
// Ensure cleanup even if a subtest fails and Stop/Uninstall subtests don't run.
|
|
||||||
t.Cleanup(func() {
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cleanup: create service config: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("cleanup: create service: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the subtests already cleaned up, there's nothing to do.
|
|
||||||
if _, err := s.Status(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Stop(); err != nil {
|
|
||||||
t.Errorf("cleanup: stop service: %v", err)
|
|
||||||
}
|
|
||||||
if err := s.Uninstall(); err != nil {
|
|
||||||
t.Errorf("cleanup: uninstall service: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
|
||||||
installCmd.SetContext(ctx)
|
|
||||||
err := installCmd.RunE(installCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
status, err := s.Status()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEqual(t, service.StatusUnknown, status)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Start", func(t *testing.T) {
|
|
||||||
startCmd.SetContext(ctx)
|
|
||||||
err := startCmd.RunE(startCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, running)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Restart", func(t *testing.T) {
|
|
||||||
restartCmd.SetContext(ctx)
|
|
||||||
err := restartCmd.RunE(restartCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, running)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Reconfigure", func(t *testing.T) {
|
|
||||||
originalLogLevel := logLevel
|
|
||||||
logLevel = "debug"
|
|
||||||
defer func() {
|
|
||||||
logLevel = originalLogLevel
|
|
||||||
}()
|
|
||||||
|
|
||||||
reconfigureCmd.SetContext(ctx)
|
|
||||||
err := reconfigureCmd.RunE(reconfigureCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, running)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Stop", func(t *testing.T) {
|
|
||||||
stopCmd.SetContext(ctx)
|
|
||||||
err := stopCmd.RunE(stopCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, stopped)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Uninstall", func(t *testing.T) {
|
|
||||||
uninstallCmd.SetContext(ctx)
|
|
||||||
err := uninstallCmd.RunE(uninstallCmd, []string{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cfg, err := newSVCConfig()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctxSvc, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = s.Status()
|
|
||||||
assert.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -27,6 +31,186 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceStartTimeout = 10 * time.Second
|
||||||
|
serviceStopTimeout = 5 * time.Second
|
||||||
|
statusPollInterval = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// waitForServiceStatus waits for service to reach expected status with timeout
|
||||||
|
func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) {
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(statusPollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus)
|
||||||
|
case <-ticker.C:
|
||||||
|
status, err := s.Status()
|
||||||
|
if err != nil {
|
||||||
|
// Continue polling on transient errors
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status == expectedStatus {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServiceLifecycle tests the complete service lifecycle
|
||||||
|
func TestServiceLifecycle(t *testing.T) {
|
||||||
|
// TODO: Add support for Windows and macOS
|
||||||
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||||
|
t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("CONTAINER") == "true" {
|
||||||
|
t.Skip("Skipping service lifecycle test in container environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalServiceName := serviceName
|
||||||
|
serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
defer func() {
|
||||||
|
serviceName = originalServiceName
|
||||||
|
}()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir)
|
||||||
|
logLevel = "info"
|
||||||
|
daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir)
|
||||||
|
|
||||||
|
// Ensure cleanup even if a subtest fails and Stop/Uninstall subtests don't run.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cleanup: create service config: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cleanup: create service: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the subtests already cleaned up, there's nothing to do.
|
||||||
|
if _, err := s.Status(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Stop(); err != nil {
|
||||||
|
t.Errorf("cleanup: stop service: %v", err)
|
||||||
|
}
|
||||||
|
if err := s.Uninstall(); err != nil {
|
||||||
|
t.Errorf("cleanup: uninstall service: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("Install", func(t *testing.T) {
|
||||||
|
installCmd.SetContext(ctx)
|
||||||
|
err := installCmd.RunE(installCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
status, err := s.Status()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, service.StatusUnknown, status)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Start", func(t *testing.T) {
|
||||||
|
startCmd.SetContext(ctx)
|
||||||
|
err := startCmd.RunE(startCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Restart", func(t *testing.T) {
|
||||||
|
restartCmd.SetContext(ctx)
|
||||||
|
err := restartCmd.RunE(restartCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reconfigure", func(t *testing.T) {
|
||||||
|
originalLogLevel := logLevel
|
||||||
|
logLevel = "debug"
|
||||||
|
defer func() {
|
||||||
|
logLevel = originalLogLevel
|
||||||
|
}()
|
||||||
|
|
||||||
|
reconfigureCmd.SetContext(ctx)
|
||||||
|
err := reconfigureCmd.RunE(reconfigureCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, running)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Stop", func(t *testing.T) {
|
||||||
|
stopCmd.SetContext(ctx)
|
||||||
|
err := stopCmd.RunE(stopCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Uninstall", func(t *testing.T) {
|
||||||
|
uninstallCmd.SetContext(ctx)
|
||||||
|
err := uninstallCmd.RunE(uninstallCmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := newSVCConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctxSvc, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.Status()
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestServiceEnvVars tests environment variable parsing
|
// TestServiceEnvVars tests environment variable parsing
|
||||||
func TestServiceEnvVars(t *testing.T) {
|
func TestServiceEnvVars(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
nbstatus "github.com/netbirdio/netbird/client/status"
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
@@ -110,10 +111,11 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the active profile's display name via the daemon, which runs
|
pm := profilemanager.NewProfileManager()
|
||||||
// as root and can read the per-user profile files. The local profile
|
var profName string
|
||||||
// manager only knows the active profile ID, not its display name.
|
if activeProf, err := pm.GetActiveProfile(); err == nil {
|
||||||
profName := getActiveProfileName(ctx)
|
profName = activeProf.Name
|
||||||
|
}
|
||||||
|
|
||||||
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{
|
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{
|
||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
@@ -165,25 +167,6 @@ func getStatus(ctx context.Context, fullPeerStatus bool, shouldRunProbes bool) (
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getActiveProfileName asks the daemon for the active profile's display
|
|
||||||
// name. The daemon runs as root and can read the per-user profile files to
|
|
||||||
// resolve the ID to its human-readable name. Returns an empty string on any
|
|
||||||
// error so status output degrades gracefully.
|
|
||||||
func getActiveProfileName(ctx context.Context) string {
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.GetProfileName()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFilters() error {
|
func parseFilters() error {
|
||||||
switch strings.ToLower(statusFilter) {
|
switch strings.ToLower(statusFilter) {
|
||||||
case "", "idle", "connecting", "connected":
|
case "", "idle", "connecting", "connected":
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
iv, _ := validator.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
iv, _ := integrations.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -128,9 +128,16 @@ func upFunc(cmd *cobra.Command, args []string) error {
|
|||||||
var profileSwitched bool
|
var profileSwitched bool
|
||||||
// switch profile if provided
|
// switch profile if provided
|
||||||
if profileName != "" {
|
if profileName != "" {
|
||||||
if err := switchOrCreateProfile(cmd.Context(), pm, profileName, username.Username); err != nil {
|
err = switchProfile(cmd.Context(), profileName, username.Username)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("switch profile: %v", err)
|
return fmt.Errorf("switch profile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = pm.SwitchProfile(profileName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("switch profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
profileSwitched = true
|
profileSwitched = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,52 +152,6 @@ func upFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
|
return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
|
||||||
}
|
}
|
||||||
|
|
||||||
// switchOrCreateProfile switches the active profile to the one identified by
|
|
||||||
// handle, creating it first when it does not exist yet. This restores the
|
|
||||||
// pre-0.73 behaviour where `netbird up --profile <name>` auto-creates a
|
|
||||||
// missing profile instead of failing.
|
|
||||||
func switchOrCreateProfile(ctx context.Context, pm *profilemanager.ProfileManager, handle, username string) error {
|
|
||||||
resolvedID, err := switchProfile(ctx, handle, username)
|
|
||||||
if err != nil {
|
|
||||||
st, ok := gstatus.FromError(err)
|
|
||||||
if !ok || st.Code() != codes.NotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Don't fail immediately on a create error: a concurrent run may
|
|
||||||
// have created the profile between the NotFound above and this
|
|
||||||
// call, in which case the retried switch still succeeds. Only
|
|
||||||
// surface the create error if the switch also fails.
|
|
||||||
_, createErr := createProfile(ctx, handle, username)
|
|
||||||
if resolvedID, err = switchProfile(ctx, handle, username); err != nil {
|
|
||||||
if createErr != nil {
|
|
||||||
return fmt.Errorf("create profile: %w", createErr)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pm.SwitchProfile(resolvedID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createProfile dials the daemon and creates a new profile with the given
|
|
||||||
// display name, returning its generated ID. Use addProfileOnDaemon directly
|
|
||||||
// when a daemon client is already available to reuse the connection.
|
|
||||||
func createProfile(ctx context.Context, profileName, username string) (profilemanager.ID, error) {
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
||||||
if err != nil {
|
|
||||||
//nolint
|
|
||||||
return "", fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
||||||
"If the daemon is not running please run: "+
|
|
||||||
"\nnetbird service install \nnetbird service start\n", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
return addProfileOnDaemon(ctx, proto.NewDaemonServiceClient(conn), profileName, username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
|
func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
|
||||||
// override the default profile filepath if provided
|
// override the default profile filepath if provided
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
@@ -229,7 +190,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
|
|||||||
|
|
||||||
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, providedSetupKey, activeProf.ID)
|
err = foregroundLogin(ctx, cmd, config, providedSetupKey, activeProf.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -300,10 +261,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the new config
|
// set the new config
|
||||||
req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.ID.String(), username.Username)
|
req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.Name, username.Username)
|
||||||
if _, err := client.SetConfig(ctx, req); err != nil {
|
if _, err := client.SetConfig(ctx, req); err != nil {
|
||||||
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable {
|
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable {
|
||||||
log.Warnf("setConfig method is not available in the daemon: %s", st.Message())
|
log.Warnf("setConfig method is not available in the daemon")
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("call service setConfig method: %v", err)
|
return fmt.Errorf("call service setConfig method: %v", err)
|
||||||
}
|
}
|
||||||
@@ -328,11 +289,10 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
|
|||||||
return fmt.Errorf("setup login request: %v", err)
|
return fmt.Errorf("setup login request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
profileID := activeProf.ID.String()
|
loginRequest.ProfileName = &activeProf.Name
|
||||||
loginRequest.ProfileName = &profileID
|
|
||||||
loginRequest.Username = &username
|
loginRequest.Username = &username
|
||||||
|
|
||||||
profileState, err := pm.GetProfileState(activeProf.ID)
|
profileState, err := pm.GetProfileState(activeProf.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
||||||
} else if profileState.Email != "" {
|
} else if profileState.Email != "" {
|
||||||
@@ -369,7 +329,7 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := client.Up(ctx, &proto.UpRequest{
|
if _, err := client.Up(ctx, &proto.UpRequest{
|
||||||
ProfileName: &profileID,
|
ProfileName: &activeProf.Name,
|
||||||
Username: &username,
|
Username: &username,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("call service up method: %v", err)
|
return fmt.Errorf("call service up method: %v", err)
|
||||||
@@ -479,6 +439,10 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro
|
|||||||
req.DisableIpv6 = &disableIPv6
|
req.DisableIpv6 = &disableIPv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(enableLazyConnectionFlag).Changed {
|
||||||
|
req.LazyConnectionEnabled = &lazyConnEnabled
|
||||||
|
}
|
||||||
|
|
||||||
return &req
|
return &req
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +560,9 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil
|
|||||||
ic.DisableIPv6 = &disableIPv6
|
ic.DisableIPv6 = &disableIPv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(enableLazyConnectionFlag).Changed {
|
||||||
|
ic.LazyConnectionEnabled = &lazyConnEnabled
|
||||||
|
}
|
||||||
return &ic, nil
|
return &ic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,6 +678,9 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
|
|||||||
loginRequest.DisableIpv6 = &disableIPv6
|
loginRequest.DisableIpv6 = &disableIPv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(enableLazyConnectionFlag).Changed {
|
||||||
|
loginRequest.LazyConnectionEnabled = &lazyConnEnabled
|
||||||
|
}
|
||||||
return &loginRequest, nil
|
return &loginRequest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ func TestUpDaemon(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sm := profilemanager.ServiceManager{}
|
sm := profilemanager.ServiceManager{}
|
||||||
created, err := sm.AddProfile("test1", currUser.Username)
|
err = sm.AddProfile("test1", currUser.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to add profile: %v", err)
|
t.Fatalf("failed to add profile: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||||
ID: created.ID,
|
Name: "test1",
|
||||||
Username: currUser.Username,
|
Username: currUser.Username,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,13 +12,7 @@ var (
|
|||||||
Short: "Print the NetBird's client application version",
|
Short: "Print the NetBird's client application version",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
out := version.NetbirdVersion()
|
cmd.Println(version.NetbirdVersion())
|
||||||
if version.IsDevelopmentVersion(out) {
|
|
||||||
if commit := version.NetbirdCommit(); commit != "" {
|
|
||||||
out += "-" + commit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmd.Println(out)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
||||||
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
@@ -85,12 +84,6 @@ type Options struct {
|
|||||||
DisableIPv6 bool
|
DisableIPv6 bool
|
||||||
// BlockInbound blocks all inbound connections from peers
|
// BlockInbound blocks all inbound connections from peers
|
||||||
BlockInbound bool
|
BlockInbound bool
|
||||||
// BlockLANAccess blocks the embedded peer from reaching the host's
|
|
||||||
// LAN (RFC 1918, link-local, loopback) when it's used as a routing
|
|
||||||
// peer. Mirrors profilemanager.ConfigInput.BlockLANAccess. Useful
|
|
||||||
// when the embedded client must never act as a stepping stone into
|
|
||||||
// the host's local network (e.g. the proxy's overlay peer).
|
|
||||||
BlockLANAccess bool
|
|
||||||
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
||||||
WireguardPort *int
|
WireguardPort *int
|
||||||
// MTU is the MTU for the tunnel interface.
|
// MTU is the MTU for the tunnel interface.
|
||||||
@@ -101,26 +94,6 @@ type Options struct {
|
|||||||
MTU *uint16
|
MTU *uint16
|
||||||
// DNSLabels defines additional DNS labels configured in the peer.
|
// DNSLabels defines additional DNS labels configured in the peer.
|
||||||
DNSLabels []string
|
DNSLabels []string
|
||||||
// Performance configures the tunnel's buffer pool cap and batch size.
|
|
||||||
Performance Performance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance configures the embedded client's tunnel memory/throughput knobs.
|
|
||||||
//
|
|
||||||
// These settings are process-global: any non-nil field also becomes the
|
|
||||||
// default for Clients constructed by later embed.New calls in the same
|
|
||||||
// process. Nil fields are ignored.
|
|
||||||
type Performance struct {
|
|
||||||
// PreallocatedBuffersPerPool caps the per-tunnel buffer pool. Zero
|
|
||||||
// leaves the pool unbounded. Lower values trade throughput for a
|
|
||||||
// tighter memory ceiling. May also be changed on a running Client via
|
|
||||||
// Client.SetPerformance, provided this field was nonzero at construction.
|
|
||||||
PreallocatedBuffersPerPool *uint32
|
|
||||||
// MaxBatchSize overrides the number of packets the tunnel reads or
|
|
||||||
// writes per syscall, which also bounds eager buffer allocation per
|
|
||||||
// worker. Zero uses the platform default. Applied at construction
|
|
||||||
// only; ignored by Client.SetPerformance.
|
|
||||||
MaxBatchSize *uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCredentials checks that exactly one credential type is provided
|
// validateCredentials checks that exactly one credential type is provided
|
||||||
@@ -202,7 +175,6 @@ func New(opts Options) (*Client, error) {
|
|||||||
DisableClientRoutes: &opts.DisableClientRoutes,
|
DisableClientRoutes: &opts.DisableClientRoutes,
|
||||||
DisableIPv6: &opts.DisableIPv6,
|
DisableIPv6: &opts.DisableIPv6,
|
||||||
BlockInbound: &opts.BlockInbound,
|
BlockInbound: &opts.BlockInbound,
|
||||||
BlockLANAccess: &opts.BlockLANAccess,
|
|
||||||
WireguardPort: opts.WireguardPort,
|
WireguardPort: opts.WireguardPort,
|
||||||
MTU: opts.MTU,
|
MTU: opts.MTU,
|
||||||
DNSLabels: parsedLabels,
|
DNSLabels: parsedLabels,
|
||||||
@@ -220,13 +192,6 @@ func New(opts Options) (*Client, error) {
|
|||||||
config.PrivateKey = opts.PrivateKey
|
config.PrivateKey = opts.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Performance.PreallocatedBuffersPerPool != nil {
|
|
||||||
wgdevice.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool)
|
|
||||||
}
|
|
||||||
if opts.Performance.MaxBatchSize != nil {
|
|
||||||
wgdevice.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
deviceName: opts.DeviceName,
|
deviceName: opts.DeviceName,
|
||||||
setupKey: opts.SetupKey,
|
setupKey: opts.SetupKey,
|
||||||
@@ -279,12 +244,6 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-startCtx.Done():
|
case <-startCtx.Done():
|
||||||
// ConnectClient.Stop now cancels its own run context and waits for the
|
|
||||||
// run loop to tear the engine down, so this cancel() is no longer
|
|
||||||
// required to break the deadlock and could be removed. It is kept as a
|
|
||||||
// defensive belt-and-suspenders: cancelling the parent context first
|
|
||||||
// guarantees the run loop is unblocked even if Stop's contract regresses.
|
|
||||||
cancel()
|
|
||||||
if stopErr := client.Stop(); stopErr != nil {
|
if stopErr := client.Stop(); stopErr != nil {
|
||||||
return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
|
return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
|
||||||
}
|
}
|
||||||
@@ -446,21 +405,6 @@ func (c *Client) Expose(ctx context.Context, req ExposeRequest) (*ExposeSession,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdentityForIP looks up a remote peer by its tunnel IP using the
|
|
||||||
// embedded client's status recorder. Returns the peer's WireGuard public
|
|
||||||
// key and FQDN. ok=false means the IP doesn't belong to an active peer
|
|
||||||
// — offline roster peers are treated as unknown, same as foreign IPs.
|
|
||||||
func (c *Client) IdentityForIP(ip netip.Addr) (pubKey, fqdn string, ok bool) {
|
|
||||||
if !ip.IsValid() || c.recorder == nil {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
state, found := c.recorder.PeerStateByIP(ip.String())
|
|
||||||
if !found {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
return state.PubKey, state.FQDN, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status returns the current status of the client.
|
// Status returns the current status of the client.
|
||||||
func (c *Client) Status() (peer.FullStatus, error) {
|
func (c *Client) Status() (peer.FullStatus, error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -529,25 +473,6 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
|||||||
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPerformance retunes a running Client. Only PreallocatedBuffersPerPool
|
|
||||||
// takes effect, and only when it was nonzero at construction;
|
|
||||||
// MaxBatchSize is construction-only and returns an error if set here.
|
|
||||||
//
|
|
||||||
// Returns ErrClientNotStarted / ErrEngineNotStarted if the Client is not
|
|
||||||
// running yet.
|
|
||||||
func (c *Client) SetPerformance(t Performance) error {
|
|
||||||
if t.MaxBatchSize != nil {
|
|
||||||
return errors.New("MaxBatchSize is construction-only and cannot be changed at runtime")
|
|
||||||
}
|
|
||||||
engine, err := c.getEngine()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return engine.SetPerformance(internal.Performance{
|
|
||||||
PreallocatedBuffersPerPool: t.PreallocatedBuffersPerPool,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartCapture begins capturing packets on this client's tunnel device.
|
// StartCapture begins capturing packets on this client's tunnel device.
|
||||||
// Only one capture can be active at a time; starting a new one stops the previous.
|
// Only one capture can be active at a time; starting a new one stops the previous.
|
||||||
// Call StopCapture (or CaptureSession.Stop) to end it.
|
// Call StopCapture (or CaptureSession.Stop) to end it.
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
package embed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/peers"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
|
||||||
mgmt "github.com/netbirdio/netbird/management/server"
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
|
||||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
|
||||||
"github.com/netbirdio/netbird/management/server/job"
|
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
const testSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
|
||||||
|
|
||||||
// TestClientStartTimeoutRollback reproduces a deadlock between Engine.Start and
|
|
||||||
// Engine.Stop. The signal endpoint accepts gRPC connections but never serves the
|
|
||||||
// SignalExchange service, so Engine.Start parks in WaitStreamConnected while
|
|
||||||
// holding the engine mutex. When the Start context expires, the rollback path
|
|
||||||
// calls ConnectClient.Stop, which must not block forever acquiring that mutex.
|
|
||||||
func TestClientStartTimeoutRollback(t *testing.T) {
|
|
||||||
signalAddr := startBlackholeSignal(t)
|
|
||||||
mgmAddr := startManagement(t, signalAddr)
|
|
||||||
|
|
||||||
wgPort := 0
|
|
||||||
client, err := New(Options{
|
|
||||||
DeviceName: "embed-rollback-test",
|
|
||||||
SetupKey: testSetupKey,
|
|
||||||
ManagementURL: "http://" + mgmAddr,
|
|
||||||
WireguardPort: &wgPort,
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "embed client creation must succeed")
|
|
||||||
|
|
||||||
startCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
startErr := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
startErr <- client.Start(startCtx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-startErr:
|
|
||||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
||||||
case <-time.After(60 * time.Second):
|
|
||||||
t.Fatal("client.Start did not return after its context expired: Engine.Stop deadlocked against Engine.Start waiting for the signal stream")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startBlackholeSignal starts a gRPC server without the SignalExchange service
|
|
||||||
// registered. Connections succeed, but the signal stream can never be
|
|
||||||
// established, which keeps Engine.Start parked in WaitStreamConnected.
|
|
||||||
func startBlackholeSignal(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", "localhost:0")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
s := grpc.NewServer()
|
|
||||||
go func() {
|
|
||||||
if err := s.Serve(lis); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
t.Cleanup(s.Stop)
|
|
||||||
|
|
||||||
return lis.Addr().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startManagement(t *testing.T, signalAddr string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cfg := &config.Config{
|
|
||||||
Stuns: []*config.Host{},
|
|
||||||
TURNConfig: &config.TURNConfig{},
|
|
||||||
Relay: &config.Relay{
|
|
||||||
Addresses: []string{"127.0.0.1:1234"},
|
|
||||||
CredentialsTTL: util.Duration{Duration: time.Hour},
|
|
||||||
Secret: "222222222222222222",
|
|
||||||
},
|
|
||||||
Signal: &config.Host{
|
|
||||||
Proto: "http",
|
|
||||||
URI: signalAddr,
|
|
||||||
},
|
|
||||||
Datadir: t.TempDir(),
|
|
||||||
HttpConfig: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", "localhost:0")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
s := grpc.NewServer()
|
|
||||||
|
|
||||||
testStore, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", cfg.Datadir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(cleanUp)
|
|
||||||
|
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
|
||||||
|
|
||||||
permissionsManager := permissions.NewManager(testStore)
|
|
||||||
peersManager := peers.NewManager(testStore, permissionsManager)
|
|
||||||
jobManager := job.NewJobManager(nil, testStore, peersManager)
|
|
||||||
|
|
||||||
cacheStore, err := nbcache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond, 100)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
iv, err := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
t.Cleanup(ctrl.Finish)
|
|
||||||
settingsMockManager := settings.NewMockManager(ctrl)
|
|
||||||
settingsMockManager.EXPECT().
|
|
||||||
GetSettings(gomock.Any(), gomock.Any(), gomock.Any()).
|
|
||||||
Return(&types.Settings{}, nil).
|
|
||||||
AnyTimes()
|
|
||||||
settingsMockManager.EXPECT().
|
|
||||||
GetExtraSettings(gomock.Any(), gomock.Any()).
|
|
||||||
Return(&types.ExtraSettings{}, nil).
|
|
||||||
AnyTimes()
|
|
||||||
|
|
||||||
groupsManager := groups.NewManagerMock()
|
|
||||||
|
|
||||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
|
||||||
requestBuffer := mgmt.NewAccountRequestBuffer(context.Background(), testStore)
|
|
||||||
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg)
|
|
||||||
accountManager, err := mgmt.BuildManager(context.Background(), cfg, testStore, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
secretsManager, err := nbgrpc.NewTimeBasedAuthSecretsManager(updateManager, cfg.TURNConfig, cfg.Relay, settingsMockManager, groupsManager)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
mgmtServer, err := nbgrpc.NewServer(cfg, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := s.Serve(lis); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
t.Cleanup(s.Stop)
|
|
||||||
|
|
||||||
return lis.Addr().String()
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package iptables
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
@@ -422,17 +421,12 @@ func (m *aclManager) updateState() {
|
|||||||
currentState.Lock()
|
currentState.Lock()
|
||||||
defer currentState.Unlock()
|
defer currentState.Unlock()
|
||||||
|
|
||||||
// Clone the maps so the persisted state holds a private snapshot. The
|
|
||||||
// live maps keep being mutated by subsequent rule operations while the
|
|
||||||
// state manager marshals the state from its periodic-save goroutine.
|
|
||||||
// Sharing them by reference races the two and aborts the process with a
|
|
||||||
// concurrent map iteration and write.
|
|
||||||
if m.v6 {
|
if m.v6 {
|
||||||
currentState.ACLEntries6 = maps.Clone(m.entries)
|
currentState.ACLEntries6 = m.entries
|
||||||
currentState.ACLIPsetStore6 = m.ipsetStore.clone()
|
currentState.ACLIPsetStore6 = m.ipsetStore
|
||||||
} else {
|
} else {
|
||||||
currentState.ACLEntries = maps.Clone(m.entries)
|
currentState.ACLEntries = m.entries
|
||||||
currentState.ACLIPsetStore = m.ipsetStore.clone()
|
currentState.ACLIPsetStore = m.ipsetStore
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.stateManager.UpdateState(currentState); err != nil {
|
if err := m.stateManager.UpdateState(currentState); err != nil {
|
||||||
|
|||||||
199
client/firewall/iptables/dnat_refcount_linux_test.go
Normal file
199
client/firewall/iptables/dnat_refcount_linux_test.go
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
package iptables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func iptRefcountIfaceV4() *iFaceMock {
|
||||||
|
return &iFaceMock{
|
||||||
|
NameFunc: func() string { return "wt-refcount" },
|
||||||
|
AddressFunc: func() wgaddr.Address {
|
||||||
|
return wgaddr.Address{
|
||||||
|
IP: netip.MustParseAddr("10.20.0.1"),
|
||||||
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iptRefcountIfaceDual() *iFaceMock {
|
||||||
|
return &iFaceMock{
|
||||||
|
NameFunc: func() string { return "wt-refcount" },
|
||||||
|
AddressFunc: func() wgaddr.Address {
|
||||||
|
return wgaddr.Address{
|
||||||
|
IP: netip.MustParseAddr("10.20.0.1"),
|
||||||
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
||||||
|
IPv6: netip.MustParseAddr("fd00::1"),
|
||||||
|
IPv6Net: netip.MustParsePrefix("fd00::/64"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIptRefcountManager(t *testing.T, dual bool) *Manager {
|
||||||
|
t.Helper()
|
||||||
|
var ifMock *iFaceMock
|
||||||
|
if dual {
|
||||||
|
ifMock = iptRefcountIfaceDual()
|
||||||
|
} else {
|
||||||
|
ifMock = iptRefcountIfaceV4()
|
||||||
|
}
|
||||||
|
m, err := Create(ifMock, iface.DefaultMTU)
|
||||||
|
require.NoError(t, err, "create manager")
|
||||||
|
require.NoError(t, m.Init(nil), "init manager")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, m.Close(nil), "close manager")
|
||||||
|
})
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func iptDnatV4(port uint16) fw.ForwardRule {
|
||||||
|
return fw.ForwardRule{
|
||||||
|
Protocol: fw.ProtocolTCP,
|
||||||
|
DestinationPort: fw.Port{Values: []uint16{port}},
|
||||||
|
TranslatedAddress: netip.MustParseAddr("10.20.0.2"),
|
||||||
|
TranslatedPort: fw.Port{Values: []uint16{80}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iptDnatV6(port uint16) fw.ForwardRule {
|
||||||
|
return fw.ForwardRule{
|
||||||
|
Protocol: fw.ProtocolTCP,
|
||||||
|
DestinationPort: fw.Port{Values: []uint16{port}},
|
||||||
|
TranslatedAddress: netip.MustParseAddr("fd00::2"),
|
||||||
|
TranslatedPort: fw.Port{Values: []uint16{80}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIptablesDNAT_RefcountBalancedV4 covers a Balanced Add/Delete pair on v4.
|
||||||
|
func TestIptablesDNAT_RefcountBalancedV4(t *testing.T) {
|
||||||
|
m := newIptRefcountManager(t, false)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(iptDnatV4(7081))
|
||||||
|
require.NoError(t, err, "add v4 dnat 1")
|
||||||
|
v4, v6 := state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "v4 refcount after first add")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
|
||||||
|
r2, err := m.AddDNATRule(iptDnatV4(7082))
|
||||||
|
require.NoError(t, err, "add v4 dnat 2")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 2, v4, "v4 refcount after second add")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1))
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "v4 refcount after first delete")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r2))
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount after second delete")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIptablesDNAT_RefcountBalancedV6 checks the v6 path increments v6 only and
|
||||||
|
// decrements back to zero.
|
||||||
|
func TestIptablesDNAT_RefcountBalancedV6(t *testing.T) {
|
||||||
|
m := newIptRefcountManager(t, true)
|
||||||
|
require.NotNil(t, m.router6, "v6 router")
|
||||||
|
require.Same(t, m.router.ipFwdState, m.router6.ipFwdState, "shared state")
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(iptDnatV6(9081))
|
||||||
|
require.NoError(t, err, "add v6 dnat 1")
|
||||||
|
v4, v6 := state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 1, v6, "v6 refcount after first add")
|
||||||
|
|
||||||
|
r2, err := m.AddDNATRule(iptDnatV6(9082))
|
||||||
|
require.NoError(t, err, "add v6 dnat 2")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
||||||
|
require.Equal(t, 2, v6, "v6 refcount after second add")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1))
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
||||||
|
require.Equal(t, 1, v6, "v6 refcount after first delete")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r2))
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount after second delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIptablesDNAT_DuplicateAddNoLeak verifies the duplicate-rule path returns
|
||||||
|
// without bumping the refcount.
|
||||||
|
func TestIptablesDNAT_DuplicateAddNoLeak(t *testing.T) {
|
||||||
|
m := newIptRefcountManager(t, true)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
rule := iptDnatV4(7083)
|
||||||
|
r1, err := m.AddDNATRule(rule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
v4, _ := state.Counts()
|
||||||
|
require.Equal(t, 1, v4)
|
||||||
|
|
||||||
|
_, err = m.AddDNATRule(rule)
|
||||||
|
require.NoError(t, err, "duplicate add")
|
||||||
|
v4, _ = state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "duplicate add must not increment")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1))
|
||||||
|
v4, _ = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "single delete must drop to zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIptablesDNAT_DeleteMissingNoUnderflow verifies Delete on an unknown rule
|
||||||
|
// neither errors nor releases the refcount.
|
||||||
|
func TestIptablesDNAT_DeleteMissingNoUnderflow(t *testing.T) {
|
||||||
|
m := newIptRefcountManager(t, true)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
phantom := iptDnatV4(7099)
|
||||||
|
require.NoError(t, m.DeleteDNATRule(&phantom), "delete missing v4")
|
||||||
|
v4, v6 := state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 0, v6)
|
||||||
|
|
||||||
|
phantom6 := iptDnatV6(9099)
|
||||||
|
require.NoError(t, m.DeleteDNATRule(&phantom6), "delete missing v6")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 0, v6)
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(iptDnatV4(7100))
|
||||||
|
require.NoError(t, err)
|
||||||
|
v4, _ = state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "real add still increments after phantom delete")
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIptablesDNAT_DoubleDeleteNoUnderflow verifies a second Delete on the same
|
||||||
|
// rule is a no-op.
|
||||||
|
func TestIptablesDNAT_DoubleDeleteNoUnderflow(t *testing.T) {
|
||||||
|
m := newIptRefcountManager(t, true)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(iptDnatV6(9083))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, v6 := state.Counts()
|
||||||
|
require.Equal(t, 1, v6)
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "first delete")
|
||||||
|
_, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v6)
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "second delete must be no-op")
|
||||||
|
_, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v6, "double delete must not underflow")
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ func (m *Manager) createIPv6Components(wgIface iFaceMapper, mtu uint16) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Share the same IP forwarding state with the v4 router, since
|
// Share the same IP forwarding state with the v4 router, since
|
||||||
// EnableIPForwarding controls both v4 and v6 sysctls.
|
// Forwarding refcounter is per-family but shared between v4 and v6 routers.
|
||||||
m.router6.ipFwdState = m.router.ipFwdState
|
m.router6.ipFwdState = m.router.ipFwdState
|
||||||
|
|
||||||
m.aclMgr6, err = newAclManager(ip6Client, wgIface)
|
m.aclMgr6, err = newAclManager(ip6Client, wgIface)
|
||||||
@@ -402,17 +402,33 @@ func (m *Manager) SetLogLevel(log.Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) EnableRouting() error {
|
func (m *Manager) EnableRouting() error {
|
||||||
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
|
if err := m.router.ipFwdState.RequestForwarding(false); err != nil {
|
||||||
return fmt.Errorf("enable IP forwarding: %w", err)
|
return fmt.Errorf("enable IPv4 forwarding: %w", err)
|
||||||
|
}
|
||||||
|
// v6 only when the overlay actually has v6.
|
||||||
|
if m.router6 == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := m.router.ipFwdState.RequestForwarding(true); err != nil {
|
||||||
|
if rerr := m.router.ipFwdState.ReleaseForwarding(false); rerr != nil {
|
||||||
|
log.Warnf("rollback v4 forwarding: %v", rerr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("enable IPv6 forwarding: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
|
var merr *multierror.Error
|
||||||
return fmt.Errorf("disable IP forwarding: %w", err)
|
if err := m.router.ipFwdState.ReleaseForwarding(false); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("disable IPv4 forwarding: %w", err))
|
||||||
}
|
}
|
||||||
return nil
|
if m.router6 != nil {
|
||||||
|
if err := m.router.ipFwdState.ReleaseForwarding(true); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("disable IPv6 forwarding: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDNATRule adds a DNAT rule
|
// AddDNATRule adds a DNAT rule
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build privileged
|
|
||||||
|
|
||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package iptables
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -102,7 +101,7 @@ func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint1
|
|||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
v6: iptablesClient.Proto() == iptables.ProtocolIPv6,
|
v6: iptablesClient.Proto() == iptables.ProtocolIPv6,
|
||||||
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ipsetCounter = refcounter.New(
|
r.ipsetCounter = refcounter.New(
|
||||||
@@ -750,17 +749,11 @@ func (r *router) updateState() {
|
|||||||
currentState.Lock()
|
currentState.Lock()
|
||||||
defer currentState.Unlock()
|
defer currentState.Unlock()
|
||||||
|
|
||||||
// Clone the rule map so the persisted state holds a private snapshot. The
|
|
||||||
// live map keeps being mutated by subsequent rule operations while the
|
|
||||||
// state manager marshals the state from its periodic-save goroutine.
|
|
||||||
// Sharing it by reference races the two and aborts the process with a
|
|
||||||
// concurrent map iteration and write. The ipset counter guards itself
|
|
||||||
// during marshaling, so it can be shared directly.
|
|
||||||
if r.v6 {
|
if r.v6 {
|
||||||
currentState.RouteRules6 = maps.Clone(r.rules)
|
currentState.RouteRules6 = r.rules
|
||||||
currentState.RouteIPsetCounter6 = r.ipsetCounter
|
currentState.RouteIPsetCounter6 = r.ipsetCounter
|
||||||
} else {
|
} else {
|
||||||
currentState.RouteRules = maps.Clone(r.rules)
|
currentState.RouteRules = r.rules
|
||||||
currentState.RouteIPsetCounter = r.ipsetCounter
|
currentState.RouteIPsetCounter = r.ipsetCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,10 +763,6 @@ func (r *router) updateState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
||||||
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
||||||
return rule, nil
|
return rule, nil
|
||||||
@@ -848,6 +837,16 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|||||||
r.rules[key] = ruleInfo.rule
|
r.rules[key] = ruleInfo.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.ipFwdState.RequestForwarding(r.v6); err != nil {
|
||||||
|
if rollbackErr := r.rollbackRules(rules); rollbackErr != nil {
|
||||||
|
log.Errorf("rollback failed: %v", rollbackErr)
|
||||||
|
}
|
||||||
|
for key := range rules {
|
||||||
|
delete(r.rules, key)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("enable forwarding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
r.updateState()
|
r.updateState()
|
||||||
return rule, nil
|
return rule, nil
|
||||||
}
|
}
|
||||||
@@ -868,12 +867,15 @@ func (r *router) rollbackRules(rules map[string]ruleInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
||||||
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
|
|
||||||
|
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
|
||||||
|
_, hadSNAT := r.rules[ruleKey+snatSuffix]
|
||||||
|
_, hadFWD := r.rules[ruleKey+fwdSuffix]
|
||||||
|
if !hadDNAT && !hadSNAT && !hadFWD {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
||||||
if err := r.iptablesClient.Delete(tableNat, chainRTRDR, dnatRule...); err != nil {
|
if err := r.iptablesClient.Delete(tableNat, chainRTRDR, dnatRule...); err != nil {
|
||||||
@@ -896,6 +898,10 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
delete(r.rules, ruleKey+fwdSuffix)
|
delete(r.rules, ruleKey+fwdSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.ipFwdState.ReleaseForwarding(r.v6); err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
r.updateState()
|
r.updateState()
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android && privileged
|
//go:build !android
|
||||||
|
|
||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import "encoding/json"
|
||||||
"encoding/json"
|
|
||||||
"maps"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ipList struct {
|
type ipList struct {
|
||||||
ips map[string]struct{}
|
ips map[string]struct{}
|
||||||
@@ -22,14 +19,6 @@ func (s *ipList) addIP(ip string) {
|
|||||||
s.ips[ip] = struct{}{}
|
s.ips[ip] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clone returns a deep copy of the ipList with its own ips map.
|
|
||||||
func (s *ipList) clone() *ipList {
|
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &ipList{ips: maps.Clone(s.ips)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler
|
// MarshalJSON implements json.Marshaler
|
||||||
func (s *ipList) MarshalJSON() ([]byte, error) {
|
func (s *ipList) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(struct {
|
return json.Marshal(struct {
|
||||||
@@ -66,19 +55,6 @@ func newIpsetStore() *ipsetStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clone returns a deep copy of the ipsetStore with its own ipsets map and
|
|
||||||
// independent ipList entries.
|
|
||||||
func (s *ipsetStore) clone() *ipsetStore {
|
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cloned := &ipsetStore{ipsets: make(map[string]*ipList, len(s.ipsets))}
|
|
||||||
for name, list := range s.ipsets {
|
|
||||||
cloned.ipsets[name] = list.clone()
|
|
||||||
}
|
|
||||||
return cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ipsetStore) ipset(ipsetName string) (*ipList, bool) {
|
func (s *ipsetStore) ipset(ipsetName string) (*ipList, bool) {
|
||||||
r, ok := s.ipsets[ipsetName]
|
r, ok := s.ipsets[ipsetName]
|
||||||
return r, ok
|
return r, ok
|
||||||
|
|||||||
208
client/firewall/nftables/dnat_refcount_linux_test.go
Normal file
208
client/firewall/nftables/dnat_refcount_linux_test.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package nftables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nftRefcountIfaceV4() *iFaceMock {
|
||||||
|
return &iFaceMock{
|
||||||
|
NameFunc: func() string { return "wt-refcount" },
|
||||||
|
AddressFunc: func() wgaddr.Address {
|
||||||
|
return wgaddr.Address{
|
||||||
|
IP: netip.MustParseAddr("100.96.0.1"),
|
||||||
|
Network: netip.MustParsePrefix("100.96.0.0/16"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nftRefcountIfaceDual() *iFaceMock {
|
||||||
|
return &iFaceMock{
|
||||||
|
NameFunc: func() string { return "wt-refcount" },
|
||||||
|
AddressFunc: func() wgaddr.Address {
|
||||||
|
return wgaddr.Address{
|
||||||
|
IP: netip.MustParseAddr("100.96.0.1"),
|
||||||
|
Network: netip.MustParsePrefix("100.96.0.0/16"),
|
||||||
|
IPv6: netip.MustParseAddr("fd00::1"),
|
||||||
|
IPv6Net: netip.MustParsePrefix("fd00::/64"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNftRefcountManager(t *testing.T, dual bool) *Manager {
|
||||||
|
t.Helper()
|
||||||
|
if check() != NFTABLES {
|
||||||
|
t.Skip("nftables not supported on this system")
|
||||||
|
}
|
||||||
|
var ifMock *iFaceMock
|
||||||
|
if dual {
|
||||||
|
ifMock = nftRefcountIfaceDual()
|
||||||
|
} else {
|
||||||
|
ifMock = nftRefcountIfaceV4()
|
||||||
|
}
|
||||||
|
m, err := Create(ifMock, iface.DefaultMTU)
|
||||||
|
require.NoError(t, err, "create manager")
|
||||||
|
require.NoError(t, m.Init(nil), "init manager")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, m.Close(nil), "close manager")
|
||||||
|
})
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnatV4(port uint16) fw.ForwardRule {
|
||||||
|
return fw.ForwardRule{
|
||||||
|
Protocol: fw.ProtocolTCP,
|
||||||
|
DestinationPort: fw.Port{Values: []uint16{port}},
|
||||||
|
TranslatedAddress: netip.MustParseAddr("100.96.0.2"),
|
||||||
|
TranslatedPort: fw.Port{Values: []uint16{80}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnatV6(port uint16) fw.ForwardRule {
|
||||||
|
return fw.ForwardRule{
|
||||||
|
Protocol: fw.ProtocolTCP,
|
||||||
|
DestinationPort: fw.Port{Values: []uint16{port}},
|
||||||
|
TranslatedAddress: netip.MustParseAddr("fd00::2"),
|
||||||
|
TranslatedPort: fw.Port{Values: []uint16{80}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNftablesDNAT_RefcountBalancedV4 verifies that Add/Delete pairs leave the
|
||||||
|
// v4 refcount at zero.
|
||||||
|
func TestNftablesDNAT_RefcountBalancedV4(t *testing.T) {
|
||||||
|
m := newNftRefcountManager(t, false)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(dnatV4(8081))
|
||||||
|
require.NoError(t, err, "add v4 dnat 1")
|
||||||
|
v4, v6 := state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "v4 refcount after first add")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
|
||||||
|
r2, err := m.AddDNATRule(dnatV4(8082))
|
||||||
|
require.NoError(t, err, "add v4 dnat 2")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 2, v4, "v4 refcount after second add")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "delete v4 dnat 1")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "v4 refcount after first delete")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r2), "delete v4 dnat 2")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount after second delete")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNftablesDNAT_RefcountBalancedV6 verifies the v6 path increments v6 only
|
||||||
|
// and decrements back to zero on Delete.
|
||||||
|
func TestNftablesDNAT_RefcountBalancedV6(t *testing.T) {
|
||||||
|
m := newNftRefcountManager(t, true)
|
||||||
|
require.NotNil(t, m.router6, "v6 router")
|
||||||
|
require.Same(t, m.router.ipFwdState, m.router6.ipFwdState, "shared state")
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(dnatV6(9091))
|
||||||
|
require.NoError(t, err, "add v6 dnat 1")
|
||||||
|
v4, v6 := state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
||||||
|
require.Equal(t, 1, v6, "v6 refcount after first add")
|
||||||
|
|
||||||
|
r2, err := m.AddDNATRule(dnatV6(9092))
|
||||||
|
require.NoError(t, err, "add v6 dnat 2")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 2, v6, "v6 refcount after second add")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "delete v6 dnat 1")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
||||||
|
require.Equal(t, 1, v6, "v6 refcount after first delete")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r2), "delete v6 dnat 2")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount after second delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNftablesDNAT_DuplicateAddNoLeak verifies that a duplicate Add (same
|
||||||
|
// ForwardRule) does not double-increment the refcount.
|
||||||
|
func TestNftablesDNAT_DuplicateAddNoLeak(t *testing.T) {
|
||||||
|
m := newNftRefcountManager(t, true)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
rule := dnatV4(8083)
|
||||||
|
r1, err := m.AddDNATRule(rule)
|
||||||
|
require.NoError(t, err, "add v4 dnat")
|
||||||
|
v4, _ := state.Counts()
|
||||||
|
require.Equal(t, 1, v4)
|
||||||
|
|
||||||
|
// duplicate add: same rule ID, must be a no-op for the refcount.
|
||||||
|
_, err = m.AddDNATRule(rule)
|
||||||
|
require.NoError(t, err, "duplicate add")
|
||||||
|
v4, _ = state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "duplicate add must not increment")
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "delete v4 dnat")
|
||||||
|
v4, _ = state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "single delete must drop to zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNftablesDNAT_DeleteMissingNoUnderflow verifies deleting a rule that was
|
||||||
|
// never added does not underflow the refcount.
|
||||||
|
func TestNftablesDNAT_DeleteMissingNoUnderflow(t *testing.T) {
|
||||||
|
m := newNftRefcountManager(t, true)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
// Construct a Rule reference for something never added. The router stores
|
||||||
|
// rules by ID(), and DeleteDNATRule looks them up in r.rules; a missing
|
||||||
|
// entry must be a no-op rather than calling Release.
|
||||||
|
phantom := dnatV4(8099)
|
||||||
|
require.NoError(t, m.DeleteDNATRule(&phantom), "delete missing v4 dnat")
|
||||||
|
v4, v6 := state.Counts()
|
||||||
|
require.Equal(t, 0, v4, "v4 refcount unaffected by missing delete")
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unaffected")
|
||||||
|
|
||||||
|
phantom6 := dnatV6(9099)
|
||||||
|
require.NoError(t, m.DeleteDNATRule(&phantom6), "delete missing v6 dnat")
|
||||||
|
v4, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v4)
|
||||||
|
require.Equal(t, 0, v6, "v6 refcount unaffected by missing delete")
|
||||||
|
|
||||||
|
// And after a phantom delete, a real add still results in count=1.
|
||||||
|
r1, err := m.AddDNATRule(dnatV4(8100))
|
||||||
|
require.NoError(t, err, "add v4 dnat after phantom delete")
|
||||||
|
v4, _ = state.Counts()
|
||||||
|
require.Equal(t, 1, v4, "real add still increments after phantom delete")
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNftablesDNAT_DoubleDeleteNoUnderflow verifies that deleting the same rule
|
||||||
|
// twice does not underflow the refcount (the second delete is a no-op).
|
||||||
|
func TestNftablesDNAT_DoubleDeleteNoUnderflow(t *testing.T) {
|
||||||
|
m := newNftRefcountManager(t, true)
|
||||||
|
state := m.router.ipFwdState
|
||||||
|
|
||||||
|
r1, err := m.AddDNATRule(dnatV6(9093))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, v6 := state.Counts()
|
||||||
|
require.Equal(t, 1, v6)
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "first delete")
|
||||||
|
_, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v6)
|
||||||
|
|
||||||
|
require.NoError(t, m.DeleteDNATRule(r1), "second delete must be no-op")
|
||||||
|
_, v6 = state.Counts()
|
||||||
|
require.Equal(t, 0, v6, "double delete must not underflow")
|
||||||
|
}
|
||||||
@@ -105,8 +105,8 @@ func (m *Manager) createIPv6Components(tableName string, wgIface iFaceMapper, mt
|
|||||||
return fmt.Errorf("create v6 router: %w", err)
|
return fmt.Errorf("create v6 router: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Share the same IP forwarding state with the v4 router, since
|
// Share the per-family forwarding refcounter with the v4 router so a v4
|
||||||
// EnableIPForwarding controls both v4 and v6 sysctls.
|
// rule and a v6 rule against the same state machine cooperate cleanly.
|
||||||
m.router6.ipFwdState = m.router.ipFwdState
|
m.router6.ipFwdState = m.router.ipFwdState
|
||||||
|
|
||||||
m.aclManager6, err = newAclManager(workTable6, wgIface, chainNameRoutingFw)
|
m.aclManager6, err = newAclManager(workTable6, wgIface, chainNameRoutingFw)
|
||||||
@@ -530,17 +530,33 @@ func (m *Manager) SetLogLevel(log.Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) EnableRouting() error {
|
func (m *Manager) EnableRouting() error {
|
||||||
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
|
if err := m.router.ipFwdState.RequestForwarding(false); err != nil {
|
||||||
return fmt.Errorf("enable IP forwarding: %w", err)
|
return fmt.Errorf("enable IPv4 forwarding: %w", err)
|
||||||
|
}
|
||||||
|
// v6 only when the overlay actually has v6.
|
||||||
|
if m.router6 == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := m.router.ipFwdState.RequestForwarding(true); err != nil {
|
||||||
|
if rerr := m.router.ipFwdState.ReleaseForwarding(false); rerr != nil {
|
||||||
|
log.Warnf("rollback v4 forwarding: %v", rerr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("enable IPv6 forwarding: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
|
var merr *multierror.Error
|
||||||
return fmt.Errorf("disable IP forwarding: %w", err)
|
if err := m.router.ipFwdState.ReleaseForwarding(false); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("disable IPv4 forwarding: %w", err))
|
||||||
}
|
}
|
||||||
return nil
|
if m.router6 != nil {
|
||||||
|
if err := m.router.ipFwdState.ReleaseForwarding(true); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("disable IPv6 forwarding: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush rule/chain/set operations from the buffer
|
// Flush rule/chain/set operations from the buffer
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build privileged
|
|
||||||
|
|
||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*rou
|
|||||||
rules: make(map[string]*nftables.Rule),
|
rules: make(map[string]*nftables.Rule),
|
||||||
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
|
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
|
||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1550,10 +1550,6 @@ func (r *router) refreshRulesMap() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
||||||
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
||||||
return rule, nil
|
return rule, nil
|
||||||
@@ -1564,7 +1560,18 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|||||||
return nil, fmt.Errorf("convert protocol to number: %w", err)
|
return nil, fmt.Errorf("convert protocol to number: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request forwarding before queueing rules: addDnatRedirect/addDnatMasq
|
||||||
|
// buffer netlink messages on r.conn that the next caller's Flush would
|
||||||
|
// commit if we returned without flushing them ourselves.
|
||||||
|
v6 := r.af.tableFamily == nftables.TableFamilyIPv6
|
||||||
|
if err := r.ipFwdState.RequestForwarding(v6); err != nil {
|
||||||
|
return nil, fmt.Errorf("enable forwarding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.addDnatRedirect(rule, protoNum, ruleKey); err != nil {
|
if err := r.addDnatRedirect(rule, protoNum, ruleKey); err != nil {
|
||||||
|
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
|
||||||
|
log.Warnf("rollback forwarding refcount: %v", rerr)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1576,6 +1583,11 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|||||||
// TODO: find chains with drop policies and add rules there
|
// TODO: find chains with drop policies and add rules there
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
|
||||||
|
log.Warnf("rollback forwarding refcount: %v", rerr)
|
||||||
|
}
|
||||||
|
delete(r.rules, ruleKey+dnatSuffix)
|
||||||
|
delete(r.rules, ruleKey+snatSuffix)
|
||||||
return nil, fmt.Errorf("flush rules: %w", err)
|
return nil, fmt.Errorf("flush rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1778,16 +1790,18 @@ func (r *router) addDnatMasq(rule firewall.ForwardRule, protoNum uint8, ruleKey
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
||||||
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
return fmt.Errorf(refreshRulesMapError, err)
|
return fmt.Errorf(refreshRulesMapError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
|
||||||
|
_, hadSNAT := r.rules[ruleKey+snatSuffix]
|
||||||
|
if !hadDNAT && !hadSNAT {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
var needsFlush bool
|
var needsFlush bool
|
||||||
|
|
||||||
@@ -1824,6 +1838,10 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
delete(r.rules, ruleKey+snatSuffix)
|
delete(r.rules, ruleKey+snatSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.ipFwdState.ReleaseForwarding(r.af.tableFamily == nftables.TableFamilyIPv6); err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android && privileged
|
//go:build !android
|
||||||
|
|
||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
|
|||||||
@@ -362,10 +362,6 @@ func (f *Forwarder) injectICMPv6Reply(id stack.TransportEndpointID, icmpPayload
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if pc := f.endpoint.capture.Load(); pc != nil {
|
|
||||||
(*pc).Offer(fullPacket, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(fullPacket)
|
return len(fullPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type ICEBind struct {
|
|||||||
*wgConn.StdNetBind
|
*wgConn.StdNetBind
|
||||||
|
|
||||||
transportNet transport.Net
|
transportNet transport.Net
|
||||||
|
filterFn udpmux.FilterFn
|
||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
mtu uint16
|
mtu uint16
|
||||||
|
|
||||||
@@ -60,11 +61,12 @@ type ICEBind struct {
|
|||||||
ipv6Conn *net.UDPConn
|
ipv6Conn *net.UDPConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewICEBind(transportNet transport.Net, address wgaddr.Address, mtu uint16) *ICEBind {
|
func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wgaddr.Address, mtu uint16) *ICEBind {
|
||||||
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
||||||
ib := &ICEBind{
|
ib := &ICEBind{
|
||||||
StdNetBind: b,
|
StdNetBind: b,
|
||||||
transportNet: transportNet,
|
transportNet: transportNet,
|
||||||
|
filterFn: filterFn,
|
||||||
address: address,
|
address: address,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
endpoints: make(map[netip.Addr]net.Conn),
|
endpoints: make(map[netip.Addr]net.Conn),
|
||||||
@@ -263,6 +265,7 @@ func (s *ICEBind) createOrUpdateMux() {
|
|||||||
udpmux.UniversalUDPMuxParams{
|
udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: muxConn,
|
UDPConn: muxConn,
|
||||||
Net: s.transportNet,
|
Net: s.transportNet,
|
||||||
|
FilterFn: s.filterFn,
|
||||||
WGAddress: s.address,
|
WGAddress: s.address,
|
||||||
MTU: s.mtu,
|
MTU: s.mtu,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ func setupICEBind(t *testing.T) *ICEBind {
|
|||||||
IP: netip.MustParseAddr("100.64.0.1"),
|
IP: netip.MustParseAddr("100.64.0.1"),
|
||||||
Network: netip.MustParsePrefix("100.64.0.0/10"),
|
Network: netip.MustParsePrefix("100.64.0.0/10"),
|
||||||
}
|
}
|
||||||
return NewICEBind(transportNet, address, 1280)
|
return NewICEBind(transportNet, nil, address, 1280)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDualStackConns(t *testing.T) (*net.UDPConn, *net.UDPConn) {
|
func createDualStackConns(t *testing.T) (*net.UDPConn, *net.UDPConn) {
|
||||||
|
|||||||
@@ -17,15 +17,12 @@ import (
|
|||||||
|
|
||||||
type KernelConfigurer struct {
|
type KernelConfigurer struct {
|
||||||
deviceName string
|
deviceName string
|
||||||
statsCache *statsCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKernelConfigurer(deviceName string) *KernelConfigurer {
|
func NewKernelConfigurer(deviceName string) *KernelConfigurer {
|
||||||
c := &KernelConfigurer{
|
return &KernelConfigurer{
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
}
|
}
|
||||||
c.statsCache = newStatsCache(statsCacheTTL, c.fetchStats)
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *KernelConfigurer) ConfigureInterface(privateKey string, port int) error {
|
func (c *KernelConfigurer) ConfigureInterface(privateKey string, port int) error {
|
||||||
@@ -249,6 +246,12 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// validate if device with name exists
|
||||||
|
_, err = wg.Device(c.deviceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return wg.ConfigureDevice(c.deviceName, config)
|
return wg.ConfigureDevice(c.deviceName, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,14 +300,6 @@ func (c *KernelConfigurer) FullStats() (*Stats, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
|
func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
|
||||||
return c.statsCache.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *KernelConfigurer) LastActivities() map[string]monotime.Time {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *KernelConfigurer) fetchStats() (map[string]WGStats, error) {
|
|
||||||
stats := make(map[string]WGStats)
|
stats := make(map[string]WGStats)
|
||||||
wg, err := wgctrl.New()
|
wg, err := wgctrl.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -331,3 +326,7 @@ func (c *KernelConfigurer) fetchStats() (map[string]WGStats, error) {
|
|||||||
}
|
}
|
||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *KernelConfigurer) LastActivities() map[string]monotime.Time {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package configurer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sync/singleflight"
|
|
||||||
)
|
|
||||||
|
|
||||||
const statsCacheTTL = 1 * time.Second
|
|
||||||
|
|
||||||
type statsCache struct {
|
|
||||||
ttl time.Duration
|
|
||||||
fetch func() (map[string]WGStats, error)
|
|
||||||
|
|
||||||
mu sync.RWMutex
|
|
||||||
value map[string]WGStats
|
|
||||||
expireAt time.Time
|
|
||||||
|
|
||||||
sf singleflight.Group
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStatsCache(ttl time.Duration, fetch func() (map[string]WGStats, error)) *statsCache {
|
|
||||||
return &statsCache{ttl: ttl, fetch: fetch}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *statsCache) get() (map[string]WGStats, error) {
|
|
||||||
c.mu.RLock()
|
|
||||||
if c.value != nil && time.Now().Before(c.expireAt) {
|
|
||||||
value := c.value
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
value, err, _ := c.sf.Do("stats", func() (interface{}, error) {
|
|
||||||
res, err := c.fetch()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
c.value = res
|
|
||||||
c.expireAt = time.Now().Add(c.ttl)
|
|
||||||
c.mu.Unlock()
|
|
||||||
return res, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return value.(map[string]WGStats), nil
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package configurer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStatsCache_CachesWithinTTL(t *testing.T) {
|
|
||||||
var calls atomic.Int64
|
|
||||||
c := newStatsCache(50*time.Millisecond, func() (map[string]WGStats, error) {
|
|
||||||
calls.Add(1)
|
|
||||||
return map[string]WGStats{"p": {}}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
_, err := c.get()
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.Equal(t, int64(1), calls.Load(), "within TTL only one underlying fetch")
|
|
||||||
|
|
||||||
time.Sleep(60 * time.Millisecond)
|
|
||||||
_, err := c.get()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, int64(2), calls.Load(), "after TTL expiry a fresh fetch happens")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatsCache_SingleFlight(t *testing.T) {
|
|
||||||
var calls atomic.Int64
|
|
||||||
release := make(chan struct{})
|
|
||||||
c := newStatsCache(time.Minute, func() (map[string]WGStats, error) {
|
|
||||||
calls.Add(1)
|
|
||||||
<-release
|
|
||||||
return map[string]WGStats{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
const n = 50
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(n)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
_, _ = c.get()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
close(release)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
require.Equal(t, int64(1), calls.Load(), "concurrent misses collapse into one fetch")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatsCache_ErrorNotCached(t *testing.T) {
|
|
||||||
var calls atomic.Int64
|
|
||||||
wantErr := errors.New("dump failed")
|
|
||||||
c := newStatsCache(time.Minute, func() (map[string]WGStats, error) {
|
|
||||||
calls.Add(1)
|
|
||||||
return nil, wantErr
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err := c.get()
|
|
||||||
require.ErrorIs(t, err, wantErr)
|
|
||||||
_, err = c.get()
|
|
||||||
require.ErrorIs(t, err, wantErr)
|
|
||||||
require.Equal(t, int64(2), calls.Load(), "errors are not cached; each call retries")
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,6 @@ type WGUSPConfigurer struct {
|
|||||||
device *device.Device
|
device *device.Device
|
||||||
deviceName string
|
deviceName string
|
||||||
activityRecorder *bind.ActivityRecorder
|
activityRecorder *bind.ActivityRecorder
|
||||||
statsCache *statsCache
|
|
||||||
|
|
||||||
uapiListener net.Listener
|
uapiListener net.Listener
|
||||||
}
|
}
|
||||||
@@ -51,19 +50,16 @@ func NewUSPConfigurer(device *device.Device, deviceName string, activityRecorder
|
|||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
activityRecorder: activityRecorder,
|
activityRecorder: activityRecorder,
|
||||||
}
|
}
|
||||||
wgCfg.statsCache = newStatsCache(statsCacheTTL, wgCfg.fetchStats)
|
|
||||||
wgCfg.startUAPI()
|
wgCfg.startUAPI()
|
||||||
return wgCfg
|
return wgCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUSPConfigurerNoUAPI(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer {
|
func NewUSPConfigurerNoUAPI(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer {
|
||||||
wgCfg := &WGUSPConfigurer{
|
return &WGUSPConfigurer{
|
||||||
device: device,
|
device: device,
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
activityRecorder: activityRecorder,
|
activityRecorder: activityRecorder,
|
||||||
}
|
}
|
||||||
wgCfg.statsCache = newStatsCache(statsCacheTTL, wgCfg.fetchStats)
|
|
||||||
return wgCfg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error {
|
func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error {
|
||||||
@@ -352,10 +348,6 @@ func (t *WGUSPConfigurer) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *WGUSPConfigurer) GetStats() (map[string]WGStats, error) {
|
func (t *WGUSPConfigurer) GetStats() (map[string]WGStats, error) {
|
||||||
return t.statsCache.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *WGUSPConfigurer) fetchStats() (map[string]WGStats, error) {
|
|
||||||
ipc, err := t.device.IpcGet()
|
ipc, err := t.device.IpcGet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ipc get: %w", err)
|
return nil, fmt.Errorf("ipc get: %w", err)
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime/debug"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.zx2c4.com/wireguard/tun"
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,13 +41,10 @@ type PacketCapture interface {
|
|||||||
type FilteredDevice struct {
|
type FilteredDevice struct {
|
||||||
tun.Device
|
tun.Device
|
||||||
|
|
||||||
filter PacketFilter
|
filter PacketFilter
|
||||||
capture atomic.Pointer[PacketCapture]
|
capture atomic.Pointer[PacketCapture]
|
||||||
// panicHandler is invoked after a panic in the underlying device is
|
mutex sync.RWMutex
|
||||||
// recovered in Read or Write.
|
closeOnce sync.Once
|
||||||
panicHandler atomic.Pointer[func()]
|
|
||||||
mutex sync.RWMutex
|
|
||||||
closeOnce sync.Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDeviceFilter constructor function
|
// newDeviceFilter constructor function
|
||||||
@@ -76,7 +70,7 @@ func (d *FilteredDevice) Close() error {
|
|||||||
|
|
||||||
// Read wraps read method with filtering feature
|
// Read wraps read method with filtering feature
|
||||||
func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
||||||
if n, err = d.deviceRead(bufs, sizes, offset); err != nil {
|
if n, err = d.Device.Read(bufs, sizes, offset); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +112,7 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
|||||||
d.mutex.RUnlock()
|
d.mutex.RUnlock()
|
||||||
|
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
return d.deviceWrite(bufs, offset)
|
return d.Device.Write(bufs, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredBufs := make([][]byte, 0, len(bufs))
|
filteredBufs := make([][]byte, 0, len(bufs))
|
||||||
@@ -131,44 +125,9 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := d.deviceWrite(filteredBufs, offset)
|
n, err := d.Device.Write(filteredBufs, offset)
|
||||||
if err != nil {
|
n += dropped
|
||||||
return n, err
|
return n, err
|
||||||
}
|
|
||||||
return n + dropped, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deviceRead calls the underlying device Read, recovering from panics in the
|
|
||||||
// wintun read path and converting them into errors.
|
|
||||||
func (d *FilteredDevice) deviceRead(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
|
||||||
defer d.recoverFromPanic("read", &n, &err)
|
|
||||||
return d.Device.Read(bufs, sizes, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deviceWrite calls the underlying device Write, recovering from panics in the
|
|
||||||
// wintun write path and converting them into errors.
|
|
||||||
func (d *FilteredDevice) deviceWrite(bufs [][]byte, offset int) (n int, err error) {
|
|
||||||
defer d.recoverFromPanic("write", &n, &err)
|
|
||||||
return d.Device.Write(bufs, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// recoverFromPanic converts a panic in the underlying device into a regular
|
|
||||||
// error and invokes the registered panic handler. The wintun read path is
|
|
||||||
// known to panic on zero-length packets that third-party filter drivers can
|
|
||||||
// place in the ring.
|
|
||||||
func (d *FilteredDevice) recoverFromPanic(op string, n *int, err *error) {
|
|
||||||
r := recover()
|
|
||||||
if r == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Errorf("recovered panic in tun device %s: %v\n%s", op, r, debug.Stack())
|
|
||||||
*n = 0
|
|
||||||
*err = fmt.Errorf("tun device %s panic: %v", op, r)
|
|
||||||
|
|
||||||
if handler := d.panicHandler.Load(); handler != nil {
|
|
||||||
(*handler)()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFilter sets packet filter to device
|
// SetFilter sets packet filter to device
|
||||||
@@ -178,17 +137,6 @@ func (d *FilteredDevice) SetFilter(filter PacketFilter) {
|
|||||||
d.mutex.Unlock()
|
d.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPanicHandler registers a handler invoked after a recovered panic in Read
|
|
||||||
// or Write. The device is unusable after such a panic; the handler should
|
|
||||||
// trigger recreation of the interface. Pass nil to remove.
|
|
||||||
func (d *FilteredDevice) SetPanicHandler(handler func()) {
|
|
||||||
if handler == nil {
|
|
||||||
d.panicHandler.Store(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.panicHandler.Store(&handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCapture sets or clears the packet capture sink. Pass nil to disable.
|
// SetCapture sets or clears the packet capture sink. Pass nil to disable.
|
||||||
// Uses atomic store so the hot path (Read/Write) is a single pointer load
|
// Uses atomic store so the hot path (Read/Write) is a single pointer load
|
||||||
// with no locking overhead when capture is off.
|
// with no locking overhead when capture is off.
|
||||||
|
|||||||
@@ -221,60 +221,3 @@ func TestDeviceWrapperRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeviceWrapperReadPanic(t *testing.T) {
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
tun := mocks.NewMockDevice(ctrl)
|
|
||||||
tun.EXPECT().Read(gomock.Any(), gomock.Any(), gomock.Any()).
|
|
||||||
DoAndReturn(func(bufs [][]byte, sizes []int, offset int) (int, error) {
|
|
||||||
// Reproduce the wintun zero-length packet panic (index out of range).
|
|
||||||
packet := make([]byte, 0)
|
|
||||||
return int(packet[0]), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapped := newDeviceFilter(tun)
|
|
||||||
|
|
||||||
handlerCalled := false
|
|
||||||
wrapped.SetPanicHandler(func() { handlerCalled = true })
|
|
||||||
|
|
||||||
n, err := wrapped.Read([][]byte{{}}, []int{0}, 0)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error from recovered panic, got nil")
|
|
||||||
}
|
|
||||||
if n != 0 {
|
|
||||||
t.Errorf("expected n=0, got %d", n)
|
|
||||||
}
|
|
||||||
if !handlerCalled {
|
|
||||||
t.Errorf("expected panic handler to be called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeviceWrapperWritePanic(t *testing.T) {
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
tun := mocks.NewMockDevice(ctrl)
|
|
||||||
tun.EXPECT().Write(gomock.Any(), gomock.Any()).
|
|
||||||
DoAndReturn(func(bufs [][]byte, offset int) (int, error) {
|
|
||||||
packet := make([]byte, 0)
|
|
||||||
return int(packet[0]), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapped := newDeviceFilter(tun)
|
|
||||||
|
|
||||||
handlerCalled := false
|
|
||||||
wrapped.SetPanicHandler(func() { handlerCalled = true })
|
|
||||||
|
|
||||||
n, err := wrapped.Write([][]byte{{0x45, 0x00}}, 0)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error from recovered panic, got nil")
|
|
||||||
}
|
|
||||||
if n != 0 {
|
|
||||||
t.Errorf("expected n=0, got %d", n)
|
|
||||||
}
|
|
||||||
if !handlerCalled {
|
|
||||||
t.Errorf("expected panic handler to be called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type TunKernelDevice struct {
|
|||||||
link *wgLink
|
link *wgLink
|
||||||
udpMuxConn net.PacketConn
|
udpMuxConn net.PacketConn
|
||||||
udpMux *udpmux.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
|
|
||||||
|
filterFn udpmux.FilterFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
||||||
@@ -102,6 +104,7 @@ func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|||||||
bindParams := udpmux.UniversalUDPMuxParams{
|
bindParams := udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: nbnet.WrapPacketConn(rawSock),
|
UDPConn: nbnet.WrapPacketConn(rawSock),
|
||||||
Net: t.transportNet,
|
Net: t.transportNet,
|
||||||
|
FilterFn: t.filterFn,
|
||||||
WGAddress: t.address,
|
WGAddress: t.address,
|
||||||
MTU: t.mtu,
|
MTU: t.mtu,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type WGIFaceOpts struct {
|
|||||||
MTU uint16
|
MTU uint16
|
||||||
MobileArgs *device.MobileIFaceArguments
|
MobileArgs *device.MobileIFaceArguments
|
||||||
TransportNet transport.Net
|
TransportNet transport.Net
|
||||||
|
FilterFn udpmux.FilterFn
|
||||||
DisableDNS bool
|
DisableDNS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
||||||
|
|
||||||
var tun WGTunDevice
|
var tun WGTunDevice
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
||||||
|
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
||||||
|
|
||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunFd),
|
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunFd),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// NewWGIFace Creates a new WireGuard interface instance
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
||||||
return &WGIface{
|
return &WGIface{
|
||||||
tun: device.NewNetstackDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr()),
|
tun: device.NewNetstackDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr()),
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
@@ -30,7 +30,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if device.ModuleTunIsLoaded() {
|
if device.ModuleTunIsLoaded() {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU)
|
||||||
return &WGIface{
|
return &WGIface{
|
||||||
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind),
|
tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind),
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build privileged
|
|
||||||
|
|
||||||
package iface
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -20,6 +22,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FilterFn is a function that filters out candidates based on the address.
|
||||||
|
// If it returns true, the address is to be filtered. It also returns the prefix of matching route.
|
||||||
|
type FilterFn func(address netip.Addr) (bool, netip.Prefix, error)
|
||||||
|
|
||||||
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn
|
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn
|
||||||
// It then passes packets to the UDPMux that does the actual connection muxing.
|
// It then passes packets to the UDPMux that does the actual connection muxing.
|
||||||
type UniversalUDPMuxDefault struct {
|
type UniversalUDPMuxDefault struct {
|
||||||
@@ -37,6 +43,7 @@ type UniversalUDPMuxParams struct {
|
|||||||
UDPConn net.PacketConn
|
UDPConn net.PacketConn
|
||||||
XORMappedAddrCacheTTL time.Duration
|
XORMappedAddrCacheTTL time.Duration
|
||||||
Net transport.Net
|
Net transport.Net
|
||||||
|
FilterFn FilterFn
|
||||||
WGAddress wgaddr.Address
|
WGAddress wgaddr.Address
|
||||||
MTU uint16
|
MTU uint16
|
||||||
}
|
}
|
||||||
@@ -61,6 +68,7 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef
|
|||||||
PacketConn: params.UDPConn,
|
PacketConn: params.UDPConn,
|
||||||
mux: m,
|
mux: m,
|
||||||
logger: params.Logger,
|
logger: params.Logger,
|
||||||
|
filterFn: params.FilterFn,
|
||||||
address: params.WGAddress,
|
address: params.WGAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +115,15 @@ func (m *UniversalUDPMuxDefault) ReadFromConn(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDPConn is a wrapper around UDPMux conn that overrides WriteTo to drop packets destined for the overlay subnet.
|
// UDPConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets
|
||||||
type UDPConn struct {
|
type UDPConn struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
mux *UniversalUDPMuxDefault
|
mux *UniversalUDPMuxDefault
|
||||||
logger logging.LeveledLogger
|
logger logging.LeveledLogger
|
||||||
address wgaddr.Address
|
filterFn FilterFn
|
||||||
|
// TODO: reset cache on route changes
|
||||||
|
addrCache sync.Map
|
||||||
|
address wgaddr.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPacketConn returns the underlying PacketConn
|
// GetPacketConn returns the underlying PacketConn
|
||||||
@@ -121,18 +132,67 @@ func (u *UDPConn) GetPacketConn() net.PacketConn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
udpAddr, ok := addr.(*net.UDPAddr)
|
if u.filterFn == nil {
|
||||||
if !ok {
|
|
||||||
return u.PacketConn.WriteTo(b, addr)
|
return u.PacketConn.WriteTo(b, addr)
|
||||||
}
|
}
|
||||||
dst := udpAddr.AddrPort().Addr().Unmap()
|
|
||||||
if (u.address.Network.IsValid() && u.address.Network.Contains(dst)) || (u.address.IPv6Net.IsValid() && u.address.IPv6Net.Contains(dst)) {
|
if isRouted, found := u.addrCache.Load(addr.String()); found {
|
||||||
log.Warnf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
return u.handleCachedAddress(isRouted.(bool), b, addr)
|
||||||
return 0, fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
}
|
||||||
|
|
||||||
|
return u.handleUncachedAddress(b, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UDPConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) {
|
||||||
|
if isRouted {
|
||||||
|
return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr)
|
||||||
}
|
}
|
||||||
return u.PacketConn.WriteTo(b, addr)
|
return u.PacketConn.WriteTo(b, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UDPConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) {
|
||||||
|
if err := u.performFilterCheck(addr); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return u.PacketConn.WriteTo(b, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UDPConn) performFilterCheck(addr net.Addr) error {
|
||||||
|
host, err := getHostFromAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to get host from address %s: %v", addr, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := netip.ParseAddr(host)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to parse address %s: %v", addr, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.address.Network.Contains(a) {
|
||||||
|
log.Warnf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
|
return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRouted, prefix, err := u.filterFn(a); err != nil {
|
||||||
|
log.Errorf("Failed to check if address %s is routed: %v", addr, err)
|
||||||
|
} else {
|
||||||
|
u.addrCache.Store(addr.String(), isRouted)
|
||||||
|
if isRouted {
|
||||||
|
// Extra log, as the error only shows up with ICE logging enabled
|
||||||
|
log.Infof("address %s is part of routed network %s, refusing to write", addr, prefix)
|
||||||
|
return fmt.Errorf("address %s is part of routed network %s, refusing to write", addr, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostFromAddr(addr net.Addr) (string, error) {
|
||||||
|
host, _, err := net.SplitHostPort(addr.String())
|
||||||
|
return host, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetSharedConn returns the shared udp conn
|
// GetSharedConn returns the shared udp conn
|
||||||
func (m *UniversalUDPMuxDefault) GetSharedConn() net.PacketConn {
|
func (m *UniversalUDPMuxDefault) GetSharedConn() net.PacketConn {
|
||||||
return m.params.UDPConn
|
return m.params.UDPConn
|
||||||
@@ -165,13 +225,6 @@ func (m *UniversalUDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.A
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
src := udpAddr.AddrPort().Addr().Unmap()
|
|
||||||
wg := m.params.WGAddress
|
|
||||||
if (wg.Network.IsValid() && wg.Network.Contains(src)) || (wg.IPv6Net.IsValid() && wg.IPv6Net.Contains(src)) {
|
|
||||||
log.Debugf("dropping STUN message from overlay source %s", udpAddr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.isXORMappedResponse(msg, udpAddr.String()) {
|
if m.isXORMappedResponse(msg, udpAddr.String()) {
|
||||||
err := m.handleXORMappedResponse(udpAddr, msg)
|
err := m.handleXORMappedResponse(udpAddr, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -136,11 +136,6 @@ func (p *ProxyBind) CloseConn() error {
|
|||||||
return p.close()
|
return p.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectPacket is a no-op for the userspace proxy: first-packet reinjection is kernel-only.
|
|
||||||
func (p *ProxyBind) InjectPacket(_ []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyBind) close() error {
|
func (p *ProxyBind) close() error {
|
||||||
if p.remoteConn == nil {
|
if p.remoteConn == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -219,17 +219,6 @@ func (p *ProxyWrapper) RedirectAs(endpoint *net.UDPAddr) {
|
|||||||
p.pausedCond.L.Unlock()
|
p.pausedCond.L.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectPacket writes b to the remote peer over the underlying transport.
|
|
||||||
func (p *ProxyWrapper) InjectPacket(b []byte) error {
|
|
||||||
if p.remoteConn == nil {
|
|
||||||
return errors.New("proxy not started")
|
|
||||||
}
|
|
||||||
if _, err := p.remoteConn.Write(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseConn close the remoteConn and automatically remove the conn instance from the map
|
// CloseConn close the remoteConn and automatically remove the conn instance from the map
|
||||||
func (p *ProxyWrapper) CloseConn() error {
|
func (p *ProxyWrapper) CloseConn() error {
|
||||||
if p.cancel == nil {
|
if p.cancel == nil {
|
||||||
|
|||||||
@@ -18,9 +18,4 @@ type Proxy interface {
|
|||||||
RedirectAs(endpoint *net.UDPAddr)
|
RedirectAs(endpoint *net.UDPAddr)
|
||||||
CloseConn() error
|
CloseConn() error
|
||||||
SetDisconnectListener(disconnected func())
|
SetDisconnectListener(disconnected func())
|
||||||
|
|
||||||
// InjectPacket writes a raw packet directly to the remote peer over the underlying transport,
|
|
||||||
// bypassing WireGuard. Used to replay the captured lazyconn handshake initiation. Only the
|
|
||||||
// kernel-mode proxies act on it; the userspace proxy is a no-op since reinjection is kernel-only.
|
|
||||||
InjectPacket(b []byte) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux && !android && privileged
|
//go:build linux && !android
|
||||||
|
|
||||||
package wgproxy
|
package wgproxy
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ func seedProxyForProxyCloseByRemoteConn() ([]proxyInstance, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
iceBind := bind.NewICEBind(nil, wgAddress, 1280)
|
iceBind := bind.NewICEBind(nil, nil, wgAddress, 1280)
|
||||||
endpointAddress := &net.UDPAddr{
|
endpointAddress := &net.UDPAddr{
|
||||||
IP: net.IPv4(10, 0, 0, 1),
|
IP: net.IPv4(10, 0, 0, 1),
|
||||||
Port: 1234,
|
Port: 1234,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux || !privileged
|
//go:build !linux
|
||||||
|
|
||||||
package wgproxy
|
package wgproxy
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ func seedProxyForProxyCloseByRemoteConn() ([]proxyInstance, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
iceBind := bind.NewICEBind(nil, wgAddress, 1280)
|
iceBind := bind.NewICEBind(nil, nil, wgAddress, 1280)
|
||||||
endpointAddress := &net.UDPAddr{
|
endpointAddress := &net.UDPAddr{
|
||||||
IP: net.IPv4(10, 0, 0, 1),
|
IP: net.IPv4(10, 0, 0, 1),
|
||||||
Port: 1234,
|
Port: 1234,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux && !android && privileged
|
//go:build linux && !android
|
||||||
|
|
||||||
package wgproxy
|
package wgproxy
|
||||||
|
|
||||||
@@ -26,6 +26,64 @@ func compareUDPAddr(addr1, addr2 net.Addr) bool {
|
|||||||
return udpAddr1.IP.Equal(udpAddr2.IP) && udpAddr1.Port == udpAddr2.Port
|
return udpAddr1.IP.Equal(udpAddr2.IP) && udpAddr1.Port == udpAddr2.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRedirectAs_eBPF_IPv4 tests RedirectAs with eBPF proxy using IPv4 addresses
|
||||||
|
func TestRedirectAs_eBPF_IPv4(t *testing.T) {
|
||||||
|
wgPort := 51850
|
||||||
|
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
||||||
|
if err := ebpfProxy.Listen(); err != nil {
|
||||||
|
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := ebpfProxy.Free(); err != nil {
|
||||||
|
t.Errorf("failed to free ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
||||||
|
|
||||||
|
// NetBird UDP address of the remote peer
|
||||||
|
nbAddr := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("100.108.111.177"),
|
||||||
|
Port: 38746,
|
||||||
|
}
|
||||||
|
|
||||||
|
p2pEndpoint := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("192.168.0.56"),
|
||||||
|
Port: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedirectAs_eBPF_IPv6 tests RedirectAs with eBPF proxy using IPv6 addresses
|
||||||
|
func TestRedirectAs_eBPF_IPv6(t *testing.T) {
|
||||||
|
wgPort := 51851
|
||||||
|
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
||||||
|
if err := ebpfProxy.Listen(); err != nil {
|
||||||
|
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := ebpfProxy.Free(); err != nil {
|
||||||
|
t.Errorf("failed to free ebpf proxy: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
||||||
|
|
||||||
|
// NetBird UDP address of the remote peer
|
||||||
|
nbAddr := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("100.108.111.177"),
|
||||||
|
Port: 38746,
|
||||||
|
}
|
||||||
|
|
||||||
|
p2pEndpoint := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("fe80::56"),
|
||||||
|
Port: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
// TestRedirectAs_UDP_IPv4 tests RedirectAs with UDP proxy using IPv4 addresses
|
// TestRedirectAs_UDP_IPv4 tests RedirectAs with UDP proxy using IPv4 addresses
|
||||||
func TestRedirectAs_UDP_IPv4(t *testing.T) {
|
func TestRedirectAs_UDP_IPv4(t *testing.T) {
|
||||||
wgPort := 51852
|
wgPort := 51852
|
||||||
@@ -198,64 +256,6 @@ func testRedirectAs(t *testing.T, proxy Proxy, wgPort int, nbAddr, p2pEndpoint *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRedirectAs_eBPF_IPv4 tests RedirectAs with eBPF proxy using IPv4 addresses
|
|
||||||
func TestRedirectAs_eBPF_IPv4(t *testing.T) {
|
|
||||||
wgPort := 51850
|
|
||||||
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
|
||||||
if err := ebpfProxy.Listen(); err != nil {
|
|
||||||
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := ebpfProxy.Free(); err != nil {
|
|
||||||
t.Errorf("failed to free ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
|
||||||
|
|
||||||
// NetBird UDP address of the remote peer
|
|
||||||
nbAddr := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("100.108.111.177"),
|
|
||||||
Port: 38746,
|
|
||||||
}
|
|
||||||
|
|
||||||
p2pEndpoint := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("192.168.0.56"),
|
|
||||||
Port: 51820,
|
|
||||||
}
|
|
||||||
|
|
||||||
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedirectAs_eBPF_IPv6 tests RedirectAs with eBPF proxy using IPv6 addresses
|
|
||||||
func TestRedirectAs_eBPF_IPv6(t *testing.T) {
|
|
||||||
wgPort := 51851
|
|
||||||
ebpfProxy := ebpf.NewWGEBPFProxy(wgPort, 1280)
|
|
||||||
if err := ebpfProxy.Listen(); err != nil {
|
|
||||||
t.Fatalf("failed to initialize ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := ebpfProxy.Free(); err != nil {
|
|
||||||
t.Errorf("failed to free ebpf proxy: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
proxy := ebpf.NewProxyWrapper(ebpfProxy)
|
|
||||||
|
|
||||||
// NetBird UDP address of the remote peer
|
|
||||||
nbAddr := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("100.108.111.177"),
|
|
||||||
Port: 38746,
|
|
||||||
}
|
|
||||||
|
|
||||||
p2pEndpoint := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP("fe80::56"),
|
|
||||||
Port: 51820,
|
|
||||||
}
|
|
||||||
|
|
||||||
testRedirectAs(t, proxy, wgPort, nbAddr, p2pEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedirectAs_Multiple_Switches tests switching between multiple endpoints
|
// TestRedirectAs_Multiple_Switches tests switching between multiple endpoints
|
||||||
func TestRedirectAs_Multiple_Switches(t *testing.T) {
|
func TestRedirectAs_Multiple_Switches(t *testing.T) {
|
||||||
wgPort := 51856
|
wgPort := 51856
|
||||||
|
|||||||
@@ -147,17 +147,6 @@ func (p *WGUDPProxy) RedirectAs(endpoint *net.UDPAddr) {
|
|||||||
p.sendPkg = p.srcFakerConn.SendPkg
|
p.sendPkg = p.srcFakerConn.SendPkg
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectPacket writes b to the remote peer over the underlying transport.
|
|
||||||
func (p *WGUDPProxy) InjectPacket(b []byte) error {
|
|
||||||
if p.remoteConn == nil {
|
|
||||||
return errors.New("proxy not started")
|
|
||||||
}
|
|
||||||
if _, err := p.remoteConn.Write(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseConn close the localConn
|
// CloseConn close the localConn
|
||||||
func (p *WGUDPProxy) CloseConn() error {
|
func (p *WGUDPProxy) CloseConn() error {
|
||||||
if p.cancel == nil {
|
if p.cancel == nil {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/mitchellh/hashstructure/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
@@ -31,13 +30,11 @@ type Manager interface {
|
|||||||
|
|
||||||
// DefaultManager uses firewall manager to handle
|
// DefaultManager uses firewall manager to handle
|
||||||
type DefaultManager struct {
|
type DefaultManager struct {
|
||||||
firewall firewall.Manager
|
firewall firewall.Manager
|
||||||
ipsetCounter int
|
ipsetCounter int
|
||||||
peerRulesPairs map[id.RuleID][]firewall.Rule
|
peerRulesPairs map[id.RuleID][]firewall.Rule
|
||||||
routeRules map[id.RuleID]struct{}
|
routeRules map[id.RuleID]struct{}
|
||||||
previousConfigHash uint64
|
mutex sync.Mutex
|
||||||
hasAppliedConfig bool
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultManager(fm firewall.Manager) *DefaultManager {
|
func NewDefaultManager(fm firewall.Manager) *DefaultManager {
|
||||||
@@ -60,23 +57,6 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRout
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the full rebuild + flush when the inputs that drive the firewall
|
|
||||||
// state are byte-for-byte identical to the last successfully applied
|
|
||||||
// update. Management re-sends the same network map far more often than it
|
|
||||||
// actually changes (account-wide updates, peer meta churn), and rebuilding
|
|
||||||
// every peer/route ACL and flushing the firewall on every such sync is the
|
|
||||||
// dominant client-side cost when nothing changed. Mirrors the same guard the
|
|
||||||
// DNS server already uses (previousConfigHash). Only the fields ApplyFiltering
|
|
||||||
// consumes participate in the hash, so an unrelated map change cannot mask a
|
|
||||||
// real ACL change.
|
|
||||||
hash, err := d.firewallConfigHash(networkMap, dnsRouteFeatureFlag)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("unable to hash firewall configuration, applying unconditionally: %v", err)
|
|
||||||
} else if d.hasAppliedConfig && d.previousConfigHash == hash {
|
|
||||||
log.Debugf("not applying the firewall configuration update as there is nothing new (hash: %d)", hash)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
total := 0
|
total := 0
|
||||||
@@ -90,49 +70,13 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRout
|
|||||||
|
|
||||||
d.applyPeerACLs(networkMap)
|
d.applyPeerACLs(networkMap)
|
||||||
|
|
||||||
routeErr := d.applyRouteACLs(networkMap.RoutesFirewallRules, dnsRouteFeatureFlag)
|
if err := d.applyRouteACLs(networkMap.RoutesFirewallRules, dnsRouteFeatureFlag); err != nil {
|
||||||
if routeErr != nil {
|
log.Errorf("Failed to apply route ACLs: %v", err)
|
||||||
log.Errorf("Failed to apply route ACLs: %v", routeErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flushErr := d.firewall.Flush()
|
if err := d.firewall.Flush(); err != nil {
|
||||||
if flushErr != nil {
|
log.Error("failed to flush firewall rules: ", err)
|
||||||
log.Error("failed to flush firewall rules: ", flushErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only remember the hash once the firewall actually reflects this config.
|
|
||||||
// If applying or flushing failed, leave the previous hash untouched so the
|
|
||||||
// next (possibly identical) update is not skipped and gets a chance to
|
|
||||||
// reconcile the firewall state.
|
|
||||||
if err == nil && routeErr == nil && flushErr == nil {
|
|
||||||
d.previousConfigHash = hash
|
|
||||||
d.hasAppliedConfig = true
|
|
||||||
} else {
|
|
||||||
d.hasAppliedConfig = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// firewallConfigHash hashes exactly the inputs ApplyFiltering uses to build the
|
|
||||||
// firewall state, so an identical hash means an identical resulting ruleset.
|
|
||||||
func (d *DefaultManager) firewallConfigHash(networkMap *mgmProto.NetworkMap, dnsRouteFeatureFlag bool) (uint64, error) {
|
|
||||||
return hashstructure.Hash(struct {
|
|
||||||
PeerRules []*mgmProto.FirewallRule
|
|
||||||
PeerRulesIsEmpty bool
|
|
||||||
RouteRules []*mgmProto.RouteFirewallRule
|
|
||||||
RouteRulesIsEmpty bool
|
|
||||||
DNSRouteFeatureFlag bool
|
|
||||||
}{
|
|
||||||
PeerRules: networkMap.GetFirewallRules(),
|
|
||||||
PeerRulesIsEmpty: networkMap.GetFirewallRulesIsEmpty(),
|
|
||||||
RouteRules: networkMap.GetRoutesFirewallRules(),
|
|
||||||
RouteRulesIsEmpty: networkMap.GetRoutesFirewallRulesIsEmpty(),
|
|
||||||
DNSRouteFeatureFlag: dnsRouteFeatureFlag,
|
|
||||||
}, hashstructure.FormatV2, &hashstructure.HashOptions{
|
|
||||||
ZeroNil: true,
|
|
||||||
IgnoreZeroValue: true,
|
|
||||||
SlicesAsSets: true,
|
|
||||||
UseStringer: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
|
func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -486,149 +485,3 @@ func TestPortInfoEmpty(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestApplyFilteringSkipsUnchangedConfig verifies that an identical network map
|
|
||||||
// re-applied is recognized as a no-op (hash unchanged), while a real change to
|
|
||||||
// any firewall-relevant input forces a re-apply (hash changes). This is the
|
|
||||||
// guard that prevents a full ruleset rebuild + flush on every redundant sync.
|
|
||||||
func TestApplyFilteringSkipsUnchangedConfig(t *testing.T) {
|
|
||||||
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
|
||||||
t.Setenv(firewall.EnvForceUserspaceFirewall, "true")
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
|
||||||
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
|
||||||
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
|
||||||
network := netip.MustParsePrefix("172.0.0.1/32")
|
|
||||||
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
|
||||||
ifaceMock.EXPECT().Address().Return(wgaddr.Address{
|
|
||||||
IP: network.Addr(),
|
|
||||||
Network: network,
|
|
||||||
}).AnyTimes()
|
|
||||||
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
|
||||||
|
|
||||||
fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, fw.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
acl := NewDefaultManager(fw)
|
|
||||||
|
|
||||||
networkMap := &mgmProto.NetworkMap{
|
|
||||||
FirewallRules: []*mgmProto.FirewallRule{
|
|
||||||
{
|
|
||||||
PeerIP: "10.93.0.1",
|
|
||||||
Direction: mgmProto.RuleDirection_IN,
|
|
||||||
Action: mgmProto.RuleAction_ACCEPT,
|
|
||||||
Protocol: mgmProto.RuleProtocol_TCP,
|
|
||||||
Port: "22",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FirewallRulesIsEmpty: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
acl.ApplyFiltering(networkMap, false)
|
|
||||||
require.True(t, acl.hasAppliedConfig, "config should be marked applied after first apply")
|
|
||||||
firstHash := acl.previousConfigHash
|
|
||||||
require.NotZero(t, firstHash)
|
|
||||||
|
|
||||||
// Re-applying the identical map must not change the recorded hash: the
|
|
||||||
// expensive rebuild path was skipped.
|
|
||||||
acl.ApplyFiltering(networkMap, false)
|
|
||||||
assert.Equal(t, firstHash, acl.previousConfigHash,
|
|
||||||
"identical re-apply must be a no-op (hash unchanged)")
|
|
||||||
|
|
||||||
// A real change must produce a different hash and re-apply.
|
|
||||||
networkMap.FirewallRules[0].Action = mgmProto.RuleAction_DROP
|
|
||||||
acl.ApplyFiltering(networkMap, false)
|
|
||||||
assert.NotEqual(t, firstHash, acl.previousConfigHash,
|
|
||||||
"changing a rule's action must force a re-apply (hash changed)")
|
|
||||||
|
|
||||||
// The dnsRouteFeatureFlag also participates in the hash.
|
|
||||||
changedHash := acl.previousConfigHash
|
|
||||||
acl.ApplyFiltering(networkMap, true)
|
|
||||||
assert.NotEqual(t, changedHash, acl.previousConfigHash,
|
|
||||||
"flipping dnsRouteFeatureFlag must force a re-apply (hash changed)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildNetworkMap(peerRules, routeRules int) *mgmProto.NetworkMap {
|
|
||||||
nm := &mgmProto.NetworkMap{
|
|
||||||
FirewallRulesIsEmpty: peerRules == 0,
|
|
||||||
RoutesFirewallRulesIsEmpty: routeRules == 0,
|
|
||||||
}
|
|
||||||
for i := range peerRules {
|
|
||||||
nm.FirewallRules = append(nm.FirewallRules, &mgmProto.FirewallRule{
|
|
||||||
PeerIP: fmt.Sprintf("10.%d.%d.%d", i>>16&0xff, i>>8&0xff, i&0xff),
|
|
||||||
Direction: mgmProto.RuleDirection_IN,
|
|
||||||
Action: mgmProto.RuleAction_ACCEPT,
|
|
||||||
Protocol: mgmProto.RuleProtocol_TCP,
|
|
||||||
Port: fmt.Sprintf("%d", 1024+i%64511),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for i := range routeRules {
|
|
||||||
nm.RoutesFirewallRules = append(nm.RoutesFirewallRules, &mgmProto.RouteFirewallRule{
|
|
||||||
Destination: fmt.Sprintf("192.168.%d.0/24", i%256),
|
|
||||||
SourceRanges: []string{fmt.Sprintf("10.0.%d.0/24", i%256)},
|
|
||||||
Action: mgmProto.RuleAction_ACCEPT,
|
|
||||||
Protocol: mgmProto.RuleProtocol_ALL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nm
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkFirewallConfigHash_Small(b *testing.B) {
|
|
||||||
d := &DefaultManager{}
|
|
||||||
nm := buildNetworkMap(10, 5)
|
|
||||||
b.ResetTimer()
|
|
||||||
for b.Loop() {
|
|
||||||
_, _ = d.firewallConfigHash(nm, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkFirewallConfigHash_Medium(b *testing.B) {
|
|
||||||
d := &DefaultManager{}
|
|
||||||
nm := buildNetworkMap(100, 50)
|
|
||||||
b.ResetTimer()
|
|
||||||
for b.Loop() {
|
|
||||||
_, _ = d.firewallConfigHash(nm, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkFirewallConfigHash_Large(b *testing.B) {
|
|
||||||
d := &DefaultManager{}
|
|
||||||
nm := buildNetworkMap(1000, 200)
|
|
||||||
b.ResetTimer()
|
|
||||||
for b.Loop() {
|
|
||||||
_, _ = d.firewallConfigHash(nm, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFirewallConfigHashDeterministic verifies the hash is stable for equal
|
|
||||||
// inputs and order-independent for the rule slices (management does not
|
|
||||||
// guarantee rule order).
|
|
||||||
func TestFirewallConfigHashDeterministic(t *testing.T) {
|
|
||||||
d := &DefaultManager{}
|
|
||||||
|
|
||||||
nm1 := &mgmProto.NetworkMap{
|
|
||||||
FirewallRules: []*mgmProto.FirewallRule{
|
|
||||||
{PeerIP: "10.0.0.1", Direction: mgmProto.RuleDirection_IN, Action: mgmProto.RuleAction_ACCEPT, Protocol: mgmProto.RuleProtocol_TCP, Port: "22"},
|
|
||||||
{PeerIP: "10.0.0.2", Direction: mgmProto.RuleDirection_IN, Action: mgmProto.RuleAction_DROP, Protocol: mgmProto.RuleProtocol_TCP, Port: "80"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Same rules, reversed order.
|
|
||||||
nm2 := &mgmProto.NetworkMap{
|
|
||||||
FirewallRules: []*mgmProto.FirewallRule{
|
|
||||||
nm1.FirewallRules[1],
|
|
||||||
nm1.FirewallRules[0],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, err := d.firewallConfigHash(nm1, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
h2, err := d.firewallConfigHash(nm2, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, h1, h2, "hash must be order-independent for rule slices")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ func (a *Auth) setSystemInfoFlags(info *system.Info) {
|
|||||||
a.config.BlockLANAccess,
|
a.config.BlockLANAccess,
|
||||||
a.config.BlockInbound,
|
a.config.BlockInbound,
|
||||||
a.config.DisableIPv6,
|
a.config.DisableIPv6,
|
||||||
|
a.config.LazyConnectionEnabled,
|
||||||
a.config.EnableSSHRoot,
|
a.config.EnableSSHRoot,
|
||||||
a.config.EnableSSHSFTP,
|
a.config.EnableSSHSFTP,
|
||||||
a.config.EnableSSHLocalPortForwarding,
|
a.config.EnableSSHLocalPortForwarding,
|
||||||
|
|||||||
@@ -360,13 +360,7 @@ func isRedirectURLPortUsed(redirectURL string, excludedRanges []excludedPortRang
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// FreeBSD 15 disables connecting to INADDR_ANY (0.0.0.0) as a localhost
|
addr := fmt.Sprintf(":%s", port)
|
||||||
// alias by default, ensure explicit ip for localhost.
|
|
||||||
host := parsedURL.Hostname()
|
|
||||||
if host == "" {
|
|
||||||
host = "127.0.0.1"
|
|
||||||
}
|
|
||||||
addr := net.JoinHostPort(host, port)
|
|
||||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -16,16 +16,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
// lazyForce is the resolved local decision for lazy connections, layered above the
|
|
||||||
// management feature flag. lazyForceNone defers to management.
|
|
||||||
type lazyForce int
|
|
||||||
|
|
||||||
const (
|
|
||||||
lazyForceNone lazyForce = iota
|
|
||||||
lazyForceOn
|
|
||||||
lazyForceOff
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections.
|
// ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections.
|
||||||
//
|
//
|
||||||
// The connection manager is responsible for:
|
// The connection manager is responsible for:
|
||||||
@@ -38,7 +28,7 @@ type ConnMgr struct {
|
|||||||
peerStore *peerstore.Store
|
peerStore *peerstore.Store
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
iface lazyconn.WGIface
|
iface lazyconn.WGIface
|
||||||
force lazyForce
|
enabledLocally bool
|
||||||
rosenpassEnabled bool
|
rosenpassEnabled bool
|
||||||
|
|
||||||
lazyConnMgr *manager.Manager
|
lazyConnMgr *manager.Manager
|
||||||
@@ -53,34 +43,28 @@ func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerSto
|
|||||||
peerStore: peerStore,
|
peerStore: peerStore,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
iface: iface,
|
iface: iface,
|
||||||
force: resolveLazyForce(engineConfig.LazyConnection),
|
|
||||||
rosenpassEnabled: engineConfig.RosenpassEnabled,
|
rosenpassEnabled: engineConfig.RosenpassEnabled,
|
||||||
}
|
}
|
||||||
|
if engineConfig.LazyConnectionEnabled || lazyconn.IsLazyConnEnabledByEnv() {
|
||||||
|
e.enabledLocally = true
|
||||||
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start initializes the connection manager. It starts the lazy connection manager when a
|
// Start initializes the connection manager and starts the lazy connection manager if enabled by env var or cmd line option.
|
||||||
// local override forces it on; with no local override it waits for the management feature flag.
|
|
||||||
func (e *ConnMgr) Start(ctx context.Context) {
|
func (e *ConnMgr) Start(ctx context.Context) {
|
||||||
if e.lazyConnMgr != nil {
|
if e.lazyConnMgr != nil {
|
||||||
log.Errorf("lazy connection manager is already started")
|
log.Errorf("lazy connection manager is already started")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch e.force {
|
if !e.enabledLocally {
|
||||||
case lazyForceOff:
|
log.Infof("lazy connection manager is disabled")
|
||||||
log.Infof("lazy connection manager is disabled by local override (%s or MDM policy)", lazyconn.EnvLazyConn)
|
|
||||||
e.statusRecorder.UpdateLazyConnection(false)
|
|
||||||
return
|
|
||||||
case lazyForceNone:
|
|
||||||
log.Infof("lazy connection manager is managed by the management feature flag")
|
|
||||||
e.statusRecorder.UpdateLazyConnection(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.rosenpassEnabled {
|
if e.rosenpassEnabled {
|
||||||
log.Warnf("rosenpass connection manager is enabled, lazy connection manager will not be started")
|
log.Warnf("rosenpass connection manager is enabled, lazy connection manager will not be started")
|
||||||
e.statusRecorder.UpdateLazyConnection(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +76,8 @@ func (e *ConnMgr) Start(ctx context.Context) {
|
|||||||
// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again.
|
// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again.
|
||||||
// If disabled, then it closes the lazy connection manager and open the connections to all peers.
|
// If disabled, then it closes the lazy connection manager and open the connections to all peers.
|
||||||
func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error {
|
func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error {
|
||||||
// a local override (NB_LAZY_CONN or local config) takes precedence over management
|
// do not disable lazy connection manager if it was enabled by env var
|
||||||
if e.force != lazyForceNone {
|
if e.enabledLocally {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +89,6 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er
|
|||||||
|
|
||||||
if e.rosenpassEnabled {
|
if e.rosenpassEnabled {
|
||||||
log.Infof("rosenpass connection manager is enabled, lazy connection manager will not be started")
|
log.Infof("rosenpass connection manager is enabled, lazy connection manager will not be started")
|
||||||
e.statusRecorder.UpdateLazyConnection(false)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +98,6 @@ func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) er
|
|||||||
return e.addPeersToLazyConnManager()
|
return e.addPeersToLazyConnManager()
|
||||||
} else {
|
} else {
|
||||||
if e.lazyConnMgr == nil {
|
if e.lazyConnMgr == nil {
|
||||||
e.statusRecorder.UpdateLazyConnection(false)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Infof("lazy connection manager is disabled by management feature flag")
|
log.Infof("lazy connection manager is disabled by management feature flag")
|
||||||
@@ -327,25 +309,6 @@ func (e *ConnMgr) isStartedWithLazyMgr() bool {
|
|||||||
return e.lazyConnMgr != nil && e.lazyCtxCancel != nil
|
return e.lazyConnMgr != nil && e.lazyCtxCancel != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveLazyForce determines the local override. NB_LAZY_CONN takes precedence; when it
|
|
||||||
// is unset the MDM policy override (mdmState) applies. Either wins in both directions over
|
|
||||||
// the management feature flag; StateUnset for both defers to management.
|
|
||||||
func resolveLazyForce(mdmState lazyconn.State) lazyForce {
|
|
||||||
state := lazyconn.EnvState()
|
|
||||||
if state == lazyconn.StateUnset {
|
|
||||||
state = mdmState
|
|
||||||
}
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case lazyconn.StateOn:
|
|
||||||
return lazyForceOn
|
|
||||||
case lazyconn.StateOff:
|
|
||||||
return lazyForceOff
|
|
||||||
default:
|
|
||||||
return lazyForceNone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func inactivityThresholdEnv() *time.Duration {
|
func inactivityThresholdEnv() *time.Duration {
|
||||||
envValue := os.Getenv(lazyconn.EnvInactivityThreshold)
|
envValue := os.Getenv(lazyconn.EnvInactivityThreshold)
|
||||||
if envValue == "" {
|
if envValue == "" {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResolveLazyForce(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
env string
|
|
||||||
envSet bool
|
|
||||||
mdm lazyconn.State
|
|
||||||
want lazyForce
|
|
||||||
}{
|
|
||||||
{name: "env unset, mdm unset -> defer to management", mdm: lazyconn.StateUnset, want: lazyForceNone},
|
|
||||||
{name: "env on -> force on", env: "on", envSet: true, mdm: lazyconn.StateUnset, want: lazyForceOn},
|
|
||||||
{name: "env off -> force off", env: "off", envSet: true, mdm: lazyconn.StateUnset, want: lazyForceOff},
|
|
||||||
{name: "env unset, mdm on -> force on", mdm: lazyconn.StateOn, want: lazyForceOn},
|
|
||||||
{name: "env unset, mdm off -> force off", mdm: lazyconn.StateOff, want: lazyForceOff},
|
|
||||||
{name: "env on beats mdm off", env: "on", envSet: true, mdm: lazyconn.StateOff, want: lazyForceOn},
|
|
||||||
{name: "env off beats mdm on", env: "off", envSet: true, mdm: lazyconn.StateOn, want: lazyForceOff},
|
|
||||||
{name: "unrecognized env, mdm on -> mdm wins", env: "auto", envSet: true, mdm: lazyconn.StateOn, want: lazyForceOn},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Setenv(lazyconn.EnvLazyConn, tt.env)
|
|
||||||
if !tt.envSet {
|
|
||||||
os.Unsetenv(lazyconn.EnvLazyConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := resolveLazyForce(tt.mdm); got != tt.want {
|
|
||||||
t.Fatalf("resolveLazyForce(%v) = %v, want %v", tt.mdm, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
@@ -27,7 +25,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/listener"
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/metrics"
|
"github.com/netbirdio/netbird/client/internal/metrics"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
@@ -56,10 +53,6 @@ var androidRunOverride func(c *ConnectClient, runningChan chan struct{}, logPath
|
|||||||
|
|
||||||
type ConnectClient struct {
|
type ConnectClient struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
runCancel context.CancelFunc
|
|
||||||
runExited chan struct{}
|
|
||||||
runOnce sync.Once
|
|
||||||
runStarted atomic.Bool
|
|
||||||
config *profilemanager.Config
|
config *profilemanager.Config
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
|
|
||||||
@@ -76,14 +69,8 @@ func NewConnectClient(
|
|||||||
config *profilemanager.Config,
|
config *profilemanager.Config,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
) *ConnectClient {
|
) *ConnectClient {
|
||||||
// Derive the run context here so Stop owns the cancel that unblocks the run
|
|
||||||
// loop. runCancel is set once at construction, so Stop can call it without
|
|
||||||
// racing the run loop's startup. Callers therefore need not cancel before Stop.
|
|
||||||
runCtx, runCancel := context.WithCancel(ctx)
|
|
||||||
return &ConnectClient{
|
return &ConnectClient{
|
||||||
ctx: runCtx,
|
ctx: ctx,
|
||||||
runCancel: runCancel,
|
|
||||||
runExited: make(chan struct{}),
|
|
||||||
config: config,
|
config: config,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
engineMutex: sync.Mutex{},
|
engineMutex: sync.Mutex{},
|
||||||
@@ -130,8 +117,6 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
stateFilePath string,
|
stateFilePath string,
|
||||||
cacheDir string,
|
|
||||||
logFilePath string,
|
|
||||||
) error {
|
) error {
|
||||||
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
debug.SetGCPercent(5)
|
debug.SetGCPercent(5)
|
||||||
@@ -141,17 +126,11 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
StateFilePath: stateFilePath,
|
StateFilePath: stateFilePath,
|
||||||
TempDir: cacheDir,
|
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, logFilePath)
|
return c.run(mobileDependency, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan struct{}, logPath string) error {
|
func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan struct{}, logPath string) error {
|
||||||
// Mark the loop as started and signal exit on return so Stop can wait for
|
|
||||||
// the loop to finish (and skip the wait if the loop never ran).
|
|
||||||
c.runStarted.Store(true)
|
|
||||||
defer c.runOnce.Do(func() { close(c.runExited) })
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
rec := c.statusRecorder
|
rec := c.statusRecorder
|
||||||
@@ -307,7 +286,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
log.Debug(err)
|
log.Debug(err)
|
||||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||||
state.Set(StatusNeedsLogin)
|
state.Set(StatusNeedsLogin)
|
||||||
c.runCancel()
|
_ = c.Stop()
|
||||||
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
||||||
}
|
}
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
@@ -315,10 +294,6 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.clientMetrics.RecordLoginDuration(engineCtx, time.Since(loginStarted), true)
|
c.clientMetrics.RecordLoginDuration(engineCtx, time.Since(loginStarted), true)
|
||||||
c.statusRecorder.MarkManagementConnected()
|
c.statusRecorder.MarkManagementConnected()
|
||||||
|
|
||||||
if metricsConfig := loginResp.GetNetbirdConfig().GetMetrics(); metricsConfig != nil {
|
|
||||||
c.clientMetrics.UpdatePushFromMgm(c.ctx, metricsConfig.GetEnabled())
|
|
||||||
}
|
|
||||||
|
|
||||||
localPeerState := peer.LocalPeerState{
|
localPeerState := peer.LocalPeerState{
|
||||||
IP: loginResp.GetPeerConfig().GetAddress(),
|
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||||
PubKey: myPrivateKey.PublicKey().String(),
|
PubKey: myPrivateKey.PublicKey().String(),
|
||||||
@@ -371,11 +346,6 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
engineConfig.TempDir = mobileDependency.TempDir
|
engineConfig.TempDir = mobileDependency.TempDir
|
||||||
// Leave StateDir empty when there is no state path so a disk-backed
|
|
||||||
// syncstore falls back to os.TempDir() instead of filepath.Dir("") == ".".
|
|
||||||
if path != "" {
|
|
||||||
engineConfig.StateDir = filepath.Dir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), engineConfig.MTU)
|
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), engineConfig.MTU)
|
||||||
c.statusRecorder.SetRelayMgr(relayManager)
|
c.statusRecorder.SetRelayMgr(relayManager)
|
||||||
@@ -404,7 +374,6 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
StateManager: stateManager,
|
StateManager: stateManager,
|
||||||
UpdateManager: c.updateManager,
|
UpdateManager: c.updateManager,
|
||||||
ClientMetrics: c.clientMetrics,
|
ClientMetrics: c.clientMetrics,
|
||||||
MetricsCtx: c.ctx,
|
|
||||||
}, mobileDependency)
|
}, mobileDependency)
|
||||||
engine.SetSyncResponsePersistence(c.persistSyncResponse)
|
engine.SetSyncResponsePersistence(c.persistSyncResponse)
|
||||||
c.engine = engine
|
c.engine = engine
|
||||||
@@ -432,10 +401,14 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.engine = nil
|
c.engine = nil
|
||||||
c.engineMutex.Unlock()
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
log.Infof("ensuring wg interface is removed, Netbird engine context cancelled")
|
// todo: consider to remove this condition. Is not thread safe.
|
||||||
|
// We should always call Stop(), but we need to verify that it is idempotent
|
||||||
|
if engine.wgInterface != nil {
|
||||||
|
log.Infof("ensuring %s is removed, Netbird engine context cancelled", engine.wgInterface.Name())
|
||||||
|
|
||||||
if err := engine.Stop(); err != nil {
|
if err := engine.Stop(); err != nil {
|
||||||
log.Errorf("Failed to stop engine: %v", err)
|
log.Errorf("Failed to stop engine: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.statusRecorder.ClientTeardown()
|
c.statusRecorder.ClientTeardown()
|
||||||
|
|
||||||
@@ -451,12 +424,12 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.statusRecorder.ClientStart()
|
c.statusRecorder.ClientStart()
|
||||||
err = backoff.Retry(operation, backoff.WithContext(backOff, c.ctx))
|
err = backoff.Retry(operation, backOff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||||
state.Set(StatusNeedsLogin)
|
state.Set(StatusNeedsLogin)
|
||||||
c.runCancel()
|
_ = c.Stop()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -534,9 +507,11 @@ func (c *ConnectClient) Status() StatusType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) Stop() error {
|
func (c *ConnectClient) Stop() error {
|
||||||
c.runCancel()
|
engine := c.Engine()
|
||||||
if c.runStarted.Load() {
|
if engine != nil {
|
||||||
<-c.runExited
|
if err := engine.Stop(); err != nil {
|
||||||
|
return fmt.Errorf("stop engine: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -602,7 +577,7 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf
|
|||||||
BlockInbound: config.BlockInbound,
|
BlockInbound: config.BlockInbound,
|
||||||
DisableIPv6: config.DisableIPv6,
|
DisableIPv6: config.DisableIPv6,
|
||||||
|
|
||||||
LazyConnection: lazyconn.ParseState(config.LazyConnection),
|
LazyConnectionEnabled: config.LazyConnectionEnabled,
|
||||||
|
|
||||||
MTU: selectMTU(config.MTU, peerConfig.Mtu),
|
MTU: selectMTU(config.MTU, peerConfig.Mtu),
|
||||||
LogPath: logPath,
|
LogPath: logPath,
|
||||||
@@ -676,6 +651,7 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte,
|
|||||||
config.BlockLANAccess,
|
config.BlockLANAccess,
|
||||||
config.BlockInbound,
|
config.BlockInbound,
|
||||||
config.DisableIPv6,
|
config.DisableIPv6,
|
||||||
|
config.LazyConnectionEnabled,
|
||||||
config.EnableSSHRoot,
|
config.EnableSSHRoot,
|
||||||
config.EnableSSHSFTP,
|
config.EnableSSHSFTP,
|
||||||
config.EnableSSHLocalPortForwarding,
|
config.EnableSSHLocalPortForwarding,
|
||||||
|
|||||||
@@ -250,13 +250,10 @@ type BundleGenerator struct {
|
|||||||
syncResponse *mgmProto.SyncResponse
|
syncResponse *mgmProto.SyncResponse
|
||||||
logPath string
|
logPath string
|
||||||
tempDir string
|
tempDir string
|
||||||
statePath string
|
|
||||||
cpuProfile []byte
|
cpuProfile []byte
|
||||||
capturePath string
|
capturePath string
|
||||||
refreshStatus func() // Optional callback to refresh status before bundle generation
|
refreshStatus func() // Optional callback to refresh status before bundle generation
|
||||||
clientMetrics MetricsExporter
|
clientMetrics MetricsExporter
|
||||||
daemonVersion string
|
|
||||||
cliVersion string
|
|
||||||
|
|
||||||
anonymize bool
|
anonymize bool
|
||||||
includeSystemInfo bool
|
includeSystemInfo bool
|
||||||
@@ -277,13 +274,10 @@ type GeneratorDependencies struct {
|
|||||||
SyncResponse *mgmProto.SyncResponse
|
SyncResponse *mgmProto.SyncResponse
|
||||||
LogPath string
|
LogPath string
|
||||||
TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used.
|
TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used.
|
||||||
StatePath string // Path to the state file. If empty, the ServiceManager default path is used.
|
|
||||||
CPUProfile []byte
|
CPUProfile []byte
|
||||||
CapturePath string
|
CapturePath string
|
||||||
RefreshStatus func()
|
RefreshStatus func()
|
||||||
ClientMetrics MetricsExporter
|
ClientMetrics MetricsExporter
|
||||||
DaemonVersion string
|
|
||||||
CliVersion string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
|
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
|
||||||
@@ -301,13 +295,10 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
|
|||||||
syncResponse: deps.SyncResponse,
|
syncResponse: deps.SyncResponse,
|
||||||
logPath: deps.LogPath,
|
logPath: deps.LogPath,
|
||||||
tempDir: deps.TempDir,
|
tempDir: deps.TempDir,
|
||||||
statePath: deps.StatePath,
|
|
||||||
cpuProfile: deps.CPUProfile,
|
cpuProfile: deps.CPUProfile,
|
||||||
capturePath: deps.CapturePath,
|
capturePath: deps.CapturePath,
|
||||||
refreshStatus: deps.RefreshStatus,
|
refreshStatus: deps.RefreshStatus,
|
||||||
clientMetrics: deps.ClientMetrics,
|
clientMetrics: deps.ClientMetrics,
|
||||||
daemonVersion: deps.DaemonVersion,
|
|
||||||
cliVersion: deps.CliVersion,
|
|
||||||
|
|
||||||
anonymize: cfg.Anonymize,
|
anonymize: cfg.Anonymize,
|
||||||
includeSystemInfo: cfg.IncludeSystemInfo,
|
includeSystemInfo: cfg.IncludeSystemInfo,
|
||||||
@@ -468,11 +459,9 @@ func (g *BundleGenerator) addStatus() error {
|
|||||||
protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus)
|
protoFullStatus := nbstatus.ToProtoFullStatus(fullStatus)
|
||||||
protoFullStatus.Events = g.statusRecorder.GetEventHistory()
|
protoFullStatus.Events = g.statusRecorder.GetEventHistory()
|
||||||
overview := nbstatus.ConvertToStatusOutputOverview(protoFullStatus, nbstatus.ConvertOptions{
|
overview := nbstatus.ConvertToStatusOutputOverview(protoFullStatus, nbstatus.ConvertOptions{
|
||||||
Anonymize: g.anonymize,
|
Anonymize: g.anonymize,
|
||||||
ProfileName: profName,
|
ProfileName: profName,
|
||||||
DaemonVersion: g.daemonVersion,
|
|
||||||
})
|
})
|
||||||
overview.CliVersion = g.cliVersion
|
|
||||||
statusOutput := overview.FullDetailSummary()
|
statusOutput := overview.FullDetailSummary()
|
||||||
|
|
||||||
statusReader := strings.NewReader(statusOutput)
|
statusReader := strings.NewReader(statusOutput)
|
||||||
@@ -519,14 +508,6 @@ func (g *BundleGenerator) addConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Surface the set of MDM-enforced keys so a support engineer reading
|
|
||||||
// the bundle can tell which field values are user-set vs MDM-overridden.
|
|
||||||
// Same semantics as the mDMManagedFields list returned by the
|
|
||||||
// GetConfig RPC consumed by `netbird debug config`.
|
|
||||||
if managed := g.internalConfig.Policy().ManagedKeys(); len(managed) > 0 {
|
|
||||||
configContent.WriteString(fmt.Sprintf("MDMManagedFields: %v\n", managed))
|
|
||||||
}
|
|
||||||
|
|
||||||
configReader := strings.NewReader(configContent.String())
|
configReader := strings.NewReader(configContent.String())
|
||||||
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
|
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
|
||||||
return fmt.Errorf("add config file to zip: %w", err)
|
return fmt.Errorf("add config file to zip: %w", err)
|
||||||
@@ -681,7 +662,7 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder)
|
|||||||
configContent.WriteString(fmt.Sprintf("ClientCertKeyPath: %s\n", g.internalConfig.ClientCertKeyPath))
|
configContent.WriteString(fmt.Sprintf("ClientCertKeyPath: %s\n", g.internalConfig.ClientCertKeyPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
configContent.WriteString(fmt.Sprintf("LazyConnection: %q\n", g.internalConfig.LazyConnection))
|
configContent.WriteString(fmt.Sprintf("LazyConnectionEnabled: %v\n", g.internalConfig.LazyConnectionEnabled))
|
||||||
configContent.WriteString(fmt.Sprintf("MTU: %d\n", g.internalConfig.MTU))
|
configContent.WriteString(fmt.Sprintf("MTU: %d\n", g.internalConfig.MTU))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,8 +798,6 @@ func (g *BundleGenerator) addSyncResponse() error {
|
|||||||
AllowPartial: true,
|
AllowPartial: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
g.maskSecrets()
|
|
||||||
|
|
||||||
jsonBytes, err := options.Marshal(g.syncResponse)
|
jsonBytes, err := options.Marshal(g.syncResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generate json: %w", err)
|
return fmt.Errorf("generate json: %w", err)
|
||||||
@@ -831,33 +810,9 @@ func (g *BundleGenerator) addSyncResponse() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) maskSecrets() {
|
|
||||||
if g.syncResponse == nil || g.syncResponse.NetbirdConfig == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.syncResponse.NetbirdConfig.Flow != nil {
|
|
||||||
g.syncResponse.NetbirdConfig.Flow.TokenPayload = maskedValue
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.syncResponse.NetbirdConfig.Relay != nil {
|
|
||||||
g.syncResponse.NetbirdConfig.Relay.TokenPayload = maskedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range g.syncResponse.NetbirdConfig.Turns {
|
|
||||||
if g.syncResponse.NetbirdConfig.Turns[i] != nil {
|
|
||||||
g.syncResponse.NetbirdConfig.Turns[i].Password = maskedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *BundleGenerator) addStateFile() error {
|
func (g *BundleGenerator) addStateFile() error {
|
||||||
path := g.statePath
|
sm := profilemanager.NewServiceManager("")
|
||||||
if path == "" {
|
path := sm.GetStatePath()
|
||||||
sm := profilemanager.NewServiceManager("")
|
|
||||||
path = sm.GetStatePath()
|
|
||||||
}
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1084,8 +1039,7 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This regex will match both logs rotated by us and logrotate on linux
|
pattern := filepath.Join(logDir, "client-*.log.gz")
|
||||||
pattern := filepath.Join(logDir, "client*.log.*")
|
|
||||||
files, err := filepath.Glob(pattern)
|
files, err := filepath.Glob(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to glob rotated logs: %v", err)
|
log.Warnf("failed to glob rotated logs: %v", err)
|
||||||
@@ -1118,12 +1072,7 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
|
|||||||
|
|
||||||
for i := 0; i < maxFiles; i++ {
|
for i := 0; i < maxFiles; i++ {
|
||||||
name := filepath.Base(files[i])
|
name := filepath.Base(files[i])
|
||||||
if strings.HasSuffix(name, ".gz") {
|
if err := g.addSingleLogFileGz(files[i], name); err != nil {
|
||||||
err = g.addSingleLogFileGz(files[i], name)
|
|
||||||
} else {
|
|
||||||
err = g.addSingleLogfile(files[i], name)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to add rotated log %s: %v", name, err)
|
log.Warnf("failed to add rotated log %s: %v", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
//go:build ios
|
|
||||||
|
|
||||||
package debug
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// swiftLogFile is the Swift app log written by the iOS app into the same log
|
|
||||||
// directory as the Go client log, so it can be collected into the bundle.
|
|
||||||
const swiftLogFile = "swift-log.log"
|
|
||||||
|
|
||||||
// addPlatformLog collects logs for the iOS debug bundle. iOS has no logcat or
|
|
||||||
// systemd journal, so we rely on file-based logs. addLogfile handles the Go
|
|
||||||
// client log (logPath) with rotation, the stderr/stdout companions and
|
|
||||||
// anonymization. The iOS app writes its own Swift log into the same directory,
|
|
||||||
// so we add it alongside the Go log.
|
|
||||||
func (g *BundleGenerator) addPlatformLog() error {
|
|
||||||
if err := g.addLogfile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.logPath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
swiftLogPath := filepath.Join(filepath.Dir(g.logPath), swiftLogFile)
|
|
||||||
if err := g.addSingleLogfile(swiftLogPath, swiftLogFile); err != nil {
|
|
||||||
// The Swift log is best-effort: the app may not have written it yet.
|
|
||||||
log.Warnf("failed to add %s to debug bundle: %v", swiftLogFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -844,6 +844,10 @@ func collectSysctls() string {
|
|||||||
[]string{"net.ipv4.conf.all.src_valid_mark", "net.ipv4.conf.default.src_valid_mark"},
|
[]string{"net.ipv4.conf.all.src_valid_mark", "net.ipv4.conf.default.src_valid_mark"},
|
||||||
listInterfaceSysctls("ipv4", "src_valid_mark")...,
|
listInterfaceSysctls("ipv4", "src_valid_mark")...,
|
||||||
))
|
))
|
||||||
|
writeSysctlGroup(&builder, "accept_ra", append(
|
||||||
|
[]string{"net.ipv6.conf.all.accept_ra", "net.ipv6.conf.default.accept_ra"},
|
||||||
|
listInterfaceSysctls("ipv6", "accept_ra")...,
|
||||||
|
))
|
||||||
writeSysctlGroup(&builder, "conntrack", []string{
|
writeSysctlGroup(&builder, "conntrack", []string{
|
||||||
"net.netfilter.nf_conntrack_acct",
|
"net.netfilter.nf_conntrack_acct",
|
||||||
"net.netfilter.nf_conntrack_tcp_loose",
|
"net.netfilter.nf_conntrack_tcp_loose",
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
package debug
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestAddRotatedLogFiles_PicksUpAllVariants asserts that the rotated-log
|
|
||||||
// glob picks up logs rotated by timberjack (gzipped) and by logrotate (plain
|
|
||||||
// and gzipped), and skips unrelated files.
|
|
||||||
func TestAddRotatedLogFiles_PicksUpAllVariants(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
writeFile(t, filepath.Join(dir, "client.log"), "active log\n")
|
|
||||||
writeFile(t, filepath.Join(dir, "other.log"), "unrelated\n")
|
|
||||||
|
|
||||||
timberjackRotated := "client-2026-05-21T10-30-45.000.log.gz"
|
|
||||||
writeGzFile(t, filepath.Join(dir, timberjackRotated), "timberjack rotated content\n")
|
|
||||||
|
|
||||||
logrotatePlain := "client.log.1"
|
|
||||||
writeFile(t, filepath.Join(dir, logrotatePlain), "logrotate plain content\n")
|
|
||||||
|
|
||||||
logrotateGz := "client.log.2.gz"
|
|
||||||
writeGzFile(t, filepath.Join(dir, logrotateGz), "logrotate gz content\n")
|
|
||||||
|
|
||||||
names := runAddRotatedLogFiles(t, dir, 10)
|
|
||||||
|
|
||||||
require.Contains(t, names, timberjackRotated, "timberjack rotated file should be in bundle")
|
|
||||||
require.Contains(t, names, logrotatePlain, "logrotate plain rotated file should be in bundle")
|
|
||||||
require.Contains(t, names, logrotateGz, "logrotate gzipped rotated file should be in bundle")
|
|
||||||
require.NotContains(t, names, "client.log", "active log should not be added by addRotatedLogFiles")
|
|
||||||
require.NotContains(t, names, "other.log", "unrelated files should not be in bundle")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAddRotatedLogFiles_RespectsLogFileCount asserts that only the newest
|
|
||||||
// logFileCount rotated files are bundled, ordered by mtime.
|
|
||||||
func TestAddRotatedLogFiles_RespectsLogFileCount(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
oldest := filepath.Join(dir, "client.log.3")
|
|
||||||
middle := filepath.Join(dir, "client.log.2")
|
|
||||||
newest := filepath.Join(dir, "client.log.1")
|
|
||||||
writeFile(t, oldest, "old\n")
|
|
||||||
writeFile(t, middle, "mid\n")
|
|
||||||
writeFile(t, newest, "new\n")
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
require.NoError(t, os.Chtimes(oldest, now.Add(-2*time.Hour), now.Add(-2*time.Hour)))
|
|
||||||
require.NoError(t, os.Chtimes(middle, now.Add(-1*time.Hour), now.Add(-1*time.Hour)))
|
|
||||||
require.NoError(t, os.Chtimes(newest, now, now))
|
|
||||||
|
|
||||||
names := runAddRotatedLogFiles(t, dir, 2)
|
|
||||||
|
|
||||||
require.Contains(t, names, "client.log.1")
|
|
||||||
require.Contains(t, names, "client.log.2")
|
|
||||||
require.NotContains(t, names, "client.log.3", "oldest file should be dropped when logFileCount=2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// runAddRotatedLogFiles calls addRotatedLogFiles against a fresh in-memory
|
|
||||||
// zip writer and returns the set of entry names that ended up in the archive.
|
|
||||||
func runAddRotatedLogFiles(t *testing.T, dir string, logFileCount uint32) map[string]struct{} {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
g := &BundleGenerator{
|
|
||||||
archive: zip.NewWriter(&buf),
|
|
||||||
logFileCount: logFileCount,
|
|
||||||
}
|
|
||||||
g.addRotatedLogFiles(dir)
|
|
||||||
require.NoError(t, g.archive.Close())
|
|
||||||
|
|
||||||
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
names := make(map[string]struct{}, len(zr.File))
|
|
||||||
for _, f := range zr.File {
|
|
||||||
names[f.Name] = struct{}{}
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFile(t *testing.T, path, content string) {
|
|
||||||
t.Helper()
|
|
||||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeGzFile(t *testing.T, path, content string) {
|
|
||||||
t.Helper()
|
|
||||||
var buf bytes.Buffer
|
|
||||||
gw := gzip.NewWriter(&buf)
|
|
||||||
_, err := io.WriteString(gw, content)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, gw.Close())
|
|
||||||
require.NoError(t, os.WriteFile(path, buf.Bytes(), 0o644))
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android && !ios
|
//go:build !android
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
|
|||||||
@@ -843,8 +843,6 @@ func TestAddConfig_AllFieldsCovered(t *testing.T) {
|
|||||||
"PreSharedKey": "sensitive: WireGuard pre-shared key",
|
"PreSharedKey": "sensitive: WireGuard pre-shared key",
|
||||||
"SSHKey": "sensitive: SSH private key",
|
"SSHKey": "sensitive: SSH private key",
|
||||||
"ClientCertKeyPair": "non-config: parsed cert pair, not serialized",
|
"ClientCertKeyPair": "non-config: parsed cert pair, not serialized",
|
||||||
"Name": "non-config: profile name is not needed for debug purposes",
|
|
||||||
"policy": "non-config: in-memory MDM policy snapshot, surfaced via Config.Policy() / GetConfigResponse.MDMManagedFields",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mURL, _ := url.Parse("https://api.example.com:443")
|
mURL, _ := url.Parse("https://api.example.com:443")
|
||||||
@@ -885,7 +883,7 @@ func TestAddConfig_AllFieldsCovered(t *testing.T) {
|
|||||||
DNSRouteInterval: 5 * time.Second,
|
DNSRouteInterval: 5 * time.Second,
|
||||||
ClientCertPath: "/tmp/cert",
|
ClientCertPath: "/tmp/cert",
|
||||||
ClientCertKeyPath: "/tmp/key",
|
ClientCertKeyPath: "/tmp/key",
|
||||||
LazyConnection: "on",
|
LazyConnectionEnabled: true,
|
||||||
MTU: 1280,
|
MTU: 1280,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -339,7 +339,8 @@ func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
|
|||||||
case entry.Pattern == ".":
|
case entry.Pattern == ".":
|
||||||
return true
|
return true
|
||||||
case entry.IsWildcard:
|
case entry.IsWildcard:
|
||||||
return strings.HasSuffix(qname, "."+entry.Pattern)
|
parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
|
||||||
|
return len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
|
||||||
default:
|
default:
|
||||||
// For non-wildcard patterns:
|
// For non-wildcard patterns:
|
||||||
// If handler wants subdomain matching, allow suffix match
|
// If handler wants subdomain matching, allow suffix match
|
||||||
|
|||||||
@@ -164,54 +164,6 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
|||||||
matchSubdomains: true,
|
matchSubdomains: true,
|
||||||
shouldMatch: true,
|
shouldMatch: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "wildcard label-boundary mismatch (suffix overlap)",
|
|
||||||
handlerDomain: "*.b.test.",
|
|
||||||
queryDomain: "x.ab.test.",
|
|
||||||
isWildcard: true,
|
|
||||||
matchSubdomains: false,
|
|
||||||
shouldMatch: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard label-boundary match",
|
|
||||||
handlerDomain: "*.b.test.",
|
|
||||||
queryDomain: "x.b.test.",
|
|
||||||
isWildcard: true,
|
|
||||||
matchSubdomains: false,
|
|
||||||
shouldMatch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard multi-label match",
|
|
||||||
handlerDomain: "*.b.test.",
|
|
||||||
queryDomain: "x.y.b.test.",
|
|
||||||
isWildcard: true,
|
|
||||||
matchSubdomains: false,
|
|
||||||
shouldMatch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard no match on multi-label apex",
|
|
||||||
handlerDomain: "*.b.test.",
|
|
||||||
queryDomain: "b.test.",
|
|
||||||
isWildcard: true,
|
|
||||||
matchSubdomains: false,
|
|
||||||
shouldMatch: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard no match on unrelated suffix containment",
|
|
||||||
handlerDomain: "*.example.com.",
|
|
||||||
queryDomain: "notexample.com.",
|
|
||||||
isWildcard: true,
|
|
||||||
matchSubdomains: false,
|
|
||||||
shouldMatch: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard accepts pattern registered without trailing dot",
|
|
||||||
handlerDomain: "*.b.test",
|
|
||||||
queryDomain: "x.b.test.",
|
|
||||||
isWildcard: true,
|
|
||||||
matchSubdomains: false,
|
|
||||||
shouldMatch: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -321,19 +273,6 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
expectedCalls: 1,
|
expectedCalls: 1,
|
||||||
expectedHandler: 2, // highest priority matching handler should be called
|
expectedHandler: 2, // highest priority matching handler should be called
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "overlapping wildcard suffixes route to correct handler",
|
|
||||||
handlers: []struct {
|
|
||||||
pattern string
|
|
||||||
priority int
|
|
||||||
}{
|
|
||||||
{pattern: "*.b.test.", priority: nbdns.PriorityDNSRoute},
|
|
||||||
{pattern: "*.ab.test.", priority: nbdns.PriorityDNSRoute},
|
|
||||||
},
|
|
||||||
queryDomain: "app.ab.test.",
|
|
||||||
expectedCalls: 1,
|
|
||||||
expectedHandler: 1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "root zone with specific domain",
|
name: "root zone with specific domain",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
|
|||||||
@@ -26,19 +26,6 @@ type resolver interface {
|
|||||||
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerConnectivity reports whether a tunnel IP belongs to a peer the
|
|
||||||
// client knows about and whether that peer is currently connected. The
|
|
||||||
// local resolver uses this to suppress A/AAAA answers whose RDATA points
|
|
||||||
// at a disconnected peer (typical case: a synthesized private-service
|
|
||||||
// record pointing at an embedded proxy peer that just went offline).
|
|
||||||
//
|
|
||||||
// known=false means the IP isn't in the local peerstore at all — the
|
|
||||||
// record is left alone (it points at something outside our mesh, e.g.
|
|
||||||
// a non-peer upstream).
|
|
||||||
type PeerConnectivity interface {
|
|
||||||
IsConnectedByIP(ip string) (known, connected bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
records map[dns.Question][]dns.RR
|
records map[dns.Question][]dns.RR
|
||||||
@@ -46,11 +33,6 @@ type Resolver struct {
|
|||||||
// zones maps zone domain -> NonAuthoritative (true = non-authoritative, user-created zone)
|
// zones maps zone domain -> NonAuthoritative (true = non-authoritative, user-created zone)
|
||||||
zones map[domain.Domain]bool
|
zones map[domain.Domain]bool
|
||||||
resolver resolver
|
resolver resolver
|
||||||
// peerConn, when non-nil, is consulted on every A/AAAA answer to
|
|
||||||
// drop records pointing at disconnected peers. nil disables the
|
|
||||||
// filter and preserves the legacy "return whatever is registered"
|
|
||||||
// behaviour for callers that never wire a status source.
|
|
||||||
peerConn PeerConnectivity
|
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -67,15 +49,6 @@ func NewResolver() *Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPeerConnectivity wires the per-IP connectivity check used to filter
|
|
||||||
// out A/AAAA answers pointing at disconnected peers. Pass nil to disable.
|
|
||||||
// Safe to call multiple times; the latest value wins.
|
|
||||||
func (d *Resolver) SetPeerConnectivity(p PeerConnectivity) {
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
d.peerConn = p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Resolver) MatchSubdomains() bool {
|
func (d *Resolver) MatchSubdomains() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -122,7 +95,6 @@ func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
replyMessage.RecursionAvailable = true
|
replyMessage.RecursionAvailable = true
|
||||||
|
|
||||||
result := d.lookupRecords(logger, question)
|
result := d.lookupRecords(logger, question)
|
||||||
result.records = d.filterDisconnectedPeerAnswers(logger, question, result.records)
|
|
||||||
replyMessage.Authoritative = !result.hasExternalData
|
replyMessage.Authoritative = !result.hasExternalData
|
||||||
replyMessage.Answer = result.records
|
replyMessage.Answer = result.records
|
||||||
replyMessage.Rcode = d.determineRcode(question, result)
|
replyMessage.Rcode = d.determineRcode(question, result)
|
||||||
@@ -464,78 +436,6 @@ func (d *Resolver) logDNSError(logger *log.Entry, hostname string, qtype uint16,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterDisconnectedPeerAnswers drops A/AAAA records whose RDATA matches
|
|
||||||
// a known but disconnected peer. The synthesized private-service zones
|
|
||||||
// emit one A record per connected proxy peer in a cluster; when a peer
|
|
||||||
// goes offline, the server-side refresh removes the record from the
|
|
||||||
// next netmap, but the client may still hold the previous netmap for a
|
|
||||||
// short window. This filter is the local belt to that braces — even on
|
|
||||||
// the stale netmap, the resolver hides the offline target.
|
|
||||||
//
|
|
||||||
// Records pointing at unknown IPs (outside the local peerstore, e.g.
|
|
||||||
// non-mesh upstreams) are never dropped. Non-A/AAAA records pass
|
|
||||||
// through untouched.
|
|
||||||
//
|
|
||||||
// Escape hatch: if filtering would leave the answer empty AND at least
|
|
||||||
// one record was filtered, the original list is returned. Better to
|
|
||||||
// hand the client a record that may not respond than NXDOMAIN it
|
|
||||||
// completely when every proxy peer is offline (the upstream may still
|
|
||||||
// be reachable some other way, or the peerstore may be stale).
|
|
||||||
func (d *Resolver) filterDisconnectedPeerAnswers(logger *log.Entry, question dns.Question, records []dns.RR) []dns.RR {
|
|
||||||
if len(records) < 2 {
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
d.mu.RLock()
|
|
||||||
checker := d.peerConn
|
|
||||||
d.mu.RUnlock()
|
|
||||||
if checker == nil {
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
kept := make([]dns.RR, 0, len(records))
|
|
||||||
var dropped int
|
|
||||||
for _, rr := range records {
|
|
||||||
ip := extractRecordIP(rr)
|
|
||||||
if ip == "" {
|
|
||||||
kept = append(kept, rr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
known, connected := checker.IsConnectedByIP(ip)
|
|
||||||
if known && !connected {
|
|
||||||
dropped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kept = append(kept, rr)
|
|
||||||
}
|
|
||||||
if dropped == 0 {
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
if len(kept) == 0 {
|
|
||||||
logger.Debugf("all %d answers for %s point at disconnected peers; returning the original list", dropped, question.Name)
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
logger.Tracef("dropped %d disconnected-peer answer(s) for %s, returning %d", dropped, question.Name, len(kept))
|
|
||||||
return kept
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractRecordIP returns the dotted-decimal / colon-hex IP carried by
|
|
||||||
// an A or AAAA record, or "" for any other record type.
|
|
||||||
func extractRecordIP(rr dns.RR) string {
|
|
||||||
switch r := rr.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
if r.A == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return r.A.String()
|
|
||||||
case *dns.AAAA:
|
|
||||||
if r.AAAA == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return r.AAAA.String()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update replaces all zones and their records
|
// Update replaces all zones and their records
|
||||||
func (d *Resolver) Update(customZones []nbdns.CustomZone) {
|
func (d *Resolver) Update(customZones []nbdns.CustomZone) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user